Ingeniería de sistemas
Ingeniería de colas y trabajos en segundo plano
Colas que puede dejar en paz - trabajos idempotentes, reintentos con espera creciente, una ruta de fallo que alguien mira de verdad, y workers que sobreviven a un despliegue sin perder trabajo.
Una cola es la parte de una aplicación Laravel más fácil de añadir y más
difícil de operar. dispatch() es una línea, y todo lo que marca la diferencia
entre una cola y una cola fiable ocurre después de esa línea.
Las cuatro preguntas que una cola tiene que responder
¿Qué pasa cuando el trabajo falla? No si falla. Una API de terceros agota el tiempo de espera, un registro se borra entre el envío y la ejecución, un despliegue reinicia el worker a mitad del trabajo. La respuesta tiene que estar escrita como una política de reintentos con espera creciente, un número máximo de intentos y un destino para lo que siga fallando al final.
¿Qué pasa cuando se ejecuta dos veces? La entrega al menos una vez es la garantía normal, lo que significa que cada trabajo debe ser seguro de repetir o estar protegido por algo que haga inofensiva la repetición. Un correo enviado dos veces es una vergüenza. Un pago cobrado dos veces es una devolución y un hilo de soporte.
¿Quién se entera? Una tabla failed_jobs que se llena en silencio es el
fallo de producción más habitual que nos piden mirar, y nunca es realmente un
fallo de la cola. El trabajo falló, el framework lo registró exactamente como
estaba previsto, y no había nada enganchado al registro.
¿Qué pasa durante un despliegue? Un worker que sostiene un trabajo mientras el código cambia debajo de él o se reinicia limpiamente o se mata. Cuál de las dos depende de una configuración que la mayoría de los equipos hereda en lugar de elegir.
Qué hacemos
Hacer los trabajos idempotentes. Un identificador que el trabajo lleva consigo, aplicado donde el efecto aterriza de verdad: una restricción única, una clave de idempotencia en el proveedor de pagos, una transición de estado que solo puede ocurrir una vez. No un comprobar-y-actuar en PHP, que dos workers atravesarán a la vez el día en que importe.
Fijar políticas de reintento por trabajo, no por aplicación. Un fallo HTTP transitorio quiere varios intentos con retraso creciente. Un fallo de validación quiere cero: reintentarlo solo quema la cola y retrasa todo lo que va detrás. Distinguirlos es un cambio de dos líneas y casi nunca se hace.
Dar a los fallos un sitio adonde ir. Trabajos fallidos comunicados a lo que su equipo ya mira, con contexto suficiente para actuar. Una ruta de descarte para lo que no se puede reintentar. Una alerta sobre la tasa y no sobre el evento individual, para que sea señal y no ruido.
Dimensionar la topología. Separar colas por requisito de latencia en lugar de por funcionalidad, para que una exportación nocturna no pueda retrasar un restablecimiento de contraseña. Workers dimensionados según la forma real del trabajo. Horizon configurado con supervisores que encajen con eso, y métricas que hagan visible un atasco creciente antes de que sea un incidente.
Hacer que el trabajo largo sobreviva. Trabajos agrupados con progreso, troceados para que un reinicio cueste un trozo y no la ejecución entera, y con una forma de reanudar en lugar de empezar de nuevo.
El trabajo programado, que tiene los mismos problemas
El planificador recibe menos atención que la cola y falla de las mismas formas. Una tarea que se solapa consigo misma porque la ejecución anterior sigue en marcha. Una tarea que deja de funcionar en silencio porque la entrada de cron estaba en un servidor que se reemplazó. Una tarea cuyo fallo es una línea de registro que nadie lee.
Tratamos las tareas programadas como trabajos con un disparador: protección contra solapamiento donde importa, un latido para que se note una tarea que deja de ejecutarse, y la misma ruta de fallo que todo lo demás.
Cómo transcurre un encargo
Mándenos las clases de los jobs y, si las guarda, una semana de registros de trabajos fallidos. Lo que ya ha salido mal dice más sobre el diseño que el código.
Lo que vuelve es un alcance: cada job con su comportamiento ante fallos, la topología de colas, la monitorización, y qué entra en la fase uno. Lleva un precio, y el contrato se refiere a él y no a una conversación.
El trabajo llega como pull requests revisables en su repositorio, con el comportamiento de reintento e idempotencia probado en lugar de descrito.
Qué recibe
Una revisión de cada trabajo y cada tarea programada de la aplicación con su comportamiento ante fallos documentado, las correcciones como pull requests revisables, la topología de colas y la configuración de workers como código, y la monitorización conectada a lo que ya use.
El entregable que nos importa es el más difícil de enseñar: una cola en la que nadie ha pensado en un mes, porque no ha necesitado a nadie.
Todo el diseño descansa sobre una suposición: cada trabajo se ejecutará dos veces. La mayoría de los fallos de cola son una violación de ella. El segundo más común es un worker que todavía ejecuta el código de la semana pasada, un fallo de despliegue que se presenta como un fallo de cola. Si los trabajos son lentos y no incorrectos, se empieza por el trabajo de rendimiento.
