Skip to content

Commit cc2b5ac

Browse files
committed
Introduced RateLimiter component
1 parent e6e1ca3 commit cc2b5ac

20 files changed

+839
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/Tests export-ignore
2+
/phpunit.xml.dist export-ignore
3+
/.gitattributes export-ignore
4+
/.gitignore export-ignore
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
composer.lock
2+
phpunit.xml
3+
vendor/
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHANGELOG
2+
=========
3+
4+
5.2.0
5+
-----
6+
7+
* added the component
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
class MaxWaitDurationExceededException extends \RuntimeException
18+
{
19+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2016-2020 Fabien Potencier
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is furnished
8+
to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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 Symfony\Component\Lock\LockInterface;
15+
use Symfony\Component\RateLimiter\Exception\MaxWaitDurationExceededException;
16+
use Symfony\Component\RateLimiter\Storage\StorageInterface;
17+
18+
/**
19+
* @author Wouter de Jong <wouter@wouterj.nl>
20+
*/
21+
class Limiter implements LimiterInterface
22+
{
23+
private $maxBurst;
24+
private $bucket;
25+
private $storage;
26+
private $lock;
27+
28+
public function __construct(float $maxBurst, TokenBucket $bucket, StorageInterface $storage, LockInterface $lock)
29+
{
30+
$this->maxBurst = $maxBurst;
31+
$this->bucket = $bucket;
32+
$this->storage = $storage;
33+
$this->lock = $lock;
34+
}
35+
36+
/**
37+
* {@inheritDoc}
38+
*
39+
* @throws \InvalidArgumentException if $tokens is larger than the maximum burst size
40+
*/
41+
public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation
42+
{
43+
if ($tokens > $this->maxBurst) {
44+
throw new \InvalidArgumentException(sprintf('Cannot reserve more tokens (%d) than the burst size of the rate limiter (%d).', $tokens, $this->maxBurst));
45+
}
46+
47+
$this->lock->acquire(true);
48+
49+
try {
50+
$now = microtime(true);
51+
$bucket = $this->storage->fetch($this->bucket->getId());
52+
if (null !== $bucket) {
53+
$this->bucket = $bucket;
54+
}
55+
$elapsed = $now - $this->bucket->getTimer();
56+
57+
$availableTokens = $this->bucket->getAvailableTokens($elapsed);
58+
if ($availableTokens >= $tokens) {
59+
// tokens are now available, update bucket
60+
$this->bucket->setTokens($availableTokens - $tokens);
61+
$this->bucket->setTimer($now);
62+
63+
$reservation = new Reservation($tokens, $now);
64+
} else {
65+
$remainingTokens = $tokens - $availableTokens;
66+
$waitDuration = $this->bucket->calculateTimeForTokens($remainingTokens);
67+
68+
if (null !== $maxTime && $waitDuration > $maxTime) {
69+
// process needs to wait longer than set interval
70+
throw new MaxWaitDurationExceededException(sprintf('The rate limiter wait time ("%d" seconds) is longer than the provided maximum time ("%d" seconds).', $waitDuration, $maxTime));
71+
}
72+
73+
// at $now + $waitDuration all tokens will be reserved for this process,
74+
// so no tokens are left for other processes.
75+
$this->bucket->setTokens(0);
76+
$this->bucket->setTimer($now + $waitDuration);
77+
78+
$reservation = new Reservation($tokens, $this->bucket->getTimer());
79+
}
80+
81+
$this->storage->save($this->bucket);
82+
} finally {
83+
$this->lock->release();
84+
}
85+
86+
return $reservation;
87+
}
88+
89+
/**
90+
* Use this method if you intend to drop if the required number
91+
* of tokens is unavailable.
92+
*
93+
* @param int $tokens the number of tokens required
94+
*/
95+
public function consume(int $tokens = 1): bool
96+
{
97+
try {
98+
$this->reserve($tokens, 0);
99+
100+
return true;
101+
} catch (MaxWaitDurationExceededException $e) {
102+
return false;
103+
}
104+
}
105+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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 Symfony\Component\RateLimiter\Storage\InMemoryStorage;
15+
16+
/**
17+
* @author Wouter de Jong <wouter@wouterj.nl>
18+
*/
19+
class LimiterFactory
20+
{
21+
/**
22+
* Creates a limiter of size $burst which is filled at $rate.
23+
*/
24+
public function createLimiter(int $burst, Rate $rate, ?string $id = null): LimiterInterface
25+
{
26+
$bucketId = $id ?? uniqid();
27+
28+
return new Limiter($bucketId, $burst, new InMemoryStorage(new TokenBucket($bucketId, $burst, $rate, microtime(true))), new NoLock());
29+
}
30+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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 Symfony\Component\RateLimiter\Exception\MaxWaitDurationExceededException;
15+
16+
/**
17+
* @author Wouter de Jong <wouter@wouterj.nl>
18+
*/
19+
interface LimiterInterface
20+
{
21+
/**
22+
* Use this method if you intend to wait until the required number
23+
* of tokens is available.
24+
*
25+
* The reserved tokens will be taken into account when calculating
26+
* future token consumptions. Do not use this method if you intend
27+
* to skip this process.
28+
*
29+
* @throws MaxWaitDurationExceededException if $maxTime is set and the process needs to wait longer than its value (in seconds)
30+
*/
31+
public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation;
32+
33+
/**
34+
* Use this method if you intend to drop if the required number
35+
* of tokens is unavailable.
36+
*
37+
* @param int $tokens the number of tokens required
38+
*/
39+
public function consume(int $tokens = 1): bool;
40+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
/**
15+
* Implements a non limiting limiter.
16+
*
17+
* This can be used in cases where an implementation requires a
18+
* limiter, but no rate limit should be enforced.
19+
*
20+
* @author Wouter de Jong <wouter@wouterj.nl>
21+
*/
22+
class NoLimiter implements LimiterInterface
23+
{
24+
public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation
25+
{
26+
return new Reservation($tokens, microtime(true));
27+
}
28+
29+
public function consume(int $tokens = 1): bool
30+
{
31+
return true;
32+
}
33+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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 Symfony\Component\Lock\LockInterface;
15+
16+
/**
17+
* A non locking lock.
18+
*
19+
* This is used when no mutex is required for this rate limiter
20+
* (e.g. when using in memory or session storage).
21+
*
22+
* @author Wouter de Jong <wouter@wouterj.nl>
23+
*
24+
* @final
25+
*/
26+
class NoLock implements LockInterface
27+
{
28+
public function acquire(bool $blocking = false)
29+
{
30+
}
31+
32+
public function refresh(float $ttl = null)
33+
{
34+
}
35+
36+
public function isAcquired()
37+
{
38+
return true;
39+
}
40+
41+
public function release()
42+
{
43+
}
44+
45+
public function isExpired()
46+
{
47+
return false;
48+
}
49+
50+
public function getRemainingLifetime()
51+
{
52+
}
53+
}

0 commit comments

Comments
 (0)