Skip to content

Commit 82b811d

Browse files
bug #51666 [RateLimiter] CompoundLimiter was accepting requests even when some limiters already consumed all tokens (10n)
This PR was merged into the 6.3 branch. Discussion ---------- [RateLimiter] CompoundLimiter was accepting requests even when some limiters already consumed all tokens | Q | A | ------------- | --- | Branch? | 6.3 | Bug fix? | yes | New feature? | no | Deprecations? | no | Tickets | - | License | MIT | Doc PR | - CompoundLimiter is accepting requests when the limit was reached previously. When processing the limiters and the first one consumes exactly all the remaining tokens (remaining=0, accepted=true) and the next one already reached the limit previously (remaining=0, accepted=0) the $minimalRateLimit is considered the first one that will accept the request (even if it's not the most restrictive). For example: CompoundLimiter includes 2 limiters: - limiter 1 - remaining 2 tokens - limiter 2 - remaining 0 tokens After consuming 2 tokens each each limiter generates to limits: - `limiter1`->consume(2), generates a limit indicating `0` remaining tokens, **accepts** the request (it was last permitted) - `limiter2`->consume(2), generates a limit indicating `0` remaining tokens, **did not accept** the request (it did not have 2 tokens to satisfy the request) Because both of them have at this moment `0` remaining tokens, the minimum limit that is returned will be the limit from the `limiter1` . This means that the CompundLimiter will accept the request, even if the `limiter2` should be more restrictive. If we switch the order in the constructor, the request will be denied. The order should not matter. Commits ------- 65ce7f8 [RateLimiter] CompoundLimiter was accepting requests even when some limiters already consumed all tokens
2 parents 7610bc2 + 65ce7f8 commit 82b811d

File tree

2 files changed

+43
-9
lines changed

2 files changed

+43
-9
lines changed

src/Symfony/Component/RateLimiter/CompoundLimiter.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ public function consume(int $tokens = 1): RateLimit
4242
foreach ($this->limiters as $limiter) {
4343
$rateLimit = $limiter->consume($tokens);
4444

45-
if (null === $minimalRateLimit || $rateLimit->getRemainingTokens() < $minimalRateLimit->getRemainingTokens()) {
45+
if (
46+
null === $minimalRateLimit
47+
|| $rateLimit->getRemainingTokens() < $minimalRateLimit->getRemainingTokens()
48+
|| ($minimalRateLimit->isAccepted() && !$rateLimit->isAccepted())
49+
) {
4650
$minimalRateLimit = $rateLimit;
4751
}
4852
}

src/Symfony/Component/RateLimiter/Tests/CompoundLimiterTest.php

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,49 @@ public function testConsume()
3636
{
3737
$limiter1 = $this->createLimiter(4, new \DateInterval('PT1S'));
3838
$limiter2 = $this->createLimiter(8, new \DateInterval('PT10S'));
39-
$limiter3 = $this->createLimiter(12, new \DateInterval('PT30S'));
39+
$limiter3 = $this->createLimiter(16, new \DateInterval('PT30S'));
4040
$limiter = new CompoundLimiter([$limiter1, $limiter2, $limiter3]);
4141

42-
$this->assertEquals(0, $limiter->consume(4)->getRemainingTokens(), 'Limiter 1 reached the limit');
42+
$rateLimit = $limiter->consume(4);
43+
$this->assertEquals(0, $rateLimit->getRemainingTokens(), 'Limiter 1 reached the limit');
44+
$this->assertTrue($rateLimit->isAccepted(), 'All limiters accept (exact limit on limiter 1)');
45+
46+
$rateLimit = $limiter->consume(1);
47+
$this->assertEquals(0, $rateLimit->getRemainingTokens(), 'Limiter 1 reached the limit');
48+
$this->assertFalse($rateLimit->isAccepted(), 'Limiter 1 did not accept limit');
49+
4350
sleep(1); // reset limiter1's window
44-
$this->assertTrue($limiter->consume(3)->isAccepted());
4551

46-
$this->assertEquals(0, $limiter->consume()->getRemainingTokens(), 'Limiter 2 has no remaining tokens left');
47-
sleep(10); // reset limiter2's window
48-
$this->assertTrue($limiter->consume(3)->isAccepted());
52+
$rateLimit = $limiter->consume(3);
53+
$this->assertEquals(0, $rateLimit->getRemainingTokens(), 'Limiter 2 consumed exactly the remaining tokens');
54+
$this->assertTrue($rateLimit->isAccepted(), 'All accept the request (exact limit on limiter 2)');
55+
56+
$rateLimit = $limiter->consume(1);
57+
$this->assertEquals(0, $rateLimit->getRemainingTokens(), 'Limiter 2 had remaining tokens left');
58+
$this->assertFalse($rateLimit->isAccepted(), 'Limiter 2 did not accept the request');
59+
60+
sleep(1); // reset limiter1's window again, to make sure that the limiter2 overrides limiter1
61+
62+
// make sure to consume all allowed by limiter1, limiter2 already had 0 remaining
63+
$rateLimit = $limiter->consume(4);
64+
$this->assertEquals(
65+
0,
66+
$rateLimit->getRemainingTokens(),
67+
'Limiter 1 consumed the remaining tokens (accept), Limiter 2 did not have any remaining (not accept)'
68+
);
69+
$this->assertFalse($rateLimit->isAccepted(), 'Limiter 2 reached the limit already');
70+
71+
sleep(10); // reset limiter2's window (also limiter1)
72+
73+
$rateLimit = $limiter->consume(3);
74+
$this->assertEquals(0, $rateLimit->getRemainingTokens(), 'Limiter 3 had exactly 3 tokens (accept)');
75+
$this->assertTrue($rateLimit->isAccepted());
76+
77+
$rateLimit = $limiter->consume(1);
78+
$this->assertFalse($rateLimit->isAccepted(), 'Limiter 3 reached the limit previously');
79+
80+
sleep(30); // reset limiter3's window (also limiter1 and limiter2)
4981

50-
$this->assertEquals(0, $limiter->consume()->getRemainingTokens(), 'Limiter 3 reached the limit');
51-
sleep(20); // reset limiter3's window
5282
$this->assertTrue($limiter->consume()->isAccepted());
5383
}
5484

0 commit comments

Comments
 (0)
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