diff --git a/src/Symfony/Component/RateLimiter/Policy/Window.php b/src/Symfony/Component/RateLimiter/Policy/Window.php index 42d00de913c0e..49eddf3e7d46e 100644 --- a/src/Symfony/Component/RateLimiter/Policy/Window.php +++ b/src/Symfony/Component/RateLimiter/Policy/Window.php @@ -77,7 +77,9 @@ public function calculateTimeForTokens(int $tokens, float $now): int return 0; } - return (int) ceil($this->timer + $this->intervalInSeconds - $now); + $inWindow = ceil($this->hitCount / $this->maxSize); + + return (int) ceil($this->timer + ($this->intervalInSeconds * $inWindow) - $now); } public function __serialize(): array diff --git a/src/Symfony/Component/RateLimiter/Tests/Policy/FixedWindowLimiterTest.php b/src/Symfony/Component/RateLimiter/Tests/Policy/FixedWindowLimiterTest.php index 3e422fbec55b0..fa787c8a6fc32 100644 --- a/src/Symfony/Component/RateLimiter/Tests/Policy/FixedWindowLimiterTest.php +++ b/src/Symfony/Component/RateLimiter/Tests/Policy/FixedWindowLimiterTest.php @@ -76,6 +76,23 @@ public function testConsumeOutsideInterval(string $dateIntervalString) $this->assertTrue($rateLimit->isAccepted()); } + public function testReserveOutsideWindow() + { + $limiter = $this->createLimiter(); + + // initial reserve + $limiter->reserve(10); + + // Reserve the first window and the second window + $firstReservation = $limiter->reserve(10); + $secondReservation = $limiter->reserve(10); + + $this->assertFalse($firstReservation->getRateLimit()->isAccepted()); + $this->assertFalse($secondReservation->getRateLimit()->isAccepted()); + $this->assertEquals(60, ceil($firstReservation->getWaitDuration())); + $this->assertEquals(120, ceil($secondReservation->getWaitDuration())); + } + public function testWaitIntervalOnConsumeOverLimit() { $limiter = $this->createLimiter();