Challenge 2: Add Retry Logic — Possible Solution ==================================================================== // app/Jobs/ProcessOrder.php namespace App\Jobs; use App\Models\Order; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Throwable; class ProcessOrder implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public int $tries = 3; public int $backoff = 10; // seconds between each retry attempt public function __construct( public Order $order ) {} public function handle(): void { // ... e.g. call a payment gateway, which might throw a // ConnectionException if the gateway is briefly unreachable ... logger()->info("Processing order #{$this->order->id}"); } public function failed(Throwable $exception): void { logger()->error("Order #{$this->order->id} permanently failed: {$exception->getMessage()}"); // Could also notify an admin, flag the order for manual review, etc. } } WHAT CHANGES COMPARED TO A JOB WITH NO RETRY CONFIGURATION ----------------------------------------------------------------- Without $tries and $backoff set, a job defaults to Laravel's queue connection-level retry configuration (often just 1 attempt, depending on config/queue.php) — if handle() throws any exception, the job is typically marked as failed immediately, moved to the failed_jobs table, and failed() (if defined) runs right away. With $tries = 3 and $backoff = 10 configured, if handle() throws an exception on its first attempt, Laravel automatically re-queues the SAME job to try again — waiting 10 seconds before the second attempt, and another 10 seconds before a third attempt if the second also fails. Only after all 3 attempts have failed does Laravel give up, mark the job as permanently failed, and call failed($exception) with the exception from that final attempt. This means a transient failure — the payment gateway being briefly unreachable for a few seconds, a momentary network blip — has two more chances to succeed automatically before the job is considered a real, permanent failure worth logging and investigating. WHY THIS WORKS -------------- - $tries and $backoff are simple public properties on the Job class itself — Laravel's queue worker reads them directly when deciding whether and how long to wait before re-attempting a failed job, no separate configuration needed beyond these two lines. - failed(Throwable $exception) is only called once all retry attempts are exhausted — it's the designated place for "this genuinely didn't work, do something about it" logic (logging, alerting, flagging for manual follow-up), directly parallel to how a Celery task's exception handling or dead-letter-queue logic would be structured after max_retries is reached. - This mirrors the Django course's Celery retry chapter almost exactly in intent — autoretry_for/retry_backoff/max_retries there map directly onto $tries/$backoff/failed() here, just expressed as class properties and a method instead of decorator arguments.