Skip to content

fix(ci): a fatia do servidor não trava no process.exit; os arquivos saem sozinhos, sem --test-force-exit - #862

Merged
leandrotcawork merged 9 commits into
mainfrom
feat/conexus-ci-server-hang
Nov 8, 2026
Merged

leandrotcawork merged 9 commits into
mainfrom
feat/conexus-ci-server-hang

Conversation

@leandrotcawork

Copy link
Copy Markdown
Collaborator

O defeito

Às vezes a fatia server (3) parava de imprimir e batia o tempo limite de 10 min, sem nenhum teste falhar e sem disparar o --test-timeout. Desde que agents.eval.test.ts entrou (2026-11-07), 7 das ~79 execuções da server (3) travaram assim, cerca de 1 em 11. Todas pararam logo depois dos dois testes pulados de agents.eval.test.ts:

Run Job Fatia
42417426925 128538432810 3/6
42425067016 128561472103 3/6
42481897892 128735442105 3/6
42497922500 128784286916 3/6
42504358254 (tentativas 1 e 2) 128803677316, 128806809277 3/6
42506916456 128811613372 3/9

Nos jobs da 3/6, o órfão que o runner encerra no cancelamento tem o pid do runner + 6, que é o primeiro filho: o processo do próprio agents.eval.test.ts, que nunca saiu.

Uma correção ao brief: a tabela "Test files" do job 128806809277 que vimos com gh run view --job é da tentativa 3, que passou. As duas tentativas canceladas param depois dos pulos do eval, como as outras. O job 128628213122 (04:40) é outro caso: imprimiu até o cancelamento, então foi lentidão, não travamento.

A causa

O gdb no filho travado, no runner do GitHub (ubuntu-24.04, Node 24.21.0), mostra um deadlock dentro de process.exit():

MainThread: Environment::Exit → DefaultProcessExitHandlerInternal → NodePlatform::Shutdown
            → WorkerThreadsTaskRunner::Shutdown → uv_thread_join
V8Worker:   ConcurrentBaselineCompiler::JobDispatcher::Run → BaselineCompiler::Build
            → HeapAllocator::AllocateRawWithLightRetrySlowPath
            → CollectionBarrier::AwaitCollectionBackground

A thread principal espera os workers do V8 terminarem. Um worker, no meio de uma compilação concorrente (Sparkplug ou Maglev), espera a thread principal coletar lixo. Quem chama esse process.exit() em cada arquivo é o --test-force-exit (node:internal/test_runner/test:1492).

O agents.eval.test.ts cai nessa janela porque carrega ~2000 módulos com o cache de compilação frio, pula os dois testes e sai uns 2 s depois, com o V8 ainda compilando em segundo plano.

É o bug aberto do Node nodejs/node#54918: process.exit() junta os workers sem descartar o isolate. A correção nodejs/node#66171 ainda está aberta. Quando o processo termina sozinho, o caminho de saída descarta o isolate, e isso acorda o worker parado.

A correção

  • apps/server/package.json: sai o --test-force-exit. O processo de cada arquivo termina quando nada o segura, sem process.exit().
  • Os prazos dos testes deixam de segurar o processo, que era a mesma classe escondida pelo --test-force-exit:
    • within (test-questions.ts, 86 usos) deixava um sleep de até 30 s vivo depois que a promessa respondia. Agora usa ref: false, opção nativa de node:timers/promises.
    • Os prazos de conversation-preview.test.ts:66 e code-tool.test.ts:165 seguem o mesmo padrão (ref: false e .unref()), como os prazos vizinhos já faziam.
  • ci.yml: o comentário do passo diz por que não há --test-force-exit.

Antes, o motivo do --test-force-exit (d98064c) era um teste falho com um turno parado no cartão manter o processo vivo pelo heartbeat. Desde então, o fechamento do Hub ganhou o prazo de 15 s e a limpeza roda depois dele. Para conferir, forcei a falha em approval.test.ts logo depois do cartão: sem --test-force-exit, o teste falhou em 1,8 s e o arquivo saiu em 4 s.

Contornos descartados, com medição

Tentativa Travamentos na cabeça da fatia 3/6 no CI
como está no main 25 / 75
--no-concurrent-sparkplug 2 / 16 (o Maglev também para)
--no-concurrent-sparkplug --no-maglev-build-code-on-background 1 / 75 (o Maglev ainda aloca DeoptimizationData no worker)
sem --test-force-exit (este PR) 0 / 75

O Node 24.21 recusa --no-concurrent-maglev. Desligar compiladores um a um seria remendo e não fecha a classe.

O que rodei

As provas rodaram num workflow temporário no meu branch, já removido no último commit:

  • Cabeça da 3/6 (os 4 primeiros arquivos, cache frio a cada vez), 3 × 25 execuções, com o comando deste PR: 0 travamentos. Com o comando do main: 25 em 75, todos com a pilha acima (run 42519629274).
  • As 6 fatias com npm test -w @conexus/server deste PR (run 42525250258): todas com exit 0. Wall menos o tempo de teste dá ~1,4–2,0 s por arquivo, que é a partida do processo; nenhum arquivo fica esperando timer.
Fatia Wall Segundos de teste Arquivos
1/6 397 s 305 47
2/6 287 s 220 48
3/6 397 s 299 49
4/6 387 s 318 48
5/6 336 s 263 48
6/6 341 s 275 49
  • Localmente, sem --test-force-exit: conversation-update.test.ts levava 35,1 s e agora leva 5 s (o sleep de 30 s de within, achado com async_hooks). conversation-preview.test.ts e code-tool.test.ts passam e saem logo. Typecheck do servidor e Biome nos arquivos tocados passam.

O que não provei

  • Não há teste automatizado do deadlock: ele é probabilístico e vem do Node. A prova é a medição acima.
  • Não há teste do within em si (helper de teste, T5). A prova é o tempo de saída medido.
  • O e2e da raiz também usa --test-force-exit e está exposto ao mesmo bug do Node, mas nunca travou no CI. Sem --test-force-exit, a execução saiu normalmente (292 s) e um teste falhou com 502 em tests/e2e/versions.test.ts:109. Não consegui separar essa falha da mudança, então o e2e fica como está. Proponho uma issue para ele.
  • Quando a correção do Node chegar a uma versão fixada, nada aqui precisa voltar: o processo que sai sozinho é o caminho nativo.

@leandrotcawork
leandrotcawork merged commit 21219ed into main Nov 8, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants