Sv hup unicorn pierde tráfico

He tenido un tiempo de inactividad de ~40s al recargar mi sitio. Está un poco más allá de mis habilidades técnicas, así que le pedí ayuda a Claude.

Asegura que el problema es que el lanzador de unicorn está enviando la señal incorrecta al maestro antiguo (SIGTERM en lugar de SIGQUIT), lo que lo mata antes de que el nuevo pueda comenzar a servir.

La solución parece ser una actualización de 6 líneas, pero me gustaría verificarla aquí primero.

Haz clic para ver el informe generado por Claude

Resumen

En la ruta de código de pitchfork, config/unicorn_launcher’s on_reload_pitchfork() retira al maestro saliente tan pronto como existe un proceso trabajador, sin verificar nunca que el nuevo maestro pueda servir una solicitud, y lo retira con kill (SIGTERM, apagado inmediato) en lugar de kill -s QUIT (apagado ordenado).

La ruta de unicorn inmediatamente arriba, on_reload_unicorn(), hace ambas cosas correctamente: emite curl $LOCAL_WEB para calentar al nuevo maestro y luego envía QUIT.

El resultado observado de un solo sv hup unicorn es de ~40 segundos durante los cuales no se completa ninguna solicitud, terminando exactamente cuando la nueva generación registra Booted Rails.

Entorno

  • Discourse 2026.8.0-latest.1, Rails 8.0.5.1, Ruby 3.4.10
  • pitchfork 0.18.2, RUN_PITCHFORK no establecido (por lo que la rama de pitchfork está activa)
  • contenedor independiente, nginx → http://127.0.0.1:3000
  • droplet de 2 vCPU / 4 GB; la carga de la aplicación tarda ~40s

El código

config/unicorn_launcher:

function on_reload_unicorn() {
  ...
  while [ ... workers not up ... ]; do sleep 1; done

  curl $LOCAL_WEB &>/dev/null      # warm the new master
  kill -s QUIT $UNICORN_PID        # graceful
}

function on_reload_pitchfork() {
  log "Reloading pitchfork ($UNICORN_PID)"
  OLD_PID=$UNICORN_PID
  pitchfork "${PITCHFORK_ARGS[@]}" &
  UNICORN_PID=$!

  count=0
  while [ "$count" -lt 180 -a -z "$(pgrep -f -P $UNICORN_PID worker)" ]; do
    log "Waiting for new pitchfork workers under $UNICORN_PID to start up..."
    count=$((count + 1))
    sleep 1
  done

  kill $OLD_PID 2>/dev/null        # SIGTERM, and no warm-up first
}

pgrep -f -P $UNICORN_PID worker devuelve tan pronto como se genera un proceso trabajador. En pitchfork, el trabajador existe mucho antes de que la aplicación haya terminado de cargar, por lo que el bucle termina temprano y el maestro antiguo se desmonta mientras el nuevo aún está arrancando.

Comportamiento observado

production.log, una recarga (marcas de tiempo UTC):

23:16:49  Completed 200 OK in 95ms        <- generación saliente, caliente
          (44 segundos; ni una sola línea Completed)
23:17:33  Booted Rails 8.0.5.1 application in production environment
23:17:36  Completed 200 OK in 4128ms      <- primer renderizado en la nueva generación, frío
23:17:39  Completed 200 OK in 200ms

Un monitor que consultaba GET / cada ~2s a través de nginx durante esa ventana registró:

23:17:11  499  19.697s   (el cliente se rindió; el upstream nunca respondió)
23:17:29  503  16.301s
23:17:31  502   1.448s

y el registro de errores de nginx para el 502:

2026/08/14 23:17:31 [error] 70#70: *110675 recv() failed (104: Connection reset by peer)
while reading response header from upstream, upstream: "http://127.0.0.1:3000/"

upstream_response_time es igual a request_time en cada línea fallida, por lo que nginx no es el cuello de botella: las solicitudes llegan a un proceso que no puede responderlas. La reinicialización a las 23:17:31 es consistente con SIGTERM desmontando al maestro saliente con conexiones aún en cola.

Esto es reproducible: dos recargas en un solo despliegue produjeron dos ventanas idénticas de ~40s, cada una terminando en una línea Booted Rails.

No es presión de recursos: no hay muertes OOM en dmesg, intercambio en 141 MB de 2048 usados, promedio de carga 0.07, y ambas cargas se completaron normalmente.

Solución sugerida

Imitar la rama de unicorn: esperar hasta que el nuevo maestro realmente sirva antes de retirar al antiguo, y retirarlo de manera ordenada:

   count=0
   while [ "$count" -lt 180 -a -z "$(pgrep -f -P $UNICORN_PID worker)" ]; do
     log "Waiting for new pitchfork workers under $UNICORN_PID to start up..."
     count=$((count + 1))
     sleep 1
   done

-  kill $OLD_PID 2>/dev/null
+  count=0
+  until curl -sf -o /dev/null "$LOCAL_WEB" || [ "$count" -ge 180 ]; do
+    log "Waiting for the new pitchfork master to serve a request..."
+    count=$((count + 1))
+    sleep 1
+  done
+
+  kill -s QUIT $OLD_PID 2>/dev/null

La mitad de QUIT en lugar de TERM es inequívoca: pitchfork hereda la semántica de señales de unicorn, donde QUIT drena y TERM es la parada inmediata.

PD: Este es mi primer informe asistido por IA. Háganmelo saber si estoy rompiendo alguna etiqueta. :sweat_smile: