diff --git a/src/Symfony/Component/RateLimiter/Policy/SlidingWindowLimiter.php b/src/Symfony/Component/RateLimiter/Policy/SlidingWindowLimiter.php index bf62b89ffc7f9..75715cce62eaf 100644 --- a/src/Symfony/Component/RateLimiter/Policy/SlidingWindowLimiter.php +++ b/src/Symfony/Component/RateLimiter/Policy/SlidingWindowLimiter.php @@ -65,21 +65,35 @@ public function reserve(int $tokens = 1, float $maxTime = null): Reservation $now = microtime(true); $hitCount = $window->getHitCount(); $availableTokens = $this->getAvailableTokens($hitCount); - if ($availableTokens >= $tokens) { + if (0 !== $tokens && $availableTokens > $tokens) { $window->add($tokens); $reservation = new Reservation($now, new RateLimit($this->getAvailableTokens($window->getHitCount()), \DateTimeImmutable::createFromFormat('U', floor($now)), true, $this->limit)); } else { + if ($availableTokens === $tokens) { + $window->add($tokens); + } + $waitDuration = $window->calculateTimeForTokens($this->limit, max(1, $tokens)); - if (null !== $maxTime && $waitDuration > $maxTime) { + if ($availableTokens !== $tokens && 0 !== $tokens && null !== $maxTime && $waitDuration > $maxTime) { // process needs to wait longer than set interval 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)); } - $window->add($tokens); + if ($availableTokens !== $tokens) { + $window->add($tokens); + } + + if ($availableTokens === $tokens || 0 === $tokens) { + $accepted = true; + $timeToAct = $now; + } else { + $accepted = false; + $timeToAct = $now + $waitDuration; + } - $reservation = new Reservation($now + $waitDuration, new RateLimit($this->getAvailableTokens($window->getHitCount()), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->limit)); + $reservation = new Reservation($timeToAct, new RateLimit($this->getAvailableTokens($window->getHitCount()), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), $accepted, $this->limit)); } if (0 < $tokens) { diff --git a/src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowLimiterTest.php b/src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowLimiterTest.php index 21deb69c3932b..f6e15a10255b0 100644 --- a/src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowLimiterTest.php +++ b/src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowLimiterTest.php @@ -54,6 +54,44 @@ public function testConsume() $this->assertSame(10, $rateLimit->getLimit()); } + public function testConsumeLastToken() + { + $limiter = $this->createLimiter(); + $limiter->reset(); + $limiter->consume(9); + + $rateLimit = $limiter->consume(1); + $this->assertSame(0, $rateLimit->getRemainingTokens()); + $this->assertTrue($rateLimit->isAccepted()); + $this->assertEquals( + \DateTimeImmutable::createFromFormat('U', (string) floor(microtime(true) + 12 / 10)), + $rateLimit->getRetryAfter() + ); + } + + public function testConsumeZeroTokens() + { + $limiter = $this->createLimiter(); + $limiter->reset(); + + $rateLimit = $limiter->consume(0); + $this->assertTrue($rateLimit->isAccepted()); + $this->assertEquals( + \DateTimeImmutable::createFromFormat('U', (string) floor(microtime(true))), + $rateLimit->getRetryAfter() + ); + + $limiter->reset(); + $limiter->consume(10); + + $rateLimit = $limiter->consume(0); + $this->assertTrue($rateLimit->isAccepted()); + $this->assertEquals( + \DateTimeImmutable::createFromFormat('U', (string) floor(microtime(true) + 12 / 10)), + $rateLimit->getRetryAfter() + ); + } + public function testWaitIntervalOnConsumeOverLimit() { $limiter = $this->createLimiter(); @@ -76,6 +114,9 @@ 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()); } private function createLimiter(): SlidingWindowLimiter