Ir al contenido principal
Laravel, shipping fast.
Capitulo 7 · Colas y procesos en segundo plano

Jobs que se pueden ejecutar dos veces sin riesgo

Julian Beaujardin

La verdad incómoda sobre las colas: tu job se ejecutará más de una vez, tarde o temprano. Un worker muere a mitad de ejecución durante un despliegue. Un job supera su timeout y el driver de cola asume que está muerto y se lo pasa a otro worker, justo cuando el proceso del primero aún está terminando. Las colas de Laravel son al menos una vez, no exactamente una vez, y ninguna opción de configuración cambia eso. El job tiene que poder ejecutarse dos veces y producir el mismo estado final que ejecutándose una.

Los borrados son naturalmente indulgentes: borrar algo que ya no está es una operación nula, no un error. Las creaciones son lo contrario. Llamar dos veces al endpoint de creación con la misma entrada crea dos licencias, no una.

// app/Http/Integrations/Statamic/StatamicAPI.php
public function addLicense(string $name, string $domain): array
{
    $response = $this->send('POST', '/sites', [
        'name' => trim($name),
        'domain' => trim($domain),
    ]);

    $data = $response->json('data');

    if (! is_array($data) || ! isset($data['key'])) {
        throw new \RuntimeException(
            'Statamic addLicense returned a malformed response: '
            .json_encode(['status' => $response->status(), 'body' => $response->body()])
        );
    }

    return $data;
}

Fíjate en lo que no hace: no trata calladamente un 2xx sin key como «probablemente bien, inténtalo otra vez». Lanza. Una llamada de creación que devuelve algo que no puede analizar es justo el caso ambiguo donde un reintento ingenuo produce una licencia duplicada, así que el driver se niega a adivinar y obliga a que la política de reintentos de quien llama tome esa decisión explícitamente en lugar de por accidente.

// Bad: retries a create with no way to tell "already succeeded" from "actually failed"
public function handle(): void
{
    try {
        LicenseFacade::addLicense(name: $this->url, domain: $this->url);
    } catch (Throwable $e) {
        $this->release(30); // if the first call actually succeeded, this makes a second license
    }
}
// Good: collapse duplicate deliveries with an atomic claim before doing the work
if (! Cache::add("license:site:{$site->id}:issuing", true, now()->addMinutes(10))) {
    return; // another delivery of this job already claimed it
}

Cache::add() sólo tiene éxito para el primer llamante; cada reentrega posterior ve la clave ya presente y se retira. La idempotencia no es una propiedad que los jobs tengan por defecto. Es algo que construyes, una vez, con una clave de reclamación, y reutilizas allí donde una entrega duplicada haría daño real.