Skip to content

Ctrl-C grava no checkpoint as linhas concluídas, e a retomada não as paga de novo - #259

Open
bdcdo wants to merge 6 commits into
mainfrom
fix/ctrl-c-grava-checkpoint
Open

bdcdo wants to merge 6 commits into
mainfrom
fix/ctrl-c-grava-checkpoint

Conversation

@bdcdo

@bdcdo bdcdo commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Problema

Um Ctrl-C (KeyboardInterrupt) no meio da execução não gravava o checkpoint. O arquivo ficava na última gravação de batch_size, e a retomada pagava de novo as linhas do lote em andamento. Com 12 linhas, batch_size=5 e Ctrl-C durante a 9ª chamada, o modo sequencial tinha 8 chamadas pagas e 5 linhas processed no arquivo, e a retomada chamava t06, t07 e t08 de novo. Com parallel_requests, 9 chamadas pagas, as mesmas 5 no arquivo e 4 linhas pagas duas vezes. O resultado é igual com entrada pandas e polars; com pandas, as linhas pagas ainda ficam no DataFrame do usuário, alterado no lugar, mas o arquivo fica igualmente atrasado.

A interrupção por ProviderUsageLimitError já gravava o checkpoint, e o teste de test_retomada_assinada.py registrava essa diferença.

O que muda

  • _process_rows e _process_rows_parallel tratam o KeyboardInterrupt num envoltório dos laços: gravam as linhas registradas desde a última gravação e relançam a interrupção. Sem linha nova desde a última gravação, o arquivo fica como estava; um Ctrl-C antes da primeira linha de uma execução que recusou o checkpoint anterior, por exemplo, não o sobrescreve.
  • No sequencial, a linha cuja chamada o Ctrl-C cortou não chegou a ser registrada e fica pendente.
  • No paralelo, a saída do ThreadPoolExecutor já espera as chamadas em andamento, e elas chegam ao envoltório registradas. A cópia do DataFrame sai sob a trava das linhas, e a gravação passa pelo _SnapshotWriter, que descarta o snapshot sem linha contada desde o último gravado e não grava ao mesmo tempo que uma thread.
  • A falha ao gravar segue a semântica de _try_save_checkpoint: vira aviso, e o KeyboardInterrupt sai como entrou.
  • Sem checkpoint_path, nada muda.
  • A FAQ, em docs/guides/faq.md e docs/en/guides/faq.md, diz o que o Ctrl-C faz com o lote em andamento.

O tratamento de cada linha (sucesso, erro, abort, limite de uso) não muda. O diff reindenta o corpo dos dois laços, que passam a ficar dentro do try; git diff -w mostra só o envoltório.

Decisão sobre o modo paralelo

Pela decisão posta na issue, a gravação registra só as linhas concluídas, e as que estavam em andamento ficam pendentes para a retomada. Na prática, o primeiro Ctrl-C espera as chamadas em andamento terminarem, e elas entram na gravação. Só um segundo Ctrl-C durante essa espera deixa chamadas sem terminar, e elas ficam pendentes; o teste test_segundo_ctrl_c_grava_so_as_linhas_concluidas cobre esse caso.

Testes

  • test_ctrl_c_grava_as_linhas_pagas_e_a_retomada_nao_as_chama_de_novo: o caso da issue, com entrada pandas e polars, no sequencial e com 3 workers. O Ctrl-C é um SIGINT mandado ao thread principal, como o do terminal.
  • test_falha_ao_gravar_no_ctrl_c_avisa_e_mantem_o_keyboard_interrupt, nos dois modos.
  • test_ctrl_c_antes_de_terminar_uma_linha_nao_reescreve_o_checkpoint, nos dois modos.
  • test_ctrl_c_sem_checkpoint_deixa_as_linhas_pagas_na_entrada_pandas, nos dois modos.
  • test_execucao_recusada_e_interrompida_nao_adota_o_checkpoint_antigo passa a interromper B depois de uma linha: com o limite de uso e com o Ctrl-C, B grava o próprio checkpoint, e C retoma dele sem nenhuma resposta de A.

Closes #213

bdcdo added 6 commits October 21, 2026 22:07
O Ctrl-C é simulado com SIGINT ao thread principal: no sequencial ele corta
a chamada em curso, e no paralelo chega enquanto o thread principal espera
as linhas. O caso da issue, 12 linhas, batch_size=5 e Ctrl-C na 9ª chamada,
roda com entrada pandas e polars nos dois modos e confere que o checkpoint
tem as linhas pagas e que a retomada não as chama de novo.

O teste da execução recusada passa a interromper B depois de uma linha: com
o limite de uso e com o Ctrl-C, B grava o próprio checkpoint e C retoma dele.
Os dois laços passam a tratar o KeyboardInterrupt num envoltório: gravam
as linhas registradas desde a última gravação e relançam. Sem linha nova,
o arquivo fica como estava. No paralelo, a saída do executor espera as
chamadas em andamento, e a cópia sob a trava leva só as que terminaram;
a gravação passa pelo _SnapshotWriter, que não regride o arquivo. A falha
de gravação vira aviso em _try_save_checkpoint e não troca a interrupção.

O tratamento de cada linha não muda; o diff reindenta o corpo dos laços.
No pytest 7.0, o piso do extra dev que o job lowest-direct instala, um
pytest.warns dentro do outro engole os avisos do de fora, e o aviso de
outra configuração não chegava ao pytest.warns que o esperava.
…inha vai para process_row

O try em volta do laço inteiro reindentava o registro da linha, e qualquer mudança dentro dele conflitaria com este ramo. O registro volta à indentação da main numa função interna, e só o laço que pula, conta, grava e espera fica dentro do try. A gravação segue no frame de _process_rows, e o aviso de falha continua apontando a chamada do usuário.
O try em volta do with do tqdm reindentava o despacho e a coleta das linhas. Um gerenciador de contexto, _on_ctrl_c, entra no with que já existia e chama a gravação no Ctrl-C antes de relançá-lo. Ele fica fora do tqdm, que fecha antes da gravação, como no try. A gravação vira a função interna save_registered_rows, com o comentário que estava no except, e o PLR0912 que o try exigia sai do noqa.

This branch has not been deployed

No deployments
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.

Ctrl-C não grava o checkpoint, e a retomada paga de novo o lote em andamento

1 participant