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.
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:restartNo 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:restartReinicie 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.
