Le trafic de Sv hup unicorn chute

Je rencontre une interruption d’environ 40 secondes lors du rechargement de mon site. C’est un peu au-delà de mes compétences techniques, donc j’ai demandé de l’aide à Claude.

Il affirme que le problème vient du fait que le lanceur Unicorn envoie le mauvais signal à l’ancien processus maître (SIGTERM au lieu de SIGQUIT), ce qui le tue avant que le nouveau puisse commencer à servir les requêtes.

La correction semble être une mise à jour de 6 lignes, mais je voudrais la vérifier ici d’abord.

Cliquez pour voir le rapport généré par Claude

Résumé

Dans la branche de code pitchfork, la fonction on_reload_pitchfork() de config/unicorn_launcher met hors service le processus maître sortant dès qu’un processus worker existe, sans jamais vérifier que le nouveau maître peut servir une requête, et il le met hors service avec kill (SIGTERM, arrêt immédiat) plutôt qu’avec kill -s QUIT (arrêt propre).

La branche unicorn juste au-dessus, on_reload_unicorn(), fait les deux correctement — elle exécute curl $LOCAL_WEB pour réchauffer le nouveau maître, puis envoie QUIT.

Le résultat observé d’un seul sv hup unicorn est une interruption d’environ 40 secondes pendant laquelle aucune requête n’est complétée, se terminant exactement lorsque la nouvelle génération journalise Booted Rails.

Environnement

  • Discourse 2026.8.0-latest.1, Rails 8.0.5.1, Ruby 3.4.10
  • pitchfork 0.18.2, RUN_PITCHFORK non défini (donc la branche pitchfork est active)
  • conteneur autonome, nginx → http://127.0.0.1:3000
  • 2 vCPU / 4 Go de droplet ; le démarrage de l’application prend ~40s

Le code

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 retourne dès qu’un processus worker est créé. Avec pitchfork, le worker existe bien avant que l’application n’ait fini de charger, donc la boucle se termine prématurément et l’ancien maître est détruit pendant que le nouveau est encore en cours de démarrage.

Comportement observé

production.log, un rechargement (horodatages UTC) :

23:16:49  Completed 200 OK in 95ms        <- outgoing generation, warm
          (44 seconds; not one Completed line)
23:17:33  Booted Rails 8.0.5.1 application in production environment
23:17:36  Completed 200 OK in 4128ms      <- first render on the new generation, cold
23:17:39  Completed 200 OK in 200ms

Un moniteur interrogeant GET / toutes les ~2s via nginx pendant cette fenêtre a enregistré :

23:17:11  499  19.697s   (client gave up; upstream never answered)
23:17:29  503  16.301s
23:17:31  502   1.448s

et le journal d’erreurs de nginx pour le 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 est égal à request_time dans chaque ligne en échec, donc nginx n’est pas le goulot d’étranglement — les requêtes atteignent un processus qui ne peut pas y répondre. La réinitialisation à 23:17:31 est cohérente avec SIGTERM détruisant le maître sortant avec des connexions toujours en attente dessus.

Ceci est reproductible : deux rechargements dans un déploiement ont produit deux fenêtres identiques d’environ 40s, chacune se terminant sur une ligne Booted Rails.

Pas de pression sur les ressources : pas de tuages OOM dans dmesg, swap à 141 Mo sur 2048 utilisés, moyenne de charge 0.07, et les deux démarrages se sont terminés normalement.

Correction suggérée

Mimer la branche unicorn — attendre que le nouveau maître serve réellement avant de mettre hors service l’ancien, et le mettre hors service proprement :

   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 moitié QUIT-au-lieu-de-TERM est sans ambiguïté : pitchfork hérite de la sémantique des signaux d’unicorn, où QUIT vid les connexions et TERM est l’arrêt immédiat.

PS : C’est mon premier rapport assisté par IA. Faites-moi savoir si je viole l’étiquette d’une manière ou d’une autre. :sweat_smile: