0031 — Mescla concorrente: duas defesas contra deadlock (40P01)¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-10 · Decidido em: 2026-07-27
Contexto¶
Desde o envio em lote (0032), um .zip traz N planilhas e cada uma
vira um processamento próprio no executor processamento (4 threads): dois arquivos do mesmo lote
mesclam em paralelo no banco. A mescla é o UPSERT diff-aware + DELETE seletivo
(0006, 0007), uma transação
por arquivo, em bi_faturamento e bi_movimento.
Três fatos do PostgreSQL tornam isso um deadlock (40P01) latente — o Postgres derruba uma das
transações e o processamento vai a ERRO:
INSERT … ON CONFLICT … DO UPDATEtrava a linha conflitante mesmo quando oWHERE … IS DISTINCT FROMnão atualiza nada — linha inalterada também entra na disputa.- Sem ordem definida, cada transação adquire os row locks na ordem em que o
SELECTdaTEMP TABLEdevolve as linhas; duas transações que tocam as mesmas chaves em ordens diferentes se travam mutuamente. - O DELETE seletivo, na mesma transação, trava linhas na ordem do scan, que nenhum
ORDER BYcontrola.
O bi-comercial-xls tinha o mesmo risco e o corrigiu com o ORDER BY. Aqui o risco é maior, por
causa do DELETE seletivo.
Decisão¶
Duas defesas, ambas obrigatórias (commit ff1f811, release 2.1.3):
ORDER BYpela chave natural noSELECTdoINSERT … SELECT … ON CONFLICTdoBulkUpserter: a ordem de aquisição de row lock fica determinística entre transações concorrentes (convenção da frota bi-*).- Advisory lock de mescla:
ProcessamentoTransactionalHelper.mesclarDadosDoPeriodoabre, como primeira instrução, comBulkUpserter.adquirirLockMescla→SELECT pg_advisory_xact_lock(<chave>). Serializa a fase de banco inteira e cobre o que oORDER BYnão cobre: UPSERT × DELETE seletivo e DELETE × DELETE.
Detalhes que fazem parte da decisão:
- Uma chave só, global (
ADVISORY_LOCK_MESCLA, o ASCII de"bitransp"), para as duas tabelas: faturamento e movimento são sempre mesclados juntos, na mesma transação. pg_advisory_xact_lock, liberado sozinho no COMMIT/ROLLBACK da transação da mescla.adquirirLockMesclanão tem@Transactional, de propósito: exige a transação do chamador (e falha alto sem ela). Numa transação própria, o lock seria liberado na saída do método — inútil, em silêncio.- Espera acima de 50 ms vai para o log do processamento ("Aguardou N ms pelo lock de mescla").
Consequências¶
- Só a fase de banco enfileira; o parse do Excel (a fase cara) segue paralelo nas 4 threads.
- O custo é throughput de banco serializado: mesclas não se sobrepõem, nem entre lotes diferentes (a chave é global).
- Remover qualquer uma das duas reintroduz o deadlock com 2+ arquivos em paralelo. O
ORDER BYé guardado peloBulkUpserterSqlTest(pré-condição estrutural no SQL gerado); a invariante está no javadoc doBulkUpsertere no gotcha doCLAUDE.md.
Alternativas consideradas¶
- Só o
ORDER BY(a correção do bi-comercial-xls): não cobre o DELETE seletivo, cuja ordem de lock segue o scan. Insuficiente aqui. - Serializar o executor inteiro (uma thread): também elimina o deadlock, mas serializa o parse, que é a parte cara. O advisory lock serializa só o banco.