Skip to content

Instantly share code, notes, and snippets.

@rodrigopedra
Last active February 8, 2023 23:16
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save rodrigopedra/ca36827783f1acdb065a98838014b8ce to your computer and use it in GitHub Desktop.
Save rodrigopedra/ca36827783f1acdb065a98838014b8ce to your computer and use it in GitHub Desktop.
Laravel 8 Job Rate Limiter that can be used without Redis
<?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'));
}
}
<?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;
}
}
<?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