Saltar al contenido
Volver al blog
LaravelDockerDevOps

Laravel 13.31 mejora las métricas y el apagado de colas

Dos cambios pequeños para medir el backlog completo y reaccionar mejor al detener workers de Laravel.

Ismael Catala7 min de lectura

El problema no es contar una cola concreta

Cuando una aplicación empieza a separar trabajos en varias colas, una métrica aislada deja de dar una imagen útil. Saber cuántos jobs hay en emails no responde necesariamente a la pregunta importante: cuánto trabajo queda pendiente en esa conexión. Hasta ahora era fácil acabar sumando métricas por estados o por nombres de cola desde código propio.

Laravel 13.31 incorpora Queue::totalSize() para obtener el tamaño total de la conexión de colas configurada. Es una API pequeña, pero encaja bien en métricas de despliegues propios donde Redis o la base de datos forman parte de la infraestructura que administramos. La versión también añade el evento JobInterrupted, orientado a saber qué job estaba en ejecución cuando el worker recibió una señal de interrupción. (github.com)

Una métrica para el backlog completo

La llamada no necesita indicar el nombre de una cola. Laravel consulta la conexión que resuelve la facade Queue y devuelve un entero con el total que gestiona ese driver. Para una conexión Database, la implementación cuenta los registros de la tabla de jobs, sin limitarse a una cola concreta. (github.com)

En Redis, Laravel recorre los nombres de cola que detecta y suma su tamaño. En Laravel Cloud, el total se calcula a partir de las colas gestionadas por esa conexión. El detalle importa porque totalSize() representa el total agregado de la conexión, no el valor de una única cola prioritaria. (github.com)

Un punto de partida para observabilidad

Yo lo usaría como una señal más, no como un panel de control completo. Un backlog alto puede indicar falta de workers, una dependencia lenta o una oleada puntual de trabajo; el número por sí solo no distingue esos casos. Aun así, registrar la cifra de forma periódica permite detectar tendencias sin montar consultas diferentes para Database y Redis.

Este ejemplo puede vivir en un comando programado, un endpoint interno o el proceso que ya envía métricas desde el servidor. No presupone ningún proveedor de observabilidad concreto, que es precisamente lo útil si gestionas el despliegue en tu propio Docker, VPS o homelab. La parte importante es etiquetar la métrica con la conexión para no mezclar cargas de infraestructuras distintas.

<?php
 
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Queue;
 
$backlog = Queue::totalSize();
 
Log::info('queue.backlog', [
    'connection' => config('queue.default'),
    'jobs' => $backlog,
]);

Qué está contando realmente

totalSize() no equivale únicamente a trabajos listos para ejecutarse. Las pruebas incluidas con el cambio cubren trabajos pendientes, retrasados y reservados, de modo que el total refleja la carga que mantiene la conexión en sus distintos estados. Esto es razonable para una alerta de saturación, porque un job reservado también sigue ocupando capacidad hasta que termina o vuelve a estar disponible. (github.com)

Si necesitas saber si los workers están dando abasto ahora mismo, acompaña esa métrica con el tamaño de las colas concretas y con la antigüedad del trabajo pendiente más antiguo. Laravel ya expone métodos específicos para esos casos, como size(), pendingSize(), delayedSize() y reservedSize(). El nuevo método no sustituye esas mediciones más precisas. (api.laravel.com)

Interrumpir no es lo mismo que terminar

El segundo cambio va por otro camino: la vida de un worker. Laravel puede recibir señales como SIGTERM, SIGQUIT o SIGINT y marcar el worker para que salga. Si en ese momento hay un job ejecutándose y ese job implementa Illuminate\Contracts\Queue\Interruptible, Laravel llama a su método interrupted(int $signal). (raw.githubusercontent.com)

Después de invocar ese método, el worker despacha Illuminate\Queue\Events\JobInterrupted. El evento expone el nombre de la conexión, el objeto de trabajo de la cola y la señal recibida. Esto permite dejar trazabilidad centralizada sin convertir cada implementación de job en un sitio de logging. (github.com)

El cleanup debe vivir en el job

Si un job abre una conexión temporal, controla un proceso externo o mantiene un recurso que debe cerrarse rápido, la limpieza pertenece al método interrupted(). El listener de JobInterrupted es útil para registrar, contabilizar o notificar, pero recibe el wrapper del job de la cola, no tu comando ya resuelto. No conviene diseñar la liberación de recursos importantes alrededor de un listener global. (raw.githubusercontent.com)

Una implementación mínima puede tener esta forma. La señal se recibe como entero, así que puedes registrarla o usarla para cambiar el estado que controle un bucle de trabajo cooperativo. El método no convierte automáticamente un job largo en cancelable: el código del job debe colaborar con la interrupción.

<?php
 
namespace App\Jobs;
 
use Illuminate\Contracts\Queue\Interruptible;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;
 
final class GenerateReport implements ShouldQueue, Interruptible
{
    use Queueable;
 
    public function handle(): void
    {
        // Generar el informe...
    }
 
    public function interrupted(int $signal): void
    {
        Log::warning('report.interrupted', [
            'signal' => $signal,
        ]);
 
        // Cerrar recursos o pedir la parada de trabajo cooperativo...
    }
}

Registrar la interrupción fuera del job

El evento sirve para separar la observabilidad de la lógica de negocio. Por ejemplo, puedes anotar el identificador del job y la conexión en el log de la aplicación. Con eso es más fácil correlacionar un reinicio de contenedor con los trabajos que estaban activos en ese momento.

<?php
 
use Illuminate\Queue\Events\JobInterrupted;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
 
Event::listen(JobInterrupted::class, function (JobInterrupted $event): void {
    Log::warning('queue.job_interrupted', [
        'connection' => $event->connectionName,
        'job_id' => $event->job->getJobId(),
        'signal' => $event->signal,
    ]);
});

Docker es un caso práctico, no una garantía

En un contenedor, esto solo funciona si la señal llega realmente al proceso del worker. También requiere que PHP tenga disponible la extensión pcntl, porque Laravel solo registra manejadores asíncronos de señales cuando puede usarla. Revisaría ambas cosas en la imagen antes de confiar en esta vía para limpiar recursos. (raw.githubusercontent.com)

El patrón tiene sentido para workers ejecutados con php artisan queue:work. Son procesos de larga duración y Laravel recomienda reiniciarlos durante los despliegues para que carguen el código nuevo. JobInterrupted añade una pieza de contexto cuando ese reinicio coincide con un job que estaba trabajando. (laravel.com)

La letra pequeña de totalSize()

No todos los drivers pueden ofrecer un total global útil. En el cambio de Laravel 13.31, Database, Redis y Cloud calculan un total; otros drivers devuelven cero en su implementación. Antes de crear una alerta genérica para todos tus entornos, prueba el comportamiento de la conexión que usas en producción. (github.com)

Tampoco es una lectura transaccional de todo el sistema. Entre tomar el valor y enviarlo a tu plataforma de métricas pueden entrar o salir jobs, así que debe tratarse como una fotografía puntual. No lo uses para decidir que un proceso de negocio ha terminado ni para reemplazar confirmaciones explícitas.

La letra pequeña de JobInterrupted

JobInterrupted no se emite por cualquier parada del worker. No se dispara si no había un job en curso, ni si el job en ejecución no implementa Interruptible. Laravel también exige que el job se esté ejecutando mediante el manejador habitual de jobs en cola antes de notificarle la señal. (raw.githubusercontent.com)

Tampoco cubre una terminación forzosa, un fallo del proceso o un timeout que mate al worker. Es una oportunidad para reaccionar ante las señales que Laravel registra, no un mecanismo de recuperación ante cualquier caída. Para recursos críticos, mantendría además operaciones idempotentes, estados persistidos y reintentos diseñados de forma consciente.

Un cambio pequeño que evita código pegajoso

Queue::totalSize() elimina una suma repetida y dependiente del driver cuando solo quieres medir carga agregada. JobInterrupted ofrece un punto concreto para diferenciar un job realmente interrumpido de un worker que simplemente recibió una señal. Ninguna de las dos novedades cambia la arquitectura de una aplicación, pero ambas reducen código auxiliar en los sitios donde más importa que el comportamiento sea predecible: métricas y despliegues.