Backend 5 min de lectura 1033 palabras

Laravel Fuse: Circuit Breaker para Queue Jobs

EN
Laravel Fuse: Circuit Breaker para Queue Jobs

Acaba de anunciarse en Laracon India 2026 un paquete interesante: Laravel Fuse. Es un circuit breaker implementado como middleware para jobs de Laravel Queue, y resuelve un problema que todos los que trabajamos con servicios externos hemos tenido alguna vez.

El problema

Imagina este escenario: son las 11 PM y Stripe está teniendo problemas. Tus queue workers no lo saben, siguen intentando cobrar a clientes, y cada job espera 30 segundos antes de timeout. Luego reintenta. Espera otra vez. Timeout otra vez.

Si tienes 10,000 jobs de pago en cola y cada uno espera 30 segundos antes de fallar, estás mirando más de 25 horas para limpiar la cola — aunque todas las peticiones van a fallar.

Fuse soluciona esto implementando el patrón circuit breaker. Después de un número configurable de fallos, deja de hacer peticiones completamente. Los jobs fallan en milisegundos en lugar de esperar timeouts, y se liberan automáticamente a la cola para reintento posterior. Cuando el servicio se recupera, Fuse lo detecta y reanuda operaciones normales.

Cómo funciona el Circuit Breaker

El circuit breaker tiene tres estados:

CLOSED es operación normal. Todas las peticiones pasan al servicio externo. En el background, Fuse trackea tasas de éxito y fallo usando buckets basados en minutos que expiran automáticamente.

OPEN es modo protección. Una vez que la tasa de fallos excede tu threshold configurado, el circuito se abre. Los jobs fallan inmediatamente — no se hace llamada API, no hay timeout de 30 segundos. El job se libera a la cola con un delay. Tu cola sigue moviéndose.

HALF-OPEN es test de recuperación. Después de un periodo de timeout (configurable por servicio), Fuse permite una petición probe a través. Si tiene éxito, el circuito se cierra y operaciones normales se reanudan. Si falla, el circuito se reabre y espera otra vez.

Instalación

composer require harris21/laravel-fuse

Publicar el archivo de configuración:

php artisan vendor:publish --tag=fuse-config

Uso básico

Añade el middleware a cualquier job que llame a un servicio externo:

use Harris21\Fuse\Middleware\CircuitBreakerMiddleware;

class ChargeCustomer implements ShouldQueue
{
    public $tries = 0;
    public $maxExceptions = 3;

    public function middleware(): array
    {
        return [new CircuitBreakerMiddleware('stripe')];
    }

    public function handle(): void
    {
        Stripe::charges()->create([
            'amount' => $this->amount,
            'currency' => 'usd',
            'customer' => $this->customerId,
        ]);
    }
}

Configurar $tries = 0 permite releases ilimitados (ya que los jobs liberados no son “retries” en el sentido de Laravel), mientras que $maxExceptions = 3 limita los fallos reales. El job mismo no necesita cambios — Fuse lo envuelve.

Configuración

El archivo de configuración publicado permite establecer defaults y overrides por servicio:

// config/fuse.php
return [
    'enabled' => env('FUSE_ENABLED', true),

    'default_threshold' => 50,
    'default_timeout' => 60,
    'default_min_requests' => 10,

    'services' => [
        'stripe' => [
            'threshold' => 50,
            'timeout' => 30,
            'min_requests' => 5,
        ],
        'mailgun' => [
            'threshold' => 60,
            'timeout' => 120,
            'min_requests' => 10,
        ],
    ],
];

El threshold es un porcentaje de tasa de fallos. Si el 50% de las peticiones fallan dentro de la ventana de tracking, el circuito se abre. El timeout es cuántos segundos esperar antes de testear recuperación. Y min_requests previene que el circuito se abra con tamaños de muestra pequeños — necesitas al menos esta cantidad de peticiones antes de evaluar la tasa de fallos.

