Skip to content

Commit c612584

Browse files
committed
[RateLimiter] fix incorrect retryAfter of FixedWindow
A FixedWindow always ends `intervalInSeconds` after the start time (`timer`). So when calculating the time to consume some tokens the tokens will always be available at `timer + intervalInSeconds`. But currently this is reported incorrectly as `calculateTimeForTokens()` calculates the time based on the desired amount of tokens (and cycles) while it doesn't take into account `maxSize` amount of tokens become available at the windows end. Furthermore calculating the amount of needed cycles is incorrect. This as all tokens become available at once (at the windows end) and you can't consume more tokens than `maxSize` (which is validated at the start of `FixedWindowLimiter::reserve`, in case of `tokens > limit` it throws).
1 parent 39cd93a commit c612584

File tree

3 files changed

+7
-5
lines changed

3 files changed

+7
-5
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public function reserve(int $tokens = 1, float $maxTime = null): Reservation
6868

6969
$reservation = new Reservation($now, new RateLimit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now)), true, $this->limit));
7070
} else {
71-
$waitDuration = $window->calculateTimeForTokens($tokens);
71+
$waitDuration = $window->calculateTimeForTokens($tokens, $now);
7272

7373
if (null !== $maxTime && $waitDuration > $maxTime) {
7474
// process needs to wait longer than set interval

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

+2-4
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,13 @@ public function getAvailableTokens(float $now)
7575
return $this->maxSize - $this->hitCount;
7676
}
7777

78-
public function calculateTimeForTokens(int $tokens): int
78+
public function calculateTimeForTokens(int $tokens, float $now): int
7979
{
8080
if (($this->maxSize - $this->hitCount) >= $tokens) {
8181
return 0;
8282
}
8383

84-
$cyclesRequired = ceil($tokens / $this->maxSize);
85-
86-
return $cyclesRequired * $this->intervalInSeconds;
84+
return ceil($this->timer + $this->intervalInSeconds - $now);
8785
}
8886

8987
public function __serialize(): array

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

+4
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ protected function setUp(): void
3737

3838
public function testConsume()
3939
{
40+
$now = time();
4041
$limiter = $this->createLimiter();
4142

4243
// fill 9 tokens in 45 seconds
@@ -51,6 +52,9 @@ public function testConsume()
5152
$rateLimit = $limiter->consume();
5253
$this->assertFalse($rateLimit->isAccepted());
5354
$this->assertSame(10, $rateLimit->getLimit());
55+
// Window ends after 1 minute
56+
$retryAfter = \DateTimeImmutable::createFromFormat('U', $now + 60);
57+
$this->assertEquals($retryAfter, $rateLimit->getRetryAfter());
5458
}
5559

5660
/**

0 commit comments

Comments
 (0)