-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
Description
Symfony version(s) affected: tested on 5.2, but seems that this is not fixed in any newer verison (5.3, 5.4, 6.0).
Description
TokenBucket policy is not adding tokens if the interval between requests is smaller than the interval provided in config.
How to reproduce
Basic configuration is sufficient. Storage type doesn't matter. It does not matter if locks are configured or not.
Limit: 1200, Interval: 1 minute, Amount: 1200
If the interval between requests is less than 1 minute then the bucket is never getting replenished. You will get throttled eventually.
Limit: 1200, Interval: 1 seconds, Amount: 20
If the interval between requests is less than 1 second then the bucket won't get replenished.
If you make the requests a bit slower then you will see that the bucket is getting new tokens.
Possible Solution
No suggestions so far but I've tracked the cause of this.
My theory is that after every successful consume request the policy writes it's execution timestamp to storage:
$now = microtime(true); | |
$availableTokens = $bucket->getAvailableTokens($now); | |
if ($availableTokens >= $tokens) { | |
// tokens are now available, update bucket | |
$bucket->setTokens($availableTokens - $tokens); | |
$bucket->setTimer($now); | |
$reservation = new Reservation($now, new RateLimit($bucket->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now)), true, $this->maxBurst)); | |
} else { |
The next request uses the saved timestamp and current timestamp to add new tokens to the bucket:
symfony/src/Symfony/Component/RateLimiter/Policy/TokenBucket.php
Lines 68 to 73 in 0cb1b5d
public function getAvailableTokens(float $now): int | |
{ | |
$elapsed = $now - $this->timer; | |
return min($this->burstSize, $this->tokens + $this->rate->calculateNewTokensDuringInterval($elapsed)); | |
} |
symfony/src/Symfony/Component/RateLimiter/Policy/Rate.php
Lines 82 to 92 in 0cb1b5d
/** | |
* Calculates the number of new free tokens during $duration. | |
* | |
* @param float $duration interval in seconds | |
*/ | |
public function calculateNewTokensDuringInterval(float $duration): int | |
{ | |
$cycles = floor($duration / TimeUtil::dateIntervalToSeconds($this->refillTime)); | |
return $cycles * $this->refillAmount; | |
} |
The
$cycles
variable will be 0 if $duration
is less than $this->refillTime
. If a user sets $this->refillTime
in hours or days then the algorithm is not usable at all. If $this->refillTime
is a few seconds then it can be somewhat usable in some conditions.
Additional context
In the end we've moved to sliding window policy (the burst limit was the same as fill rate anyway) and it looks like this.
User that is limited to 120 requests per minute (exceeds usage, gets throttled frequently):
User that is limited to 1200 requests per minute (shouldn't be throttled at all):