Expand description
Compaction + oplog GC (slice B4 of docs/proposals/multi-device-sync.md,
§“Deep dive: the checkpoint / compaction / GC protocol”).
Compaction = checkpoint at the stable frontier, then drop the ops below it. Three guards make that safe, each straight from the proposal:
- The frontier is the min acked HLC over every known device
(
AckTable::stable_frontier) — “GC of a CRDT log is only safe once every replica has folded past the truncation point”. Compaction refuses to run at all when a device present in the log has no ack entry (CompactError::UnackedDevice) or when nothing is acked (CompactError::NothingAcked): ops above ANY device’s acked frontier are data another device hasn’t seen, and are never dropped. (Device eviction — the horizonHthat unpins a dead laptop — is relay policy, B3.) - Checkpoint durable FIRST, then truncate
(
compact_and_truncate). The crash-ordering invariant: a crash after the checkpoint fsync but before the journal rename leaves the full journal plus a redundant checkpoint (harmless — rerunning compaction recomputes the identical content-addressed file); the rename itself is atomic, so mid-truncation crashes leave either the old complete journal or the new complete tail. At no point does acknowledged data exist only in a file that isn’t durably written. - Retention is applied to the checkpoint state, never to the tail.
The proposal’s per-surface retention table
(
RetentionPolicy::proposal_default) trims what the snapshot keeps; ops above the frontier are untouched by policy. Replay surfaces (routing observations — path-dependent, replayed from genesis;crate::oplog::Surface::is_replay_stream) additionally reject any rule but keep-all (CompactError::EventStreamRetention), so an unacked (or acked!) observation tail can never be compacted away. (Conversation is an event-stream multiset too — B2 — but its turns are independent, so it toleratesLastN; only replay streams are forbidden.) Every retention-dropped id-bearing entry leaves a minimal tombstone stub ({"id": …, "tombstone": true}, originalop_id/hlckept), so a"supersedes"reference always resolves against a tombstone, not a hole — including a reference that arrives AFTER compaction. (Preserving only the references visible at compaction time was a reproduced divergence: a later tail op superseding an already-dropped entry made the global fold retain what the compacted device could not. Universal stubs are time-hole-free: both sides reduce the same record to the same stub, deterministically. Entries with no entity id — e.g. content-hash-keyed conversation turns — cannot be referenced by id and drop entirely.) Stubs are carried unchanged by later retention passes and never count against a surface’s retention quota.
Determinism note (the “same frontier ⇒ same snapshot hash” invariant):
retention is deterministic over a fixed state, and the age-based rules’
reference instant defaults to as_of_from_ops — the max payload
"timestamp" among the ops at/below the frontier, i.e. pure over the
same inputs the fold already consumes, so every device compacting the
same frontier derives the same instant with no out-of-band agreement
(this crate never reads a clock; a caller may still pass an explicit
Some(as_of_ms)). The derived value is conservative: a stale max
under-drops, and undated entries never age-drop anyway.
Ordering/recency come from the payload’s numeric "timestamp" field
(ms) and grouping from "agent_id"; an entry with no timestamp is
treated as newest / never age-dropped — undated data is never silently
discarded.
Structs§
- AckTable
- The fold-frontier / ack bookkeeping B3’s relay
ack(frontier)reports against: per-device max folded HLC, monotone-only advance, persisted alongside the checkpoint (temp + atomic rename, like everything durable here). This is the proposal’sacked[device]table, device-local. - Compaction
Outcome - The outcome of an executed compaction.
- Compaction
Plan - A computed (not yet executed) compaction: the retained checkpoint, the ops that stay in the journal, and what happened.
- Retention
Policy - Per-surface retention policy: surface tag → rule, with a conservative keep-all default for unknown surfaces. Applies to the grow-only log tier only — the LWW registry tier is already one compact record per id, and the proposal’s table assigns it no retention.
- Retention
Report - What retention did: per-surface counts of entries dropped entirely
(id-less — nothing can reference them) and the entries reduced to
tombstone stubs, as
(surface_tag, key).
Enums§
- Compact
Error - A compaction failure. No
PartialEq(carriesio::Error); match on variants. - Retention
Rule - How one surface’s checkpoint retention trims — the proposal’s table rows.
Constants§
- RUNS_
MAX_ AGE_ MS - …and drop runs older than 30 days (in ms), whichever is more
restrictive — exactly
RunStore::gc’s rule, made globally coherent. - RUNS_
MAX_ PER_ AGENT - Parity constants with today’s run GC (
car-server-core::run_store): keep the 50 most recent runs per agent…
Functions§
- apply_
retention - Apply per-surface retention to a folded (checkpoint) state — the
proposal’s “the snapshot applies each surface’s retention”, with the
§“Two free properties fall out” referential-integrity rule realized as
universal tombstone stubs: every dropped id-bearing entry is reduced
to
{"id", "tombstone": true}(originalop_id/hlckept) rather than erased, so a"supersedes"reference — even one that arrives after compaction — always resolves. Stubs never count against a rule’s quota and are carried unchanged by later passes. Pure over its inputs;as_of_msis the reference instant for the age rules (no clock reads here —plan_compactiondefaults it viaas_of_from_ops). - as_
of_ from_ ops - The deterministic default reference instant for age-based retention:
the max payload
"timestamp"acrossops(0 when none carry one). Pure over the same inputs the fold consumes, so every device compacting the same frontier derives the identical value — no out-of-band agreement, no clock read. Conservative by construction: a stale max under-drops (age rules see everything as newer), and undated entries never age-drop regardless. - compact_
and_ truncate - Execute a compaction end-to-end on a live journal, enforcing the crash-ordering invariant by construction:
- is_
tombstone - Is this record a retention tombstone stub? (
"tombstone": trueis the reserved markerapply_retentionstamps.) - plan_
compaction - Plan a compaction of
opsat theAckTable’s stable frontier. Pure — no file IO;compact_and_truncateexecutes a plan durably. See the module docs for the three safety guards; per-device HLC monotonicity (enforced byverify_log) guarantees “hlc ≤ frontier” is a chain prefix, so the cut is always anchorable.