appcore-sync 2.0.0-alpha.2

Conservative sync contracts and local replication helpers for AppCore Runtime.
Documentation

appcore-sync

English guide | Guia em português | Guide français

Conservative leader-to-follower replication with versioned wire, log, snapshots, checkpoints, outbox, receiver and transport contracts.

Responsibility: conservative leader-to-follower replication and local durability helpers.

Internal dependencies: appcore-core, appcore-distributed-contracts, appcore-ops, appcore-transport.

Main API: node role/status/peer/heartbeat and SyncMessage; V1 wire codec; replication logs/snapshots; memory/file checkpoints and outbox; receiver state/acknowledgement; follower client; HTTP transport; peer discovery; retry, metrics and SyncError.

Identity, protocol, sequence and hash-chain validation are mandatory. This crate is not RAFT, multi-master consensus or a domain conflict resolver. Reexported opaque-envelope contracts expose the 1,024-byte MAX_OPAQUE_MESSAGE_ID_BYTES retention bound.

File logs, snapshots, checkpoints and outbox records are versioned and bounded. The receiver validates the complete incoming batch, sequence range and record sizes before mutating the replication log or checkpoint.

FileSyncCheckpointStore scans V1 through a fixed 16 KiB reader. Startup validation retains no peer map, and lookup validates the complete file while owning only the requested hash. Mutation still builds the canonical sorted map once, but writes it directly through a fixed buffer instead of keeping an additional file-sized String. The public ceilings are 8 MiB, 65,536 non-empty records and 256 UTF-8 bytes per peer ID; an encoded line is bounded before it can grow the scratch buffer. Duplicate peers keep the last value as before and the next successful mutation canonicalizes them.

FileReplicationLog scans V1 incrementally and retains only a compact sorted vector of sequence/record-index pairs plus per-record offsets, lengths and digests, not every payload. The in-memory implementation uses the same flat sorted index and binary-search lookup, avoiding hash-bucket overhead for bounded local logs. Concurrent instances validate the hash-chain anchor and scan only a new tail; atomic snapshot replacement is treated as a new generation and rebuilt safely. events_page limits reads to 1,024 records and 48 MiB before allocation. The complete events_since remains source-compatible but rejects a file read above those limits. Deployment-owned sync tools should use pages; no Runtime sync CLI is shipped. The log remains capped at 256 MiB, each payload at 1 MiB and the index at 262,144 records. Runtime HTTP batches additionally stop at 1 MiB of raw events; the encoded V1 JSON envelope is capped at 5 MiB so even the worst-case numeric byte representation of one valid payload remains transferable. The V1 encoder serializes borrowed identity, message and event fields directly into its required output string. It does not clone the complete batch before encoding, and its JSON remains byte-identical to the owned V1 contract.

ReplicationSnapshot::try_from_records consumes owned sequence/payload pairs and moves their allocations into a checksum-protected V1 snapshot. ReplicationSnapshot::validate checks the same format, count, payload, sequence and checksum invariants through a shared reference, without creating a second payload collection. Persistent providers can therefore validate before mutation while retaining one semantic snapshot owner. Memory-backed consumers that own the snapshot can call InMemoryReplicationLog::restore_snapshot_owned to validate and move payloads directly into the log, avoiding simultaneous snapshot and destination copies.

In 1.0.2-rc, ReplicationLog::len, last_index and is_empty return SyncResult. Persistent providers surface observation failures instead of substituting zero or stale state. Consumers must handle the result before updating; see release/fallible-replication-log-observations.md.

The 1.0.2-rc FileSyncOutbox uses the explicit appcore-sync-outbox-v2 append-only binary journal. Enqueue and acknowledgement sync one integrity-chained frame; readers scan only a new tail, and bounded compaction atomically retains pending messages. Only an incomplete final frame is recoverable. A complete corrupt, V1, unversioned or future-format file fails closed. Drain the V1 queue before upgrading and the V2 queue before rollback; see release/outbox-v2-migration.md.

The resident file-outbox state contains only batch IDs, journal offsets, encoded lengths, payload digests and retry metadata. Enqueue measures JSON in a bounded pass and then writes it through a fixed 64 KiB buffer; it does not keep an encoded copy. Front and page reads decode and verify one indexed message at a time. Each batch ID is shared with transactional tail-scan state, so refresh clones only handles instead of copying every pending identifier. The source-compatible messages() snapshot still returns an owned Vec, so memory-sensitive consumers must use bounded pages.

InMemorySyncOutbox also obtains the exact encoded length with an overflow-checked JSON counter rather than allocating and discarding a complete encoded message. A valid 4 MiB batch measured 10.92 ms p50 and 17.52 MiB peak RSS on Apple M1, down from 23.55 ms and 45.73 MiB with the temporary buffer.

The receiver's 10,000-entry processed-batch window retains one shared allocation per batch_id across duplicate lookup and acceptance-order eviction. Applying 10,000 batches with 128-byte IDs measured 58.27 ms p50 and 15.27 MiB peak RSS on Apple M1, down from 61.29 ms and 17.45 MiB with duplicate strings. Receiver and outbox boundaries reject empty IDs, control characters and IDs above 1,024 UTF-8 bytes before retaining them. This keeps the fixed entry window bounded by bytes as well as count. Duplicate rejection and oldest-first eviction are unchanged.

The additive 1.0.2-rc SyncOutbox paging contract exposes peek, stats, mark_attempt, next_ready and ordered partial receipts. Page reads are capped at 1,024 messages and 48 MiB before payload clones. The in-memory provider and file providers implement exact paging and retry observations. File attempts and ordered receipts are hash-chained journal frames that survive restart. A provider can call encoded_sync_message_bytes to obtain the exact compact JSON size without an encoded copy, then call write_sync_message_json to stream that same canonical representation to a bounded writer. String escaping remains Serde-compatible and event bytes are emitted through a fixed 16 KiB scratch buffer. A receipt is measured and serialized directly through the fixed 64 KiB writer; the maximum escaped-ID fixture no longer materializes its 2,086,913-byte JSON buffer in production. Journal scans borrow unescaped IDs from the frame. An external provider relying on the compatibility defaults still compiles: it returns at most the front message, reports unknown extended statistics and rejects persisted attempts or multi-message receipts explicitly.

FollowerSyncClient uses this bounded contract directly. Each failed transport call records retry readiness, success applies an exact receipt, and draining exposes the last acknowledged batch for checkpoint progress. The complete pending_messages snapshot remains for source compatibility; new consumers should use pending_page and outbox_stats.

HttpSyncTransport owns a reusable bounded HTTP client. with_timeout_ms keeps the uniform V1 deadline, while with_timeouts selects independent connect/admission, read and write deadlines.

cargo test -p appcore-sync

The default ReplicationLog::events_page is a full-read adapter for external providers: it validates limits, calls events_since, then moves selected payloads into the bounded result. It does not bound that initial read. Providers must override paging to enforce record/byte limits before reads or clones; internal memory/file providers already do so. A smaller returned page alone is not proof of bounded materialization. The consumer regression test is cargo test -p appcore-sync --test external_log_paging.

Maturity: stable conservative RC profile with strict V1 decoding.

Stable documentation

Stable ID: ACR-012. See the supplemental architecture and integration guide. This permanent ID remains valid if the wiki page moves.