Skip to content

Commit ca7ff0a

Browse files
committed
calculateTimeForTokens for SlidingWindow
1 parent 521d210 commit ca7ff0a

File tree

3 files changed

+58
-15
lines changed

3 files changed

+58
-15
lines changed

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,27 @@ public function getRetryAfter(): \DateTimeImmutable
8989
return \DateTimeImmutable::createFromFormat('U.u', sprintf('%.6F', $this->windowEndAt));
9090
}
9191

92+
public function calculateTimeForTokens(int $maxSize, int $tokens): int
93+
{
94+
$remaining = $maxSize - $this->getHitCount();
95+
if ($remaining >= $tokens) {
96+
return 0;
97+
}
98+
99+
$startOfWindow = $this->windowEndAt - $this->intervalInSeconds;
100+
$percentOfCurrentTimeFrame = min((microtime(true) - $startOfWindow) / $this->intervalInSeconds, 1);
101+
$releasable = $maxSize - floor($this->hitCountForLastWindow * (1 - $percentOfCurrentTimeFrame));
102+
$remainingWindow = (microtime(true) - $startOfWindow) - $this->intervalInSeconds;
103+
$timePerToken = $remainingWindow / $releasable;
104+
$needed = $tokens - $remaining;
105+
106+
if ($releasable <= $needed) {
107+
return (int) ceil($needed * $timePerToken);
108+
}
109+
110+
return (int) ($this->windowEndAt - microtime(true)) + ceil(($needed - $releasable) * ($this->intervalInSeconds / $maxSize));
111+
}
112+
92113
public function __serialize(): array
93114
{
94115
return [

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

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
namespace Symfony\Component\RateLimiter\Policy;
1313

1414
use Symfony\Component\Lock\LockInterface;
15-
use Symfony\Component\RateLimiter\Exception\ReserveNotSupportedException;
1615
use Symfony\Component\RateLimiter\LimiterInterface;
16+
use Symfony\Component\RateLimiter\Exception\MaxWaitDurationExceededException;
1717
use Symfony\Component\RateLimiter\RateLimit;
1818
use Symfony\Component\RateLimiter\Reservation;
1919
use Symfony\Component\RateLimiter\Storage\StorageInterface;
@@ -48,11 +48,10 @@ public function __construct(string $id, int $limit, \DateInterval $interval, Sto
4848

4949
public function reserve(int $tokens = 1, float $maxTime = null): Reservation
5050
{
51-
throw new ReserveNotSupportedException(__CLASS__);
52-
}
51+
if ($tokens > $this->limit) {
52+
throw new \InvalidArgumentException(sprintf('Cannot reserve more tokens (%d) than the size of the rate limiter (%d).', $tokens, $this->limit));
53+
}
5354

54-
public function consume(int $tokens = 1): RateLimit
55-
{
5655
$this->lock?->acquire(true);
5756

5857
try {
@@ -63,22 +62,43 @@ public function consume(int $tokens = 1): RateLimit
6362
$window = SlidingWindow::createFromPreviousWindow($window, $this->interval);
6463
}
6564

65+
$now = microtime(true);
6666
$hitCount = $window->getHitCount();
6767
$availableTokens = $this->getAvailableTokens($hitCount);
68-
if ($availableTokens < $tokens) {
69-
return new RateLimit($availableTokens, $window->getRetryAfter(), false, $this->limit);
70-
}
68+
if ($availableTokens >= $tokens) {
69+
$window->add($tokens);
7170

72-
$window->add($tokens);
71+
$reservation = new Reservation($now, new RateLimit($this->getAvailableTokens($window->getHitCount()), \DateTimeImmutable::createFromFormat('U', floor($now)), true, $this->limit));
72+
} else {
73+
$waitDuration = $window->calculateTimeForTokens($this->limit, max(1, $tokens));
74+
75+
if (null !== $maxTime && $waitDuration > $maxTime) {
76+
// process needs to wait longer than set interval
77+
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));
78+
}
79+
80+
$window->add($tokens);
81+
82+
$reservation = new Reservation($now + $waitDuration, new RateLimit($this->getAvailableTokens($window->getHitCount()), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->limit));
83+
}
7384

7485
if (0 < $tokens) {
7586
$this->storage->save($window);
7687
}
77-
78-
return new RateLimit($this->getAvailableTokens($window->getHitCount()), $window->getRetryAfter(), true, $this->limit);
7988
} finally {
8089
$this->lock?->release();
8190
}
91+
92+
return $reservation;
93+
}
94+
95+
public function consume(int $tokens = 1): RateLimit
96+
{
97+
try {
98+
return $this->reserve($tokens, 0)->getRateLimit();
99+
} catch (MaxWaitDurationExceededException $e) {
100+
return $e->getRateLimit();
101+
}
82102
}
83103

84104
private function getAvailableTokens(int $hitCount): int

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Bridge\PhpUnit\ClockMock;
16-
use Symfony\Component\RateLimiter\Exception\ReserveNotSupportedException;
1716
use Symfony\Component\RateLimiter\Policy\SlidingWindowLimiter;
1817
use Symfony\Component\RateLimiter\RateLimit;
1918
use Symfony\Component\RateLimiter\Storage\InMemoryStorage;
@@ -66,14 +65,17 @@ public function testWaitIntervalOnConsumeOverLimit()
6665

6766
$start = microtime(true);
6867
$rateLimit->wait(); // wait 12 seconds
69-
$this->assertEqualsWithDelta($start + 12, microtime(true), 1);
68+
$this->assertEqualsWithDelta($start + (12 / 5), microtime(true), 1);
69+
$this->assertTrue($limiter->consume()->isAccepted());
7070
}
7171

7272
public function testReserve()
7373
{
74-
$this->expectException(ReserveNotSupportedException::class);
74+
$limiter = $this->createLimiter();
75+
$limiter->consume(8);
7576

76-
$this->createLimiter()->reserve();
77+
// 2 over the limit, causing the WaitDuration to become 2/10th of the 12s interval
78+
$this->assertEqualsWithDelta(12 / 5, $limiter->reserve(4)->getWaitDuration(), 1);
7779
}
7880

7981
public function testPeekConsume()

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