Skip to content

Commit 459b332

Browse files
committed
calculateTimeForTokens for SlidingWindow
1 parent e8826b7 commit 459b332

File tree

2 files changed

+52
-11
lines changed

2 files changed

+52
-11
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 ceil($needed * $timePerToken);
108+
}
109+
110+
return ($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
@@ -13,7 +13,7 @@
1313

1414
use Symfony\Component\Lock\LockInterface;
1515
use Symfony\Component\Lock\NoLock;
16-
use Symfony\Component\RateLimiter\Exception\ReserveNotSupportedException;
16+
use Symfony\Component\RateLimiter\Exception\MaxWaitDurationExceededException;
1717
use Symfony\Component\RateLimiter\LimiterInterface;
1818
use Symfony\Component\RateLimiter\RateLimit;
1919
use Symfony\Component\RateLimiter\Reservation;
@@ -49,11 +49,10 @@ public function __construct(string $id, int $limit, \DateInterval $interval, Sto
4949

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

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

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

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

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

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

85105
private function getAvailableTokens(int $hitCount): int

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