8000 [RateLimiter] Added reserve() to LimiterInterface and rename Limiter … · symfony/symfony@cd34f21 · GitHub
[go: up one dir, main page]

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 cd34f21

Browse files
wouterjfabpot
authored andcommitted
[RateLimiter] Added reserve() to LimiterInterface and rename Limiter to RateLimiter
1 parent b3a1851 commit cd34f21

23 files changed

+219
-64
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,8 @@
128128
use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface;
129129
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
130130
use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface;
131-
use Symfony\Component\RateLimiter\Limiter;
132131
use Symfony\Component\RateLimiter\LimiterInterface;
132+
use Symfony\Component\RateLimiter\RateLimiter;
133133
use Symfony\Component\RateLimiter\Storage\CacheStorage;
134134
use Symfony\Component\Routing\Loader\AnnotationDirectoryLoader;
135135
use Symfony\Component\Routing\Loader\AnnotationFileLoader;
@@ -2266,7 +2266,7 @@ public static function registerRateLimiter(ContainerBuilder $container, string $
22662266
$limiterConfig['id'] = $name;
22672267
$limiter->replaceArgument(0, $limiterConfig);
22682268

2269-
$container->registerAliasForArgument($limiterId, Limiter::class, $name.'.limiter');
2269+
$container->registerAliasForArgument($limiterId, RateLimiter::class, $name.'.limiter');
22702270
}
22712271

22722272
private function resolveTrustedHeaders(array $headers): int

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
1313

14-
use Symfony\Component\RateLimiter\Limiter;
14+
use Symfony\Component\RateLimiter\RateLimiter;
1515

1616
return static function (ContainerConfigurator $container) {
1717
$container->services()
1818
->set('cache.rate_limiter')
1919
->parent('cache.app')
2020
->tag('cache.pool')
2121

22-
->set('limiter', Limiter::class)
22+
->set('limiter', RateLimiter::class)
2323
->abstract()
2424
->args([
2525
abstract_arg('config'),

src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginThrottlingFactory.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
use Symfony\Component\DependencyInjection\ContainerBuilder;
1919
use Symfony\Component\DependencyInjection\Reference;
2020
use Symfony\Component\HttpFoundation\RateLimiter\RequestRateLimiterInterface;
21-
use Symfony\Component\RateLimiter\Limiter;
21+
use Symfony\Component\RateLimiter\RateLimiter;
2222
use Symfony\Component\Security\Http\EventListener\LoginThrottlingListener;
2323
use Symfony\Component\Security\Http\RateLimiter\DefaultLoginRateLimiter;
2424

@@ -63,7 +63,7 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal
6363
throw new \LogicException('Login throttling requires symfony/security-http:^5.2.');
6464
}
6565

66-
if (!class_exists(Limiter::class)) {
66+
if (!class_exists(RateLimiter::class)) {
6767
throw new \LogicException('Login throttling requires symfony/rate-limiter to be installed and enabled.');
6868
}
6969

src/Symfony/Component/RateLimiter/CompoundLimiter.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\RateLimiter;
1313

14+
use Symfony\Component\RateLimiter\Exception\ReserveNotSupportedException;
15+
1416
/**
1517
* @author Wouter de Jong <wouter@wouterj.nl>
1618
*
@@ -31,6 +33,11 @@ public function __construct(array $limiters)
3133
$this->limiters = $limiters;
3234
}
3335

36+
public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation
37+
{
38+
throw new ReserveNotSupportedException(__CLASS__);
39+
}
40+
3441
public function consume(int $tokens = 1): Limit
3542
{
3643
$minimalLimit = null;

src/Symfony/Component/RateLimiter/Exception/MaxWaitDurationExceededException.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,26 @@
1111

1212
namespace Symfony\Component\RateLimiter\Exception;
1313

14+
use Symfony\Component\RateLimiter\Limit;
15+
1416
/**
1517
* @author Wouter de Jong <wouter@wouterj.nl>
1618
*
1719
* @experimental in 5.2
1820
*/
1921
class MaxWaitDurationExceededException extends \RuntimeException
2022
{
23+
private $limit;
24+
25+
public function __construct(string $message, Limit $limit, int $code = 0, ?\Throwable $previous = null)
26+
{
27+
parent::__construct($message, $code, $previous);
28+
29+
$this->limit = $limit;
30+
}
31+
32+
public function getLimit(): Limit
33+
{
34+
return $this->limit;
35+
}
2136
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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\Exception;
13+
14+
/**
15+
* @author Wouter de Jong <wouter@wouterj.nl>
16+
*
17+
* @experimental in 5.2
18+
*/
19+
class ReserveNotSupportedException extends \BadMethodCallException
20+
{
21+
public function __construct(string $limiterClass, int $code = 0, ?\Throwable $previous = null)
22+
{
23+
parent::__construct(sprintf('Reserving tokens is not supported by "%s".', $limiterClass), $code, $previous);
24+
}
25+
}

src/Symfony/Component/RateLimiter/FixedWindowLimiter.php

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

1414
use Symfony\Component\Lock\LockInterface;
1515
use Symfony\Component\Lock\NoLock;
16+
use Symfony\Component\RateLimiter\Exception\MaxWaitDurationExceededException;
1617
use Symfony\Component\RateLimiter\Storage\StorageInterface;
1718
use Symfony\Component\RateLimiter\Util\TimeUtil;
1819

@@ -33,49 +34,72 @@ final class FixedWindowLimiter implements LimiterInterface
3334

3435
public function __construct(string $id, int $limit, \DateInterval $interval, StorageInterface $storage, ?LockInterface $lock = null)
3536
{
37+
if ($limit < 1) {
38+
throw new \InvalidArgumentException(sprintf('Cannot set the limit of "%s" to 0, as that would never accept any hit.', __CLASS__));
39+
}
40+
3641
$this->storage = $storage;
3742
$this->lock = $lock ?? new NoLock();
3843
$this->id = $id;
3944
$this->limit = $limit;
4045
$this->interval = TimeUtil::dateIntervalToSeconds($interval);
4146
}
4247

43-
/**
44-
* {@inheritdoc}
45-
*/
46-
public function consume(int $tokens = 1): Limit
48+
public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation
4749
{
50+
if ($tokens > $this->limit) {
51+
throw new \InvalidArgumentException(sprintf('Cannot reserve more tokens (%d) than the size of the rate limiter (%d).', $tokens, $this->limit));
52+
}
53+
4854
$this->lock->acquire(true);
4955

5056
try {
5157
$window = $this->storage->fetch($this->id);
5258
if (!$window instanceof Window) {
53-
$window = new Window($this->id, $this->interval);
59+
$window = new Window($this->id, $this->interval, $this->limit);
5460
}
5561

56-
$hitCount = $window->getHitCount();
57-
$availableTokens = $this->getAvailableTokens($hitCount);
58-
$windowStart = \DateTimeImmutable::createFromFormat('U', time());
59-
if ($availableTokens < $tokens) {
60-
return new Limit($availableTokens, $this->getRetryAfter($windowStart), false);
61-
}
62+
$now = microtime(true);
63+
$availableTokens = $window->getAvailableTokens($now);
64+
if ($availableTokens >= $tokens) {
65+
$window->add($tokens);
6266

63-
$window->add($tokens);
64-
$this->storage->save($window);
67+
$reservation = new Reservation($now, new Limit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now)), true));
68+
} else {
69+
$remainingTokens = $tokens - $availableTokens;
70+
$waitDuration = $window->calculateTimeForTokens($remainingTokens);
6571

66-
return new Limit($this->getAvailableTokens($window->getHitCount()), $this->getRetryAfter($windowStart), true);
72+
if (null !== $maxTime && $waitDuration > $maxTime) {
73+
// process needs to wait longer than set interval
74+
throw new MaxWaitDurationExceededException(sprintf('The rate limiter wait time ("%d" seconds) is longer than the provided maximum time ("%d" seconds).', $waitDuration, $maxTime), new Limit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false));
75+
}
76+
77+
$window->add($tokens);
78+
79+
$reservation = new Reservation($now + $waitDuration, new Limit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false));
80+
}
81+
$this->storage->save($window);
6782
} finally {
6883
$this->lock->release();
6984
}
85+
86+
return $reservation;
7087
}
7188

