8000 calculateTimeForTokens for SlidingWindow · symfony/symfony@520bf28 · GitHub
[go: up one dir, main page]

Skip to content

Commit 520bf28

Browse files
committed
calculateTimeForTokens for SlidingWindow
1 parent fcb754a commit 520bf28

File tree

3 files changed

+61
-18
lines changed

3 files changed

+61
-18
lines changed

src/Symfony/Component/RateLimiter/Policy/SlidingWindow.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,27 @@ public function getRetryAfter(): \DateTimeImmutable
105105
return \DateTimeImmutable::createFromFormat('U.u', sprintf('%.6F', $this->windowEndAt));
106106
}
107107

108+
public function calculateTimeForTokens(int $maxSize, int $tokens): int
109+
{
110+
$remaining = $maxSize - $this->getHitCount();
111+
if ($remaining >= $tokens) {
112+
return 0;
113+
}
114+
115+
$startOfWindow = $this->windowEndAt - $this->intervalInSeconds;
116+
$percentOfCurrentTimeFrame = min((microtime(true) - $startOfWindow) / $this->intervalInSeconds, 1);
117+
$releasable = $maxSize - floor($this->hitCountForLastWindow * (1 - $percentOfCurrentTimeFrame));
118+
$remainingWindow = (microtime(true) - $startOfWindow) - $this->intervalInSeconds;
119+
$timePerToken = $remainingWindow / $releasable;
120+
$needed = $tokens - $remaining;
121+
122+
if ($releasable <= $needed) {
123+
return (int) ceil($needed * $timePerToken);
124+
}
125+
126+
return (int) (($this->windowEndAt - microtime(true)) + ceil(($needed - $releasable) * ($this->intervalInSeconds / $maxSize)));
127+
}
128+
108129
public function __serialize(): array
109130
{
110131
return [

src/Symfony/Component/RateLimiter/Policy/SlidingWindowLimiter.php

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
use Symfony\Component\Lock\LockInterface;
1515
use Symfony\Component\Lock\NoLock;
16-
use Symfony\Component\RateLimiter\Exception\ReserveNotSupportedException;
16+
use Symfony\Component\RateLimiter\Exception\MaxWaitDurationExceededException;
1717
use Symfony\Component\RateLimiter\LimiterInterface;
1818
use Symfony\Component\RateLimiter\RateLimit;
1919
use Symfony\Component\RateLimiter\Reservation;
@@ -53,14 +53,10 @@ public function __construct(string $id, int $limit, \DateInterval $interval, Sto
5353

5454
public function reserve(int $tokens = 1, float $maxTime = null): Reservation
5555
{
56-
throw new ReserveNotSupportedException(__CLASS__);
57-
}
56+
if ($tokens > $this->limit) {
57+
throw new \InvalidArgumentException(sprintf('Cannot reserve more tokens (%d) than the size of the rate limiter (%d).', $tokens, $this->limit));
58+
}
5859

59-
/**
60-
* {@inheritdoc}
61-
*/
62-
public function consume(int $tokens = 1): RateLimit
63-
{
6460
$this->lock->acquire(true);
6561

6662
try {
@@ -71,19 +67,43 @@ public function consume(int $tokens = 1): RateLimit
7167
$window = SlidingWindow::createFromPreviousWindow($window, $this->interval);
7268
}
7369

70+
$now = microtime(true);
7471
$hitCount = $window->getHitCount();
7572
$availableTokens = $this->getAvailableTokens($hitCount);
76-
if ($availableTokens < $tokens) {
77-
return new RateLimit($availableTokens, $window->getRetryAfter(), false, $this->limit);
78-
}
73+
if ($availableTokens >= $tokens) {
74+
$window->add($tokens);
75+
76+
$reservation = new Reservation($now, new RateLimit($this->getAvailableTokens($window->getHitCount()), \DateTimeImmutable::createFromFormat('U', floor($now)), true, $this->limit));
77+
} else {
78+
$waitDuration = $window->calculateTimeForTokens($this->limit, max(1, $tokens));
7979

80-
$window->add($tokens);
81-
$this->storage->save($window);
80+
if (null !== $maxTime && $waitDuration > $maxTime) {
81+
// process needs to wait longer than set interval
82+
throw new MaxWaitDurationExceededException(sprintf('The rate limiter wait time ("%d" seconds) is longer than the provided maximum time ("%d" seconds).', $waitDuration, $maxTime), new RateLimit($this->getAvailableTokens($window->getHitCount()), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->limit));
83+
}
8284

83-
return new RateLimit($this->getAvailableTokens($window->getHitCount()), $window->getRetryAfter(), true, $this->limit);
85+
$window->add($tokens);
86+
87+
$reservation = new Reservation($now + $waitDuration, new RateLimit($this->getAvailableTokens($window->getHitCount()), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->limit));
88+
}
89+
90+
if (0 < $tokens) {
91+
$this->storage->save($window);
92+
}
8493
} finally {
8594
$this->lock->release();
8695
}
96+
97+
return $reservation;
98+
}
99+
100+
public function consume(int $tokens = 1): RateLimit
101+
{
102+
try {
103+
return $this->reserve($tokens, 0)->getRateLimit();
104+
} catch (MaxWaitDurationExceededException $e) {
105+
return $e->getRateLimit();
106+
}
87107
}
88108

89109
private function getAvailableTokens(int $hitCount): int

src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowLimiterTest.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Bridge\PhpUnit\ClockMock;
16-
use Symfony\Component\RateLimiter\Exception\ReserveNotSupportedException;
1716
use Symfony\Component\RateLimiter\Policy\SlidingWindowLimiter;
1817
use Symfony\Component\RateLimiter\RateLimit;
1918
use Symfony\Component\RateLimiter\Storage\InMemoryStorage;
@@ -66,14 +65,17 @@ public function testWaitIntervalOnConsumeOverLimit()
6665

6766
$start = microtime(true);
6867
$rateLimit->wait(); // wait 12 seconds
69-
$this->assertEqualsWithDelta($start + 12, microtime(true), 1);
68+
$this->assertEqualsWithDelta($start + (12 / 5), microtime(true), 1);
69+
$this->assertTrue($limiter->consume()->isAccepted());
7070
}
7171

7272
public function testReserve()
7373
{
74-
$this->expectException(ReserveNotSupportedException::class);
74+
$limiter = $this->createLimiter();
75+
$limiter->consume(8);< 5E44 /div>
7576

76-
$this->createLimiter()->reserve();
77+
// 2 over the limit, causing the WaitDuration to become 2/10th of the 12s interval
78+
$this->assertEqualsWithDelta(12 / 5, $limiter->reserve(4)->getWaitDuration(), 1);
7779
}
7880

7981
private function createLimiter(): SlidingWindowLimiter

0 commit comments

Comments
 (0)
0