Ir al contenido

Sus workers ejecutan el código de la semana pasada

Un worker es un proceso PHP de vida larga que cargó su aplicación una vez y no volvió a mirar. Los despliegues no le llegan, y los fallos que eso provoca siguen en producción.

5 min de lectura

Un desarrollador arregla un fallo en un trabajo, lo revisa, lo integra, ve el despliegue ponerse verde, y el mismo fallo aparece en el registro diez minutos después. Vuelve a desplegar. Pasa otra vez. Cerca del tercer intento llega la sospecha de que producción no está ejecutando el código del repositorio, y la sospecha es correcta.

Por qué un despliegue no llega a un worker

El ciclo de vida de una petición en PHP es lo que hace esto sorprendente. Cada petición HTTP arranca el framework, hace su trabajo y sale, así que el código nuevo se recoge por definición: la siguiente petición carga los ficheros nuevos porque no había ningún proceso antiguo que se quedara con los viejos.

Un worker de cola es lo contrario. php artisan queue:work arranca el framework una vez y luego entra en bucle, sacando trabajos de la cola mientras viva. Las clases que cargó al arrancar se quedan cargadas. Sustituya todos los ficheros de debajo y al proceso en marcha ni le consta ni le importa: sostiene su propia copia compilada de su aplicación, de cuando fuera que arrancó.

Así que después de un despliegue tiene una capa web ejecutando la entrega nueva y una capa de colas ejecutando lo que fuera actual la última vez que esos procesos arrancaron, que puede ser la entrega anterior, o una de hace tres semanas.

Qué aspecto tiene cuando sale mal

Los modos de fallo son reconocibles una vez que sabe qué buscar.

Un fallo que arregló sigue ocurriendo, exclusivamente en el trabajo en cola. Un trabajo llama a un método que no existe en el código en ejecución, o no llama a uno que se acaba de añadir. Una migración añade una columna, el controlador escribe en ella tan contento, y el trabajo que lee el mismo modelo revienta porque su copia del esquema es anterior a la columna.

Lo peor es la división de versiones. Reinicie los workers poco a poco - o déjelos reciclarse por sus propios límites de memoria - y durante un rato algunos procesos ejecutan el código nuevo y otros el viejo. Los trabajos se reparten entre ellos arbitrariamente. Ahora tiene fallos que aparecen en más o menos la mitad de los intentos y no se reproducen en ninguno, que es la forma más cara que puede tomar un fallo.

El arreglo es una línea en el script de despliegue

php artisan queue:restart

No mata nada. Escribe una fecha en la caché, y cada worker comprueba esa fecha entre trabajos; un worker que arrancó antes termina su trabajo actual y sale. Su supervisor de procesos - systemd, Supervisor, lo que ofrezca la plataforma - ve la salida y arranca un worker nuevo, que carga el código nuevo.

Dos cosas tienen que ser ciertas para que funcione siquiera, y las dos se pasan por alto lo bastante a menudo como para decirlas.

El almacén de caché tiene que ser compartido. La fecha se escribe en la caché. Use el driver array y se va a la memoria del proceso que ejecutó artisan, que no es el worker. Use file con los workers en otra máquina y pasa lo mismo. Redis, Memcached o la base de datos: cualquier cosa que puedan leer los dos lados.

Algo tiene que rearrancar el worker que salió. queue:restart para workers. No los arranca. Sin un supervisor vigilando, un despliegue le deja en silencio sin ningún worker, y los trabajos se amontonan hasta que alguien se fija en la profundidad de la cola.

Con Horizon la llamada es php artisan horizon:terminate, y Horizon trae su propia supervisión, pero sigue habiendo que avisarle, porque no puede ver su despliegue.

El orden importa más que el comando

Dónde cae el reinicio dentro del script decide si la ventana entre código viejo y nuevo es peligrosa:

# primero el código nuevo en su sitio
php artisan migrate --force
php artisan config:cache
php artisan queue:restart

Reinicie antes de que el código nuevo esté en disco y los workers frescos cargarán la entrega antigua, que es justo el fallo que intentaba arreglar. Ejecute las migraciones después del reinicio y los workers nuevos se encontrarán un esquema que todavía no ha cambiado.

El caso difícil es una migración que quita algo. Una columna eliminada rompe los workers antiguos al instante, y habrá workers antiguos durante todo lo que tarden sus trabajos actuales en terminar. Cualquier cambio de esquema que quite algo quiere partirse en dos despliegues: deje de usarlo, entregue, y después elimínelo.

Confirmar que funcionó

No se fíe del script. Pregúnteles a los workers:

ps -eo lstart,cmd | grep "[q]ueue:work"

Horas de arranque anteriores al despliegue significan que el reinicio no les llegó, y eso conviene saberlo ahora y no durante el siguiente incidente. En Horizon la misma respuesta está en el panel, en la lista de procesos del supervisor.

Una línea en un script de despliegue, y una clase entera de fallos que se come tardes enteras deja de existir.

Este es uno de los cuatro pasos que deciden si un despliegue es invisible; los otros tres están aquí. Si lo que no se puede confiar es la propia cola, con trabajos que desaparecen, se duplican o fallan donde nadie mira, lo tomamos como un encargo aparte.

Preguntas relacionadas

¿queue:restart mata los trabajos en curso?
No, y ese es justamente el punto. Escribe una marca con una fecha; cada worker comprueba esa marca entre trabajos y sale limpiamente si arrancó antes de la fecha. El trabajo ya en curso termina primero. Matar el proceso es lo que pierde un trabajo a medias.
Usamos Horizon. ¿Está resuelto?
Horizon supervisa los workers y los reinicia al terminar, lo cual resuelve bien la mecánica. No sabe que ha habido un despliegue a menos que el despliegue se lo diga, así que la llamada de artisan sigue perteneciendo al script - lo que cambia es qué comando se llama.
¿Por qué fallaron solo algunos trabajos?
Porque solo se sustituyeron algunos workers. Una cola servida por seis procesos que se reinician según van terminando pasa una ventana ejecutando dos versiones de su aplicación a la vez, y qué versión le toca a un trabajo es cara o cruz. Por eso también los fallos parecen intermitentes e irreproducibles.
¿Es por esto que el trabajo dice que una columna no existe?
Muy probablemente. Una migración que añade una columna se ejecuta contra la base de datos de inmediato; un worker que sostiene el modelo de la entrega anterior no sabe nada de ella. Al revés es peor: una migración que elimina una columna mientras los workers antiguos siguen escribiendo en ella.

← Volver a todos los artículos

Llamar+1 848 272 7583WhatsApp+90 850 308 5436Correoinfo@codefacture.comPágina de contacto