[RateLimiter] rename Limit to RateLimit and add RateLimit::getLimit()

This commit is contained in:
Kevin Bond 2020-10-16 12:07:55 -04:00 committed by Fabien Potencier
parent 1d445cce63
commit c5361cfc58
16 changed files with 110 additions and 92 deletions

View File

@ -12,9 +12,9 @@
namespace Symfony\Component\HttpFoundation\RateLimiter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\RateLimiter\Limit;
use Symfony\Component\RateLimiter\LimiterInterface;
use Symfony\Component\RateLimiter\NoLimiter;
use Symfony\Component\RateLimiter\RateLimit;
/**
* An implementation of RequestRateLimiterInterface that
@ -26,23 +26,23 @@ use Symfony\Component\RateLimiter\NoLimiter;
*/
abstract class AbstractRequestRateLimiter implements RequestRateLimiterInterface
{
public function consume(Request $request): Limit
public function consume(Request $request): RateLimit
{
$limiters = $this->getLimiters($request);
if (0 === \count($limiters)) {
$limiters = [new NoLimiter()];
}
$minimalLimit = null;
$minimalRateLimit = null;
foreach ($limiters as $limiter) {
$limit = $limiter->consume(1);
$rateLimit = $limiter->consume(1);
if (null === $minimalLimit || $limit->getRemainingTokens() < $minimalLimit->getRemainingTokens()) {
$minimalLimit = $limit;
if (null === $minimalRateLimit || $rateLimit->getRemainingTokens() < $minimalRateLimit->getRemainingTokens()) {
$minimalRateLimit = $rateLimit;
}
}
return $minimalLimit;
return $minimalRateLimit;
}
public function reset(Request $request): void

View File

@ -12,7 +12,7 @@
namespace Symfony\Component\HttpFoundation\RateLimiter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\RateLimiter\Limit;
use Symfony\Component\RateLimiter\RateLimit;
/**
* A special type of limiter that deals with requests.
@ -26,7 +26,7 @@ use Symfony\Component\RateLimiter\Limit;
*/
interface RequestRateLimiterInterface
{
public function consume(Request $request): Limit;
public function consume(Request $request): RateLimit;
public function reset(Request $request): void;
}

View File

@ -38,18 +38,18 @@ final class CompoundLimiter implements LimiterInterface
throw new ReserveNotSupportedException(__CLASS__);
}
public function consume(int $tokens = 1): Limit
public function consume(int $tokens = 1): RateLimit
{
$minimalLimit = null;
$minimalRateLimit = null;
foreach ($this->limiters as $limiter) {
$limit = $limiter->consume($tokens);
$rateLimit = $limiter->consume($tokens);
if (null === $minimalLimit || $limit->getRemainingTokens() < $minimalLimit->getRemainingTokens()) {
$minimalLimit = $limit;
if (null === $minimalRateLimit || $rateLimit->getRemainingTokens() < $minimalRateLimit->getRemainingTokens()) {
$minimalRateLimit = $rateLimit;
}
}
return $minimalLimit;
return $minimalRateLimit;
}
public function reset(): void

View File

@ -11,7 +11,7 @@
namespace Symfony\Component\RateLimiter\Exception;
use Symfony\Component\RateLimiter\Limit;
use Symfony\Component\RateLimiter\RateLimit;
/**
* @author Wouter de Jong <wouter@wouterj.nl>
@ -20,17 +20,17 @@ use Symfony\Component\RateLimiter\Limit;
*/
class MaxWaitDurationExceededException extends \RuntimeException
{
private $limit;
private $rateLimit;
public function __construct(string $message, Limit $limit, int $code = 0, ?\Throwable $previous = null)
public function __construct(string $message, RateLimit $rateLimit, int $code = 0, ?\Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->limit = $limit;
$this->rateLimit = $rateLimit;
}
public function getLimit(): Limit
public function getRateLimit(): RateLimit
{
return $this->limit;
return $this->rateLimit;
}
}

View File

@ -11,7 +11,7 @@
namespace Symfony\Component\RateLimiter\Exception;
use Symfony\Component\RateLimiter\Limit;
use Symfony\Component\RateLimiter\RateLimit;
/**
* @author Kevin Bond <kevinbond@gmail.com>
@ -20,27 +20,32 @@ use Symfony\Component\RateLimiter\Limit;
*/
class RateLimitExceededException extends \RuntimeException
{
private $limit;
private $rateLimit;
public function __construct(Limit $limit, $code = 0, \Throwable $previous = null)
public function __construct(RateLimit $rateLimit, $code = 0, \Throwable $previous = null)
{
parent::__construct('Rate Limit Exceeded', $code, $previous);
$this->limit = $limit;
$this->rateLimit = $rateLimit;
}
public function getLimit(): Limit
public function getRateLimit(): RateLimit
{
return $this->limit;
return $this->rateLimit;
}
public function getRetryAfter(): \DateTimeImmutable
{
return $this->limit->getRetryAfter();
return $this->rateLimit->getRetryAfter();
}
public function getRemainingTokens(): int
{
return $this->limit->getRemainingTokens();
return $this->rateLimit->getRemainingTokens();
}
public function getLimit(): int
{
return $this->rateLimit->getLimit();
}
}

View File

@ -64,19 +64,19 @@ final class FixedWindowLimiter implements LimiterInterface
if ($availableTokens >= $tokens) {
$window->add($tokens);
$reservation = new Reservation($now, new Limit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now)), true));
$reservation = new Reservation($now, new RateLimit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now)), true, $this->limit));
} else {
$remainingTokens = $tokens - $availableTokens;
$waitDuration = $window->calculateTimeForTokens($remainingTokens);
if (null !== $maxTime && $waitDuration > $maxTime) {
// process needs to wait longer than set interval
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));
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));
}
$window->add($tokens);
$reservation = new Reservation($now + $waitDuration, new Limit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false));
$reservation = new Reservation($now + $waitDuration, new RateLimit($window->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->limit));
}
$this->storage->save($window);
} finally {
@ -89,12 +89,12 @@ final class FixedWindowLimiter implements LimiterInterface
/**
* {@inheritdoc}
*/
public function consume(int $tokens = 1): Limit
public function consume(int $tokens = 1): RateLimit
{
try {
return $this->reserve($tokens, 0)->getLimit();
return $this->reserve($tokens, 0)->getRateLimit();
} catch (MaxWaitDurationExceededException $e) {
return $e->getLimit();
return $e->getRateLimit();
}
}

View File

@ -43,7 +43,7 @@ interface LimiterInterface
*
* @param int $tokens the number of tokens required
*/
public function consume(int $tokens = 1): Limit;
public function consume(int $tokens = 1): RateLimit;
/**
* Resets the limit.

View File

@ -25,12 +25,12 @@ final class NoLimiter implements LimiterInterface
{
public function reserve(int $tokens = 1, ?float $maxTime = null): Reservation
{
return new Reservation(time(), new Limit(\INF, new \DateTimeImmutable(), true));
return new Reservation(time(), new RateLimit(\INF, new \DateTimeImmutable(), true, \INF));
}
public function consume(int $tokens = 1): Limit
public function consume(int $tokens = 1): RateLimit
{
return new Limit(\INF, new \DateTimeImmutable(), true);
return new RateLimit(\INF, new \DateTimeImmutable(), true, \INF);
}
public function reset(): void

View File

@ -18,17 +18,19 @@ use Symfony\Component\RateLimiter\Exception\RateLimitExceededException;
*
* @experimental in 5.2
*/
class Limit
class RateLimit
{
private $availableTokens;
private $retryAfter;
private $accepted;
private $limit;
public function __construct(int $availableTokens, \DateTimeImmutable $retryAfter, bool $accepted)
public function __construct(int $availableTokens, \DateTimeImmutable $retryAfter, bool $accepted, int $limit)
{
$this->availableTokens = $availableTokens;
$this->retryAfter = $retryAfter;
$this->accepted = $accepted;
$this->limit = $limit;
}
public function isAccepted(): bool
@ -58,6 +60,11 @@ class Limit
return $this->availableTokens;
}
public function getLimit(): int
{
return $this->limit;
}
public function wait(): void
{
sleep(($this->retryAfter->getTimestamp() - time()) * 1e6);

View File

@ -19,15 +19,15 @@ namespace Symfony\Component\RateLimiter;
final class Reservation
{
private $timeToAct;
private $limit;
private $rateLimit;
/**
* @param float $timeToAct Unix timestamp in seconds when this reservation should act
*/
public function __construct(float $timeToAct, Limit $limit)
public function __construct(float $timeToAct, RateLimit $rateLimit)
{
$this->timeToAct = $timeToAct;
$this->limit = $limit;
$this->rateLimit = $rateLimit;
}
public function getTimeToAct(): float
@ -40,9 +40,9 @@ final class Reservation
return max(0, (-microtime(true)) + $this->timeToAct);
}
public function getLimit(): Limit
public function getRateLimit(): RateLimit
{
return $this->limit;
return $this->rateLimit;
}
public function wait(): void

View File

@ -76,7 +76,7 @@ final class SlidingWindowLimiter implements LimiterInterface
/**
* {@inheritdoc}
*/
public function consume(int $tokens = 1): Limit
public function consume(int $tokens = 1): RateLimit
{
$this->lock->acquire(true);
@ -91,13 +91,13 @@ final class SlidingWindowLimiter implements LimiterInterface
$hitCount = $window->getHitCount();
$availableTokens = $this->getAvailableTokens($hitCount);
if ($availableTokens < $tokens) {
return new Limit($availableTokens, $window->getRetryAfter(), false);
return new RateLimit($availableTokens, $window->getRetryAfter(), false, $this->limit);
}
$window->add($tokens);
$this->storage->save($window);
return new Limit($this->getAvailableTokens($window->getHitCount()), $window->getRetryAfter(), true);
return new RateLimit($this->getAvailableTokens($window->getHitCount()), $window->getRetryAfter(), true, $this->limit);
} finally {
$this->lock->release();
}

View File

@ -41,10 +41,12 @@ class FixedWindowLimiterTest extends TestCase
sleep(5);
}
$limit = $limiter->consume();
$this->assertTrue($limit->isAccepted());
$limit = $limiter->consume();
$this->assertFalse($limit->isAccepted());
$rateLimit = $limiter->consume();
$this->assertSame(10, $rateLimit->getLimit());
$this->assertTrue($rateLimit->isAccepted());
$rateLimit = $limiter->consume();
$this->assertFalse($rateLimit->isAccepted());
$this->assertSame(10, $rateLimit->getLimit());
}
public function testConsumeOutsideInterval()
@ -58,18 +60,18 @@ class FixedWindowLimiterTest extends TestCase
$limiter->consume(9);
// ...try bursting again at the start of the next window
sleep(10);
$limit = $limiter->consume(10);
$this->assertEquals(0, $limit->getRemainingTokens());
$this->assertTrue($limit->isAccepted());
$rateLimit = $limiter->consume(10);
$this->assertEquals(0, $rateLimit->getRemainingTokens());
$this->assertTrue($rateLimit->isAccepted());
}
public function testWrongWindowFromCache()
{
$this->storage->save(new DummyWindow());
$limiter = $this->createLimiter();
$limit = $limiter->consume();
$this->assertTrue($limit->isAccepted());
$this->assertEquals(9, $limit->getRemainingTokens());
$rateLimit = $limiter->consume();
$this->assertTrue($rateLimit->isAccepted());
$this->assertEquals(9, $rateLimit->getRemainingTokens());
}
private function createLimiter(): FixedWindowLimiter

