From 169e383b351ec1d5f66765da96190ce0f00b4f44 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Fri, 29 Dec 2023 22:26:47 +0100 Subject: [PATCH 1/2] Reintroduce peek consume test for sliding window policy --- .../Tests/Policy/SlidingWindowLimiterTest.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowLimiterTest.php b/src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowLimiterTest.php index 21deb69c3932..ffc5a1a83245 100644 --- a/src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowLimiterTest.php +++ b/src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowLimiterTest.php @@ -78,6 +78,34 @@ public function testReserve() $this->assertEqualsWithDelta(12 / 5, $limiter->reserve(4)->getWaitDuration(), 1); } + 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.u', sprintf('%.6F', microtime(true))), + $rateLimit->getRetryAfter() + ); + } + + $limiter->consume(); + + $rateLimit = $limiter->consume(0); + $this->assertEquals(0, $rateLimit->getRemainingTokens()); + $this->assertTrue($rateLimit->isAccepted()); + $this->assertEquals( + \DateTimeImmutable::createFromFormat('U.u', sprintf('%.6F', microtime(true) + 12)), + $rateLimit->getRetryAfter() + ); + } + private function createLimiter(): SlidingWindowLimiter { return new SlidingWindowLimiter('test', 10, new \DateInterval('PT12S'), $this->storage); From 677b8b7fefdb7ef327992ff628d46f79173b538c Mon Sep 17 00:00:00 2001 From: Evgeny Ruban Date: Thu, 30 Nov 2023 21:53:12 +0400 Subject: [PATCH 2/2] [RateLimit] Allow to get RateLimit without consuming again --- .../Component/RateLimiter/Policy/SlidingWindowLimiter.php | 8 +++++++- .../RateLimiter/Tests/Policy/SlidingWindowLimiterTest.php | 8 ++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/RateLimiter/Policy/SlidingWindowLimiter.php b/src/Symfony/Component/RateLimiter/Policy/SlidingWindowLimiter.php index bf62b89ffc7f..468b0d05ba58 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 ffc5a1a83245..835c6cc767da 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,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()); } public function testPeekConsume() @@ -90,7 +94,7 @@ public function testPeekConsume() $this->assertTrue($rateLimit->isAccepted()); $this->assertSame(10, $rateLimit->getLimit()); $this->assertEquals( - \DateTimeImmutable::createFromFormat('U.u', sprintf('%.6F', microtime(true))), + \DateTimeImmutable::createFromFormat('U', (string) floor(microtime(true))), $rateLimit->getRetryAfter() ); } @@ -101,7 +105,7 @@ public function testPeekConsume() $this->assertEquals(0, $rateLimit->getRemainingTokens()); $this->assertTrue($rateLimit->isAccepted()); $this->assertEquals( - \DateTimeImmutable::createFromFormat('U.u', sprintf('%.6F', microtime(true) + 12)), + \DateTimeImmutable::createFromFormat('U', (string) floor(microtime(true) + 12)), $rateLimit->getRetryAfter() ); }