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:
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:
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";rows: 1 lock: laravel-cache-laravel_unique_job:App\Jobs\RebuildBestsellers:
ThrottlesExceptions, Skip::when() and FailOnException complete the built-in set.