Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 3ecabb2

Browse filesBrowse files
committed
add the ability to use a clock in late limiters
1 parent d489cfc commit 3ecabb2
Copy full SHA for 3ecabb2

16 files changed

+151
-46
lines changed

‎src/Symfony/Bundle/FrameworkBundle/Resources/config/rate_limiter.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/rate_limiter.php
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
abstract_arg('config'),
2626
abstract_arg('storage'),
2727
null,
28+
service('clock')->nullOnInvalid(),
2829
])
2930
;
3031
};
+32Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\RateLimiter;
13+
14+
use Psr\Clock\ClockInterface;
15+
16+
/**
17+
* @internal
18+
*/
19+
trait ClockTrait
20+
{
21+
private ?ClockInterface $clock;
22+
23+
public function setClock(?ClockInterface $clock): void
24+
{
25+
$this->clock = $clock;
26+
}
27+
28+
private function now(): float
29+
{
30+
return (float) ($this->clock?->now()->format('U.u') ?? microtime(true));
31+
}
32+
}

‎src/Symfony/Component/RateLimiter/Policy/FixedWindowLimiter.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/RateLimiter/Policy/FixedWindowLimiter.php
+13-6Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
namespace Symfony\Component\RateLimiter\Policy;
1313

14+
use Psr\Clock\ClockInterface;
1415
use Symfony\Component\Lock\LockInterface;
16+
use Symfony\Component\RateLimiter\ClockTrait;
1517
use Symfony\Component\RateLimiter\Exception\MaxWaitDurationExceededException;
1618
use Symfony\Component\RateLimiter\LimiterInterface;
1719
use Symfony\Component\RateLimiter\RateLimit;
@@ -24,6 +26,7 @@
2426
*/
2527
final class FixedWindowLimiter implements LimiterInterface
2628
{
29+
use ClockTrait;
2730
use ResetLimiterTrait;
2831

2932
private int $interval;
@@ -34,6 +37,7 @@ public function __construct(
3437
\DateInterval $interval,
3538
StorageInterface $storage,
3639
?LockInterface $lock = null,
40+
?ClockInterface $clock = null,
3741
) {
3842
if ($limit < 1) {
3943
throw new \InvalidArgumentException(\sprintf('Cannot set the limit of "%s" to 0, as that would never accept any hit.', __CLASS__));
@@ -43,6 +47,7 @@ public function __construct(
4347
$this->lock = $lock;
4448
$this->id = $id;
4549
$this->interval = TimeUtil::dateIntervalToSeconds($interval);
50+
$this->setClock($clock);
4651
}
4752

4853
public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation
@@ -56,30 +61,32 @@ public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation
5661
try {
5762
$window = $this->storage->fetch($this->id);
5863
if (!$window instanceof Window) {
59-
$window = new Window($this->id, $this->interval, $this->limit);
64+
$window = new Window($this->id, $this->interval, $this->limit, $this->clock);
65+
} else {
66+
$window->setClock($this->clock);
6067
}
6168

62-
$now = microtime(true);
69+
$now = $this->now();
6370
$availableTokens = $window->getAvailableTokens($now);
6471

6572
if (0 === $tokens) {
6673
$waitDuration = $window->calculateTimeForTokens(1, $now);
67-
$reservation = new Reservation($now + $waitDuration, new RateLimit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), true, $this->limit));
74+
$reservation = new Reservation($now + $waitDuration, new RateLimit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), true, $this->limit), $this->clock);
6875
} elseif ($availableTokens >= $tokens) {
6976
$window->add($tokens, $now);
7077

71-
$reservation = new Reservation($now, new RateLimit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now)), true, $this->limit));
78+
$reservation = new Reservation($now, new RateLimit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now)), true, $this->limit, $this->clock), $this->clock);
7279
} else {
7380
$waitDuration = $window->calculateTimeForTokens($tokens, $now);
7481

7582
if (null !== $maxTime && $waitDuration > $maxTime) {
7683
// 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($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->limit));
84+
throw new MaxWaitDurationExceededException(\sprintf('The rate limiter wait time ("%d" seconds) is longer than the provided maximum time ("%d" seconds).', $waitDuration, $maxTime), new RateLimit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->limit, $this->clock));
7885
}
7986

8087
$window->add($tokens, $now);
8188

82-
$reservation = new Reservation($now + $waitDuration, new RateLimit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->limit));
89+
$reservation = new Reservation($now + $waitDuration, new RateLimit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->limit, $this->clock), $this->clock);
8390
}
8491

8592
if (0 < $tokens) {

‎src/Symfony/Component/RateLimiter/Policy/NoLimiter.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/RateLimiter/Policy/NoLimiter.php
+11-2Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\RateLimiter\Policy;
1313

14+
use Psr\Clock\ClockInterface;
15+
use Symfony\Component\RateLimiter\ClockTrait;
1416
use Symfony\Component\RateLimiter\LimiterInterface;
1517
use Symfony\Component\RateLimiter\RateLimit;
1618
use Symfony\Component\RateLimiter\Reservation;
@@ -25,14 +27,21 @@
2527
*/
2628
final class NoLimiter implements LimiterInterface
2729
{
30+
use ClockTrait;
31+
32+
public function __construct(?ClockInterface $clock = null)
33+
{
34+
$this->setClock($clock);
35+
}
36+
2837
public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation
2938
{
30-
return new Reservation(microtime(true), new RateLimit(\PHP_INT_MAX, new \DateTimeImmutable(), true, \PHP_INT_MAX));
39+
return new Reservation($this->now(), new RateLimit(\PHP_INT_MAX, new \DateTimeImmutable(), true, \PHP_INT_MAX, $this->clock), $this->clock);
3140
}
3241

3342
public function consume(int $tokens = 1): RateLimit
3443
{
35-
return new RateLimit(\PHP_INT_MAX, new \DateTimeImmutable(), true, \PHP_INT_MAX);
44+
return new RateLimit(\PHP_INT_MAX, new \DateTimeImmutable(), true, \PHP_INT_MAX, $this->clock);
3645
}
3746

3847
public function reset(): void

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/RateLimiter/Policy/SlidingWindow.php
+13-7Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\RateLimiter\Policy;
1313

14+
use Psr\Clock\ClockInterface;
15+
use Symfony\Component\RateLimiter\ClockTrait;
1416
use Symfony\Component\RateLimiter\Exception\InvalidIntervalException;
1517
use Symfony\Component\RateLimiter\LimiterStateInterface;
1618

@@ -21,26 +23,30 @@
2123
*/
2224
final class SlidingWindow implements LimiterStateInterface
2325
{
26+
use ClockTrait;
27+
2428
private int $hitCount = 0;
2529
private int $hitCountForLastWindow = 0;
2630
private float $windowEndAt;
2731

2832
public function __construct(
2933
private string $id,
3034
private int $intervalInSeconds,
35+
?ClockInterface $clock = null,
3136
) {
3237
if ($intervalInSeconds < 1) {
3338
throw new InvalidIntervalException(\sprintf('The interval must be positive integer, "%d" given.', $intervalInSeconds));
3439
}
35-
$this->windowEndAt = microtime(true) + $intervalInSeconds;
40+
$this->setClock($clock);
41+
$this->windowEndAt = $this->now() + $intervalInSeconds;
3642
}
3743

38-
public static function createFromPreviousWindow(self $window, int $intervalInSeconds): self
44+
public static function createFromPreviousWindow(self $window, int $intervalInSeconds, ?ClockInterface $clock = null): self
3945
{
4046
$new = new self($window->id, $intervalInSeconds);
4147
$windowEndAt = $window->windowEndAt + $intervalInSeconds;
4248

43-
if (microtime(true) < $windowEndAt) {
49+
if (($clock?->now()->format('U.u') ?? microtime(true)) < $windowEndAt) {
4450
$new->hitCountForLastWindow = $window->hitCount;
4551
$new->windowEndAt = $windowEndAt;
4652
}
@@ -58,12 +64,12 @@ public function getId(): string
5864
*/
5965
public function getExpirationTime(): int
6066
{
61-
return (int) ($this->windowEndAt + $this->intervalInSeconds - microtime(true));
67+
return (int) ($this->windowEndAt + $this->intervalInSeconds - $this->now());
6268
}
6369

6470
public function isExpired(): bool
6571
{
66-
return microtime(true) > $this->windowEndAt;
72+
return $this->now() > $this->windowEndAt;
6773
}
6874

6975
public function add(int $hits = 1): void
@@ -77,7 +83,7 @@ public function add(int $hits = 1): void
7783
public function getHitCount(): int
7884
{
7985
$startOfWindow = $this->windowEndAt - $this->intervalInSeconds;
80-
$percentOfCurrentTimeFrame = min((microtime(true) - $startOfWindow) / $this->intervalInSeconds, 1);
86+
$percentOfCurrentTimeFrame = min(($this->now() - $startOfWindow) / $this->intervalInSeconds, 1);
8187

8288
return (int) floor($this->hitCountForLastWindow * (1 - $percentOfCurrentTimeFrame) + $this->hitCount);
8389
}
@@ -89,7 +95,7 @@ public function calculateTimeForTokens(int $maxSize, int $tokens): float
8995
return 0;
9096
}
9197

92-
$time = microtime(true);
98+
$time = $this->now();
9399
$startOfWindow = $this->windowEndAt - $this->intervalInSeconds;
94100
$timePassed = $time - $startOfWindow;
95101
$windowPassed = min($timePassed / $this->intervalInSeconds, 1);

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/RateLimiter/Policy/SlidingWindowLimiter.php
+18-8Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
namespace Symfony\Component\RateLimiter\Policy;
1313

14+
use Psr\Clock\ClockInterface;
1415
use Symfony\Component\Lock\LockInterface;
16+
use Symfony\Component\RateLimiter\ClockTrait;
1517
use Symfony\Component\RateLimiter\Exception\MaxWaitDurationExceededException;
1618
use Symfony\Component\RateLimiter\LimiterInterface;
1719
use Symfony\Component\RateLimiter\RateLimit;
@@ -32,6 +34,7 @@
3234
*/
3335
final class SlidingWindowLimiter implements LimiterInterface
3436
{
37+
use ClockTrait;
3538
use ResetLimiterTrait;
3639

3740
private int $interval;
@@ -42,11 +45,13 @@ public function __construct(
4245
\DateInterval $interval,
4346
StorageInterface $storage,
4447
?LockInterface $lock = null,
48+
?ClockInterface $clock = null,
4549
) {
4650
$this->storage = $storage;
4751
$this->lock = $lock;
4852
$this->id = $id;
4953
$this->interval = TimeUtil::dateIntervalToSeconds($interval);
54+
$this->setClock($clock);
5055
}
5156

5257
public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation
@@ -59,25 +64,30 @@ public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation
5964

6065
try {
6166
$window = $this->storage->fetch($this->id);
62-
if (!$window instanceof SlidingWindow) {
63-
$window = new SlidingWindow($this->id, $this->interval);
64-
} elseif ($window->isExpired()) {
65-
$window = SlidingWindow::createFromPreviousWindow($window, $this->interval);
67+
68+
if ($window instanceof SlidingWindow) {
69+
$window->setClock($this->clock);
70+
71+
if ($window->isExpired()) {
72+
$window = SlidingWindow::createFromPreviousWindow($window, $this->interval, $this->clock);
73+
}
74+
} else {
75+
$window = new SlidingWindow($this->id, $this->interval, $this->clock);
6676
}
6777

68-
$now = microtime(true);
78+
$now = $this->now();
6979
$hitCount = $window->getHitCount();
7080
$availableTokens = $this->getAvailableTokens($hitCount);
7181
if (0 === $tokens) {
7282
$resetDuration = $window->calculateTimeForTokens($this->limit, $window->getHitCount());
7383
$resetTime = \DateTimeImmutable::createFromFormat('U', $availableTokens ? floor($now) : floor($now + $resetDuration));
7484

75-
return new Reservation($now, new RateLimit($availableTokens, $resetTime, true, $this->limit));
85+
return new Reservation($now, new RateLimit($availableTokens, $resetTime, true, $this->limit, $this->clock), $this->clock);
7686
}
7787
if ($availableTokens >= $tokens) {
7888
$window->add($tokens);
7989

80-
$reservation = new Reservation($now, new RateLimit($this->getAvailableTokens($window->getHitCount()), \DateTimeImmutable::createFromFormat('U', floor($now)), true, $this->limit));
90+
$reservation = new Reservation($now, new RateLimit($this->getAvailableTokens($window->getHitCount()), \DateTimeImmutable::createFromFormat('U', floor($now)), true, $this->limit), $this->clock);
8191
} else {
8292
$waitDuration = $window->calculateTimeForTokens($this->limit, $tokens);
8393

@@ -88,7 +98,7 @@ public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation
8898

8999
$window->add($tokens);
90100

91-
$reservation = new Reservation($now + $waitDuration, new RateLimit($this->getAvailableTokens($window->getHitCount()), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->limit));
101+
$reservation = new Reservation($now + $waitDuration, new RateLimit($this->getAvailableTokens($window->getHitCount()), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->limit), $this->clock);
92102
}
93103

94104
if (0 < $tokens) {

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/RateLimiter/Policy/TokenBucket.php
+8-4Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\RateLimiter\Policy;
1313

14+
use Psr\Clock\ClockInterface;
15+
use Symfony\Component\RateLimiter\ClockTrait;
1416
use Symfony\Component\RateLimiter\LimiterStateInterface;
1517

1618
/**
@@ -20,6 +22,8 @@
2022
*/
2123
final class TokenBucket implements LimiterStateInterface
2224
{
25+
use ClockTrait;
26+
2327
private int $tokens;
2428
private int $burstSize;
2529
private float $timer;
@@ -28,22 +32,22 @@ final class TokenBucket implements LimiterStateInterface
2832
* @param string $id unique identifier for this bucket
2933
* @param int $initialTokens the initial number of tokens in the bucket (i.e. the max burst size)
3034
* @param Rate $rate the fill rate and time of this bucket
31-
* @param float|null $timer the current timer of the bucket, defaulting to microtime(true)
35+
* @param float|null $timer the current timer of the bucket, defaulting to the current time in microseconds
3236
*/
3337
public function __construct(
3438
private string $id,
3539
int $initialTokens,
3640
private Rate $rate,
41+
?ClockInterface $clock = null,
3742
?float $timer = null,
3843
) {
3944
if ($initialTokens < 1) {
4045
throw new \InvalidArgumentException(\sprintf('Cannot set the limit of "%s" to 0, as that would never accept any hit.', TokenBucketLimiter::class));
4146
}
4247

43-
$this->id = $id;
4448
$this->tokens = $this->burstSize = $initialTokens;
45-
$this->rate = $rate;
46-
$this->timer = $timer ?? microtime(true);
49+
$this->setClock($clock);
50+
$this->timer = $timer ?? $this->now();
4751
}
4852

4953
public function getId(): string

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.