Skip to content

Navigation Menu

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 a7af865

Browse filesBrowse files
committed
add the ability to use a Clock inside the RateLimiter
1 parent d489cfc commit a7af865
Copy full SHA for a7af865

File tree

11 files changed

+58
-30
lines changed
Filter options

11 files changed

+58
-30
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
};

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

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

1212
namespace Symfony\Component\RateLimiter\Policy;
1313

14+
use Psr\Clock\ClockInterface;
1415
use Symfony\Component\Lock\LockInterface;
1516
use Symfony\Component\RateLimiter\Exception\MaxWaitDurationExceededException;
1617
use Symfony\Component\RateLimiter\LimiterInterface;
@@ -34,6 +35,7 @@ public function __construct(
3435
\DateInterval $interval,
3536
StorageInterface $storage,
3637
?LockInterface $lock = null,
38+
private ?ClockInterface $clock = null,
3739
) {
3840
if ($limit < 1) {
3941
throw new \InvalidArgumentException(\sprintf('Cannot set the limit of "%s" to 0, as that would never accept any hit.', __CLASS__));
@@ -56,10 +58,10 @@ public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation
5658
try {
5759
$window = $this->storage->fetch($this->id);
5860
if (!$window instanceof Window) {
59-
$window = new Window($this->id, $this->interval, $this->limit);
61+
$window = new Window($this->id, $this->interval, $this->limit, null, $this->clock);
6062
}
6163

62-
$now = microtime(true);
64+
$now = (float) ($this->clock?->now()->format('U.u') ?? microtime(true));
6365
$availableTokens = $window->getAvailableTokens($now);
6466

6567
if (0 === $tokens) {
@@ -68,18 +70,18 @@ public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation
6870
} elseif ($availableTokens >= $tokens) {
6971
$window->add($tokens, $now);
7072

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

7577
if (null !== $maxTime && $waitDuration > $maxTime) {
7678
// 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));
79+
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));
7880
}
7981

8082
$window->add($tokens, $now);
8183

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

8587
if (0 < $tokens) {

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

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

1212
namespace Symfony\Component\RateLimiter\Policy;
1313

14+
use Psr\Clock\ClockInterface;
1415
use Symfony\Component\RateLimiter\LimiterInterface;
1516
use Symfony\Component\RateLimiter\RateLimit;
1617
use Symfony\Component\RateLimiter\Reservation;
@@ -25,14 +26,18 @@
2526
*/
2627
final class NoLimiter implements LimiterInterface
2728
{
29+
public function __construct(private ?ClockInterface $clock = null)
30+
{
31+
}
32+
2833
public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation
2934
{
30-
return new Reservation(microtime(true), new RateLimit(\PHP_INT_MAX, new \DateTimeImmutable(), true, \PHP_INT_MAX));
35+
return new Reservation($this->clock?->now()->format('U.u') ?? microtime(true), new RateLimit(\PHP_INT_MAX, new \DateTimeImmutable(), true, \PHP_INT_MAX, $this->clock));
3136
}
3237

3338
public function consume(int $tokens = 1): RateLimit
3439
{
35-
return new RateLimit(\PHP_INT_MAX, new \DateTimeImmutable(), true, \PHP_INT_MAX);
40+
return new RateLimit(\PHP_INT_MAX, new \DateTimeImmutable(), true, \PHP_INT_MAX, $this->clock);
3641
}
3742

3843
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
+8-6Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\RateLimiter\Policy;
1313

14+
use Psr\Clock\ClockInterface;
1415
use Symfony\Component\RateLimiter\Exception\InvalidIntervalException;
1516
use Symfony\Component\RateLimiter\LimiterStateInterface;
1617

@@ -28,19 +29,20 @@ final class SlidingWindow implements LimiterStateInterface
2829
public function __construct(
2930
private string $id,
3031
private int $intervalInSeconds,
32+
private ?ClockInterface $clock = null,
3133
) {
3234
if ($intervalInSeconds < 1) {
3335
throw new InvalidIntervalException(\sprintf('The interval must be positive integer, "%d" given.', $intervalInSeconds));
3436
}
35-
$this->windowEndAt = microtime(true) + $intervalInSeconds;
37+
$this->windowEndAt = (float) ($this->clock?->now()->format('U.u') ?? microtime(true)) + $intervalInSeconds;
3638
}
3739

38-
public static function createFromPreviousWindow(self $window, int $intervalInSeconds): self
40+
public static function createFromPreviousWindow(self $window, int $intervalInSeconds, ClockInterface $clock = null): self
3941
{
4042
$new = new self($window->id, $intervalInSeconds);
4143
$windowEndAt = $window->windowEndAt + $intervalInSeconds;
4244

43-
if (microtime(true) < $windowEndAt) {
45+
if (($clock?->now()->format('U.u') ?? microtime(true)) < $windowEndAt) {
4446
$new->hitCountForLastWindow = $window->hitCount;
4547
$new->windowEndAt = $windowEndAt;
4648
}
@@ -58,12 +60,12 @@ public function getId(): string
5860
*/
5961
public function getExpirationTime(): int
6062
{
61-
return (int) ($this->windowEndAt + $this->intervalInSeconds - microtime(true));
63+
return (int) ($this->windowEndAt + $this->intervalInSeconds - ($this->clock?->now()->format('U.u') ?? microtime(true)));
6264
}
6365

6466
public function isExpired(): bool
6567
{
66-
return microtime(true) > $this->windowEndAt;
68+
return ($this->clock?->now()->format('U.u') ?? microtime(true)) > $this->windowEndAt;
6769
}
6870

6971
public function add(int $hits = 1): void
@@ -77,7 +79,7 @@ public function add(int $hits = 1): void
7779
public function getHitCount(): int
7880
{
7981
$startOfWindow = $this->windowEndAt - $this->intervalInSeconds;
80-
$percentOfCurrentTimeFrame = min((microtime(true) - $startOfWindow) / $this->intervalInSeconds, 1);
82+
$percentOfCurrentTimeFrame = min((($this->clock?->now()->format('U.u') ?? microtime(true)) - $startOfWindow) / $this->intervalInSeconds, 1);
8183

8284
return (int) floor($this->hitCountForLastWindow * (1 - $percentOfCurrentTimeFrame) + $this->hitCount);
8385
}

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

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

1212
namespace Symfony\Component\RateLimiter\Policy;
1313

14+
use Psr\Clock\ClockInterface;
1415
use Symfony\Component\Lock\LockInterface;
1516
use Symfony\Component\RateLimiter\Exception\MaxWaitDurationExceededException;
1617
use Symfony\Component\RateLimiter\LimiterInterface;
@@ -42,6 +43,7 @@ public function __construct(
4243
\DateInterval $interval,
4344
StorageInterface $storage,
4445
?LockInterface $lock = null,
46+
private ?ClockInterface $clock = null,
4547
) {
4648
$this->storage = $storage;
4749
$this->lock = $lock;
@@ -60,9 +62,9 @@ public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation
6062
try {
6163
$window = $this->storage->fetch($this->id);
6264
if (!$window instanceof SlidingWindow) {
63-
$window = new SlidingWindow($this->id, $this->interval);
65+
$window = new SlidingWindow($this->id, $this->interval, $this->clock);
6466
} elseif ($window->isExpired()) {
65-
$window = SlidingWindow::createFromPreviousWindow($window, $this->interval);
67+
$window = SlidingWindow::createFromPreviousWindow($window, $this->interval, $this->clock);
6668
}
6769

6870
$now = microtime(true);
@@ -72,7 +74,7 @@ public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation
7274
$resetDuration = $window->calculateTimeForTokens($this->limit, $window->getHitCount());
7375
$resetTime = \DateTimeImmutable::createFromFormat('U', $availableTokens ? floor($now) : floor($now + $resetDuration));
7476

75-
return new Reservation($now, new RateLimit($availableTokens, $resetTime, true, $this->limit));
77+
return new Reservation($now, new RateLimit($availableTokens, $resetTime, true, $this->limit, $this->clock));
7678
}
7779
if ($availableTokens >= $tokens) {
7880
$window->add($tokens);

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

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

1212
namespace Symfony\Component\RateLimiter\Policy;
1313

14+
use Psr\Clock\ClockInterface;
1415
use Symfony\Component\RateLimiter\LimiterStateInterface;
1516

1617
/**
@@ -35,6 +36,7 @@ public function __construct(
3536
int $initialTokens,
3637
private Rate $rate,
3738
?float $timer = null,
39+
?ClockInterface $clock = null,
3840
) {
3941
if ($initialTokens < 1) {
4042
throw new \InvalidArgumentException(\sprintf('Cannot set the limit of "%s" to 0, as that would never accept any hit.', TokenBucketLimiter::class));
@@ -43,7 +45,7 @@ public function __construct(
4345
$this->id = $id;
4446
$this->tokens = $this->burstSize = $initialTokens;
4547
$this->rate = $rate;
46-
$this->timer = $timer ?? microtime(true);
48+
$this->timer = $timer ?? $clock?->now()->format('U.u') ?? microtime(true);
4749
}
4850

4951
public function getId(): string

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

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

1212
namespace Symfony\Component\RateLimiter\Policy;
1313

14+
use Psr\Clock\ClockInterface;
1415
use Symfony\Component\Lock\LockInterface;
1516
use Symfony\Component\RateLimiter\Exception\MaxWaitDurationExceededException;
1617
use Symfony\Component\RateLimiter\LimiterInterface;
@@ -31,6 +32,7 @@ public function __construct(
3132
private Rate $rate,
3233
StorageInterface $storage,
3334
?LockInterface $lock = null,
35+
private ?ClockInterface $clock = null,
3436
) {
3537
$this->id = $id;
3638
$this->storage = $storage;
@@ -64,7 +66,7 @@ public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation
6466
$bucket = new TokenBucket($this->id, $this->maxBurst, $this->rate);
6567
}
6668

67-
$now = microtime(true);
69+
$now = (float) ($this->clock?->now()->format('U.u') ?? microtime(true));
6870
$availableTokens = $bucket->getAvailableTokens($now);
6971

7072
if ($availableTokens >= $tokens) {
@@ -80,14 +82,14 @@ public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation
8082
$waitTime = \DateTimeImmutable::createFromFormat('U', floor($now));
8183
}
8284

83-
$reservation = new Reservation($now, new RateLimit($bucket->getAvailableTokens($now), $waitTime, true, $this->maxBurst));
85+
$reservation = new Reservation($now, new RateLimit($bucket->getAvailableTokens($now), $waitTime, true, $this->maxBurst, $this->clock));
8486
} else {
8587
$remainingTokens = $tokens - $availableTokens;
8688
$waitDuration = $this->rate->calculateTimeForTokens($remainingTokens);
8789

8890
if (null !== $maxTime && $waitDuration > $maxTime) {
8991
// process needs to wait longer than set interval
90-
$rateLimit = new RateLimit($availableTokens, \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->maxBurst);
92+
$rateLimit = new RateLimit($availableTokens, \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->maxBurst, $this->clock);
9193

9294
throw new MaxWaitDurationExceededException(\sprintf('The rate limiter wait time ("%d" seconds) is longer than the provided maximum time ("%d" seconds).', $waitDuration, $maxTime), $rateLimit);
9395
}
@@ -96,7 +98,7 @@ public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation
9698
// so no tokens are left for other processes.
9799
$bucket->setTokens($availableTokens - $tokens);
98100

99-
$reservation = new Reservation($now + $waitDuration, new RateLimit(0, \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->maxBurst));
101+
$reservation = new Reservation($now + $waitDuration, new RateLimit(0, \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->maxBurst, $this->clock));
100102
}
101103

102104
if (0 < $tokens) {

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

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

1212
namespace Symfony\Component\RateLimiter\Policy;
1313

14+
use Psr\Clock\ClockInterface;
1415
use Symfony\Component\RateLimiter\LimiterStateInterface;
1516

1617
/**
@@ -29,9 +30,10 @@ public function __construct(
2930
private int $intervalInSeconds,
3031
int $windowSize,
3132
?float $timer = null,
33+
private ?ClockInterface $clock = null,
3234
) {
3335
$this->maxSize = $windowSize;
34-
$this->timer = $timer ?? microtime(true);
36+
$this->timer = $timer ?? $this->clock?->now()->format('U.u') ?? microtime(true);
3537
}
3638

3739
public function getId(): string
@@ -46,7 +48,7 @@ public function getExpirationTime(): ?int
4648

4749
public function add(int $hits = 1, ?float $now = null): void
4850
{
49-
$now ??= microtime(true);
51+
$now ??= $this->clock?->now()->format('U.u') ?? microtime(true);
5052
if (($now - $this->timer) > $this->intervalInSeconds) {
5153
// reset window
5254
$this->timer = $now;

‎src/Symfony/Component/RateLimiter/RateLimit.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/RateLimiter/RateLimit.php
+4-1Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\RateLimiter;
1313

14+
use Psr\Clock\ClockInterface;
1415
use Symfony\Component\RateLimiter\Exception\RateLimitExceededException;
1516

1617
/**
@@ -23,6 +24,7 @@ public function __construct(
2324
private \DateTimeImmutable $retryAfter,
2425
private bool $accepted,
2526
private int $limit,
27+
private ?ClockInterface $clock = null,
2628
) {
2729
}
2830

@@ -62,7 +64,8 @@ public function getLimit(): int
6264

6365
public function wait(): void
6466
{
65-
$delta = $this->retryAfter->format('U.u') - microtime(true);
67+
$now = (float) ($this->clock?->now()->format('U.u') ?? microtime(true));
68+
$delta = $this->retryAfter->format('U.u') - $now;
6669
if ($delta <= 0) {
6770
return;
6871
}

‎src/Symfony/Component/RateLimiter/RateLimiterFactory.php

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

1212
namespace Symfony\Component\RateLimiter;
1313

14+
use Psr\Clock\ClockInterface;
1415
use Symfony\Component\Lock\LockFactory;
1516
use Symfony\Component\OptionsResolver\Options;
1617
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -32,6 +33,7 @@ public function __construct(
3233
array $config,
3334
private StorageInterface $storage,
3435
private ?LockFactory $lockFactory = null,
36+
private ?ClockInterface $clock = null,
3537
) {
3638
$options = new OptionsResolver();
3739
self::configureOptions($options);
@@ -45,10 +47,10 @@ public function create(?string $key = null): LimiterInterface
4547
$lock = $this->lockFactory?->createLock($id);
4648

4749
return match ($this->config['policy']) {
48-
'token_bucket' => new TokenBucketLimiter($id, $this->config['limit'], $this->config['rate'], $this->storage, $lock),
49-
'fixed_window' => new FixedWindowLimiter($id, $this->config['limit'], $this->config['interval'], $this->storage, $lock),
50-
'sliding_window' => new SlidingWindowLimiter($id, $this->config['limit'], $this->config['interval'], $this->storage, $lock),
51-
'no_limit' => new NoLimiter(),
50+
'token_bucket' => new TokenBucketLimiter($id, $this->config['limit'], $this->config['rate'], $this->storage, $lock, $this->clock),
51+
'fixed_window' => new FixedWindowLimiter($id, $this->config['limit'], $this->config['interval'], $this->storage, $lock, $this->clock),
52+
'sliding_window' => new SlidingWindowLimiter($id, $this->config['limit'], $this->config['interval'], $this->storage, $lock, $this->clock),
53+
'no_limit' => new NoLimiter($this->clock),
5254
default => throw new \LogicException(\sprintf('Limiter policy "%s" does not exists, it must be either "token_bucket", "sliding_window", "fixed_window" or "no_limit".', $this->config['policy'])),
5355
};
5456
}

‎src/Symfony/Component/RateLimiter/Storage/InMemoryStorage.php

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

1212
namespace Symfony\Component\RateLimiter\Storage;
1313

14+
use Psr\Clock\ClockInterface;
1415
use Symfony\Component\RateLimiter\LimiterStateInterface;
1516

1617
/**
@@ -20,6 +21,10 @@ class InMemoryStorage implements StorageInterface
2021
{
2122
private array $buckets = [];
2223

24+
public function __construct(private ?ClockInterface $clock = null)
25+
{
26+
}
27+
2328
public function save(LimiterStateInterface $limiterState): void
2429
{
2530
$this->buckets[$limiterState->getId()] = [$this->getExpireAt($limiterState), serialize($limiterState)];
@@ -32,7 +37,7 @@ public function fetch(string $limiterStateId): ?LimiterStateInterface
3237
}
3338

3439
[$expireAt, $limiterState] = $this->buckets[$limiterStateId];
35-
if (null !== $expireAt && $expireAt <= microtime(true)) {
40+
if (null !== $expireAt && $expireAt <= ($this->clock?->now()->format('U.u') ?? microtime(true))) {
3641
unset($this->buckets[$limiterStateId]);
3742

3843
return null;
@@ -53,7 +58,7 @@ public function delete(string $limiterStateId): void
5358
private function getExpireAt(LimiterStateInterface $limiterState): ?float
5459
{
5560
if (null !== $expireSeconds = $limiterState->getExpirationTime()) {
56-
return microtime(true) + $expireSeconds;
61+
return (float) ($this->clock?->now()->format('U.u') ?? microtime(true)) + $expireSeconds;
5762
}
5863

5964
return $this->buckets[$limiterState->getId()][0] ?? null;

0 commit comments

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