Pular para conteúdo

0003 — Coleta coerente: ler duas vezes e conferir

Decisão obsoleta — superada pela decisão 0004

Status: Obsoleto · Responsável: Gustavo Madruga · Atualizado em: 2026-09-08 · Decidido em: 2026-09-08

Obsoleta desde 2026-09-09, superada pela 0004. A premissa desta decisão estava errada: ela trata o pedido órfão como tendo UMA causa (o sync aterrissando entre os SELECTs) e conclui que ler duas vezes basta. Há um segundo modo — a linha que ainda não chegou ao banco local — em que as duas leituras concordam e o lote sai órfão do mesmo jeito. Foi o que três incidentes medidos mostraram (260079421, 260079618, 260078864). A leitura dupla permanece no código, agora como rede do caminho por presença; o que caiu foi a premissa de que ela era suficiente.

Contexto

O contrato pAbast (§3.7) manda escrever o lote na ordem de dependência referencial (propriedades → fones → estoque → contratos → itensped → formulas), e o Fluxo já garantia essa ordem. Só que a ordem garante o arquivo, não o conteúdo: nada obrigava o cliente a ter as linhas do pai no lote.

Incidente do pedido 260079421 (2026-09-08). O coletarPendentes fazia um SELECT por tabela. O PowerSync grava no SQLite local a qualquer momento — inclusive no meio da coleta — e o Agendador acorda justamente no gatilho de mudança, ou seja, coleta enquanto o dado aterrissa. Medido no log:

12:14:39.018  push do pedido aplicado no servidor (6 tabelas, 1 transação)
12:14:39.073  coleta: SELECT propriedades/fones/estoque → o pedido ainda não chegou
              ...as linhas aterrissam no SQLite...
              coleta: SELECT contratos/itensped/formulas → chegaram
12:14:43      próximo ciclo pega propriedades/fones/estoque

O arquivo saiu com o pedido sem o cliente. O ZIM cadastrou cliente e produto no lote seguinte, mas o pedido já tinha levado 120 -Cliente CPF ... não cadastrado. — e 1XX é terminal, não retenta. Pedido órfão, sem caminho de volta pelo cliente.

Do lado servidor não havia o que corrigir: o applyPut do integrador-server aplica o payload inteiro em uma transação e o PowerSync tem um stream só com as 6 tabelas. A publicação já era atômica; quem quebrava a atomicidade era a leitura.

Decisão

Ler duas vezes e só aceitar o lote quando as duas leituras batem (Processador.coletarPendentes). Duas leituras iguais significam que nada aterrissou entre elas — logo a segunda é uma foto coerente. Diferentes significam que aterrissou: vale a segunda e confere de novo, até três vezes. Banco que não sossega devolve lote vazio e espera o próximo ciclo; o dado não some, só espera um momento mais calmo.

A comparação é por id + cod_retorno na ordem — o par que decide quem entra no lote. Mudança só de campo de dado (o servidor reescrevendo um valor de linha que já estava lá) não é detectada: ela não cria pedido órfão, que é o que esta conferência existe para impedir.

O laço de leitura (lerTodasAsTabelas) é exatamente o de antes. O custo é uma passada extra de SELECTs por ciclo, em tabelas locais pequenas — irrelevante perto de rodar o zimrtmu.exe.

O que foi descartado

  • Transação de leitura no wrapper (readTransaction do SDK). Resolveria igual, mas o PowerSyncWrapper é ponte e ponte fica magra — regra da casa, no cabeçalho da própria classe.
  • SELECT composto com UNION ALL das 6 tabelas (um statement = um instante). Correto e sem tocar no wrapper, mas exige padding de NULL até a maior largura e colunas de controle: SQL gerado bem mais difícil de ler que o laço de duas leituras, para um público que programa em ZIM.
  • executar("BEGIN") … COMMIT") em volta dos SELECTs. Não funciona: o SDK avisa que statement no nível do banco pode cair em conexão diferente (pool de leitura) — o BEGIN abriria transação numa conexão e os SELECTs rodariam noutra, sem snapshot e deixando transação órfã.
  • Gate de dependência referencial (segurar a linha cujo pai não está gravado nem no lote). Chegou a ser implementado e foi removido: resolvia o mesmo bug que a leitura dupla já resolve, ao custo de ~100 linhas, cinco regras FK — uma delas apoiada numa suposição sobre o dado (formulas → kit) — e de um modo de falha novo: filho de pai morto em 1XX ficava segurado indefinidamente, com só um INFO por ciclo. Duas mecânicas para um bug é uma a mais.

Consequências

  • Um pedido cujo cliente ainda não chegou não sai — sai inteiro no ciclo seguinte.
  • Ciclo com o banco recebendo sync sem parar é adiado, não processado pela metade. Fica no log (coleta não estabilizou em N leituras).
  • Não há defesa contra órfão de causa diferente desta (ex.: o servidor publicar um pedido sem o cliente). Se aparecer, o 1XX do ZIM denuncia — e aí a decisão se revisita com o caso na mão, não por antecipação.

Lacuna conhecida (fora deste repo)

Uma linha em 1XX não tem caminho de volta: o re-importar do painel PIED reenvia o payload, mas o upsert do integrador-server preserva o write-back por regra ("mão única") e não toca o cod_retorno — a linha segue 1XX e o cliente nunca a recoleta. O botão do painel, hoje, não re-arma nada. Correção pertence ao par int-pied/integrador-server; enquanto não vier, o desempate é UPDATE ... SET cod_retorno = '000' na mão.