View File

@ -13,25 +13,25 @@ namespace Symfony\Component\RateLimiter\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\RateLimiter\Exception\RateLimitExceededException;
use Symfony\Component\RateLimiter\Limit;
use Symfony\Component\RateLimiter\RateLimit;
class LimitTest extends TestCase
class RateLimitTest extends TestCase
{
public function testEnsureAcceptedDoesNotThrowExceptionIfAccepted()
{
$limit = new Limit(10, new \DateTimeImmutable(), true);
$rateLimit = new RateLimit(10, new \DateTimeImmutable(), true, 10);
$this->assertSame($limit, $limit->ensureAccepted());
$this->assertSame($rateLimit, $rateLimit->ensureAccepted());
}
public function testEnsureAcceptedThrowsRateLimitExceptionIfNotAccepted()
{
$limit = new Limit(10, $retryAfter = new \DateTimeImmutable(), false);
$rateLimit = new RateLimit(10, $retryAfter = new \DateTimeImmutable(), false, 10);
try {
$limit->ensureAccepted();
$rateLimit->ensureAccepted();
} catch (RateLimitExceededException $exception) {
$this->assertSame($limit, $exception->getLimit());
$this->assertSame($rateLimit, $exception->getRateLimit());
$this->assertSame(10, $exception->getRemainingTokens());
$this->assertSame($retryAfter, $exception->getRetryAfter());

View File

@ -38,17 +38,19 @@ class SlidingWindowLimiterTest extends TestCase
$limiter->consume(8);
sleep(15);
$limit = $limiter->consume();
$this->assertTrue($limit->isAccepted());
$rateLimit = $limiter->consume();
$this->assertTrue($rateLimit->isAccepted());
$this->assertSame(10, $rateLimit->getLimit());
// We are 25% into the new window
$limit = $limiter->consume(5);
$this->assertFalse($limit->isAccepted());
$this->assertEquals(3, $limit->getRemainingTokens());
$rateLimit = $limiter->consume(5);
$this->assertFalse($rateLimit->isAccepted());
$this->assertEquals(3, $rateLimit->getRemainingTokens());
sleep(13);
$limit = $limiter->consume(10);
$this->assertTrue($limit->isAccepted());
$rateLimit = $limiter->consume(10);
$this->assertTrue($rateLimit->isAccepted());
$this->assertSame(10, $rateLimit->getLimit());
}
public function testReserve()

View File

@ -74,26 +74,28 @@ class TokenBucketLimiterTest extends TestCase
$limiter = $this->createLimiter(10, $rate);
// enough free tokens
$limit = $limiter->consume(5);
$this->assertTrue($limit->isAccepted());
$this->assertEquals(5, $limit->getRemainingTokens());
$this->assertEqualsWithDelta(time(), $limit->getRetryAfter()->getTimestamp(), 1);
$rateLimit = $limiter->consume(5);
$this->assertTrue($rateLimit->isAccepted());
$this->assertEquals(5, $rateLimit->getRemainingTokens());
$this->assertEqualsWithDelta(time(), $rateLimit->getRetryAfter()->getTimestamp(), 1);
$this->assertSame(10, $rateLimit->getLimit());
// there are only 5 available free tokens left now
$limit = $limiter->consume(10);
$this->assertEquals(5, $limit->getRemainingTokens());
$rateLimit = $limiter->consume(10);
$this->assertEquals(5, $rateLimit->getRemainingTokens());
$limit = $limiter->consume(5);
$this->assertEquals(0, $limit->getRemainingTokens());
$this->assertEqualsWithDelta(time(), $limit->getRetryAfter()->getTimestamp(), 1);
$rateLimit = $limiter->consume(5);
$this->assertEquals(0, $rateLimit->getRemainingTokens());
$this->assertEqualsWithDelta(time(), $rateLimit->getRetryAfter()->getTimestamp(), 1);
$this->assertSame(10, $rateLimit->getLimit());
}
public function testWrongWindowFromCache()
{
$this->storage->save(new DummyWindow());
$limiter = $this->createLimiter();
$limit = $limiter->consume();
$this->assertTrue($limit->isAccepted());
$this->assertEquals(9, $limit->getRemainingTokens());
$rateLimit = $limiter->consume();
$this->assertTrue($rateLimit->isAccepted());
$this->assertEquals(9, $rateLimit->getRemainingTokens());
}
private function createLimiter($initialTokens = 10, Rate $rate = null)

View File

@ -74,16 +74,16 @@ final class TokenBucketLimiter implements LimiterInterface
$bucket->setTokens($availableTokens - $tokens);
$bucket->setTimer($now);
$reservation = new Reservation($now, new Limit($bucket->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now)), true));
$reservation = new Reservation($now, new RateLimit($bucket->getAvailableTokens($now), \DateTimeImmutable::createFromFormat('U', floor($now)), true, $this->maxBurst));
} else {
$remainingTokens = $tokens - $availableTokens;
$waitDuration = $this->rate->calculateTimeForTokens($remainingTokens);
if (null !== $maxTime && $waitDuration > $maxTime) {
// process needs to wait longer than set interval
$limit = new Limit($availableTokens, \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false);
$rateLimit = new RateLimit($availableTokens, \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->maxBurst);
throw new MaxWaitDurationExceededException(sprintf('The rate limiter wait time ("%d" seconds) is longer than the provided maximum time ("%d" seconds).', $waitDuration, $maxTime), $limit);
throw new MaxWaitDurationExceededException(sprintf('The rate limiter wait time ("%d" seconds) is longer than the provided maximum time ("%d" seconds).', $waitDuration, $maxTime), $rateLimit);
}
// at $now + $waitDuration all tokens will be reserved for this process,
@ -91,7 +91,7 @@ final class TokenBucketLimiter implements LimiterInterface
$bucket->setTokens(0);
$bucket->setTimer($now + $waitDuration);
$reservation = new Reservation($bucket->getTimer(), new Limit(0, \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false));
$reservation = new Reservation($bucket->getTimer(), new RateLimit(0, \DateTimeImmutable::createFromFormat('U', floor($now + $waitDuration)), false, $this->maxBurst));
}
$this->storage->save($bucket);
@ -105,12 +105,12 @@ final class TokenBucketLimiter implements LimiterInterface
/**
* {@inheritdoc}
*/
public function consume(int $tokens = 1): Limit
public function consume(int $tokens = 1): RateLimit
{
try {
return $this->reserve($tokens, 0)->getLimit();
return $this->reserve($tokens, 0)->getRateLimit();
} catch (MaxWaitDurationExceededException $e) {
return $e->getLimit();
return $e->getRateLimit();
}
}
}