Skip to content

Commit 7c8b6ed

Browse files
relo-sanfabpot
authored andcommitted
[RateLimiter] TokenBucket policy fix for adding tokens with a predefined frequency
1 parent 6b8b4ab commit 7c8b6ed

File tree

4 files changed

+44
-3
lines changed

4 files changed

+44
-3
lines changed

src/Symfony/Component/RateLimiter/Policy/Rate.php

+12
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,18 @@ public function calculateNewTokensDuringInterval(float $duration): int
9595
return $cycles * $this->refillAmount;
9696
}
9797

98+
/**
99+
* Calculates total amount in seconds of refill intervals during $duration (for maintain strict refill frequency).
100+
*
101+
* @param float $duration interval in seconds
102+
*/
103+
public function calculateRefillInterval(float $duration): int
104+
{
105+
$cycleTime = TimeUtil::dateIntervalToSeconds($this->refillTime);
106+
107+
return floor($duration / $cycleTime) * $cycleTime;
108+
}
109+
98110
public function __toString(): string
99111
{
100112
return $this->refillTime->format('P%dDT%HH%iM%sS').'-'.$this->refillAmount;

src/Symfony/Component/RateLimiter/Policy/TokenBucket.php

+6-1
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,13 @@ public function setTokens(int $tokens): void
8383
public function getAvailableTokens(float $now): int
8484
{
8585
$elapsed = max(0, $now - $this->timer);
86+
$newTokens = $this->rate->calculateNewTokensDuringInterval($elapsed);
8687

87-
return min($this->burstSize, $this->tokens + $this->rate->calculateNewTokensDuringInterval($elapsed));
88+
if ($newTokens > 0) {
89+
$this->timer += $this->rate->calculateRefillInterval($elapsed);
90+
}
91+
92+
return min($this->burstSize, $this->tokens + $newTokens);
8893
}
8994

9095
public function getExpirationTime(): int

src/Symfony/Component/RateLimiter/Policy/TokenBucketLimiter.php

-2
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ public function reserve(int $tokens = 1, float $maxTime = null): Reservation
7272
if ($availableTokens >= $tokens) {
7373
// tokens are now available, update bucket
7474
$bucket->setTokens($availableTokens - $tokens);
75-
$bucket->setTimer($now);
7675

7776
$reservation = new Reservation($now, new RateLimit($bucket->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now)), true, $this->maxBurst));
7877
} else {
@@ -89,7 +88,6 @@ public function reserve(int $tokens = 1, float $maxTime = null): Reservation
8988
// at $now + $waitDuration all tokens will be reserved for this process,
9089
// so no tokens are left for other processes.
9190
$bucket->setTokens($availableTokens - $tokens);
92-
$bucket->setTimer($now);
9391

9492
$reservation = new Reservation($now + $waitDuration, new RateLimit(0, \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->maxBurst));
9593
}

src/Symfony/Component/RateLimiter/Tests/Policy/TokenBucketLimiterTest.php

+26
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,32 @@ public function testBucketResilientToTimeShifting()
128128
$this->assertSame(100, $bucket->getAvailableTokens($serverOneClock));
129129
}
130130

131+
public function testBucketRefilledWithStrictFrequency()
132+
{
133+
$limiter = $this->createLimiter(1000, new Rate(\DateInterval::createFromDateString('15 seconds'), 100));
134+
$rateLimit = $limiter->consume(300);
135+
136+
$this->assertTrue($rateLimit->isAccepted());
137+
$this->assertEquals(700, $rateLimit->getRemainingTokens());
138+
139+
$expected = 699;
140+
141+
for ($i = 1; $i <= 20; ++$i) {
142+
$rateLimit = $limiter->consume();
143+
$this->assertTrue($rateLimit->isAccepted());
144+
$this->assertEquals($expected, $rateLimit->getRemainingTokens());
145+
146+
sleep(4);
147+
--$expected;
148+
149+
if (\in_array($i, [4, 8, 12], true)) {
150+
$expected += 100;
151+
} elseif (\in_array($i, [15, 19], true)) {
152+
$expected = 999;
153+
}
154+
}
155+
}
156+
131157
private function createLimiter($initialTokens = 10, Rate $rate = null)
132158
{
133159
return new TokenBucketLimiter('test', $initialTokens, $rate ?? Rate::perSecond(10), $this->storage);

0 commit comments

Comments
 (0)