Skip to content

v0.9.0-rc.28

Choose a tag to compare

@github-actions github-actions released this 30 May 00:23
· 50 commits to main since this release
v0.9.0-rc.28
11edf8e

peat-cli round-trip-edit + tombstone-authorship unblocker — Automerge delta API, node-local Lamport clock, cross-restart persistence, and sync-receive wire-up. Four landed PRs (peat-mesh#193, peat-mesh#194, peat-mesh#197, peat-mesh#198) close peat-mesh#187, peat-mesh#192, peat-mesh#195, and peat-mesh#196. Together they replace the peat-cli wall-clock Lamport proxy with a peat-mesh-managed clock, surface the Automerge delta primitive peat-cli needs for peat update --from <PATH>, persist the clock across restarts, and wire the receive-side rule so cross-node Lamport semantics hold without consumer-side threading.

Added — Automerge delta API (peat-mesh#193, closes peat-mesh#187)

  • AutomergeStore::diff(current, proposed) -> AutomergeDelta — associated function (no store state). Wraps Automerge::get_changes_added; returns the changes in proposed not in current as an opaque delta.
  • AutomergeStore::apply_delta(&self, key, &AutomergeDelta) + apply_delta_with_origin(&self, key, &AutomergeDelta, ChangeOrigin) — read-modify-write under the per-key striped lock, routes the resulting state through put_inner so observers, the sync coordinator's change_tx, and the origin-tagged gossip_tx all fire identically to a put. The Remote(peer) origin variant suppresses change_tx per the peat-mesh#115 ping-pong invariant.
  • AutomergeDelta::to_bytes / from_bytesu32-LE-prefixed framing per change, so the delta crosses a process boundary safely. Truncation surfaces as a named-diagnostic Err.
  • 8 behavioural pins in tests/automerge_delta_api.rs: diff/apply roundtrip preserves operation history (ADR-021 invariant), empty-delta no-op, missing-key diagnostic, Local-origin observer fan-out, Remote-origin change_tx suppression, bytes round-trip, truncation rejection.

Unblocks peat-node ADR-001 Phase 4b — peat update --from <PATH> round-trip-edit flow.

Added — node-local Lamport clock (peat-mesh#194, closes peat-mesh#192)

  • AutomergeBackend::next_lamport() -> u64 — read-and-advance the node-local Lamport clock. Strictly monotonic across concurrent callers (fetch_add(Relaxed).wrapping_add(1)).
  • AutomergeBackend::current_lamport() -> u64 — non-advancing read for diagnostics.
  • AutomergeBackend::observe_lamport(observed: u64) — Lamport receive-side rule L := max(L, observed) + 1, implemented via CAS retry loop. Capped at u64::MAX - 1 so a hostile or buggy peer's observed = u64::MAX cannot saturate the atomic and wrap next_lamport to 0.
  • Seeded at construction from SystemTime::now() nanos with a 1u64 floor.
  • 6 behavioural pins in tests/lamport_clock_source.rs: monotonicity, non-advancing read, observe-jumps-clock, observe-never-regresses, uniqueness across 8 × 1024 concurrent calls, seed-above-zero.

Replaces the peat-cli delete wall-clock proxy at the right surface — strict node-local monotonicity under concurrent writes, regardless of NTP drift.

Added — Lamport persistence across restart (peat-mesh#197, closes peat-mesh#195)

  • AutomergeStore::read_lamport_highwater() -> Result<Option<u64>> + write_lamport_highwater(u64) -> Result<()> — new metadata redb table; the write is monotonic in-transaction (reads existing, skips write when existing >= value) so the periodic flush task and an explicit flush cannot interleave to regress the on-disk high-water. Corrupt entries (wrong length) log a warning and treat as None, allowing the next write to self-heal.
  • AutomergeBackend::flush_lamport_highwater() -> Result<()> — synchronous explicit flush. Called from DataSyncBackend::shutdown so the on-disk value reflects every issued value at clean exit (zero worst-case loss); also available for test rigs and explicit shutdown hooks.
  • Background flush task — 1-second cadence, skips when the atomic hasn't changed since the last flush. last_flushed seeded from persisted_highwater at startup so an idle backend doesn't re-write the same value redundantly. Holds Weak<AutomergeStore> across the sleep — task exits when the backend drops.
  • Seed formulamax(persisted_highwater, SystemTime nanos, 1). Resists wall-clock regression on tactical hardware (battery-less RTC, NTP slew, manual time-set, boot-at-epoch) — the resumed clock stays above any value the prior run could have issued, even when the wall clock has gone backwards.
  • AutomergeBackend::shutdown_and_release(&self) -> Result<()> — async teardown that awaits the iroh Router's shutdown future, so production callers (hot-restart, daemon-rotate, mobile pause/resume) and integration tests can deterministically drop the backend and immediately reopen the same data_dir in the same process. Idempotent via an AtomicBool CAS guard against signal-handling races. Bounded by NetworkedIrohBlobStore::shutdown_timeout (5s default).
  • 4 new pins in tests/lamport_clock_source.rs: cross-restart persistence (build → shutdown → rebuild → assert resumed seed > prior high-water), monotonic-write race regression, sequential idempotency of shutdown_and_release, concurrent-CAS-race safety under multi_thread runtime. Plus an in-module unit pin for corrupt-entry recovery.

Added — observe_lamport sync-receive wire-up (peat-mesh#198, closes peat-mesh#196)

  • AutomergeSyncCoordinator::set_lamport_clock(Arc<AtomicU64>) — opt-in setter, called from AutomergeBackend::with_iroh so the coordinator's tombstone-receive path advances the clock automatically. Standalone coordinator consumers (peat-mesh-node binary, unit tests) skip the call; the receive-side wire-up no-ops and preserves existing behaviour.
  • handle_incoming_tombstone observes tombstone.lamport before applying. handle_incoming_tombstone_batch observes the max Lamport across the batch in a single CAS (idempotent, equivalent to per-entry observes but cheaper by N-1 attempts).
  • Shared observe_atomic helper at pub(crate) visibility — both AutomergeBackend::observe_lamport (consumer-facing API) and the coordinator's receive-side dispatch share one CAS retry loop, one memory ordering choice (AcqRel / Acquire), and one u64::MAX - 1 cap.
  • 3 in-tree dispatch-tier pins in src/storage/automerge_sync.rs::tests: single-tombstone receive advances wired clock past inbound Lamport; batch receive picks max Lamport across entries; no-op when no clock wired (opt-in contract). Plus a u64::MAX cap regression test through the full handle_incoming_sync_stream path.

Completes the cross-node half of Lamport partial-order semantics: a locally-authored operation that follows an inbound tombstone receive sorts strictly after the observed remote Lamport in the causal order Lamport's partial order captures.

Out of scope (acknowledged follow-up)

  • Lookahead persistence for unclean-shutdown bounds. rc.28 ships persistence with a 1-second periodic flush cadence + clean-shutdown flush; worst-case loss on an unclean crash is up to one periodic interval of issued values. Tightening that (smaller interval at the cost of redb write churn) is a trade-off better made after observing real workloads.
  • Backend-level abort of the Lamport flush task on shutdown_and_release. The current shape relies on the existing Weak<AutomergeStore>-upgrade-fails-on-drop pattern (consistent with observer_task). The post-shutdown periodic-tick window is harmless because the storage-layer write is monotonic. Carried forward.