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 04e164f

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

File tree

11 files changed

+81
-20
lines changed
Filter options

11 files changed

+81
-20
lines changed

‎src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2115,6 +2115,9 @@ private function addRateLimiterSection(ArrayNodeDefinition $rootNode, callable $
21152115
->integerNode('amount')->info('Amount of tokens to add each interval')->defaultValue(1)->end()
21162116
->end()
21172117
->end()
2118+
->booleanNode('use_clock')
2119+
->defaultFalse()
2120+
->end()
21182121
->end()
21192122
->end()
21202123
->end()

‎src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+6Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2671,6 +2671,12 @@ private function registerRateLimiterConfiguration(array $config, ContainerBuilde
26712671
$limiterConfig['id'] = $name;
26722672
$limiter->replaceArgument(0, $limiterConfig);
26732673

2674+
if ($limiterConfig['use_clock']) {
2675+
$limiter->replaceArgument(3, new Reference('clock', ContainerInterface::NULL_ON_INVALID_REFERENCE));
2676+
} else {
2677+
$limiter->replaceArgument(3, null);
2678+
}
2679+
26742680
$container->registerAliasForArgument($limiterId, RateLimiterFactory::class, $name.'.limiter');
26752681
}
26762682
}

‎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+
abstract_arg('clock')
2829
])
2930
;
3031
};

‎src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,7 @@
763763
<xsd:attribute name="strategy" type="xsd:string" />
764764
<xsd:attribute name="limit" type="xsd:int" />
765765
<xsd:attribute name="interval" type="xsd:string" />
766+
<xsd:attribute name="use-clock" type="xsd:boolean" />
766767
</xsd:complexType>
767768

768769
<xsd:complexType name="rate_limiter_rate">

‎src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php
+34Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313

1414
use Symfony\Component\Config\FileLocator;
1515
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\ContainerInterface;
1617
use Symfony\Component\DependencyInjection\Exception\LogicException;
1718
use Symfony\Component\DependencyInjection\Exception\OutOfBoundsException;
1819
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
20+
use Symfony\Component\DependencyInjection\Reference;
1921
use Symfony\Component\Workflow\Exception\InvalidDefinitionException;
2022

2123
class PhpFrameworkExtensionTest extends FrameworkExtensionTest
@@ -136,4 +138,36 @@ public function testRateLimiterLockFactory()
136138

137139
$container->getDefinition('limiter.without_lock')->getArgument(2);
138140
}
141+
142+
public function testRateLimiterWithoutClock()
143+
{
144+
$container = $this->createContainerFromClosure(function (ContainerBuilder $container) {
145+
$container->loadFromExtension('framework', [
146+
'http_method_override' => false,
147+
'lock' => true,
148+
'rate_limiter' => [
149+
'without_clock' => ['policy' => 'fixed_window', 'limit' => 10, 'interval' => '1 hour'],
150+
],
151+
]);
152+
});
153+
154+
$withLock = $container->getDefinition('limiter.without_clock');
155+
$this->assertNull($withLock->getArgument(3));
156+
}
157+
158+
public function testRateLimiterWithClock()
159+
{
160+
$container = $this->createContainerFromClosure(function (ContainerBuilder $container) {
161+
$container->loadFromExtension('framework', [
162+
'http_method_override' => false,
163+
'lock' => true,
164+
'rate_limiter' => [
165+
'without_clock' => ['policy' => 'fixed_window', 'limit' => 10, 'interval' => '1 hour', 'use_clock' => true],
166+
],
167+
]);
168+
});
169+
170+
$withLock = $container->getDefinition('limiter.without_clock');
171+
$this->assertEquals(new Reference('clock', ContainerInterface::NULL_ON_INVALID_REFERENCE), $withLock->getArgument(3));
172+
}
139173
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/RateLimiter/Policy/FixedWindowLimiter.php
+5-4Lines changed: 5 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 Symfony\Component\Clock\ClockInterface;
1415
use Symfony\Component\Lock\LockInterface;
1516
use Symfony\Component\RateLimiter\Exception\MaxWaitDurationExceededException;
1617
use Symfony\Component\RateLimiter\LimiterInterface;
@@ -29,7 +30,7 @@ final class FixedWindowLimiter implements LimiterInterface
2930
private int $limit;
3031
private int $interval;
3132

