From e3906cfde91ec418bd77488696c61b354dc82d3f Mon Sep 17 00:00:00 2001 From: Jeroeny Date: Mon, 12 Sep 2022 16:26:27 +0200 Subject: [PATCH] calculateTimeForTokens for SlidingWindow --- .../RateLimiter/Policy/SlidingWindow.php | 21 ++++++++++ .../Policy/SlidingWindowLimiter.php | 42 ++++++++++++++----- .../Tests/Policy/SlidingWindowLimiterTest.php | 10 +++-- 3 files changed, 58 insertions(+), 15 deletions(-) diff --git a/src/Symfony/Component/RateLimiter/Policy/SlidingWindow.php b/src/Symfony/Component/RateLimiter/Policy/SlidingWindow.php index c3ea2b5b077d1..37115d173869a 100644 --- a/src/Symfony/Component/RateLimiter/Policy/SlidingWindow.php +++ b/src/Symfony/Component/RateLimiter/Policy/SlidingWindow.php @@ -89,6 +89,27 @@ public function getRetryAfter(): \DateTimeImmutable return \DateTimeImmutable::createFromFormat('U.u', sprintf('%.6F', $this->windowEndAt)); } + public function calculateTimeForTokens(int $maxSize, int $tokens): int + { + $remaining = $maxSize - $this->getHitCount(); + if ($remaining >= $tokens) { + return 0; + } + + $startOfWindow = $this->windowEndAt - $this->intervalInSeconds; + $percentOfCurrentTimeFrame = min((microtime(true) - $startOfWindow) / $this->intervalInSeconds, 1); + $releasable = $maxSize - floor($this->hitCountForLastWindow * (1 - $percentOfCurrentTimeFrame)); + $remainingWindow = (microtime(true) - $startOfWindow) - $this->intervalInSeconds; + $timePerToken = $remainingWindow / $releasable; + $needed = $tokens - $remaining; + + if ($releasable <= $needed) { + return (int) ceil($needed * $timePerToken); + } + + return (int) (($this->windowEndAt - microtime(true)) + ceil(($needed - $releasable) * ($this->intervalInSeconds / $maxSize))); + } + public function __serialize(): array { return [ diff --git a/src/Symfony/Component/RateLimiter/Policy/SlidingWindowLimiter.php b/src/Symfony/Component/RateLimiter/Policy/SlidingWindowLimiter.php index 07b08b2a3ae22..bf62b89ffc7f9 100644 --- a/src/Symfony/Component/RateLimiter/Policy/SlidingWindowLimiter.php +++ b/src/Symfony/Component/RateLimiter/Policy/SlidingWindowLimiter.php @@ -12,7 +12,7 @@ namespace Symfony\Component\RateLimiter\Policy; use Symfony\Component\Lock\LockInterface; -use Symfony\Component\RateLimiter\Exception\ReserveNotSupportedException; +use Symfony\Component\RateLimiter\Exception\MaxWaitDurationExceededException; use Symfony\Component\RateLimiter\LimiterInterface; use Symfony\Component\RateLimiter\RateLimit; use Symfony\Component\RateLimiter\Reservation; @@ -48,11 +48,10 @@ public function __construct(string $id, int $limit, \DateInterval $interval, Sto public function reserve(int $tokens = 1, float $maxTime = null): Reservation { - throw new ReserveNotSupportedException(__CLASS__); - } + if ($tokens > $this->limit) { + throw new \InvalidArgumentException(sprintf('Cannot reserve more tokens (%d) than the size of the rate limiter (%d).', $tokens, $this->limit)); + } - public function consume(int $tokens = 1): RateLimit - { $this->lock?->acquire(true); try { @@ -63,22 +62,43 @@ public function consume(int $tokens = 1): RateLimit $window = SlidingWindow::createFromPreviousWindow($window, $this->interval); } + $now = microtime(true); $hitCount = $window->getHitCount(); $availableTokens = $this->getAvailableTokens($hitCount); - if ($availableTokens < $tokens) { - return new RateLimit($availableTokens, $window->getRetryAfter(), false, $this->limit); - } + if ($availableTokens >= $tokens) { + $window->add($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)); + + if (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); + + $reservation = new Reservation($now + $waitDuration, new RateLimit($this->getAvailableTokens($window->getHitCount()), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->limit)); + } if (0 < $tokens) { $this->storage->save($window); } - - return new RateLimit($this->getAvailableTokens($window->getHitCount()), $window->getRetryAfter(), true, $this->limit); } finally { $this->lock?->release(); } + + return $reservation; + } + + public function consume(int $tokens = 1): RateLimit + { + try { + return $this->reserve($tokens, 0)->getRateLimit(); + } catch (MaxWaitDurationExceededException $e) { + return $e->getRateLimit(); + } } private function getAvailableTokens(int $hitCount): int diff --git a/src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowLimiterTest.php b/src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowLimiterTest.php index 59a4f399ee1c4..b5e788c0580ff 100644 --- a/src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowLimiterTest.php +++ b/src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowLimiterTest.php @@ -13,7 +13,6 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\ClockMock; -use Symfony\Component\RateLimiter\Exception\ReserveNotSupportedException; use Symfony\Component\RateLimiter\Policy\SlidingWindowLimiter; use Symfony\Component\RateLimiter\RateLimit; use Symfony\Component\RateLimiter\Storage\InMemoryStorage; @@ -66,14 +65,17 @@ public function testWaitIntervalOnConsumeOverLimit() $start = microtime(true); $rateLimit->wait(); // wait 12 seconds - $this->assertEqualsWithDelta($start + 12, microtime(true), 1); + $this->assertEqualsWithDelta($start + (12 / 5), microtime(true), 1); + $this->assertTrue($limiter->consume()->isAccepted()); } public function testReserve() { - $this->expectException(ReserveNotSupportedException::class); + $limiter = $this->createLimiter(); + $limiter->consume(8); - $this->createLimiter()->reserve(); + // 2 over the limit, causing the WaitDuration to become 2/10th of the 12s interval + $this->assertEqualsWithDelta(12 / 5, $limiter->reserve(4)->getWaitDuration(), 1); } public function testPeekConsume() pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy