diff --git a/src/Symfony/Component/RateLimiter/Policy/SlidingWindowLimiter.php b/src/Symfony/Component/RateLimiter/Policy/SlidingWindowLimiter.php index bf62b89ffc7f9..468b0d05ba585 100644 --- a/src/Symfony/Component/RateLimiter/Policy/SlidingWindowLimiter.php +++ b/src/Symfony/Component/RateLimiter/Policy/SlidingWindowLimiter.php @@ -65,12 +65,18 @@ public function reserve(int $tokens = 1, float $maxTime = null): Reservation $now = microtime(true); $hitCount = $window->getHitCount(); $availableTokens = $this->getAvailableTokens($hitCount); + if (0 === $tokens) { + $resetDuration = $window->calculateTimeForTokens($this->limit, $window->getHitCount()); + $resetTime = \DateTimeImmutable::createFromFormat('U', $availableTokens ? floor($now) : floor($now + $resetDuration)); + + return new Reservation($now, new RateLimit($availableTokens, $resetTime, true, $this->limit)); + } if ($availableTokens >= $tokens) { $window->add($tokens); $reservation = new Reservation($now, new RateLimit($this->getAvailableTokens($window->getHitCount()), \DateTimeImmutable::createFromFormat('U', floor($now)), true, $this->limit)); } else { - $waitDuration = $window->calculateTimeForTokens($this->limit, max(1, $tokens)); + $waitDuration = $window->calculateTimeForTokens($this->limit, $tokens); if (null !== $maxTime && $waitDuration > $maxTime) { // process needs to wait longer than set interval diff --git a/src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowLimiterTest.php b/src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowLimiterTest.php index 21deb69c3932b..835c6cc767da6 100644 --- a/src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowLimiterTest.php +++ b/src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowLimiterTest.php @@ -52,6 +52,7 @@ public function testConsume() $rateLimit = $limiter->consume(10); $this->assertTrue($rateLimit->isAccepted()); $this->assertSame(10, $rateLimit->getLimit()); + $this->assertSame(0, $rateLimit->getRemainingTokens()); } public function testWaitIntervalOnConsumeOverLimit() @@ -76,6 +77,37 @@ public function testReserve() // 2 over the limit, causing the WaitDuration to become 2/10th of the 12s interval $this->assertEqualsWithDelta(12 / 5, $limiter->reserve(4)->getWaitDuration(), 1); + + $limiter->reset(); + $this->assertEquals(0, $limiter->reserve(10)->getWaitDuration()); + } + + public function testPeekConsume() + { + $limiter = $this->createLimiter(); + + $limiter->consume(9); + + // peek by consuming 0 tokens twice (making sure peeking doesn't claim a token) + for ($i = 0; $i < 2; ++$i) { + $rateLimit = $limiter->consume(0); + $this->assertTrue($rateLimit->isAccepted()); + $this->assertSame(10, $rateLimit->getLimit()); + $this->assertEquals( + \DateTimeImmutable::createFromFormat('U', (string) floor(microtime(true))), + $rateLimit->getRetryAfter() + ); + } + + $limiter->consume(); + + $rateLimit = $limiter->consume(0); + $this->assertEquals(0, $rateLimit->getRemainingTokens()); + $this->assertTrue($rateLimit->isAccepted()); + $this->assertEquals( + \DateTimeImmutable::createFromFormat('U', (string) floor(microtime(true) + 12)), + $rateLimit->getRetryAfter() + ); } private function createLimiter(): SlidingWindowLimiter