32-
public function __construct(string $id, int $limit, \DateInterval $interval, StorageInterface $storage, LockInterface $lock = null)
33+
public function __construct(string $id, int $limit, \DateInterval $interval, StorageInterface $storage, LockInterface $lock = null, private ?ClockInterface $clock = null)
3334
{
3435
if ($limit < 1) {
3536
throw new \InvalidArgumentException(sprintf('Cannot set the limit of "%s" to 0, as that would never accept any hit.', __CLASS__));
@@ -62,18 +63,18 @@ public function reserve(int $tokens = 1, float $maxTime = null): Reservation
6263
if ($availableTokens >= max(1, $tokens)) {
6364
$window->add($tokens, $now);
6465

65-
$reservation = new Reservation($now, new RateLimit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now)), true, $this->limit));
66+
$reservation = new Reservation($now, new RateLimit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now)), true, $this->limit, $this->clock));
6667
} else {
6768
$waitDuration = $window->calculateTimeForTokens(max(1, $tokens));
6869

6970
if (null !== $maxTime && $waitDuration > $maxTime) {
7071
// process needs to wait longer than set interval
71-
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));
72+
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));
7273
}
7374

7475
$window->add($tokens, $now);
7576

76-
$reservation = new Reservation($now + $waitDuration, new RateLimit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->limit));
77+
$reservation = new Reservation($now + $waitDuration, new RateLimit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->limit, $this->clock));
7778
}
7879

7980
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 Symfony\Component\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(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/SlidingWindowLimiter.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/RateLimiter/Policy/SlidingWindowLimiter.php
+4-3Lines changed: 4 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 Symfony\Component\Clock\ClockInterface;
1415
use Symfony\Component\Lock\LockInterface;
1516
use Symfony\Component\RateLimiter\Exception\ReserveNotSupportedException;
1617
use Symfony\Component\RateLimiter\LimiterInterface;
@@ -37,7 +38,7 @@ final class SlidingWindowLimiter implements LimiterInterface
3738
private int $limit;
3839
private int $interval;
3940

40-
public function __construct(string $id, int $limit, \DateInterval $interval, StorageInterface $storage, LockInterface $lock = null)
41+
public function __construct(string $id, int $limit, \DateInterval $interval, StorageInterface $storage, LockInterface $lock = null, private ?ClockInterface $clock = null)
4142
{
4243
$this->storage = $storage;
4344
$this->lock = $lock;
@@ -66,7 +67,7 @@ public function consume(int $tokens = 1): RateLimit
6667
$hitCount = $window->getHitCount();
6768
$availableTokens = $this->getAvailableTokens($hitCount);
6869
if ($availableTokens < $tokens) {
69-
return new RateLimit($availableTokens, $window->getRetryAfter(), false, $this->limit);
70+
return new RateLimit($availableTokens, $window->getRetryAfter(), false, $this->limit, $this->clock);
7071
}
7172

7273
$window->add($tokens);
@@ -75,7 +76,7 @@ public function consume(int $tokens = 1): RateLimit
7576
$this->storage->save($window);
7677
}
7778

78-
return new RateLimit($this->getAvailableTokens($window->getHitCount()), $window->getRetryAfter(), true, $this->limit);
79+
return new RateLimit($this->getAvailableTokens($window->getHitCount()), $window->getRetryAfter(), true, $this->limit, $this->clock);
7980
} finally {
8081
$this->lock?->release();
8182
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/RateLimiter/Policy/TokenBucketLimiter.php
+5-4Lines changed: 5 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 Symfony\Component\Clock\ClockInterface;
1415
use Symfony\Component\Lock\LockInterface;
1516
use Symfony\Component\RateLimiter\Exception\MaxWaitDurationExceededException;
1617
use Symfony\Component\RateLimiter\LimiterInterface;
@@ -28,7 +29,7 @@ final class TokenBucketLimiter implements LimiterInterface
2829
private int $maxBurst;
2930
private Rate $rate;
3031

31-
public function __construct(string $id, int $maxBurst, Rate $rate, StorageInterface $storage, LockInterface $lock = null)
32+
public function __construct(string $id, int $maxBurst, Rate $rate, StorageInterface $storage, LockInterface $lock = null, private ?ClockInterface $clock = null)
3233
{
3334
$this->id = $id;
3435
$this->maxBurst = $maxBurst;
@@ -72,14 +73,14 @@ public function reserve(int $tokens = 1, float $maxTime = null): Reservation
7273
$bucket->setTokens($availableTokens - $tokens);
7374
$bucket->setTimer($now);
7475

75-
$reservation = new Reservation($now, new RateLimit($bucket->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now)), true, $this->maxBurst));
76+
$reservation = new Reservation($now, new RateLimit($bucket->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now)), true, $this->maxBurst, $this->clock));
7677
} else {
7778
$remainingTokens = $tokens - $availableTokens;
7879
$waitDuration = $this->rate->calculateTimeForTokens($remainingTokens);
7980

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

8485
throw new MaxWaitDurationExceededException(sprintf('The rate limiter wait time ("%d" seconds) is longer than the provided maximum time ("%d" seconds).', $waitDuration, $maxTime), $rateLimit);
8586
}
@@ -89,7 +90,7 @@ public function reserve(int $tokens = 1, float $maxTime = null): Reservation
8990
$bucket->setTokens($availableTokens - $tokens);
9091
$bucket->setTimer($now);
9192

