Skip to content

Commit

Permalink
Per-second rate limiting (laravel#48498)
Browse files Browse the repository at this point in the history
  • Loading branch information
timacdonald authored Oct 26, 2023
1 parent 6c7f670 commit 800e264
Show file tree
Hide file tree
Showing 14 changed files with 472 additions and 38 deletions.
6 changes: 3 additions & 3 deletions src/Illuminate/Cache/RateLimiting/GlobalLimit.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ class GlobalLimit extends Limit
* Create a new limit instance.
*
* @param int $maxAttempts
* @param int $decayMinutes
* @param int $decaySeconds
* @return void
*/
public function __construct(int $maxAttempts, int $decayMinutes = 1)
public function __construct(int $maxAttempts, int $decaySeconds = 60)
{
parent::__construct('', $maxAttempts, $decayMinutes);
parent::__construct('', $maxAttempts, $decaySeconds);
}
}
31 changes: 21 additions & 10 deletions src/Illuminate/Cache/RateLimiting/Limit.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ class Limit
public $key;

/**
* The maximum number of attempts allowed within the given number of minutes.
* The maximum number of attempts allowed within the given number of seconds.
*
* @var int
*/
public $maxAttempts;

/**
* The number of minutes until the rate limit is reset.
* The number of seconds until the rate limit is reset.
*
* @var int
*/
public $decayMinutes;
public $decaySeconds;

/**
* The response generator callback.
Expand All @@ -37,14 +37,25 @@ class Limit
*
* @param mixed $key
* @param int $maxAttempts
* @param int $decayMinutes
* @param int $decaySeconds
* @return void
*/
public function __construct($key = '', int $maxAttempts = 60, int $decayMinutes = 1)
public function __construct($key = '', int $maxAttempts = 60, int $decaySeconds = 60)
{
$this->key = $key;
$this->maxAttempts = $maxAttempts;
$this->decayMinutes = $decayMinutes;
$this->decaySeconds = $decaySeconds;
}

/**
* Create a new rate limit.
*
* @param int $maxAttempts
* @return static
*/
public static function perSecond($maxAttempts)
{
return new static('', $maxAttempts, 1);
}

/**
Expand All @@ -55,7 +66,7 @@ public function __construct($key = '', int $maxAttempts = 60, int $decayMinutes
*/
public static function perMinute($maxAttempts)
{
return new static('', $maxAttempts);
return new static('', $maxAttempts, 60);
}

/**
Expand All @@ -67,7 +78,7 @@ public static function perMinute($maxAttempts)
*/
public static function perMinutes($decayMinutes, $maxAttempts)
{
return new static('', $maxAttempts, $decayMinutes);
return new static('', $maxAttempts, 60 * $decayMinutes);
}

/**
Expand All @@ -79,7 +90,7 @@ public static function perMinutes($decayMinutes, $maxAttempts)
*/
public static function perHour($maxAttempts, $decayHours = 1)
{
return new static('', $maxAttempts, 60 * $decayHours);
return new static('', $maxAttempts, 60 * 60 * $decayHours);
}

/**
Expand All @@ -91,7 +102,7 @@ public static function perHour($maxAttempts, $decayHours = 1)
*/
public static function perDay($maxAttempts, $decayDays = 1)
{
return new static('', $maxAttempts, 60 * 24 * $decayDays);
return new static('', $maxAttempts, 60 * 60 * 24 * $decayDays);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Illuminate/Foundation/Exceptions/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ protected function shouldntReport(Throwable $e)
with($throttle->key ?: 'illuminate:foundation:exceptions:'.$e::class, fn ($key) => $this->hashThrottleKeys ? md5($key) : $key),
$throttle->maxAttempts,
fn () => true,
60 * $throttle->decayMinutes
$throttle->decaySeconds
);
}), rescue: false, report: false);
}
Expand Down
4 changes: 2 additions & 2 deletions src/Illuminate/Queue/Middleware/RateLimited.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public function handle($job, $next)
return (object) [
'key' => md5($this->limiterName.$limit->key),
'maxAttempts' => $limit->maxAttempts,
'decayMinutes' => $limit->decayMinutes,
'decaySeconds' => $limit->decaySeconds,
];
})->all()
);
Expand All @@ -92,7 +92,7 @@ protected function handleJob($job, $next, array $limits)
: false;
}

$this->limiter->hit($limit->key, $limit->decayMinutes * 60);
$this->limiter->hit($limit->key, $limit->decaySeconds);
}

