feat(durable-streams-rust): replicated durability — quorum-replication acks via Raft (openraft)#4686
Open
balegas wants to merge 25 commits into
Open
feat(durable-streams-rust): replicated durability — quorum-replication acks via Raft (openraft)#4686balegas wants to merge 25 commits into
balegas wants to merge 25 commits into
Conversation
Phase 0 of the write-saturation contention investigation. Adds always-on, dependency-free per-shard contention telemetry (independent of the OTLP `telemetry` feature) and a local reproduction harness, so candidate architecture changes can be judged on whether they lift the commit-path ceiling AND drop the contention they target. - ShardStats: inner/dirty lock-wait nanos + acquire counts, records staged, durability waiters woken (src/wal/telemetry.rs). - Instrument the append hot path: register_dirty, reserve_and_stage (both inner acquisitions), publish_durable wakeup fan-out (src/wal/shard.rs). - `--wal-stats <secs>`: runtime gate (one relaxed load when off, no clock reads in a default run) + stderr WAL_CONT emitter with per-interval rates. - scripts/contention-repro.sh: drive the server with the ds-bench pool client, report throughput + CPU + steady-state contention. - DS_BENCH_FAST_FSYNC (bench-only): plain fsync over F_FULLFSYNC on macOS so a RAM-disk data dir gives cheap fsync (the Linux+NVMe lock-bound regime). - CONTENTION_INVESTIGATION.md: hypothesis, method, candidate architectures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
contention-repro-linux.sh runs the server in a cpuset-pinned container with a tmpfs data dir (cheap fdatasync = the Linux+NVMe regime) driven by a separate client container, so the throughput ceiling is not confounded by client/server CPU co-location. Incremental in-container builds via a per-worktree target volume keep iteration fast. Baseline reproduces the findings' signature: a hard ~45-49k ops/s ceiling at ~80% CPU, barely helped by more shards — the gate is the commit + durability- wakeup coordination machinery, not compute or the lock (at 6 cores). Recorded in CONTENTION_INVESTIGATION.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…+ t2a (dedicated committer threads) Combine three orthogonal WAL performance changes that all overlap in src/wal/shard.rs into one build. Hand-integrated onto t2a's shard.rs (structural committer rewrite), then layered t1c and t1a back in; the single-owner files were taken verbatim from each source branch. - t1a (lock-free register_dirty): StreamState.dirty_epoch (store.rs), Shard.dirty_epoch + dirty: Mutex<Vec<Arc<StreamState>>>, epoch-CAS register_dirty, epoch-bump+drain in checkpoint's dirty-lock section. - t1c (coalesced durability wakeups): durable_lsn: AtomicU64 + waiters: Mutex<WaiterReg> (min-heap of oneshots) replacing the watch channel; fast-path/register/re-check wait_durable; prefix-drain publish_durable recording waiters_woken = fired; telemetry.rs doc updates. - t2a (dedicated committer OS threads): CommitSignal (Mutex+Condvar) replacing Notify, synchronous run_committer + commit_once (direct fdatasync, no spawn_blocking), spawn_committer/CommitterHandle, WalSet spawn/stop_committers, main.rs shutdown wiring, recovery/e2e adapters. Integration points: the Shard struct holds all of dirty_epoch+dirty Vec, durable_lsn+waiters, and commit_signal; the constructor inits all of them (watch channel + Notify fully removed). publish_durable does t1c's coalesced drain AND retains sealed_pending, and is called from t2a's synchronous committer thread (atomic store + oneshot fires are thread-safe, no tokio handle). checkpoint reads durable_lsn via the atomic and does t1a's epoch-bump+drain. t1a's post-drain test was adapted to the spawn_committer/stop API. cargo test: 95 passed; 0 failed. cargo build --release + cargo clippy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ds-bench pool write-saturation suites pass `--worker-threads 32` to size the tokio runtime independently of the cgroup cpu limit (available_parallelism reads cpu.max, so under a limit it would under-size the pool). Add the flag (defaults to available_parallelism); also used as the default WAL shard count. Required for the combined branch to boot under the canonical remote config. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… + per-append meta flush) At high stream cardinality (200k-1M) nearly every append is its stream's first touch of the checkpoint interval, which turned three amortized costs into per-op costs. Local repro (6 cores, 256 conns, 400k streams): 16.0k -> 44.2k ops/s (+2.8x), p99 144ms -> 27ms. - checkpoint drain: O(1) dirty-lock critical section (take + epoch bump only); the O(touched) tail/file capture ran under the lock and stalled every appender on the shard for 25-140ms per tick. - checkpoint body: run capture + per-stream fdatasyncs + tails/ckpt persist + recycle in ONE spawn_blocking task (was: capture/tails/recycle on the async runtime); shards checkpoint concurrently (JoinSet) instead of serially. - tails map: resident in memory (seeded once from disk); stop re-reading, re-parsing and re-sorting the whole cumulative file every 3s. - meta sidecar: append path no longer debounce-schedules write_meta_sync (JSON + File::create + rename per append -> all workers spinning on the data-dir inode rwsem, ~40% of server CPU in perf at every cardinality). WAL-staged appends just mark meta_dirty; the checkpoint flushes sidecars for drained streams after recycle. Memory-mode appends keep the debounced flush. Producer/access lag bound moves from the 100ms debounce to the checkpoint cadence (already a documented non-durable, lagging flush). - handle_append: drop the redundant second registry lookup for the is_json metric label (2x cold DashMap walk per append at 1M keys). - telemetry: WAL_CKPT per-shard checkpoint phase line (touched/tails/meta counts, capture/fsync/tails/rest/meta timings) behind --wal-stats; repro script summarizes it, plus --tmpfs / --wal-stats flags. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sults Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzV7avRUMhPqmm1SgU4z1J
…ery gap assert; add randomized crash/recovery simulation Two recovery-path bugs found by the new seeded crash/fault simulation (src/wal/sim_tests.rs), plus the harness itself: 1. Shard::open re-preallocated 1.wal unconditionally. A SEALED first segment (exactly-packed at roll) grew a zero tail that replay read as end-of-log, silently dropping every later segment's acked records and truncating the per-stream files to the stale frontier; a RECYCLED 1.wal was recreated as all-zeros, making replay recover NOTHING. Boot now opens the newest existing segment non-destructively (FileSegment::open_existing) and only preallocates on a fresh dir. Regression tests: e2e_multi_segment_acked_records_after_first_seal_ survive_crash, e2e_recycled_first_segment_acked_records_survive_crash. 2. recovery.rs debug_assert claimed a stream rebuilt purely from replay must start at file_base — false after a previous boot's recover+reset cycle (post-boot WAL records legitimately start at the recovered tail with no persisted-tails entry until the next checkpoint). Debug builds panicked on a healthy recovery. The assert now checks the real invariant: no hole between the pre-replay file end and the first replayed record. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzV7avRUMhPqmm1SgU4z1J
… in ARCHITECTURE.md ARCHITECTURE.md still described pre-fsync tail publication; the code gates reader visibility on durability (publish_durable_tail after wait_durable_lsn, PROTOCOL.md §4.1). Also clippy nits in the new sim/recovery code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzV7avRUMhPqmm1SgU4z1J
…car durable_tail proof Confirmed by the crash sim (seed 20230): a stream created after the last checkpoint whose only in-flight append was torn by power loss had NO truncation proof — zero surviving WAL records, no checkpoint tails entry — so the sidecar pass trusted tail = file size and exposed the torn fragment to readers (the C1 shape the WAL exists to prevent). The .meta sidecar now persists durable_tail, captured from Shared.durable_tail (only ever advances post-fsync, so captured values are honest) and riding along on writes that already happen: fsynced at create/close, refreshed by the checkpoint meta flush and by recovery's reconcile — zero new hot-path fsyncs. Recovery seeds every stream's frontier with max(meta proof, checkpoint tails, replayed ends); a fast path skips streams already exactly at their frontier so 1M-stream boots add no I/O; the proof is durably re-persisted before reset_after_recovery wipes the WAL + tails file. Old sidecars without the field keep the previous trust-the-file-size behavior (serde default). Regression test: e2e_wal_quiet_stream_torn_unacked_tail_truncated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzV7avRUMhPqmm1SgU4z1J
Found by the crash sim (seed 20387, clean crash, no fault injection): handle_delete acked 204 while the file + sidecar unlinks ran on a detached spawn_blocking task with no parent-dir fsync — a crash after the ack left both files on disk and the stream resurrected with all its data on reboot. DELETE now awaits delete_or_soft_delete_durable (synchronous unlinks + one parent-dir fsync, or the durable soft-delete meta write) and acks 500 on failure. The expiry sweep keeps the detached non-durable variant (re-expiry on next access is harmless). Regression test: e2e_acked_delete_is_durable_no_resurrection_after_crash. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzV7avRUMhPqmm1SgU4z1J
…es per stream The 2026-07-02 ds-bench campaign showed --durability memory LOSING to wal on writes at every cardinality (575k vs 816k @10k streams; 1.33M vs 2.05M @500k on 4 CPUs) and its SSE delivery collapsing past ~16k writes/s (16-20k del/s, p50 200ms+, against 40-62k writes — same client sustained 65k del/s under wal). Two causes, both fixed here: 1. --durability memory silently routed every binary append through the splice intercept: a fresh open() per append, pipe2 + 2x splice(2) when the body split past the head buffer, socket-readiness parking under the per-stream appender lock, and the tail cache force-disabled. At sync-sized payloads (256B) that fixed overhead loses to the buffered path's single write(2) plus an amortized group-commit fdatasync share. Most workloads have small payloads — remove the splice path entirely rather than size-gate it: memory mode is now the buffered wal path with the WAL stage/wait skipped, the Linux-only restriction is gone, and the tail cache is orthogonal again. 2. Memory mode sent one reactor wake (mutex push + eventfd write) per append — wal's group commit had been acting as an implicit fan-out batcher, and without it the reactor drowned. StreamSubs gains a wake_pending latch: wake_stream skips queue+eventfd while a wake is in flight; the reactor clears the latch BEFORE reading the tail (clear-then-read, so a racing publish is either covered by the flush or re-queues); registration resets it so a gen-stale drop can never strand a fresh subscriber. Idle streams keep per-append wakes (no added latency); hot streams converge to one wake per reactor cycle. Benefits wal mode at saturation too. cargo test: 100 passed. Bench validation to follow (ds-bench mixed suites). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
--durability replicated: Kafka-style acks — durability via quorum replication (OmniPaxos / Sequence Paxos) instead of local fsync. Log-first apply keeps every replica a deterministic function of the decided log; any node accepts writes (proposal forwarding) and serves reads. Documents guarantees, accepted trade-offs, config, deploy story. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…xos) Kafka-style durability: an append acks once a quorum of replicas has decided it — no fsync on the hot path. Log-first apply: every node (leader included) mutates its store only from decided entries, so replicas are a deterministic function of the log and fail-over needs no truncation/repair. Any node accepts writes (OmniPaxos forwards follower proposals); every node serves reads (same publish_durable_tail path). - src/replication/: entry types, vendored in-memory OmniPaxos storage, TCP mesh (len-prefixed bincode, reconnect, resend-covered drops), consensus core (10ms ticks, batch-drained proposals, periodic trim) - handlers.rs: authoritative apply twins (closed -> producer dedup -> Stream-Seq, in log order) + replicated branches in PUT/POST/DELETE building identical responses from apply outcomes; /_repl/status - main.rs: --repl-id/--repl-peers/--repl-listen/--repl-ack-timeout-ms/ --repl-trim-secs; stale-WAL refusal extended to replicated mode - tests: 3-node in-process clusters over loopback TCP — convergence, follower proposals, replicated producer dedup, close/delete, forks Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…moke - local-cluster.sh up|status|down: builds and runs 3 nodes on 4437-4439 (http) / 5437-5439 (mesh), waits for leader election - smoke.sh: end-to-end — create/append via different nodes, byte-equal reads everywhere, leader kill, post-failover append + convergence - Dockerfile + docker-compose.yml: containerized 3-node cluster - k8s.yaml: StatefulSet (ordinal-derived repl-id) + headless mesh service + client service; zone spread; Parallel pod management so quorum formation isn't readiness-deadlocked Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…al-cluster state Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…weep + REPL_STATS Three changes driven by local 3-node benchmarking (ds-bench multi-stream, 200 streams / 32 conns / 256 B): 1. CORRECTNESS: a Compaction(Trim) drained in the same message batch as the Decides it covers could delete decided-but-unapplied entries (omnipaxos try_trim guards its own decided_idx, not our applied cursor) — silent replica divergence + leaked acks (12 ack timeouts / 20 s run). The core now flushes the applier before handling any compaction message. Post-fix: 200/200 streams byte-identical. 2. PERF: applies ran inline in the core task, so one slow store write stalled consensus and every ack — measured 100-450 ms apply stalls (REPL_STATS apply_max_us) driving p99 110 ms and 5.5k ops/s. Decided entries now dispatch to 4 shard appliers (path-hashed; per-stream log order preserved; fork-creates barrier + apply inline). Per-append debounced meta rewrites (10 renames/s/stream — fs-journal churn, the stall source) replaced by a 3 s per-shard dirty sweep; replicated creates skip the meta fsync (the decided entry is the durability). Result: 39k ops/s (7.1x), p50 0.66 ms, p99 2.2 ms, zero errors. Single-node memory mode on the same box: 96k ops/s. 3. TELEMETRY: --repl-stats N emits REPL_STATS lines (proposed/decided/ applied rates, log window, pending acks, timeouts, apply_max_us); /_repl/status gains log_window/pending/timeouts; deploy/replicated/ monitor.sh samples RSS/CPU + status to CSV; local-cluster.sh grows MODE=memory|wal single-node baseline support. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hes a real omnipaxos 0.2.2 safety bug; pin fixed upstream rev sim_tests.rs drives 3 consensus instances over a seeded simulated network (drops, partition/delay windows, crash-stop incl. the leader, elections, resends, active trimming) against real stores through the real apply functions. Invariants: acked appends survive on every live node exactly once in producer-seq order; replicas byte-identical; bounded quiescence; compaction actually ran. Fault model = the production mesh contract (per-link FIFO with loss; no reorder/dup). 200 seeds in ~4 s. The harness immediately found decided-log DIVERGENCE (an acked entry vanishing on a new leader after a partition): crates.io omnipaxos 0.2.2 does not clear a follower's cached Promise message on AcceptSync, so a stale Promise resend feeds the new leader pre-sync log state. Fixed upstream in e6ddc52 (Jan 2024) but never published — dependency now pins upstream main (bd234be2) which also carries election fixes #154/#156. Bisect toggles: DS_REPL_SIM_NO_{DROP,PARTITION,CRASH}. Repro: DS_REPL_SIM_SEED0=<seed>. On 0.2.2 the test fails within ~10 seeds; on the pinned rev 200+ seeds pass. Storage vendored at the new trait (usize idx, write_atomically); take_outgoing_messages buffer reuse. Benchmarks re-validated on the pinned rev (40k ops/s @256B/32conn, zero errors, 200/200 convergence); REPLICATION.md gains benchmark results, sizing rules, validation story, and the pinned-rev rationale. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Motivation: the DST harness caught an unpublished-fix safety bug in the last released omnipaxos (REPLICATION.md 'History: why openraft') and the project is unmaintained; openraft is active and production-proven (Databend, CnosDB) and its snapshot/membership machinery matches our roadmap (state transfer, node replacement). The architecture survives intact — log-first apply, apply-outcome acks, sharded appliers (now inside RaftStateMachine::apply batches), 3s dirty meta sweep, TCP mesh, any-node writes. What changed: - types.rs: declare_raft_types (D=LogOp, R=OpOutcome) — client_write resolves with the apply outcome; the hand-rolled pending-ack map and the trim-vs-apply guard are gone (openraft owns commit/apply/purge ordering) - net.rs: request/response RPC mesh (correlation ids, auto-reconnect) carrying AppendEntries/Vote/InstallSnapshot + Forward(LogOp); a follower forwards to the leader and then waits for its OWN apply to cover the entry (read-your-writes preserved on the origin node) - sm.rs: state machine with path-sharded batch apply, fork-create barriers; snapshots are metadata-only markers (cheap to build => log purges and memory stays bounded WITHOUT needing every peer up — improvement over omnipaxos trim; installing one is refused loudly = fail-stop for laggards, v1 parity) - log_store.rs: vendored openraft reference memstore (log + vote in memory, deliberate) - --repl-trim-secs replaced by --repl-snapshot-logs N (purge cadence in entries, default 5000) - sim_tests.rs removed (drove omnipaxos sans-io; port via simulated AsyncRuntime is on the roadmap — design lives in git history) Verified: 104 tests green incl. the 4 cluster tests (frequent purge, snapshot_logs=64); smoke passes incl. leader kill-over; first benchmarks on par or better (256B/32conn 36.3k ops/s vs 40.3k; 128conn 51.5k vs 52.5k; 16KB 14.9k vs 10.7k (+39%), p99 5.7ms vs 18ms), 200/200 convergence, zero backpressure errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…g rules Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n, membership change, durable vote
The three roadmap items that turn a failed node from 'restart the whole
cluster' into a routine operation:
1. REAL SNAPSHOTS as manifests: a snapshot carries per-stream metadata
(path, id, config, tail, producer/closed state) — O(streams) to
build, so the every-5000-entries purge cadence is unaffected (bench
sanity: 36.2k ops/s, unchanged). The installing node wipes its store
and pulls each stream's [0, tail) prefix over the mesh (new
FetchStream RPC, 4 MiB chunks) — exact because prefixes are
immutable under append-only; a stream id check catches
delete+recreate races.
2. DURABLE VOTE: fsynced to <data-dir>/repl.vote on change (election
time only — never the append path) and reloaded at boot. Makes
restart-rejoin election-safe, and doubles as the initialized marker
so a restarted node skips bootstrap automatically. Replicated boots
wipe stale stream files (the log/snapshot is the source of truth).
3. MEMBERSHIP CHANGE: POST /_repl/add-learner {id,addr} (blocks until
caught up) + POST /_repl/change-membership {members} on the leader;
--repl-join boots a node waiting to be added. Mesh clients are now a
dynamic registry fed by membership addresses, so added nodes get
links without restarts.
Verified: 106 tests green, incl. learner_joins_after_purge (600 appends
past the purge horizon, fresh node catches up via manifest+fetch only,
promotes to voter, proposes writes; byte-identical) and vote-file
persistence; smoke.sh extended with a process-level restart-rejoin leg
(killed leader restarts, resyncs, serves identical bytes, takes writes).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Base automatically changed from
perf/memory-mode-drop-splice
to
perf/combined-t1a-t1c-t2a
July 3, 2026 11:10
- engine_raw: drop the linux-gated AtomicBool import left behind by the splice removal (base-branch lint, denied by clippy -D warnings) - replication: RpcClient visibility to match Clients' pub API; drop the unused ClientWriteResponse alias; enumerate instead of a manual loop counter in the state machine batch walk - prettier --write CONTENTION_INVESTIGATION.md (pre-existing, failed the repo-wide format check) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a third durability mode to the Rust durable-streams server:
--durability replicated— Kafka-style durability where an append acks once a quorum of replicas has committed and applied it (openraft), with no fsync anywhere on the hot path. A 3-node cluster survives loss of any minority with zero acked-data loss, any node accepts writes, every node serves reads, and failed nodes restart-rejoin or get replaced live.Full design/ops doc:
packages/durable-streams-rust/REPLICATION.md. Deploy kit underdeploy/replicated/(local cluster, docker compose, k8s, smoke).Reviewer guidance
Approach — log-first apply. Every state-mutating op (
PUT/POST/DELETE) is aLogOpproposed into a single Raft log; every node, including the leader, mutates its store only from committed entries, inside the state machine. That makes each replica a deterministic function of the log prefix — no divergence, no truncation/repair on fail-over. The ack is the state machine's apply outcome (client_writereturns it), so HTTP responses are byte-identical to single-node modes, including producer dedup/fencing evaluated authoritatively in log order.Key mechanics, in review order:
src/replication/sm.rs— the state machine. Applies are sharded by stream path within each batch (per-stream order preserved; fork-creates barrier): one slow fs write must not stall every other stream's ack — we measured 100–450 ms apply stalls without this. Snapshots are manifests (per-stream metadata, O(streams) to build); the installing node wipes and pulls each stream's[0, tail)prefix over the mesh, exact because prefixes are immutable under append-only.src/replication/net.rs— TCP RPC mesh (correlation ids, auto-reconnect) carrying the three Raft RPCs plusForward(proposal to leader) andFetchStream(snapshot byte pull). Clients are a dynamic registry fed by membership addresses, so added nodes need no restarts.src/replication/core.rs— propose path. On a follower: forward to leader, then wait for the local apply to cover the entry (read-your-writes on the node you wrote to).src/replication/log_store.rs— in-memory log (deliberate: durability comes from replication), durable vote (fsync at election time only; also serves as the initialized marker that makes restarts skip bootstrap).handlers.rs—apply_replicated_*are the semantic twins of the single-node paths (same check order: closed → producer → seq), kept next to the code they mirror; replicated branches in PUT/POST/DELETE;/_repl/status,/_repl/add-learner,/_repl/change-membership.Key invariants
Trade-offs (accepted, documented)
acks=allwith OS-managed flush. Zone-spread replicas are the mitigation.--tier) unsupported in this mode.Non-goals here: ds-bench harness integration (single-server assumption), sharded consensus groups, large-payload copy reduction — all in the REPLICATION.md roadmap.
Why openraft (and not the original OmniPaxos implementation, visible mid-branch): a deterministic fault-injection simulation we built caught a consensus safety bug in the last published omnipaxos release (stale cached Promise after AcceptSync → decided-log divergence, acked entry lost on leader change; fixed upstream Jan 2024 but never published), and the project is unmaintained. openraft is active and production-proven; the swap cost only the consensus adapter layer and made large payloads 18–39% faster with 3–4× lower memory (entry-count purge vs time-based trim). The simulation harness lives in git history pending an openraft port (roadmap).
Verification
Local 3-node benchmarks (one 10-core machine, so conservative; ds-bench
multi-stream/sustained, details + sizing rules in REPLICATION.md): 256 B appends 36–52k ops/s (p99 2–7 ms) vs 96–100k single-node memory mode; 16 KB 14.9k ops/s (244 MB/s); 5-minute sustained runs hold RSS flat (20 MB @ 256 B; ≤143 MB @ 16 KB) with zero ack timeouts and all replicas byte-identical.Files changed
packages/durable-streams-rust/src/replication/— the new module:types(Raft type wiring),entry(LogOp/outcomes),sm(state machine, sharded apply, manifest snapshots),net(RPC mesh),core(propose/ack, stats),log_store(in-mem log + durable vote),testspackages/durable-streams-rust/src/handlers.rs— replicated branches + apply twins + admin endpointspackages/durable-streams-rust/src/{main,store}.rs—--repl-*flags, boot wipe/resync,create_with_meta_durability,wipe_allpackages/durable-streams-rust/deploy/replicated/— local-cluster.sh (up/status/down/restart), smoke.sh, monitor.sh, Dockerfile, docker-compose.yml, k8s.yamlpackages/durable-streams-rust/{REPLICATION,README}.md— design, ops, benchmarks.changeset/replicated-durability.md— minor bump🤖 Generated with Claude Code