92-
$reservation = new Reservation($now + $waitDuration, new RateLimit(0, \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->maxBurst));
93+
$reservation = new Reservation($now + $waitDuration, new RateLimit(0, \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->maxBurst, $this->clock));
9394
}
9495

9596
if (0 < $tokens) {

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

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

1212
namespace Symfony\Component\RateLimiter;
1313

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

1617
/**
@@ -23,7 +24,7 @@ class RateLimit
2324
private bool $accepted;
2425
private int $limit;
2526

26-
public function __construct(int $availableTokens, \DateTimeImmutable $retryAfter, bool $accepted, int $limit)
27+
public function __construct(int $availableTokens, \DateTimeImmutable $retryAfter, bool $accepted, int $limit, private ?ClockInterface $clock = null)
2728
{
2829
$this->availableTokens = $availableTokens;
2930
$this->retryAfter = $retryAfter;
@@ -67,7 +68,13 @@ public function getLimit(): int
6768

6869
public function wait(): void
6970
{
70-
$delta = $this->retryAfter->format('U.u') - microtime(true);
71+
if (null !== $this->clock) {
72+
$now = $this->clock->now()->format('U.u');
73+
} else {
74+
$now = microtime(true);
75+
}
76+
77+
$delta = $this->retryAfter->format('U.u') - $now;
7178
if ($delta <= 0) {
7279
return;
7380
}

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

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

1212
namespace Symfony\Component\RateLimiter;
1313

14+
use Symfony\Component\Clock\ClockInterface;
1415
use Symfony\Component\Lock\LockFactory;
1516
use Symfony\Component\OptionsResolver\Options;
1617
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -30,7 +31,7 @@ final class RateLimiterFactory
3031
private StorageInterface $storage;
3132
private ?LockFactory $lockFactory;
3233

33-
public function __construct(array $config, StorageInterface $storage, LockFactory $lockFactory = null)
34+
public function __construct(array $config, StorageInterface $storage, LockFactory $lockFactory = null, private ?ClockInterface $clock = null)
3435
{
3536
$this->storage = $storage;
3637
$this->lockFactory = $lockFactory;
@@ -47,10 +48,10 @@ public function create(string $key = null): LimiterInterface
4748
$lock = $this->lockFactory?->createLock($id);
4849

4950
return match ($this->config['policy']) {
50-
'token_bucket' => new TokenBucketLimiter($id, $this->config['limit'], $this->config['rate'], $this->storage, $lock),
51-
'fixed_window' => new FixedWindowLimiter($id, $this->config['limit'], $this->config['interval'], $this->storage, $lock),
52-
'sliding_window' => new SlidingWindowLimiter($id, $this->config['limit'], $this->config['interval'], $this->storage, $lock),
53-
'no_limit' => new NoLimiter(),
51+
'token_bucket' => new TokenBucketLimiter($id, $this->config['limit'], $this->config['rate'], $this->storage, $lock, $this->clock),
52+
'fixed_window' => new FixedWindowLimiter($id, $this->config['limit'], $this->config['interval'], $this->storage, $lock, $this->clock),
53+
'sliding_window' => new SlidingWindowLimiter($id, $this->config['limit'], $this->config['interval'], $this->storage, $lock, $this->clock),
54+
'no_limit' => new NoLimiter($this->clock),
5455
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'])),
5556
};
5657
}

0 commit comments

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