return $next($job);
Expand Down
8 changes: 4 additions & 4 deletions src/Illuminate/Queue/Middleware/RateLimitedWithRedis.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public function __construct($limiterName)
protected function handleJob($job, $next, array $limits)
{
foreach ($limits as $limit) {
if ($this->tooManyAttempts($limit->key, $limit->maxAttempts, $limit->decayMinutes)) {
if ($this->tooManyAttempts($limit->key, $limit->maxAttempts, $limit->decaySeconds)) {
return $this->shouldRelease
? $job->release($this->getTimeUntilNextRetry($limit->key))
: false;
Expand All @@ -64,13 +64,13 @@ protected function handleJob($job, $next, array $limits)
*
* @param string $key
* @param int $maxAttempts
* @param int $decayMinutes
* @param int $decaySeconds
* @return bool
*/
protected function tooManyAttempts($key, $maxAttempts, $decayMinutes)
protected function tooManyAttempts($key, $maxAttempts, $decaySeconds)
{
$limiter = new DurationLimiter(
$this->redis, $key, $maxAttempts, $decayMinutes * 60
$this->redis, $key, $maxAttempts, $decaySeconds
);

return tap(! $limiter->acquire(), function () use ($key, $limiter) {
Expand Down
12 changes: 6 additions & 6 deletions src/Illuminate/Queue/Middleware/ThrottlesExceptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ class ThrottlesExceptions
protected $maxAttempts;

/**
* The number of minutes until the maximum attempts are reset.
* The number of seconds until the maximum attempts are reset.
*
* @var int
*/
protected $decayMinutes;
protected $decaySeconds;

/**
* The number of minutes to wait before retrying the job after an exception.
Expand Down Expand Up @@ -68,13 +68,13 @@ class ThrottlesExceptions
* Create a new middleware instance.
*
* @param int $maxAttempts
* @param int $decayMinutes
* @param int $decaySeconds
* @return void
*/
public function __construct($maxAttempts = 10, $decayMinutes = 10)
public function __construct($maxAttempts = 10, $decaySeconds = 600)
{
$this->maxAttempts = $maxAttempts;
$this->decayMinutes = $decayMinutes;
$this->decaySeconds = $decaySeconds;
}

/**
Expand All @@ -101,7 +101,7 @@ public function handle($job, $next)
throw $throwable;
}

$this->limiter->hit($jobKey, $this->decayMinutes * 60);
$this->limiter->hit($jobKey, $this->decaySeconds);

return $job->release($this->retryAfterMinutes * 60);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public function handle($job, $next)
$this->redis = Container::getInstance()->make(Redis::class);

$this->limiter = new DurationLimiter(
$this->redis, $this->getKey($job), $this->maxAttempts, $this->decayMinutes * 60
$this->redis, $this->getKey($job), $this->maxAttempts, $this->decaySeconds
);

if ($this->limiter->tooManyAttempts()) {
Expand Down
6 changes: 3 additions & 3 deletions src/Illuminate/Routing/Middleware/ThrottleRequests.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes
(object) [
'key' => $prefix.$this->resolveRequestSignature($request),
'maxAttempts' => $this->resolveMaxAttempts($request, $maxAttempts),
'decayMinutes' => $decayMinutes,
'decaySeconds' => 60 * $decayMinutes,
'responseCallback' => null,
],
]
Expand Down Expand Up @@ -129,7 +129,7 @@ protected function handleRequestUsingNamedLimiter($request, Closure $next, $limi
return (object) [
'key' => self::$shouldHashKeys ? md5($limiterName.$limit->key) : $limiterName.':'.$limit->key,
'maxAttempts' => $limit->maxAttempts,
'decayMinutes' => $limit->decayMinutes,
'decaySeconds' => $limit->decaySeconds,
'responseCallback' => $limit->responseCallback,
];
})->all()
Expand All @@ -153,7 +153,7 @@ protected function handleRequest($request, Closure $next, array $limits)
throw $this->buildException($request, $limit->key, $limit->maxAttempts, $limit->responseCallback);
}

$this->limiter->hit($limit->key, $limit->decayMinutes * 60);
$this->limiter->hit($limit->key, $limit->decaySeconds);
}

$response = $next($request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public function __construct(RateLimiter $limiter, Redis $redis)
protected function handleRequest($request, Closure $next, array $limits)
{
foreach ($limits as $limit) {
if ($this->tooManyAttempts($limit->key, $limit->maxAttempts, $limit->decayMinutes)) {
if ($this->tooManyAttempts($limit->key, $limit->maxAttempts, $limit->decaySeconds)) {
throw $this->buildException($request, $limit->key, $limit->maxAttempts, $limit->responseCallback);
}
}
Expand All @@ -80,13 +80,13 @@ protected function handleRequest($request, Closure $next, array $limits)
*
* @param string $key
* @param int $maxAttempts
* @param int $decayMinutes
* @param int $decaySeconds
* @return mixed
*/
protected function tooManyAttempts($key, $maxAttempts, $decayMinutes)
protected function tooManyAttempts($key, $maxAttempts, $decaySeconds)
{
$limiter = new DurationLimiter(
$this->getRedisConnection(), $key, $maxAttempts, $decayMinutes * 60
$this->getRedisConnection(), $key, $maxAttempts, $decaySeconds
);

return tap(! $limiter->acquire(), function () use ($key, $limiter) {
Expand Down
41 changes: 41 additions & 0 deletions tests/Cache/LimitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace Illuminate\Tests\Cache;

use Illuminate\Cache\RateLimiting\GlobalLimit;
use Illuminate\Cache\RateLimiting\Limit;
use PHPUnit\Framework\TestCase;

class LimitTest extends TestCase
{
public function testConstructors()
{
$limit = new Limit('', 3, 1);
$this->assertSame(1, $limit->decaySeconds);
$this->assertSame(3, $limit->maxAttempts);

$limit = Limit::perSecond(3);
$this->assertSame(1, $limit->decaySeconds);
$this->assertSame(3, $limit->maxAttempts);

$limit = Limit::perMinute(3);
$this->assertSame(60, $limit->decaySeconds);
$this->assertSame(3, $limit->maxAttempts);

$limit = Limit::perMinutes(2, 3);
$this->assertSame(120, $limit->decaySeconds);
$this->assertSame(3, $limit->maxAttempts);

$limit = Limit::perHour(3);
$this->assertSame(3600, $limit->decaySeconds);
$this->assertSame(3, $limit->maxAttempts);

$limit = Limit::perDay(3);
$this->assertSame(86400, $limit->decaySeconds);
$this->assertSame(3, $limit->maxAttempts);

$limit = new GlobalLimit(3);
$this->assertSame(60, $limit->decaySeconds);
$this->assertSame(3, $limit->maxAttempts);
}
}
Loading

0 comments on commit 800e264

Please sign in to comment.