From 231693282f8143b8cc889df261404e12a1cd9cb2 Mon Sep 17 00:00:00 2001 From: Robert Meijers Date: Mon, 23 Jan 2023 08:32:57 +0100 Subject: [PATCH] [RateLimiter] fix incorrect retryAfter of FixedWindow A FixedWindow always ends `intervalInSeconds` after the start time (`timer`). So when calculating the time to consume some tokens the tokens will always be available at `timer + intervalInSeconds`. But currently this is reported incorrectly as `calculateTimeForTokens()` calculates the time based on the desired amount of tokens (and cycles) while it doesn't take into account `maxSize` amount of tokens become available at the windows end. Furthermore calculating the amount of needed cycles is incorrect. This as all tokens become available at once (at the windows end) and you can't consume more tokens than `maxSize` (which is validated at the start of `FixedWindowLimiter::reserve`, in case of `tokens > limit` it throws). --- .../Component/RateLimiter/Policy/FixedWindowLimiter.php | 2 +- src/Symfony/Component/RateLimiter/Policy/Window.php | 6 ++---- .../RateLimiter/Tests/Policy/FixedWindowLimiterTest.php | 4 ++++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Component/RateLimiter/Policy/FixedWindowLimiter.php b/src/Symfony/Component/RateLimiter/Policy/FixedWindowLimiter.php index 3f982e602ce67..298cd1ba24321 100644 --- a/src/Symfony/Component/RateLimiter/Policy/FixedWindowLimiter.php +++ b/src/Symfony/Component/RateLimiter/Policy/FixedWindowLimiter.php @@ -68,7 +68,7 @@ public function reserve(int $tokens = 1, float $maxTime = null): Reservation $reservation = new Reservation($now, new RateLimit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now)), true, $this->limit)); } else { - $waitDuration = $window->calculateTimeForTokens($tokens); + $waitDuration = $window->calculateTimeForTokens($tokens, $now); if (null !== $maxTime && $waitDuration > $maxTime) { // process needs to wait longer than set interval diff --git a/src/Symfony/Component/RateLimiter/Policy/Window.php b/src/Symfony/Component/RateLimiter/Policy/Window.php index 93452797075a0..39248a53d7f7c 100644 --- a/src/Symfony/Component/RateLimiter/Policy/Window.php +++ b/src/Symfony/Component/RateLimiter/Policy/Window.php @@ -75,15 +75,13 @@ public function getAvailableTokens(float $now) return $this->maxSize - $this->hitCount; } - public function calculateTimeForTokens(int $tokens): int + public function calculateTimeForTokens(int $tokens, float $now): int { if (($this->maxSize - $this->hitCount) >= $tokens) { return 0; } - $cyclesRequired = ceil($tokens / $this->maxSize); - - return $cyclesRequired * $this->intervalInSeconds; + return (int) ceil($this->timer + $this->intervalInSeconds - $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 0cffc14e1aee6..84df7bc850aae 100644 --- a/src/Symfony/Component/RateLimiter/Tests/Policy/FixedWindowLimiterTest.php +++ b/src/Symfony/Component/RateLimiter/Tests/Policy/FixedWindowLimiterTest.php @@ -37,6 +37,7 @@ protected function setUp(): void public function testConsume() { + $now = time(); $limiter = $this->createLimiter(); // fill 9 tokens in 45 seconds @@ -51,6 +52,9 @@ public function testConsume() $rateLimit = $limiter->consume(); $this->assertFalse($rateLimit->isAccepted()); $this->assertSame(10, $rateLimit->getLimit()); + // Window ends after 1 minute + $retryAfter = \DateTimeImmutable::createFromFormat('U', $now + 60); + $this->assertEquals($retryAfter, $rateLimit->getRetryAfter()); } /** 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