Clasificación inteligente de fallos

No todos los errores significan que un servicio está caído. Si Stripe devuelve un 429 porque estás hitting rate limits, eso no es un outage — el servicio funciona bien, solo estás mandando demasiadas peticiones. Lo mismo con errores de autenticación.

Fuse solo cuenta fallos que indican problemas reales del servicio:

  • Errores de servidor 500, 502, 503 cuentan como fallos
  • Timeouts de conexión y conexiones rechazadas cuentan como fallos
  • Rate limits 429 NO cuentan
  • Errores de autenticación 401 y 403 NO cuentan

Esto previene falsos positivos. Tu circuito no se abrirá solo porque alguien hizo deploy con una API key expirada.

Soporte para horas pico

Durante horas laborales, podrías querer ser más tolerante con fallos para maximizar transacciones exitosas. Fuera de horas laborales, podrías preferir protección anterior. Fuse soporta esto:

'stripe' => [
    'threshold' => 40,
    'peak_hours_threshold' => 60,
    'peak_hours_start' => 9,
    'peak_hours_end' => 17,
],

Entre las 9 AM y las 5 PM, el circuito usa el threshold del 60%. Fuera de esas horas, usa el 40%. Esto permite tunear el tradeoff entre protección y throughput basado en cuándo las transacciones importan más.

Eventos

Fuse dispatcha eventos Laravel en cada transición de estado:

use Harris21\Fuse\Events\CircuitBreakerOpened;
use Harris21\Fuse\Events\CircuitBreakerHalfOpen;
use Harris21\Fuse\Events\CircuitBreakerClosed;

Puedes escuchar estos para alertas:

class AlertOnCircuitOpen
{
    public function handle(CircuitBreakerOpened $event): void
    {
        Log::critical("Circuit opened for {$event->service}", [
            'failure_rate' => $event->failureRate,
            'attempts' => $event->attempts,
            'failures' => $event->failures,
        ]);
        // Send to Slack, page on-call, etc.
    }
}

El evento CircuitBreakerOpened incluye el nombre del servicio, tasa de fallos actual, intentos totales, y conteo de fallos. Esto da lo que necesitas para debugging y alertas.

Uso directo

También puedes usar el circuit breaker fuera de queued jobs:

use Harris21\Fuse\CircuitBreaker;

$breaker = new CircuitBreaker('stripe');

if (!$breaker->isOpen()) {
    try {
        $result = Stripe::charges()->create([...]);
        $breaker->recordSuccess();
        return $result;
    } catch (Exception $e) {
        $breaker->recordFailure($e);
        throw $e;
    }
} else {
    return $this->fallbackResponse();
}

También puedes checkear estado y resetear manualmente:

$breaker->isClosed();
$breaker->isOpen();
$breaker->isHalfOpen();
$breaker->getStats();
$breaker->reset();

Prevención de Thundering Herd

Cuando un circuito entra en HALF-OPEN, no quieres 50 queue workers mandando peticiones probe simultáneamente. Fuse usa Cache::lock() para asegurar que solo un worker testea el servicio. Los otros siguen fallando rápido hasta que el probe completa.

Requisitos

  • PHP 8.3+
  • Laravel 11+
  • Cualquier driver de cache de Laravel (Redis recomendado para producción)

El paquete no tiene dependencias externas. Usa el sistema de cache nativo de Laravel para tracking y locks.

Conclusión

El patrón circuit breaker es una de esas técnicas que, cuando las necesitas, realmente las necesitas. La implementación de Fuse para Laravel es limpia, usa el sistema de middleware que ya conocemos, y se integra con las herramientas nativas de Laravel (cache, events, queue).

Para cualquiera que tenga jobs en cola que dependen de servicios externos, Fuse es una capa de resiliencia que puede prevenir que una caída de terceros se convierta en una caída de tu propio sistema.


Referencias