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_idcomo identidade idempotente;- source node ID;
sequence_startesequence_endinclusivos;- 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.