perf(durable-streams-rust): eliminate the write cardinality cliff — syncfs checkpoints, per-shard triggers, stream lanes#4697
Conversation
…r telemetry Skip the per-append .meta sidecar flush for plain appends (memory + wal), eliminating the memory-mode cardinality cliff (+104% @ 50k local, +14-16% NVMe) and reducing wal checkpoint work. Adds --server-stats (SRV_STATS: cpu/inflight/ svc/durwait) and --wal-fsync-parallel telemetry/knobs used to characterize the wal bottleneck as device-fdatasync-bound (not shards/CPU). Findings in CARDINALITY_CLIFF_FIX.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y3x7bcT9vLGiT4tXZeQpnk
…er vs O(N) per-stream fdatasync) Cardinality-cliff #2. The checkpoint made each touched stream's data file durable via one fdatasync per stream — O(N_touched) device barriers (measured ~29s at 200k streams), which saturate the disk and stall commit fsyncs (the cliff). --wal-checkpoint-syncfs on replaces that loop with a single syncfs() on the data fs: one barrier flushes every touched file. Linux-only (falls back to the per-stream loop elsewhere); default off. Durability-before-recycle ordering unchanged; covered by e2e_checkpoint_syncfs_recovers_acked_records. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y3x7bcT9vLGiT4tXZeQpnk
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y3x7bcT9vLGiT4tXZeQpnk
…knob + retained-WAL size budget Replace the hardcoded 3s all-shards checkpoint wave with a per-shard dual trigger: --wal-checkpoint-interval-ms (time, default 3000 = old behavior) and --wal-checkpoint-wal-bytes (size, 0 = off): checkpoint a shard as soon as its retained WAL exceeds the budget. Rationale (wal-decomp-lane0, 2026-07-13): acks never gate on checkpoint and reads never touch the WAL, so checkpoint frequency is purely a replay-time budget — a size trigger makes that budget explicit and lets shards self-stagger by their own write rates instead of storming the device together. Each shard's clock restarts when its checkpoint finishes, an in-flight guard prevents re-firing a slow shard, and due shards still checkpoint concurrently. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y3x7bcT9vLGiT4tXZeQpnk
Update: cliff eliminated — layout + syncfs validated end-to-end (2026-07-13)Two follow-up benchmark campaigns ( 1. A storage confound inflated the original cliff. In all earlier multilane runs the stream data files ( 2. With correct layout, this PR's syncfs checkpoint is cardinality-flat:
Follow-up branch |
…-path configuration Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y3x7bcT9vLGiT4tXZeQpnk
…ed; note the separate ~1M-stream wall Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y3x7bcT9vLGiT4tXZeQpnk
…cross per-device lane dirs The ~1M-stream wall (wal-1m-diag): with every stream file on ONE data device, a checkpoint's syncfs writeback of ~200k dirty files took 60-74s per shard — tiny appends to distinct files amplify to ~8-12KB of data+inode+journal writeback each, saturating the single device and starving append staging (batch_avg collapsed 53->2-6). --stream-lanes N hashes stream files across streams/<0..N>/ subdirs, one per device: N× writeback capacity, N parallel syncfs barriers (one per lane, threaded), and no single ext4 dir holds every stream. Default 1 = byte-identical historical layout. Recovery walks all lanes; e2e covers lanes=3 crash recovery + layout spread. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y3x7bcT9vLGiT4tXZeQpnk
…n (Linux), drop dead fsync fan-out, refresh WAL_TUNING - --wal-checkpoint-syncfs defaults ON for Linux (validated +51% at the cliff, neutral below; off = escape hatch). Non-Linux unchanged (no syncfs). - Remove FSYNC_FANOUT / --wal-fsync-parallel: parallel per-file checkpoint fsync regressed in every controlled test (f8 -11% NVMe, f16 -19% constrained) and the syncfs barrier supersedes it. Flag accepted as a warning no-op so old deploy scripts don't crash. Checkpoint step 2 is now two modes: per-lane syncfs (Linux default) or the serial per-file fallback. - e2e resets restore the platform default instead of hard 'false'. - WAL_TUNING.md: stream-lanes results (1M wall broken), cardinality-based lane-split guidance, updated flag set, fd-ceiling note (#4706). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y3x7bcT9vLGiT4tXZeQpnk
Config-surface audit removals (all confirmed unexercised or A/B residue of already-shipped defaults): - --wal-fsync-parallel (was already a no-op stub) — gone entirely. - --wal-checkpoint-syncfs — gone; the per-lane syncfs barrier is unconditional on Linux, the serial per-file loop is the non-Linux path. - --wal-meta-gate / --mem-meta-gate — gone; the gated (default) behavior is now the only behavior, dead A/B baseline branches deleted. - --meta-sweep-disable / --meta-sweep-stats — gone (bench-only diagnostics; sweep_meta_once simplified). - --tier local + --tier-local-dir CLI — gone (--tier is off|s3); TierKind::Local stays crate-internal for unit tests. - --durability memory + --tier is now rejected at startup (memory-mode acks are not durable; tiering presumes durable segments). - WAL codec: PAYLOAD_CHECKSUMMED is mandatory — a flag-clear header decodes as Torn (the only writer that ever left it clear was the removed splice relay); dropped the legacy decode branch and its pinning test. - Stale zero-copy-era comment on DurabilityMode::Memory corrected. Kept deliberately (audit showed live use + CI coverage): --tail-cache-bytes (fan-out dedup lever), --read-offload (cold-read isolation), --wal-stats / --server-stats (bottleneck telemetry). Docs (README, WAL_TUNING) updated. 105/105 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y3x7bcT9vLGiT4tXZeQpnk
…unt guard, panic-safe checkpoint ticker Self-review findings on #4697: - Restore packages/durable-streams-rust/Dockerfile (deleted collaterally by a bench image-build flow; docker_multiarch_image.yml builds from this path). - Persist the stream-lane count in streams/.lanes and REFUSE a mismatched --stream-lanes at open (mirrors the WAL shard count contract): a silent mismatch made every existing stream invisible. Also refuses lanes>1 over an existing flat layout; recovery skips the marker; e2e asserts the rejection. - Checkpoint ticker: map task-id -> shard so a PANICKED checkpoint task still clears its in-flight guard (previously a panic silenced that shard's checkpoints forever = unbounded retained WAL). - Drop unused AtomicBool import; mark TierKind::Local as the test-only backend (allow(dead_code)); merge the orphaned checkpoint doc comments; drop the removed-knob tombstone comment; whitespace nit. 105/105 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y3x7bcT9vLGiT4tXZeQpnk
|
Self-review pass (full-diff review) — findings and fixes, all pushed: Blocking, fixed: the branch had collaterally deleted Robustness, fixed:
Quality, fixed: unused import + never-constructed-variant warnings (0 warnings now), orphaned/tombstone doc comments, whitespace. Noting explicitly (semantic change already in the diff): WAL records with 105/105 tests pass. |
…docs, changeset - srvstats module doc: 2-space list continuations (clippy doc_overindented_list_items, -D warnings on 1.97). - README/WAL_TUNING through prettier (repo-wide markdown format check). - .changeset/wal-cardinality-cliff.md: minor bump for @electric-ax/durable-streams-server-rust (the check counts only changesets ADDED vs main). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y3x7bcT9vLGiT4tXZeQpnk
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y3x7bcT9vLGiT4tXZeQpnk
…ed data Root cause of the flaky conformance test 'should wait for new data with long-poll' (and a latent production data-skip present on main): when a long-poll hits its deadline, the sleep arm read the FRESH tail and returned an empty 204 advertising it as Stream-Next-Offset — without delivering the bytes behind it. An append whose durable-tail publish lands inside the deadline window is then jumped over by the client (which adopts the header offset from every response) and never delivered; with the conformance suite's 500ms sleep aligned to its 500ms --long-poll-timeout-ms, every run of that test sits on this boundary. The wake path itself is race-free (canonical watch subscribe/re-check); only the timeout arm violated the invariant. Fix: the sleep arm now re-checks the tail exactly like the closed-channel arm and serves the data when tail > from. Invariant: a long-poll response never advances the client's offset beyond the bytes it delivered. Regression test drives 200 appends racing the deadline (20ms window) and asserts the invariant on every response; it reproduces the bug in <100 iterations without the fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y3x7bcT9vLGiT4tXZeQpnk
|
The conformance flake ( Mechanism: the long-poll timeout arm read the fresh tail and returned an empty 204 advertising it as Fix (pushed): the sleep arm re-checks the tail like the closed-channel arm and serves observed data. New regression test races 200 appends against a 20ms deadline asserting no response advances the offset without delivering data — it reproduces the bug in <100 iterations without the fix, passes with it. 106/106 tests. |
Eliminates the WAL write cardinality cliff end-to-end. Formerly a 4-PR stack (#4704, #4705, #4707 folded in here). Full deployment guidance:
WAL_TUNING.md(in this PR). Validated by the canonical benchmark campaign 2026-07-14 (ds-bench#6:results/REPORT.md+PROVENANCE.md) run against this PR's head.The problem
WAL-durable write throughput collapsed with stream count: 10.4k ops/s @100k streams (vs ~110k @10k), degrading further under load. Two compounding causes:
fdatasyncper touched stream — an O(N) barrier storm sharing the device with commit fsyncs.syncfsmeasured at 60–74 s.The changes
syncfsbarrier per stream lane instead of O(N-touched)fdatasync(unconditional on Linux)--wal-checkpoint-interval-ms+--wal-checkpoint-wal-bytes(retained-WAL/replay budget; shards self-stagger, in-flight guard, panic-safe)--stream-lanes N--wal-fsync-parallel, meta gates, meta-sweep toggles,--tier local); memory×tier rejected at startup; flag-clear WAL records decode as tornResults — canonical campaign 2026-07-14 (this PR's head, ideal configuration)
Write saturation (
canonical-write, 256 B appends, c4d-standard-64-lssd, 3 stream lanes + 3 WAL lanes, pinned cores):vs 10.4k @100k pre-campaign = 37×. At extreme cardinality (same config): 285k @500k, 212k @1m.
Reads (unchanged by this PR, revalidated): catch-up 2.27–2.74 GiB/s, cardinality-flat 10→100 streams; SSE tail p99 2–3 ms up to 2048 connections.
Mixed interference: 100k concurrent catch-up readers cost a pinned 50k ops/s write load nothing (303 MiB/s replay served alongside). The 2026-07-02 memory-mode delivery collapse no longer reproduces on this build: delivery tracks writes 1:1 to 127k/s (wal: keeps pace to ~40k, caps ~64k del/s at full write saturation).
Correctness
memory,wal-default,wal-read-offload-always,wal-resident-cache).--stream-lanesdefaults to 1 = byte-identical layout. Changeset: patch.Follow-ups (tracked)
🤖 Generated with Claude Code
https://claude.ai/code/session_01Y3x7bcT9vLGiT4tXZeQpnk