Skip to content

perf: per-slot Arc copy-on-write snapshot isolation, uncontended datapath during a save (#576 core)#588

Merged
ELares merged 1 commit into
mainfrom
perf/576e-arc-cow
Jul 9, 2026
Merged

perf: per-slot Arc copy-on-write snapshot isolation, uncontended datapath during a save (#576 core)#588
ELares merged 1 commit into
mainfrom
perf/576e-arc-cow

Conversation

@ELares

@ELares ELares commented Jul 9, 2026

Copy link
Copy Markdown
Owner

What

The core of the #576 fix. Prior attempts BOTH failed on c7g (measured): PR-B (off-thread encode) left the p99.9 at ~3.9s; the throttle made it WORSE (16.75s at 10%). Root cause: the O(N) snapshot_chunk copy contends on memory bandwidth with the serving datapath. This eliminates that copy.

Design (per-slot Arc COW):

  • The store is now Vec<Vec<Arc<HashTable<Entry>>>>. GET reads through the Arc as a SHARED deref (one indirection, no Arc::get_mut, no atomic on the read path -- enabled by perf: interior-mutable S3-FIFO freq bump so GET is a shared read (#576 COW prerequisite) #587's shared freq bump). A write takes Arc::make_mut: not-saving is strong_count==1 (in-place, one atomic check); a frozen slot (>1) COWs on first write.
  • A per-shard saving: bool flag (single-threaded shard). begin_save() Arc-clones every non-empty slot (O(#slots), NO key copy) into Vec<FrozenSlot> and sets the flag.
  • save_shard_local hands the FrozenSlots to the dedicated ic-persist thread, which does the whole O(N) encode + fsync OFF-core reading the frozen slots DIRECTLY (no copy), drops the frozen Arcs, and signals a oneshot; the shard serves throughout and clears the flag after.

The correctness crux (deep-clone on COW): Arc::make_mut on a frozen slot clones the HashTable, which clones each Entry, and Entry::clone is ALREADY a DEEP clone (fresh pointee allocation for Str blobs + re-boxed CollEntry/CollVal for every collection variant). So a mid-save write's deep-clone becomes the live slot while the frozen Arc retains the ORIGINAL entries, sole-owned by the persist thread: a mid-save write is never in the dump and never mutates or frees a pointee the persist thread reads.

FrozenSlot(Arc<HashTable<Entry>>) + unsafe impl Send: sound because a frozen slot is de-facto immutable for the save (every write COWs first; the one shared-&self write, the S3-FIFO freq bump, is GATED OFF while saving). A static assert pins Vec<FrozenSlot>: Send.

Review + verification

  • Adversarially reviewed: SOUND -- every write path (SET/DEL/APPEND/INCR/TTL/collections/eviction/lazy-expiry/freq-dec) COWs via make_mut with no bypass; Entry::clone deep for all variants; FrozenSlot Send sound with correct freeze/thaw ordering and no race window; no UAF/double-free/data-race.
  • Miri clean on the deep-clone-on-COW + FrozenSlot drop-ordering crux.
  • Tests: cow_deep_clone_isolates_frozen_slots_from_overwrite_and_delete, point_in_time_holds_under_mass_writes_at_scale (200k), dump_frozen_slots_is_point_in_time_across_later_writes, bgsave_cow_serves_inplace_writes_and_reloads_consistently (real APPEND/INCR/DEL/SET during a BGSAVE, low latency, self-consistent reload), freq_bump_skipped_while_saving_then_resumes; crash_mid_save + torn_crc + turnkey_restart + all bgsave/off-thread/eviction pass. 1154 ironcache+server tests pass; clippy -D warnings + io_uring + invariant + prior-art + fmt clean; no dashes.

Consistency is now per-shard point-in-time as of the freeze (stronger than #571). Manifest still written LAST (#530). The #578 throttle knob is now inert for the tail (the copy it throttled is gone); the dead save_throttle_sleep_us/PERSIST_QUEUE_DEPTH were removed.

Part of #576. The real p99.9 win is re-measured on c7g next.

🤖 Generated with Claude Code

… frozen shard off-thread (#576)

The concurrent-snapshot p99.9 catastrophe (3.5s vs a ~40ms baseline on c7g) was
NOT the encode+fsync (#586 already moved those off-thread) but the O(N)
`snapshot_chunk` COPY of the whole keyspace into owned records, which HAD to stay
on the serving core (the store is `!Send`) and contended the datapath on memory
bandwidth. #571 (yield) only interleaved it and #578 (throttle) only STRETCHED it
over a longer window, both measured to help nothing / hurt.

This removes the serving-side copy entirely via per-slot Arc copy-on-write:

- STORE: `dbs` is now `Vec<Vec<Arc<HashTable<Entry>>>>`. GET reads through the Arc
  as a SHARED deref (`slot_table`) -- one pointer indirection, NO `Arc::get_mut`,
  NO atomic on the read path (keeps GET at +1-2% with #587's shared freq bump). A
  write takes `Arc::make_mut` (`slot_table_mut`): the common not-saving case is
  strong_count==1, an uncontended in-place mutate; while a save holds a frozen
  clone (strong_count>1) the first write to a slot deep-clones it.

- DEEP-CLONE-ON-COW (the correctness crux): `Arc::make_mut` on a frozen slot clones
  `HashTable<Entry>`, which clones each `Entry`, and `Entry`'s `Clone` is a DEEP
  clone (a fresh pointee allocation -- Str blob copied, CollEntry re-boxed). The
  fresh table becomes the live slot; the frozen Arc retains the ORIGINAL entries,
  sole-owned by the persist thread. So a write during a save is NEVER visible in
  the dump and NEVER mutates/frees a pointee the persist thread reads: no data
  race, no use-after-free, no double-free. A shallow (pointer-copy) clone would be
  catastrophic here; `Entry` is verified NOT `Clone`-shallow.

- FrozenSlot unsafe Send: `Arc<HashTable<Entry>>` is `!Send` (Entry is a `!Send`
  tagged NonNull). `struct FrozenSlot(Arc<..>)` with `unsafe impl Send` hands one
  slot to the persist thread. SOUND because a frozen slot is de-facto IMMUTABLE for
  the save: every datapath write COWs first (above), and the one shared-`&Entry`
  write -- the S3-FIFO freq bump (`bump_freq_shared` on GET/rmw) -- is GATED OFF via
  a per-shard `saving` flag for the save window, so no byte the persist thread reads
  is mutated. The Arc refcount is atomic, so the cross-thread move/drop is
  race-free; dropping a FrozenSlot frees pointees only when strong_count hit 1 (the
  slot was COW'd away, no live reader remains). This mirrors why the store is
  otherwise !Send (mutable aliasing on the owning core), which a frozen slot lacks.

- SAVING FLAG fast path: a plain `bool` (single-threaded shard, ADR-0002/0005;
  the persist thread never touches it). `begin_save` Arc-clones every non-empty
  slot (O(slots), no key copy) and sets the flag; `end_save` clears it. The
  datapath fast path (not saving) is one predictable branch + the Arc deref.

- SAVE PATH (coordinator `save_shard_local`): FREEZE via `begin_save`, hand the
  `Vec<FrozenSlot>` to the dedicated `ic-persist-<n>` thread (#586 pattern), which
  runs the whole O(N) encode+fsync (`dump_frozen_slots` -> `to_kvobj` + encode +
  CRC + `write_shard_dump`) OFF the serving core, drops the frozen Arcs, and signals
  a oneshot. The shard AWAITS that (a cross-thread wake, not a blocking join) and
  serves throughout. `end_save` runs on the normal-completion path only (all arms
  mean the persist thread's closure has fully exited); on task cancellation the flag
  safely stays set on an exiting process (never racing the still-running thread).

- CRASH-SAFETY preserved (#530): the persist thread writes only the per-shard file
  atomically; the manifest is still written LAST on the home core. A panicked
  persist thread -> a failed save (record_save_failed), never a hang; frozen Arcs
  dropped on unwind.

- CONSISTENCY: the dump is now a per-shard POINT-IN-TIME as of the freeze (STRONGER
  than the #571 chunked walk -- a mid-save write keeps its pre-freeze value in the
  dump, or is omitted if newly created). Cross-shard stays fuzzy (each shard freezes
  at its own instant), the accepted cache warm-start tradeoff. The #578 throttle
  knob stays settable but is inert for the tail (the copy it throttled is gone).

TESTS (this is the correctness-critical PR):
- store `cow_deep_clone_isolates_frozen_slots_from_overwrite_and_delete`: overwrite +
  delete every frozen key on the LIVE store while holding the FrozenSlots; the frozen
  view keeps its pre-freeze values byte-for-byte, the live store advances, and drop
  ordering is UAF/double-free-free.
- store `freq_bump_skipped_while_saving_then_resumes`: the soundness gate.
- store `point_in_time_holds_under_mass_writes_at_scale` (200k keys).
- persist `dump_frozen_slots_is_point_in_time_across_later_writes`: dump + RELOAD
  shows the frozen point-in-time; post-freeze overwrites/deletes never corrupt it.
- integration `bgsave_cow_serves_inplace_writes_and_reloads_consistently`: real
  APPEND/INCR/DEL/SET during a BGSAVE, each serviced with low latency, reload
  self-consistent, live writes present.
- MIRI: `cargo +nightly miri test` ran BOTH focused store tests clean (no Undefined
  Behavior / UAF / double-free), including the deep-clone-on-COW + FrozenSlot drop
  ordering crux (91s).
- clippy -D warnings clean, fmt clean, CI invariants + prior-art pass, io_uring
  build ok; existing bgsave/off-thread/backpressure/crash/torn/turnkey_restart tests
  all pass.

Part of #576.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFWCZR47TDvsFPJ1waX85X
Signed-off-by: Zeke <ezequiel.lares@outlook.com>
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

perf-gate (A5)

Same-runner ratchet of HEAD against the merge-base (both rebuilt and measured in this job).
PASS = within the noise band, WARN = a real move inside budget (does not fail), FAIL = past budget in the bad direction.

metric base head delta% band budget verdict
qps_median (peak) 84297.60 84534.24 0.28% +/-5.00% drop <= 15% PASS
bytes_per_key int 45.10 45.16 0.13% det rise <= 5% PASS
bytes_per_key embstr 60.13 60.19 0.10% det rise <= 5% PASS
bytes_per_key raw 332.43 332.51 0.02% det rise <= 5% PASS

Overall: PASS

  • qps: noisy on shared CI, so the band comes from the base reps spread (floored at 5%); a drop is only a regression past the 15% budget.
  • bytes_per_key: deterministic (allocator-true memmodel), so a tight 5% rise budget; any rise beyond it FAILs.
  • Open-loop tails / criterion micro-benches are reported-not-failed (tail noise is high) and are not part of this ratchet.
  • An intentional perf trade is landed by raising the relevant budget in this PR with a documented reason (CI never auto-commits a baseline).

@ELares
ELares merged commit 785a338 into main Jul 9, 2026
28 of 32 checks passed
ELares added a commit that referenced this pull request Jul 9, 2026
…e's encode stops stealing a serving core (#590)

The per-slot Arc-COW snapshot (#588) cut the concurrent-snapshot p99.9 from 3.5s
to 303ms on c7g by moving a save's O(N) encode+fsync onto a dedicated
ic-persist-<shard> thread. But that thread still competes for a serving core:
IronCache does NOT set per-thread CPU affinity today (the datapath is pinned only
at the process level via taskset/cpuset), so at 16 shards on 8 pinned cores the
persist thread is a 17th runnable thread the scheduler lands on one of those same
serving cores, where its encode steals serving time and stretches the during-save
tail.

This adds a `persist_cpu` knob that pins the ic-persist thread(s) to a dedicated
core off the datapath set, so the encode no longer competes for a serving core.

The knob (boot config, restart-required; TOML `persist_cpu`, env
`IRONCACHE_PERSIST_CPU`, CLI `--persist-cpu`):
  - `off` / empty (DEFAULT): no pin, the thread floats exactly as today. This is
    the safe default per the tunability tenet: dedicating a core is an
    env-dependent tradeoff (it costs a serving/spare core), so it is opt-in, never
    baked in.
  - `auto`: reserve the HIGHEST core of the process's current affinity mask.
  - explicit list: `8`, `6-7`, or `6-7,10`.

Affinity mechanism: the syscall lives in the ironcache-runtime seam
(`sched_setaffinity`/`sched_getaffinity` via libc, Linux-target-gated), because the
ironcache binary is `#![forbid(unsafe_code)]`. It is one small, contained `unsafe`
with a SAFETY note; a graceful no-op on non-Linux (a set value logs one warning and
runs unpinned). The pure parse+select logic lives in ironcache-config (unit-tested,
and `Config::validate` rejects a malformed value at boot); the binary's
`crate::affinity` glues them and applies the pin at the top of the persist-thread
closure. Because sched_setaffinity is bounded by the process cpuset (not the
inherited taskset mask), the persist thread can escape a taskset-confined datapath
onto a reserved core outside it -- the "give the server one extra core" deployment.

Determinism (ADR-0003) is untouched: affinity is a scheduling concern off the
engine decision path (no clock, no entropy, no effect on any stored value or
ordering). The Arc-COW/FrozenSlot hot-path logic is unchanged; this only sets the
persist thread's affinity.

Measurement recipe (for the c7g re-measure): headtohead.sh/tail.sh gained a
`PERSIST_CORE` env var that injects `IRONCACHE_PERSIST_CPU` on the IronCache launch.
Give the server one extra core:

  SERVER_CORES=0-7 PERSIST_CORE=8 CLIENT_CORES=9-15 scripts/bench/tail.sh

The datapath stays taskset-pinned to 0-7; the persist thread escapes to core 8.
Or, standalone: `IRONCACHE_PERSIST_CPU=8 taskset -c 0-7 ironcache --shards 8 server`.

Tests: unit tests of the core-selection logic (incl. "auto reserves the last core,
shards get the rest") in ironcache-config; a Linux integration test that pins a
thread and asserts sched_getaffinity reflects exactly the configured core (runs
green in the Docker Linux build); and a non-Linux/unset graceful-no-op test. All
existing bgsave/persist/Arc-COW/eviction/turnkey/server-config tests pass unchanged;
clippy -D warnings, fmt, the io_uring build, and both CI invariant scripts are clean.

Honest residual: pinning removes the persist thread's CPU STEAL but not its
MEMORY-BANDWIDTH share -- the thread still reads the whole frozen keyspace to encode
it, contending with the (memory-bound) datapath. So pinning is expected to close
PART, not all, of the 303ms->~15ms gap. Bounding the off-core reader's bandwidth
(lever 2 in the issue) and a cheaper encode read (lever 3) are the remaining levers;
measure pinning first, then decide. Hence "Part of", not "Closes".

Part of #589.


Claude-Session: https://claude.ai/code/session_01VFWCZR47TDvsFPJ1waX85X

Signed-off-by: Zeke <ezequiel.lares@outlook.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ELares added a commit that referenced this pull request Jul 9, 2026
#591)

Reposition the tail-latency docs from the (refuted) moat thesis to what was actually
measured on c7g, so the docs stop overclaiming:

- CONFIG.md (persist_cpu / "Dedicated persist core"): pinning the persist thread to a
  dedicated core makes the concurrent-snapshot tail WORSE (291ms float -> 1,125ms pinned,
  measured), not better -- it slows the encode and lengthens the memory-bandwidth-contention
  window. Default (float) is best for tail; the knob is retained only for CPU isolation for
  other reasons. Residual gap to ms-class is a fundamental bandwidth-headroom tradeoff.
- PERSISTENCE.md (save-backpressure throttle #577): the throttle makes the tail WORSE
  (3.5s -> 16.75s at pct=10, measured), not the ~3-4x cut originally hypothesized. Corrected
  the "queue drains" claim and the fix reference: PR-B (#586) also failed; the per-slot
  Arc-COW (#588) is the actual fix (3.5s -> 291ms, 11.5x), with a fundamental residual.
- TAIL_LATENCY.md: the "moat" framing was refuted by this very harness; state the honest
  result (baseline tail ties Dragonfly, qps/core + memory win, durable-save tail went
  catastrophic -> competitive/sub-second, NOT category-leading on the during-snapshot tail).

No code change. Documents the #576/#588/#589 measured record so future work does not repeat
the throttle/pinning dead ends (both worsen a bandwidth-duration-bound tail).

Signed-off-by: Zeke <ezequiel.lares@outlook.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ELares added a commit that referenced this pull request Jul 9, 2026
…meout-guarded, real-socket driver (#595)

Land the OPT-IN wiring that connects the merged, data-safe transport core
(upgrade/stream.rs: send_bulk/recv_bulk, send_cutover/recv_cutover,
CutoverBarrier, the CRC'd envelope) to a REAL AF_UNIX socket rendezvous, a
caller-side timeout, and the cross-shard flip decision, as a new module
crates/ironcache/src/upgrade/drive.rs.

OPT-IN + default byte-unchanged (paramount):
  HandoffPlan::from_config is Some ONLY when `handoff_socket` is configured;
  with no socket it is None, so the default #390 tmpfs/durable SAVE->reload
  path is never consulted and no boot/hot-path code changes. This is the
  single gate; a node without the knob behaves exactly as before this wiring.

Wiring as-built (correct + tested):
  - Real-socket rendezvous: bind_handoff_listener (old/sender binds the
    node-local path, unlinking a stale socket file), accept_handoff, and
    connect_handoff (new/receiver connects, retrying while the sender is
    still binding). Real tokio UnixListener/UnixStream, the same transport
    two processes use, not the in-test socketpair the core unit tests drive.
  - Caller-owned TIMEOUT: every leg (send/recv bulk, cutover, shard, and the
    rendezvous) is wrapped in tokio::time::timeout, mapping an elapsed
    deadline to a new typed HandoffError::Timeout so a hung/wedged peer
    ABORTS instead of hanging. The core flagged the timeout as caller-owned;
    this is that caller.
  - Cross-shard atomic flip: barrier_from_results folds the per-shard
    outcomes into the ALL-OR-NOTHING CutoverState via the pure CutoverBarrier
    (Commit iff every shard handed off; Abort the instant any shard failed,
    sticky/fail-closed).

Abort-safety (the #1 rule):
  The sender only READS its store (the core send_* never mutate/drop it), so
  on ANY failure (socket error, delta overflow, peer abort, OR a caller
  timeout) the OLD process keeps serving and the durable data_dir snapshot
  stays a valid fallback. A fired timeout DROPS (cancels) the in-flight
  future: the sender's dropped socket is an EOF to the peer (an abort), and
  the receiver's partial fresh store is never adopted. No path here can lose
  an acknowledged write or serve a half-loaded store.

Live vs DEFERRED (why "Part of #391", not "Closes"):
  The final live-traffic zero-downtime serve-flip is deliberately deferred,
  because each remaining piece touches the live datapath / process lifecycle
  and cannot be made correct AND tested in-harness without two real sharded
  processes:
    - the receiver boot substitution (run_drain_loop pulling over the socket
      into its thread-local ShardStore instead of load_shard_on_boot);
    - the sender freeze (driving send_bulk from a #588 frozen Arc-COW view so
      the old shard keeps serving DURING the bulk; the core send_bulk holds a
      &ShardStore borrow for the whole scan today);
    - the dispatch -LOADING write-QUIESCE across shards + the cross-thread
      coordination that stops an old shard only after the barrier commits
      (no per-shard loading flag exists today, only node-wide CLIENT PAUSE);
    - the acceptor drain-and-final-accept for the non-socket-activated
      SO_REUSEPORT case (systemd socket-activation #389 is the supported
      no-RST path: the inherited listener fd is never closed across handoff);
    - the orchestrator spawning the sibling + old draining/exiting on Commit
      or resuming on Abort.

Tests (data-safety first, over a REAL socket file, both ends on one
current_thread runtime via tokio::join! since ShardStore/ReplRing are !Send):
  - real_socket_multi_shard_handoff_serves_every_key: populated old -> new
    over bind/accept/connect, barrier commits, every key served.
  - writes_during_transfer_present_after_cutover_over_real_socket: the delta
    (create/overwrite/delete during the window) converges via the timed
    phase wrappers.
  - receiver_crash_aborts_and_old_keeps_serving: abort-safety, old intact and
    nothing adopted.
  - hung_peer_times_out_and_old_keeps_serving + connect_to_unbound_socket_
    times_out: a hung peer / unbound socket ABORTS via the timeout.
  - plan_is_none_without_socket_and_some_with_it: the opt-in gate keeps the
    default flow unchanged.

Gates: cargo test -p ironcache (green), clippy --all-targets -D warnings,
cargo build --features io_uring, cargo fmt, check-rust-invariants.sh,
check-prior-art-claims.sh. No unsafe added. ADR-0002 (per-shard streams, the
cutover barrier the one sync point) + ADR-0003 (ops-path, no engine
clock/entropy).

Part of #391.


Claude-Session: https://claude.ai/code/session_01VFWCZR47TDvsFPJ1waX85X

Signed-off-by: Zeke <ezequiel.lares@outlook.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ELares added a commit that referenced this pull request Jul 10, 2026
…TOR, ACL) (#610)

A doc-audit found stale claims. Each correction below uses only measured numbers
(c7g, 2026-07-10):

- README.md (perf/competitive): reframed from "Dragonfly leads GET / close the
  remaining gap" to the corrected thread-per-core re-bench. IronCache now LEADS GET
  by about 19% single-endpoint (2.43M vs Dragonfly 2.04M ops/sec) and by roughly 2x
  cluster-aware (4.32M vs 2.19M) once #517 zero-hop routing removes the cross-shard
  hop; the old "leads both" was a benchmark CONFIG artifact (shards oversubscribed
  relative to cores). Kept honest nuance: baseline p99.9 TIES Dragonfly (~15ms), the
  durable-save tail (291ms) is competitive not category-leading, and the decisive
  edges are GET throughput, memory, and determinism. Fixed the two-vCPU section's
  stale "Dragonfly pulls ahead at real core count" reading.

- docs/PRODUCTION_READINESS.md: goal-completion verdict is now MET on GET (#507
  CLOSED, #517 hop-elimination paid off) instead of "unproven pending a re-bench";
  moved console web-surface hardening (#369, PR #607) from the "remain" list to
  "landed"; removed the stale within-3%/re-bench finish-line items; fixed the false
  "README.md:143 claims MONITOR redaction" citation in items 27 (README documents
  SLOWLOG/INFO/logs redaction, and MONITOR is intentionally unimplemented, so there
  is no doc mismatch).

- docs/bench/TAIL_LATENCY.md: corrected the plain-GET "can trail Dragonfly" framing
  to "now LEADS Dragonfly under a proper thread-per-core config"; the durable-load
  tail nuance (already honest) is unchanged.

- docs/design/ACL.md: documented per-subcommand ACL grants (Redis 7 `+cmd|sub`),
  specifically `+slowlog|get` for the least-privilege monitor user (read-only
  SLOWLOG without the `SLOWLOG RESET` wipe), and clarified that the
  CLUSTER_CONTRACT.md "no per-subcommand carve-out" note is narrow.

- docs/research/dragonfly.md: reworded one phrase to remove a stale-deficit-grep
  false positive.

- CHANGELOG.md: annotated the historical 2026-07-03 benchmark entry with the
  corrective re-bench note (record preserved).

Not changed (already current): docs/design/CONFIG.md and docs/design/PERSISTENCE.md
already point the save-throttle (#577) warning at the per-slot Arc-COW (#588) fix
that cut the snapshot tail 3.5s -> 291ms (11.5x).

Docs currency sweep (post competitive re-bench + console completion).

Signed-off-by: Zeke <ezequiel.lares@outlook.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ELares added a commit that referenced this pull request Jul 11, 2026
The prior handoff bulk (send_bulk) scans the LIVE store: because the per-slot
tables (#570/#576) can rehash under a concurrent write, a live chunked scan can
TEAR (a pre-existing key rehashed behind the cursor is skipped, and if it was
not itself re-written after the cut it lands in NEITHER the bulk NOR the delta,
so it is LOST). It also forces the old shard to stop serving writes during bulk.

PR-1 lands the riskiest primitive of the streamed live cutover (design of
record: docs/design/UPGRADE_LIVE_CUTOVER.md): the ATOMIC CUT plus a tear-free
frozen-view bulk, so the old shard keeps serving reads AND writes during the
transfer.

- freeze_cut (stream.rs): captures the #588 Arc-COW frozen slot view
  (begin_save) AND the delta floor F = ring.head() in ONE uninterrupted,
  non-await critical section on the shard thread. With the always-on observer
  ring (every mutator assigns its offset and appends in the same non-await step
  it applies), this makes bulk(frozen @ F) UNION delta(F, E] == EXACTLY the
  mutations with offset <= E, zero gaps, zero doubles. Any await or lock-drop
  between the two reads would let a write slip into the gap and be missed by
  both bulk and delta; there is none.
- send_bulk_from_frozen (stream.rs): streams the immutable frozen slots (via the
  new store accessor) instead of the live store, so the scan is tear-free by
  construction (a concurrent write COWs a fresh live slot, leaving the frozen
  Arc untouched). A concurrent write rides the delta ring at offset > F.
  send_shard_from_frozen adds the no-quiesce convenience (ends the save on all
  paths so the freeze flag never leaks).
- frozen_snapshot_chunk + FrozenCursor (ironcache-store): a minimal read-only
  accessor that yields bounded SnapshotEntry chunks from &[FrozenSlot], the
  frozen-view counterpart of snapshot_chunk. It only ADDS a read accessor over
  the existing FrozenSlot surface (the same shape the persist dump consumes); it
  does NOT touch the frozen four primitives (begin_save / end_save / the
  Arc::make_mut COW write path), so their soundness argument is unchanged.

Tests (over UnixStream::pair, no sibling process):
- HERO: a background writer HAMMERS the shard with a churny mix (create,
  overwrite crossing encodings, delete, absolute TTLs) THROUGHOUT the frozen
  bulk scan under real concurrent load, driving thousands of live mutations
  (forcing live-slot rehashes) across many concurrent segments of the scan.
  Reconstructs the receiver's post-cutover store and asserts bulk UNION delta
  equals EXACTLY the sender's acked keyspace as of the cut: every key present
  with its last-written value + encoding + absolute TTL, zero pre-existing keys
  lost to a mid-scan rehash, zero gaps, zero doubles.
- ATOMICITY UNIT: after freeze_cut returns, F equals the ring head at the
  freeze; every subsequent write gets a strictly greater, contiguous offset; and
  the frozen view is unperturbed by those live writes (COW isolation).
- The frozen convenience path streams every key and ends the save.

The existing stream.rs socket_tests are unchanged and still pass. send_bulk is
retained (documented) for the no-concurrent-write case.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ELares added a commit that referenced this pull request Jul 24, 2026
…ms -> 30ms), not a 40x loss (#768)

The public benchmark docs still describe the PRE-#742 world and materially UNDERSELL the product.
docs/bench/TAIL_LATENCY.md's measured record was committed 2026-07-23 14:20; PR #742 (the fix)
merged 18:52 the SAME DAY, so every IronCache tail in that record is pre-fix. The post-fix c7g
confirmation was run and posted on #676 but never made it into the docs.

Corrected, using the numbers from that confirmation (same config: 8 server cores + a pinned persist
core, tmpfs, 1M keys, BGSAVE/3s):
  - during-save p99.9: 794ms -> **30ms** (~26x); p99 640ms -> 12ms; the no-save baseline is 21ms, so
    the save is now essentially invisible to the datapath.
  - vs the same pinned competitors: Dragonfly 19ms, **IronCache 30ms**, Valkey 510ms, Redis 719ms --
    i.e. from WORST-in-class into Dragonfly's class, ~17x/24x ahead of Valkey/Redis.

Files:
  - TAIL_LATENCY.md: new CURRENT record up top; the old record retitled "Superseded: PRE-#742" with a
    warning that its analysis ATTRIBUTES TO SAVE COST what was actually a scheduling defect -- notably
    the dataset-scaling table and its "~4.3ms of datapath stall per MB" conclusion were measuring how
    long the HOL block lasted, not an inherent persistence cost. Competitor numbers there stay valid,
    and the save WALL-TIME characterization (deltas cut incremental cost; a base save is O(resident))
    is still accurate, so the section is kept for provenance. The old "honest verdict" is marked
    superseded and its wrong causal conclusion called out explicitly.
  - README.md: the during-snapshot tail is no longer listed as "a known limitation at a
    memory-bandwidth floor" -- that was the most visible stale claim in the repo.
  - docs/PRODUCTION_READINESS.md: the "291ms, competitive but not category-leading" line updated.
  - docs/design/PERSISTENCE.md: the 291ms figure marked historical (it is the #588 Arc-COW change
    alone), and the "FUNDAMENTAL memory-bandwidth-headroom tradeoff" claim marked SUPERSEDED -- that
    theory was disproven twice by measurement (the #740 pacer was refuted on c7g and reverted; the
    1-shard-vs-8-shard ablation showed the BIGGER save stalling ~60x LESS).

No new measurement was taken for this: the numbers already existed on #676. This is purely making the
shipped docs match the code that merged.


Claude-Session: https://claude.ai/code/session_01FfFZ8gkkNhDBASuntB72HR

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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