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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy