Sv hup unicorn dropping tráfego

Estive enfrentando uma queda de ~40s ao recarregar meu site. Isso está um pouco além das minhas habilidades técnicas, então pedi ajuda ao Claude.

Ele afirma que o problema é que o lançador do unicorn está enviando o sinal errado para o mestre antigo (SIGTERM em vez de SIGQUIT), o que o mata antes que o novo possa começar a atender.

A correção é aparentemente uma atualização de 6 linhas, mas gostaria de verificar se faz sentido aqui primeiro.

Clique para ver o relatório gerado pelo Claude

Resumo

No caminho do código pitchfork, on_reload_pitchfork() do config/unicorn_launcher retira o mestre antigo assim que um processo worker existe, sem nunca verificar se o novo mestre pode atender a uma solicitação, e o retira com kill (SIGTERM, desligamento imediato) em vez de kill -s QUIT (desligamento gracioso).

O caminho do unicorn imediatamente acima, on_reload_unicorn(), faz ambos corretamente — ele emite curl $LOCAL_WEB para aquecer o novo mestre e depois envia QUIT.

O resultado observado de um único sv hup unicorn é de ~40 segundos durante os quais nenhuma solicitação é concluída, terminando exatamente quando a nova geração registra Booted Rails.

Ambiente

  • Discourse 2026.8.0-latest.1, Rails 8.0.5.1, Ruby 3.4.10
  • pitchfork 0.18.2, RUN_PITCHFORK não definido (então a ramificação pitchfork está ativa)
  • contêiner independente, nginx → http://127.0.0.1:3000
  • 2 vCPU / 4 GB droplet; inicialização do app leva ~40s

O código

config/unicorn_launcher:

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

  curl $LOCAL_WEB &>/dev/null      # aquece o novo mestre
  kill -s QUIT $UNICORN_PID        # gracioso
}

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, e sem aquecimento primeiro
}

pgrep -f -P $UNICORN_PID worker retorna assim que um processo worker é gerado. No pitchfork, o worker existe muito antes que a aplicação tenha terminado de carregar, então o loop sai precocemente e o mestre antigo é desmontado enquanto o novo ainda está inicializando.

Comportamento observado

production.log, uma recarga (carimbos de tempo UTC):

23:16:49  Completed 200 OK in 95ms        <- geração de saída, quente
          (44 segundos; nem uma linha Completed)
23:17:33  Booted Rails 8.0.5.1 application in production environment
23:17:36  Completed 200 OK in 4128ms      <- primeiro render na nova geração, frio
23:17:39  Completed 200 OK in 200ms

Um monitor fazendo polling GET / a cada ~2s através do nginx durante essa janela registrou:

23:17:11  499  19.697s   (cliente desistiu; upstream nunca respondeu)
23:17:29  503  16.301s
23:17:31  502   1.448s

e o log de erro do nginx para o 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 é igual a request_time em cada linha com falha, então o nginx não é o gargalo — as solicitações chegam a um processo que não pode respondê-las. O reset em 23:17:31 é consistente com o SIGTERM desmontando o mestre de saída com conexões ainda em fila nele.

Isso é reproduzível: duas recargas em uma única implantação produziram duas janelas idênticas de ~40s, cada uma terminando em uma linha Booted Rails.

Não é pressão de recursos: sem OOM kills em dmesg, swap em 141 MB de 2048 usados, média de carga 0.07, e ambas as inicializações foram concluídas normalmente.

Correção sugerida

Espelhar a ramificação do unicorn — esperar até que o novo mestre realmente atenda antes de retirar o antigo, e retirá-lo de forma graciosa:

   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

A metade QUIT-em-vez-de-TERM é inequívoca: pitchfork herda a semântica de sinal do unicorn, onde QUIT drena e TERM é a parada imediata.

PS: Este é meu primeiro relatório assistido por IA. Me avise se estou quebrando alguma etiqueta de qualquer forma. :sweat_smile: