Skip to content

feat(store): durable log + snapshots + restore + replay (Phase 2)#30

Open
dsrw wants to merge 7 commits into
mainfrom
feat/durable-store
Open

feat(store): durable log + snapshots + restore + replay (Phase 2)#30
dsrw wants to merge 7 commits into
mainfrom
feat/durable-store

Conversation

@dsrw

@dsrw dsrw commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Lands roadmap Phase 2 (docs/consistency-and-partial-sync-plan.md): the authority appends its canonical op stream to a durable, git-shaped store — append-only JSONL log segments + per-object snapshot directories — restores from snapshot + tail on restart, and serves read-only historical views via EdContext.replay(path, lsn). Design + rationale: docs/persistence.md.

Highlights

  • Append at the stamp point, between stamp_lsn and fanout; the store is a permanently-eligible subscriber, so a headless authority still captures its ops. CREATEs log once at the creation event, never in per-subscriber publish_create.
  • ctx.replaying bypasses the loopback/own-op guards during restore (entries carry our own origin — the delta short-circuit would otherwise drop every self-originated collection op) and suppresses publish/stamp/append. The LSN frontier stays active, making double-covered segments idempotent.
  • Message.epoch is now real: bumped per store open, stamped on ordered ops; a follower seeing a higher epoch from an upstream resets its frontier, closing the fanout-before-durable crash window under the default FlushPerTick durability.
  • Snapshot discipline: tmp dir + fsync + atomic rename, manifest written last; a valid snapshot at the current watermark is kept, never delete-then-replaced; restore refuses truncated history (StoreError) instead of silently canonizing a near-empty world.
  • Reserve-now slots baked into the format: entry v/txn/commit, manifest schema, codec seam for a future human-readable payload codec, platform (endian/int-width) guard.
  • Incidental fix: log_defaults dereferenced a nil thread ctx on threads using only explicit contexts.

Verification

  • nimble test 195/195 (22 new persistence cases: torn-tail vs mid-file corruption, hostile-epoch trust gate, same-id recreate replay, nested/owned restore + fresh-follower push, exactly-once follower capture, ...)
  • ASan build clean (194/194, zero errors)
  • E2E: authority process kill -9'd mid-stream → committed ops survived, un-ticked tail correctly lost, epoch bumped, time-travel view correct, network follower re-synced from the restored authority
  • A high-effort multi-agent review ran on the diff; all 9 code findings (2 data-loss, 1 hostile-input, 2 crash/perf regressions, 4 cleanups) are fixed with regression tests

Deferred (deliberately)

Serving fetch misses from the log (lands with authority eviction), TypeSchema / structure-aware tids (slots reserved), the ack/commit callback, git integration itself. Known pre-existing gap documented in persistence.md: the single-subscriber return-to-source echo violation.

🤖 Generated with Claude Code

https://claude.ai/code/session_014o1niF74BvbfxYU1oWEQ38

dsrw added 5 commits July 7, 2026 14:04
A thread that only uses explicit contexts never mints the implicit
Ed.thread_ctx, so the first emitted warn/notice under log_defaults
dereferenced a nil active_ctx.
The authority appends every canonical op to append-only JSONL log
segments at the stamp point, snapshots full state as per-object files
sealed by a manifest, restores from snapshot + tail on restart, and
serves read-only historical views via EdContext.replay(path, lsn).
The store layout is git-shaped on purpose (immutable rotated segments,
per-object snapshot files, codec seam) -- see docs/persistence.md.

- publish_changes builds/stamps/appends even with no eligible
  subscriber: the store is a permanently-eligible subscriber
- CREATEs log once at the creation event (defaults / relay_fill),
  never in per-subscriber publish_create
- ctx.replaying bypasses the loopback + own-op guards during restore
  (logged entries carry our own origin) and suppresses
  publish/stamp/append; the LSN frontier stays active for idempotency
- Message.epoch is now real: bumped per store open, stamped on ordered
  ops; a follower seeing a higher epoch from an upstream resets its
  frontier (a restarted authority may reissue LSNs it never made
  durable)
- restore refuses truncated history (no valid snapshot and a log that
  does not reach LSN 0); a valid snapshot at the current watermark is
  kept rather than replaced (no delete-then-rename crash window)
- build_create extracted from publish_create; contents=false handle
  pushes skip serialization entirely
22 cases: append/restore roundtrips, own-origin delta replay, headless
capture, torn-tail vs mid-file corruption, snapshot+tail, retention,
replay_to, epoch bump + hostile-epoch trust gate, tombstones, same-id
recreate, nested/owned restore + fresh-follower push, PACKED,
exactly-once follower capture, format roundtrips.
Stamp ED_SCHEMA_VERSION into every manifest's reserved schema slot;
open_store/replay refuse a store whose slot differs from the running
build (StoreError, overridable via allow_schema_mismatch). Bump the
constant when a persisted type changes shape -- tid = hash(name) can't
see a field/enum change, so a mismatched build would otherwise
deserialize garbage silently. The manual half of schema safety until
structure-aware tids land; the slot then gains automatic structural
detection.
The forward direction reasoned through alongside Phase 2: the trust /
authority / topology models (Model 1 target, Model 2 open), the
sequencing-vs-distribution scaling story (relay trees + spatial
interest), state roots without SSZ (per-object content hash), and the
single-serializer decision (adopt nim-serialization, retire flatty;
keep + extend the type registry as the schema owner). Companion to
consistency-and-partial-sync-plan.md; cross-linked.
@dsrw dsrw force-pushed the feat/durable-store branch from 164dd62 to d2d4eee Compare July 9, 2026 14:41
dsrw added 2 commits July 9, 2026 11:48
- persistence.md: codec seam takes over from flatty (not 'beside'),
  matching the decided single-serializer cutover; note the planned
  PERSIST flag that replaces persist-iff-synced.
- decentralization-and-scaling.md: mark the manifest schema-version gate
  done (it shipped), not a pending 'now' item.
Adopting a schema-capable serializer for the durable path is needed;
dropping flatty entirely (one codec for both jobs) is the lean, not a
constraint -- keeping flatty for the hot path is an acceptable outcome
if unifying isn't clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant