Pular para o conteúdo principal

Sync, logs, checkpoints e replay

Imagine uma loja sem internet por oito horas. O operador continua emitindo orçamentos. Quando a rede volta, o runtime precisa saber o que já foi enviado, o que é novo, o que é retry e o que é conflito.

O Runtime precisa responder:

  • quais registros foram enviados antes da queda;
  • quais registros são novos;
  • se um reenvio é retry ou payload conflitante;
  • se o batch anterior aceito combina com a chain do sender;
  • de onde o replay deve continuar após um crash.

Sync no AppCore é replicação conservadora leader-to-follower. Não é RAFT, multi-master nem resolvedor de conflitos de domínio.

O que acontece quando a loja reconecta?​

Commands locais podem continuar conforme a política de storage e command. Quando a rede volta, o receiver não confia no batch: valida identidade, protocolo, sequência, count, tamanho, hash, previous hash e checkpoint.

O receiver verifica:

  • compatibilidade da identidade de origem;
  • intervalo de sequence;
  • quantidade declarada de eventos;
  • tamanho do payload;
  • SHA-256 dos eventos;
  • hash do batch anterior;
  • sequences repetidas;
  • estado do checkpoint.

Se a mesma sequência reaparece com o mesmo payload, é retry. Se a mesma sequência reaparece com bytes diferentes, não é retry: é conflito.

O que um SyncMessage prova?​

Um batch carrega batch_id, source node, sequence_start, sequence_end, event count, hash dos eventos, timestamp, previous batch hash e payloads opacos.

  • batch_id como identidade idempotente;
  • source node ID;
  • sequence_start e sequence_end inclusivos;
  • quantidade declarada de eventos;
  • hash de metadata e payloads prefixados por tamanho;
  • creation time;
  • hash opcional do batch anterior;
  • payloads opacos dos eventos.

O hash cobre metadata e tamanho dos payloads, não apenas os bytes soltos. Isso impede aceitar o mesmo payload com metadata de sequência diferente.

O batch comprova consistência da replicação no nível do transport. Ele não prova que o evento de negócio está semanticamente correto. O Runtime protege ordem e integridade; a aplicação continua dona do significado do domínio.

Por que manter replication log?​

O log file-backed usa marcador # appcore-replication-log-v1, limite total, limite por record, sequence map, hash chain, lock e atomic write. Append por sequência é idempotente: mesma sequência e mesmo payload retorna o índice original; mesma sequência e payload diferente é conflito.

Ele:

  • usa o marcador estável # appcore-replication-log-v1;
  • limita os bytes totais;
  • limita os bytes de cada record;
  • armazena sequence e metadata da hash chain;
  • recarrega e valida records antes de append;
  • usa process locks e writes atômicos;
  • recupera um prefixo válido quando a tail é interrompida.

O log é evidência de replay. Sem log, recovery teria que confiar em projections de aplicação, que podem ter sido compactadas, migradas ou parcialmente reconstruídas.

Por que checkpoint existe se já existe replay?​

Checkpoint guarda última sequência aceita e batch hash por peer em formato # appcore-sync-checkpoint-v1. Peer IDs e hashes são validados e o arquivo é substituído atomicamente.

# appcore-sync-checkpoint-v1
peer-a=42,2f4c...
peer-b=17,

Sem checkpoint, recovery teria que replayar tudo ou inferir progresso pela projection. AppCore não faz essa inferência.

Replay sozinho não basta porque o log pode ser maior que o ponto útil de recovery e projections nem sempre são autoritativas. O checkpoint é uma promessa explícita de que os batches até aquela sequence e hash foram aceitos.

Como a outbox durável faz recovery?​

A outbox file-backed do 1.0.2-rc usa o marcador binário explícito appcore-sync-outbox-v2. Cada enqueue ou ACK acrescenta e sincroniza um frame limitado. Ordinais, comprimentos no início/fim e uma cadeia SHA-256 detectam corrupção, duplicação e reordenação. Somente um frame final incompleto é truncado após crash; um frame completo inválido falha fechado.

Espaço confirmado é recuperado por compactação atômica, que grava apenas mensagens pendentes e muda a geração. Outro processo com visão antiga detecta a mudança e recarrega. O journal continua limitado a 64 MiB e reserva tail suficiente para confirmar a mensagem frontal já aceita.

O contrato de outbox do 1.0.2-rc lê um prefixo ordenado com limites independentes de quantidade e bytes codificados antes de clonar payloads. Ele expõe stats sem payload, persiste attempts/readiness de retry e aplica somente receipts de prefixo ordenado exato. O follower e a CLI do Runtime usam esse caminho diretamente, então o crescimento da fila não define uma única alocação de entrega nem o avanço do checkpoint.

Essa é uma fronteira explícita de formato persistente. Arquivos V1, sem versão ou futuros retornam NO MORE SUPPORTED PLEASE UPDATE; o Runtime não infere nem converte. Operadores drenam V1 antes do upgrade e V2 antes do rollback.

O que torna o replay seguro?​

Idempotency de command e idempotency de batch resolvem problemas diferentes. A key de command evita duplicar retry de cliente. A sequência/checkpoint evita duplicar replicação entre peers. Os dois limites precisam existir porque os retries acontecem em fronteiras diferentes.

Persistência SQLite opcional depois do 1.0​

A prévia pós-1.0 publicada appcore-sync-sqlite 0.1.0-alpha.6 persiste apenas registros de sync do Runtime: replication log, outbox, checkpoints por peer e tombstones opacos. Ela usa schema interno versionado, WAL, sincronização completa, conexões e limites explícitos, snapshots portáveis, backup online verificado e integrity scan que falha fechado. Ela nunca expõe SQL arbitrário nem tabelas de aplicação.

O provider suporta processos locais independentes em filesystem com locking confiável. Shares de rede, SQLite multi-host e seleção automática pelo manifest V1 estável estão fora do contrato. Schemas internos desconhecidos ou futuros retornam NO MORE SUPPORTED PLEASE UPDATE; formatos file-provider não são importados por inferência. Veja a prévia appcore-sync-sqlite.

Por que AppCore não resolve conflitos automaticamente?​

Replicação multi-master exige um modelo de conflito do domínio. O Runtime não sabe se reservar estoque, editar uma nota, aprovar um orçamento e rotacionar um secret possuem a mesma semântica. Por isso sync permanece conservador e a aplicação possui a policy de conflito de negócio.

Limitações​

  • Sync é leader-to-follower, não RAFT nem multi-master.
  • AppCore detecta conflito de sequência/hash, mas não mescla alterações de negócio.
  • Checkpoint prova progresso aceito pelo runtime, não correção de projection.
  • Replay depende de handlers respeitarem idempotency.
  • Outboxes file V1 e V2 não são mutuamente legíveis; upgrade e rollback exigem fila durável vazia.
  • Partições de rede são tratadas de forma conservadora; writes que exigem liderança não prometem disponibilidade global contínua.

Próximo: operação distribuída.

Causalidade de revisões opacas​

ObjectRevisionV1, RevisionChainV1 e decide_object_revision_v1 classificam already_applied, ancestor_echo, applicable, concurrent_branch e snapshot_required. A cadeia aceita até 1 024 revisões com identidades de 128 bytes. Autentique cada evento e vincule o tenant antes de construir evidência: a validação da cadeia é estrutural, não criptográfica. Links ausentes, ciclos, objeto incompatível e identidades de evento/revisão reutilizadas são recusados. Igualdade de conteúdo ou datas não prova ancestralidade. Uma reversão intencional usa revisão/evento novos. Avance o checkpoint de transporte separadamente ao aceitar um eco; chame validate_checkpoint_head_v1 antes de alterar metadados.

Commits compostos e objetos recuperáveis (schema V3)​

SqliteSyncStore::transaction oferece SqliteSyncWork limitado para append do log, enqueue da outbox, checkpoint e stage_object_write numa transação SQLite imediata com até 1 024 operações. Propague todos os erros; falha/unwind do callback faz rollback. Não reentre no store nem faça rede/filesystem dentro do callback. Outbox cheia é erro nesta composição. Checkpoints exigem head observado autenticado. conflict_store persiste evidência limitada e resolução idempotente nos contratos Sync existentes (até 4 096 conflitos). Schemas conhecidos V1/V2 do provider migram para V3; ownership é verificado antes da migração. Schemas estrangeiros/desconhecidos atingem a barreira de versão. Preserve backup verificado anterior ao V3 para rollback; binários antigos não abrem V3. Não abra a base de uma aplicação como este provider nem infira compatibilidade entre versões diferentes de rusqlite.

ObjectWriteIntentV1 regista envelopes cifrados/opacos até 1 MiB, instalação e chave opaca do objeto junto da outbox. Até 4 096 intents; páginas admitem até 1 024 registos/16 MiB antes de ler payloads. Reaplique intents committed em FIFO com pending_object_writes, derive paths pela política segura da aplicação, publique atomicamente/sincronize/verifique o envelope e só depois chame acknowledge_object_write com o digest observado. Serialize escritas por objeto no deployment e nunca reutilize IDs concluídos. Ack regista evidência do caller, sem verificar o filesystem autonomamente. Não há atomicidade universal filesystem/SQLite. Backup online inclui conflitos/intents; o manifest conjunto de ficheiros/SQLite e a identidade de instalação pertencem ao deployment.

Retransmissão e serialização do checkpoint​

Correção ainda não publicada: uma repetição autenticada idêntica recebe ACK aceite sem novos registos; reutilizar um batch_id retido com conteúdo divergente falha com divergent batch_id reuse. O receiver retém no máximo 10.000 pares ID/digest. A janela é local ao processo, não evidência permanente; após reinício ou expulsão aplica-se a política existente de checkpoint para sequências antigas. Autentique e isole a origem antes de chamar o receiver. Receivers distintos devem partilhar o handle do log quando partilham checkpoints.

Leitura do checkpoint, verificação da sobreposição, append e publicação de sequência/hash usam o mesmo lock do log. Lotes concorrentes não publicam um checkpoint antigo menor nem combinam sequência e hash de lotes diferentes. Erros do provider ainda podem deixar o log atualizado com checkpoint antigo; a repetição verifica a sobreposição antes de publicar o checkpoint. O sender remove o recibo da outbox só após confirmação do transporte. A perda do primeiro ACK provoca retransmissão segura e posterior drenagem. Processos externos e handles de log independentes exigem coordenação transacional do provider/deployment.