Job Middleware

Job Middleware, Rate Limiting, and Overlap Prevention

A job's middleware() method wraps handle() as HTTP middleware wraps a controller. The warehouse takes two stock syncs a minute, and one product must never sync twice at once, so SyncStock (marked #[Tries(10)], logging each run) returns:

SyncStock::middleware()PHP
public function middleware(): array
{
    return [
        (new RateLimited('warehouse'))->releaseAfter(20),
        (new WithoutOverlapping("product-{$this->productId}"))->releaseAfter(5)
            ->expireAfter(60),
    ];
}

The limiter lives in AppServiceProvider::boot() (Rate Limiting Routes): RateLimiter::for('warehouse', fn (object $job) => Limit::perMinute(2));. WithoutOverlapping holds a per-product cache lock that expireAfter(60) frees if a worker dies. Of four syncs (products 1, 2, 3, 1), two ran at 08:33:27 and two at 08:34:30 on attempt 4, after releases every 20 seconds. Each release uses an attempt, hence ten tries, and the worker printed DONE even for releases, so trust the log.

A job that implements ShouldBeUnique is not queued while an identical one waits; #[UniqueFor(600)] caps the lock at ten minutes, and uniqueId() makes it per model:

Dispatching RebuildBestsellers (ShouldBeUnique) three times in tinkerSQL
use App\Jobs\RebuildBestsellers;
foreach (range(1, 3) as $i) {
    RebuildBestsellers::dispatch();
}
echo 'rows: ', DB::table('jobs')->count(), "\n";
echo 'lock: ', DB::table('cache_locks')->value('key'), "\n";
Output
rows: 1
lock: laravel-cache-laravel_unique_job:App\Jobs\RebuildBestsellers:

ThrottlesExceptions, Skip::when() and FailOnException complete the built-in set.