Last active
February 8, 2023 23:16
-
-
Save rodrigopedra/ca36827783f1acdb065a98838014b8ce to your computer and use it in GitHub Desktop.
Laravel 8 Job Rate Limiter that can be used without Redis
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Providers; | |
use Illuminate\Cache\RateLimiter; | |
use Illuminate\Cache\RateLimiting\Limit; | |
use Illuminate\Support\ServiceProvider; | |
class AppServiceProvider extends ServiceProvider | |
{ | |
/** | |
* Register any application services. | |
* | |
* @return void | |
*/ | |
public function register() | |
{ | |
} | |
/** | |
* Bootstrap any application services. | |
* | |
* @param \Illuminate\Cache\RateLimiter $rateLimiter | |
* @return void | |
*/ | |
public function boot(RateLimiter $rateLimiter) | |
{ | |
// important to call ->by(), so the limiter does not try to key | |
// by request info (route, IP, ...) as we are going to use this limiter to | |
// throttle a queued job | |
$rateLimiter->for('sample-job', static fn () => Limit::perMinute(10)->by('sample-job')); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Support\JobsMiddleware; | |
use Illuminate\Cache\RateLimiter; | |
use Illuminate\Cache\RateLimiting\Limit; | |
use Illuminate\Cache\RateLimiting\Unlimited; | |
use Illuminate\Support\Arr; | |
use Illuminate\Support\Collection; | |
class RateLimited | |
{ | |
private RateLimiter $rateLimiter; | |
public function __construct(RateLimiter $rateLimiter) | |
{ | |
$this->rateLimiter = $rateLimiter; | |
} | |
public static function middleware(string $name, bool $shouldSkipOnSync = false): string | |
{ | |
return self::class . ':' . $name . ',' . ($shouldSkipOnSync ? 'true' : 'false'); | |
} | |
public function handle($job, \Closure $next, string $name, string $shouldSkipOnSync) | |
{ | |
if ($job->connection === 'sync' && $shouldSkipOnSync === 'true') { | |
return $next($job); | |
} | |
$releaseDelay = $this->calculateReleaseDelay($name, $job); | |
if ($releaseDelay > 0) { | |
return $job->release($releaseDelay); | |
} | |
return $next($job); | |
} | |
private function calculateReleaseDelay(string $name, $job): int | |
{ | |
$limiters = Collection::make($this->resolvetRateLimit($name, $job))->filter(); | |
$releaseDelay = $limiters->reduce(function (int $releaseDelay, Limit $limit) { | |
if ($limit instanceof Unlimited) { | |
return $releaseDelay; | |
} | |
if ($this->rateLimiter->tooManyAttempts($limit->key, $limit->maxAttempts)) { | |
return \max($this->limitWillBeAvaialableIn($limit), $releaseDelay); | |
} | |
return $releaseDelay; | |
}, 0); | |
if ($releaseDelay === 0) { | |
$limiters->each(function (Limit $limit) { | |
$this->rateLimiter->hit($limit->key, $limit->decayMinutes * 60); | |
}); | |
} | |
return $releaseDelay; | |
} | |
private function resolvetRateLimit(string $name, $job): array | |
{ | |
$callback = $this->rateLimiter->limiter($name); | |
if (\is_null($callback)) { | |
return []; | |
} | |
return Arr::wrap(\call_user_func($callback, $job)); | |
} | |
private function limitWillBeAvaialableIn(Limit $limit): int | |
{ | |
return ($this->rateLimiter->availableIn($limit->key) ?? 0) + 5; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Jobs; | |
use Carbon\Carbon; | |
use Illuminate\Bus\Queueable; | |
use Illuminate\Contracts\Queue\ShouldQueue; | |
use Illuminate\Queue\InteractsWithQueue; | |
use Illuminate\Queue\SerializesModels; | |
use Psr\Log\LoggerInterface; | |
use Support\JobsMiddleware\RateLimited; | |
class SampleJob implements ShouldQueue | |
{ | |
use InteractsWithQueue; | |
use Queueable; | |
use SerializesModels; | |
public int $maxExceptions = 1; | |
public function middleware() | |
{ | |
// the 'sample-job' rate limit key needs to be configured | |
// in a service provider | |
return [RateLimited::middleware('sample-job')]; | |
} | |
public function retryUntil() | |
{ | |
return Carbon::now()->addHours(6); | |
} | |
public function handle(LoggerInterface $logger): void | |
{ | |
$logger->info('[SAMPLE JOB] Running sample job'); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment