Tries y Backoff son la política de cuánto pelea el sistema antes de admitir que un job ha fallado. Este es el job que, en el servicio que aprovisiona sitios, le pide una licencia a esta API:
// app/Jobs/IssueLicense.php, servicio de aprovisionamiento
#[Tries(5)]
#[Backoff(180, 300, 10800, 10800)]
#[DeleteWhenMissingModels]
class IssueLicense implements ShouldQueue
{
// ...
}
Lee las esperas como una secuencia, porque eso es lo que son. El intento uno se ejecuta de inmediato. El intento dos espera tres minutos, el intento tres espera cinco más, y los dos últimos esperan tres horas cada uno. Los primeros reintentos absorben un tropiezo breve. Los dos últimos están ahí para algo más parecido a una ventana de mantenimiento. La curva es una elección: fallos distintos merecen paciencias distintas.
Rendirse es un acontecimiento propio, no solo la ausencia de otro reintento:
public function failed(Throwable $exception): void
{
$this->site->markLicenseFailed();
Notification::send(
Staff::onCall(),
new ProvisioningStepFailed($this->site, $exception),
);
}
failed() se ejecuta una vez, cuando los intentos se agotan. Lo que una persona necesite para actuar ante el fallo (un cambio de estado, una notificación) va aquí y en ningún otro lugar del job. Nunca lo dejes vacío.
Hasta entonces los reintentos deberían ser silenciosos. Una excepción lanzada en el intento dos de cinco se reporta como cualquier otra, y cuatro reportes para lo que acabará siendo un éxito enseñan a la gente a ignorar el canal. El middleware de job ThrottlesExceptions de Laravel está hecho para esto. Tras cierto número de excepciones retiene el job durante un rato, y no reporta ninguna salvo que se lo pidas. Sustituye las esperas del propio job por un retraso suyo y está pensado para ir emparejado con retryUntil(), así que lee su sección antes de añadirlo. (Documentación de Laravel: Queues › Throttling Exceptions.)
La tabla de jobs fallidos necesita un dueño
Un job que agota sus reintentos aterriza en failed_jobs y ahí se queda. Nada vuelve a reintentarlo. Tratar esa tabla como un lugar donde las cosas desaparecen es la manera en que los fallos reintentables se vuelven permanentes.
Laravel te da los comandos:
php artisan queue:failed # a la espera de una decisión
php artisan queue:retry <uuid> # reintentar
php artisan queue:forget <uuid> # rendirse, a propósito
php artisan queue:prune-failed # limpiar entradas viejas
Reintentar un job fallido repite un efecto secundario. Olvidarlo descarta el único registro de que el efecto secundario nunca ocurrió. Ninguna de las dos cosas debería pasar por accidente, así que quien lo haga debería ser identificable. Si construyes una pantalla para esto, constrúyela sobre estos comandos, detrás de un Gate, con una entrada de auditoría por cada uso.