72-
public function getAvailableTokens(int $hitCount): int
89+
/**
90+
* {@inheritdoc}
91+
*/
92+
public function consume(int $tokens = 1): Limit
7393
{
74-
return $this->limit - $hitCount;
94+
try {
95+
return $this->reserve($tokens, 0)->getLimit();
96+
} catch (MaxWaitDurationExceededException $e) {
97+
return $e->getLimit();
98+
}
7599
}
76100

77-
private function getRetryAfter(\DateTimeImmutable $windowStart): \DateTimeImmutable
101+
public function getAvailableTokens(int $hitCount): int
78102
{
79-
return $windowStart->add(new \DateInterval(sprintf('PT%sS', $this->interval)));
103+
return $this->limit - $hitCount;
80104
}
81105
}

src/Symfony/Component/RateLimiter/LimiterInterface.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,32 @@
1111

1212
namespace Symfony\Component\RateLimiter;
1313

14+
use Symfony\Component\RateLimiter\Exception\MaxWaitDurationExceededException;
15+
use Symfony\Component\RateLimiter\Exception\ReserveNotSupportedException;
16+
1417
/**
1518
* @author Wouter de Jong <wouter@wouterj.nl>
1619
*
1720
* @experimental in 5.2
1821
*/
1922
interface LimiterInterface
2023
{
24+
/**
25+
* Waits until the required number of tokens is available.
26+
*
27+
* The reserved tokens will be taken into account when calculating
28+
* future token consumptions. Do not use this method if you intend
29+
* to skip this process.
30+
*
31+
* @param int $tokens the number of tokens required
32+
* @param float $maxTime maximum accepted waiting time in seconds
33+
*
34+
* @throws MaxWaitDurationExceededException if $maxTime is set and the process needs to wait longer than its value (in seconds)
35+
* @throws ReserveNotSupportedException if this limiter implementation doesn't support reserving tokens
36+
* @throws \InvalidArgumentException if $tokens is larger than the maximum burst size
37+
*/
38+
public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation;
39+
2140
/**
2241
* Use this method if you intend to drop if the required number
2342
* of tokens is unavailable.

src/Symfony/Component/RateLimiter/NoLimiter.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323
*/
2424
final class NoLimiter implements LimiterInterface
2525
{
26+
public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation
27+
{
28+
return new Reservation(time(), new Limit(\INF, new \DateTimeImmutable(), true));
29+
}
30+
2631
public function consume(int $tokens = 1): Limit
2732
{
2833
return new Limit(\INF, new \DateTimeImmutable(), true);

src/Symfony/Component/RateLimiter/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ $ composer require symfony/rate-limiter
1818

1919
```php
2020
use Symfony\Component\RateLimiter\Storage\InMemoryStorage;
21-
use Symfony\Component\RateLimiter\Limiter;
21+
use Symfony\Component\RateLimiter\RateLimiter;
2222

23-
$limiter = new Limiter([
23+
$limiter = new RateLimiter([
2424
'id' => 'login',
2525
'strategy' => 'token_bucket', // or 'fixed_window'
2626
'limit' => 10,

0 commit comments

Comments
 (0)
0