Skip to content

release(0.5.0): KV correctness (v3-4) + p=1/multi-shard throughput + Arc reply-slot UAF fix#207

Merged
pilotspacex-byte merged 38 commits into
mainfrom
perf/v3-3-p1-hotpath
Jul 4, 2026
Merged

release(0.5.0): KV correctness (v3-4) + p=1/multi-shard throughput + Arc reply-slot UAF fix#207
pilotspacex-byte merged 38 commits into
mainfrom
perf/v3-3-p1-hotpath

Conversation

@pilotspacex-byte

Copy link
Copy Markdown
Contributor

Summary

Bundles everything since v0.4.1 into one release. Two milestone bodies plus a reviewed memory-safety fix:

  1. v3-4 — KV Write Correctness & Data-Integrity Parity (supersedes fix(kv): v3-4 KV write correctness & data-integrity parity #206). No wrong-type data loss (GETDEL/GETSET/SET…GET), no integer-overflow panics/wraps, past-expire deletes, atomic MSETNX, and cross-shard coordinator local-leg durability.
  2. p=1 & multi-shard throughput. Moon now beats Redis on non-pipelined (p=1) GET/SET on both GCE arches (ARM 1.19–1.21×, x86 1.65–1.66×) via a poll-mode park (--io-busy-poll-us), and multi-shard now wins from 8 concurrent connections up (s4 c8 1.57–2.0×, c64 2.50×) after fixing the C2 reply-spin convoy (solo-conn gate) and unifying the monoio reply path on the zero-alloc ResponseSlotPool (L3b).
  3. Memory-safety fix (review-driven). The cross-shard reply path is now Arc-owned (see below).

Supersedes #206 — this branch is a clean superset (commits 1–10 are exactly #206).

Review outcomes (gate before release)

Two focused reviews + full CI:

  • Security / supply-chain — clean (0 critical, 0 high). The new vendored monoio (see below) was diffed against pristine upstream 0.2.4: delta confined to 4 files, zero new unsafe, dependency list byte-identical (no smuggled deps), behavior byte-identical when the MOON_* spin knobs are unset. MSETNX four-point wiring ✅, coordinator local-leg durability ✅.
  • Rust drop-safety — one finding, FIXED in this PR. A confirmed-mechanism cross-shard reply use-after-free on panic-unwind: the reply path sent a raw *const ResponseSlot into a pool on the connection task's stack; a panic unwinding the reply dispatch/drain would drop that pool while a target shard still held the pointer, making its later slot.fill() a UAF. (Pre-existing on the tokio path since v0.4.0; L3b extended it to the default monoio runtime.) Fix: ResponseSlotPtr now carries Arc<ResponseSlot>, so the slot outlives whichever of {pool, in-flight message} drops last — structurally closing both windows on both runtimes and removing 4 unsafe blocks (net unsafe reduction). New Arc::strong_count regression test. panic=abort was ruled out (mlua + existing catch_unwind recovery rely on unwinding).
  • CI parity — green on VM HEAD: fmt + clippy (default / --features graph / tokio) + cargo test on both runtimes (232 test-groups, 0 failures).

⚠ Reviewer attention

  • New vendored dependency: monoio 0.2.4 (vendor/monoio, ~27.7k lines, wired via [patch.crates-io]). This is a fork of upstream 0.2.4 (git sha f7827ddd, in .cargo_vcs_info.json) with a bounded "moon patch" (grep "moon patch", 4 files) implementing the --io-busy-poll-us poll-mode park. Verified vs upstream — see security review above. It ships because the default runtime is monoio and the busy-poll win depends on it.
  • ResponseSlotPtr is now Arc-owned (src/shard/dispatch.rs, src/server/response_slot.rs) — the memory-safety fix.

Perf validation

Numbers measured on GCE dedicated-vCPU instances (see BENCHMARK.md §6.6). A same-instance A/B confirmed the Arc UAF fix preserves the multi-shard c8/c64 s4 P1 win vs the pre-Arc commit (near-free: one refcount op on an already-hot cache line).

Deferred follow-ups (tracked, non-blocking)

  • Shutdown-aware bound on the reply await (now safe with Arc) — closes a partial-shutdown liveness hang.
  • CI guard re-diffing vendor/monoio vs pristine upstream (security MEDIUM).
  • SAFETY: comment on the legacy-driver spin-hook no-reentrancy invariant (security LOW).
  • Local-leg durability gap for coordinator BITOP/COPY/DEL/UNLINK.

🤖 Reviewed by security-reviewer + senior-rust-engineer subagents; findings addressed above.

tindangtts added 30 commits July 2, 2026 19:30
… v3-3 scope)

Doc-only follow-through from the 2026-07-02 KV deep review. No code changes.

- BENCHMARK.md: clarify that the 0.46-0.51x p=1 figure is a bench-production.sh
  confound (distributed -r keys + 512B-4KB values + c=100/200), not a clean
  shard-count signal; the cross-shard hop is ~10us (~2% of the ~460us baseline),
  and clean bench-compare is flat 0.79->0.82 across shards.
- CLAUDE.md: correct the pending_wakers lock rule. The cross-shard reply path
  awaits a flume oneshot directly; the pending_wakers relay is still swept each
  event-loop iteration (pub/sub + backpressure) but is no longer the reply-wake
  mechanism (test swf0 retired that premise; event_loop.rs ~1730).
- v3-3-vector-kv-polish/MILESTONE.md: fold the review's KV hot-path findings into
  scope -- kv-inline-get-nocopy (blocking.rs:1275 double-copy) and the inline-path
  std::sync::RwLock replica-check site (handler_monoio/mod.rs:524) into
  kv-dispatch-lock-discipline.

Refs: tmp/KV-DEEP-REVIEW.md (durable review deliverable, gitignored)
author: Tin Dang
GETDEL called db.remove(key) BEFORE checking the value type. For a key holding
a non-string (hash/list/set/zset), the key was destroyed and the handler then
returned WRONGTYPE -- the client saw an error implying nothing happened while
the data was already gone. Unrecoverable.

Fix: peek the type with a cheap non-cloning as_bytes() borrow first; return
WRONGTYPE without mutating if the value is not a string, and only call
db.remove() once the string type is confirmed. Matches Redis check-before-
mutate semantics (APPEND/GETRANGE already do this).

Red/green TDD: test_getdel_wrongtype_preserves_key asserts the wrong-type key
still exists after GETDEL returns WRONGTYPE -- failed before the fix (key gone),
passes after.

Refs: tmp/KV-AUDIT-CORRECTNESS.md #1 (P0 CRITICAL); tmp/KV-DEEP-REVIEW.md §7
author: Tin Dang
… (data loss)

Both SET key val GET and legacy GETSET computed the WRONGTYPE error for a
non-string key but then ran db.set/db.set_string UNCONDITIONALLY, overwriting
(destroying) the hash/list/set/zset before returning the error. The client saw
WRONGTYPE while the original value was already gone. Unrecoverable.

Fix (Redis check-before-mutate parity):
- SET: return WRONGTYPE immediately after computing old_value, BEFORE the NX/XX
  blocks and before db.set -- the wrong-type error takes precedence over NX/XX,
  matching Redis's evaluation order.
- GETSET: only call db.set_string when the old value is not a wrong-type error;
  otherwise return WRONGTYPE untouched.

Red/green TDD: test_set_get_option_wrongtype_preserves_key and
test_getset_wrongtype_preserves_key assert a plain GET after the op still reports
WRONGTYPE (key unchanged). Both failed before (GET returned the new string),
pass after. Full command::string suite green (79 tests).

Refs: tmp/KV-AUDIT-CORRECTNESS.md #1 (P0 CRITICAL); tmp/KV-DEEP-REVIEW.md §7
author: Tin Dang
…low guard

Two correctness bugs on the key-TTL path:

1. Non-positive TTL (Redis parity). EXPIRE/PEXPIRE with seconds/millis <= 0
   returned "ERR invalid expire time" and left the key in place. Redis treats a
   non-positive TTL as a past-time expiry and DELETES the key immediately,
   returning 1 (or 0 if absent). EXPIREAT already did this; EXPIRE/PEXPIRE now
   mirror it. The doc comment that mislabeled the error path as "modern Redis 7+
   behavior" is corrected.

2. Unchecked u64 overflow. now_ms + seconds*1000 wrapped silently in release and
   panicked in debug ("attempt to multiply with overflow") for large seconds
   (up to i64::MAX). Now guarded with checked_mul/checked_add; an out-of-range
   expiry returns an error and leaves the key untouched, matching Redis.

Propagation verified (advisor gate): WAL replay re-dispatches the raw command
via crate::command::dispatch, so replaying `EXPIRE k -1` re-runs the fixed
handler and deletes the key -- a WAL-recovered replica stays consistent with the
master. New test persistence::replay::replay_expire_nonpositive_deletes_key
proves this (RED before fix: replay kept the key; GREEN after). CdcOp still
classifies EXPIRE as Upsert, which is pre-existing and shared with EXPIREAT --
it does not affect KV replay (raw command is re-dispatched regardless).

Red/green TDD: test_expire_negative replaced by test_expire_nonpositive_deletes;
added test_expire_nonpositive_missing_key, test_expire_overflow_rejected,
test_pexpire_nonpositive_deletes, and the two replay guards. key + replay suites
green (68 tests).

Refs: tmp/KV-AUDIT-CORRECTNESS.md #3 (P0 HIGH); tmp/KV-DEEP-REVIEW.md §7
author: Tin Dang
…ror)

Several arithmetic sites could panic in debug (attempt to multiply/negate with
overflow) and silently wrap in release, given adversarial-but-valid i64 input:

- DECRBY key <i64::MIN>: negating i64::MIN is unrepresentable. Now checked_neg
  returns "ERR decrement would overflow" (INCRBY/DECR already used checked_add
  for the value itself; only the DECRBY pre-negation was unguarded).
- SET ... EX / EXAT, SETEX, EXPIREAT: (seconds|timestamp) * 1000 (+ now_ms)
  overflowed u64 for large values. Now guarded with checked_mul/checked_add;
  an out-of-range expiry returns "ERR invalid expire time" and performs no write,
  matching Redis. PSETEX/PEXPIRE/PEXPIREAT/SET PX/PXAT operate in ms and cannot
  overflow u64 from a valid i64, so they are left as-is (SETEX/PSETEX <=0-is-error
  is already correct and unchanged).

Red/green TDD: test_decrby_min_overflow, test_setex_overflow_rejected,
test_set_ex_overflow_rejected, test_expireat_overflow_rejected. All panicked
before the fix, return clean errors after; the affected key is untouched.
string + key suites green (141 tests).

Refs: tmp/KV-AUDIT-CORRECTNESS.md #4 (P0); tmp/KV-DEEP-REVIEW.md §7
author: Tin Dang
Formatting-only: rustfmt wrapping of long assert_eq!/assert! lines in the
EXPIRE/PEXPIRE/EXPIREAT and replay test additions. No logic change.

author: Tin Dang
Moon was missing MSETNX. Add it as a Redis-parity atomic multi-key write:
set every key/value pair iff NONE of the keys already exist; return 1 when
all were set, 0 when any key existed (writing nothing).

MSETNX's all-or-nothing contract cannot be honored atomically across shards
(like MSET, cross-shard writes scatter with no two-phase commit or rollback).
Per design decision, the coordinator REJECTS a MSETNX whose keys span more
than one shard with a CROSSSLOT error and writes nothing; when the keys are
co-located on a single shard (naturally or via a {hash-tag}) it runs the
whole command atomically on that shard's owner via run_on_owner.

- string::msetnx: single-shard two-phase handler (check-all, then set-all;
  no await between phases -> atomic on one database).
- coordinate_msetnx: groups keys by shard, CROSSSLOT on span>1, else routes
  the intact command to the owning shard (local dispatch or remote MultiExecute).
- Wired into is_multi_key_command, coordinate_multi_key, the phf metadata
  table (arity -3, write, keys 1..-1 step 2), and the (6,'m') dispatch arm.

Tests (red/green verified):
- unit: all-new -> 1, one-exists -> 0 (no partial), odd-arity error.
- integration (tests/msetnx_cross_shard_reject.rs, --shards 4): provably
  cross-shard keys -> CROSSSLOT + no partial write; co-located {t} keys ->
  atomic 1/0. RED confirmed by neutralizing only the CROSSSLOT branch (the
  cross-shard case regressed to Int(1) while co-located stayed green).
- scripts: MSETNX entries in test-consistency.sh (hash-tagged for 1/4/12
  shard parity with Redis) and test-commands.sh.

author: Tin Dang
…TL wrap

The expire-overflow guards added earlier in this milestone bounded the arithmetic
against u64::MAX, but expiry is stored as u64 and read back through PTTL/PEXPIRETIME
as i64 — an absolute expiry in (i64::MAX, u64::MAX] was accepted, then surfaced as a
NEGATIVE TTL on a key that is very much alive (e.g. `EXPIRE k 15000000000000000` ->
PTTL < 0). Redis rejects such out-of-range expiries outright (`when > LLONG_MAX/1000`).

Add a shared `expiry_ms_in_range` helper (u64 <= i64::MAX) and apply it at every
expiry-setting site: EXPIRE, PEXPIRE, EXPIREAT (key.rs) and SET EX/PX/EXAT, SETEX,
PSETEX (string_write.rs). Also reorder EXPIRE/EXPIREAT so an extreme negative
(`seconds < i64::MIN/1000`) errors instead of taking the past-time delete branch —
matching Redis, which bounds negatives before deleting.

Found by the v3-4 adversarial code review (Findings 2 & 3): tightens the
kv-integer-overflow-guard task so the exit criterion "not a wrapped TTL" holds on
the READ path too, not just the set path.

Red/green TDD: 9 new unit tests (i64-bound reject + extreme-negative preserve) across
key.rs and string/mod.rs — RED before, GREEN after. Full default lib suite 3642 pass,
tokio+jemalloc 2984 pass, both clippy gates + fmt clean.

author: Tin Dang
…sclosure)

Record the whole-diff adversarial code review (SHIP verdict) in the v3-4
MILESTONE.md ship-review:
- Finding 2/3 (i64-domain expiry bound) FIXED in d6b4136 — evidence row updated
  with the new commit and the 9 red/green tests.
- Finding 1 (pre-existing cross-shard coordinator local-leg WAL durability
  asymmetry) disclosed with the verified blast radius and deferred to a dedicated
  P0 follow-up (out of v3-4 scope — a co-located MSET/MSETNX on the connection's
  own shard is not persisted, while the remote leg is; NOT an MSETNX regression).

Also commits the .add/state.json registration of v3-4 as the active milestone.

author: Tin Dang
… AOF

The cross-shard coordinator's LOCAL leg executed co-located multi-key
writes in memory but never appended them to the owning shard's AOF, while
the REMOTE leg (MultiExecute in spsc_handler) always persists via
wal_append_and_fanout. So a co-located MSET/MSETNX was durable when the
owner was a remote shard but silently non-durable when the owner was the
connection's own shard.

Blast radius: multi-shard (--shards >1) + appendonly + keys hashing to the
connection's own shard. Single-shard (the recommended default) was never
affected — the coordinator is bypassed for num_shards<=1 and normal
dispatch persists. Pre-existing; not introduced by MSETNX (it copied the
existing MSET coordinator pattern).

Fix: the local leg now persists to the owning shard's AOF via a new
persist_local_leg helper — the same AofWriterPool::issue_append_lsn +
try_send_append_durable path every local single-key write already uses
(the local-write contract, not the SPSC remote path; ChannelMesh has no
self-send slot, so a local leg cannot route through wal_append_and_fanout).
Granularity: the whole command for a co-located owner (MSETNX; MSET fast
path), and a synthesized MSET over only the local keys for a scattered
MSET's local slice (never the full scattered command — my_shard does not
own the remote keys, and replay re-dispatches raw commands). On AOF append
failure the leg returns AOF_FSYNC_ERR instead of a false +OK
(design-for-failure).

This strictly adds durability and changes nothing about replication (live
fan-out via replica_txs is SPSC-only and untouched).

Red/green crash-recovery TDD in tests/coordinator_local_leg_durability.rs
(--shards 4 --appendonly yes; one co-located group per shard so exactly one
is the local leg regardless of SO_REUSEPORT landing; SIGKILL -> restart ->
reload): RED before, GREEN after, on both monoio and tokio. fmt + both
clippy gates clean; full lib suites green on both runtimes.

Remaining (tracked follow-up, same mechanism, out of this KV milestone's
command scope): coordinator BITOP / COPY (via run_on_owner's local branch)
and DEL / UNLINK (via coordinate_multi_del_or_exists) carry the identical
local-leg non-durability and are not yet fixed.

Resolves v3-4 milestone Finding 1 (HIGH).
author: Tin Dang
The inline GET fast path materialized every hit's value through an
intermediate `val.to_vec()` (heap alloc + memcpy) before framing it into
`write_buf`. Frame the `$<len>\r\n<bytes>\r\n` reply directly from the
borrowed `&[u8]` inside the `with_shard_db` closure instead, so the value
is copied exactly once (borrow -> write_buf). The closure stays
non-reentrant and await-free.

Byte-parity test pins the reply framing across the CompactValue SSO
boundary (0/1/12/13/65536-byte values) so the refactor cannot change a
single reply byte.

Part of the kv-s1-outperform goal (p=1 hot-path polish, v3-3).

author: Tin Dang
…hot path

GCloud c4a (ARM Axion) n=3 showed Moon losing p=1 single-op latency to
Redis by ~8% while winning on x86 — an asymmetry that points at
synchronization cost (CAS/RMW atomics are pricier under ARM's weak
memory model than x86 TSO). A perf/strace profile plus a full code trace
of the inline dispatch path found four per-op costs Redis does not pay,
all off the sharding model (Moon already does 2 syscalls/op vs Redis 3):

1. Replica gate: the inline path took `std::sync::RwLock::try_read()`
   on repl_state EVERY op. The lock-free `is_replica_mirror` added for
   exactly this cost (S3.5a, 2026-04-27, after ARM perf-annotate showed
   the try_read CAS dominating a sibling fn) was never propagated here.
   Now a single AtomicBool Acquire load — and unlike try_read, accurate
   while the lock is held.

2. Inline SET refcount churn: `value.clone()` into Entry::new_string,
   `key.clone()` into db.set, and `CompactKey::from(key.clone())` in
   Database::set were all dead clones (last use). Move semantics +
   borrow drop 3 atomic RMW pairs per SET.

3. Eviction gate: every inline SET paid a parking_lot RwLock read pair
   on runtime_config just to learn eviction has nothing to do. New
   lock-free pre-gate (`inline_write_can_skip_eviction`) proves the
   no-op case from two Relaxed atomic hints published at startup and on
   CONFIG SET maxmemory; unpublished sentinel fails safe into the
   existing locked path, so OOM/noeviction semantics are unchanged.

4. `ClientLiveState::touch()` called `Instant::now()`-equivalent per
   batch, violating the timestamp-caching invariant. It now takes the
   caller's shard-cached epoch ms (cold CLIENT LIST/INFO paths pass
   `current_time_ms()`), with saturating math for cached-clock skew.

Tests: pure-core gate test (deterministic — the process-global hints
race CONFIG SET tests in the parallel suite, learned the hard way),
touch caller-clock + saturation test, existing inline byte-parity suite.

Part of the kv-s1-outperform goal (beat Redis at p=1 on BOTH arches);
GCloud cross-arch re-measure to follow.

author: Tin Dang
The env contract documents MOON_NO_URING=1 as "force-disable io_uring
everywhere (monoio runtime + tokio bridge)", but the monoio factory
built FusionDriver unconditionally — the kill-switch only gated the
tokio bridge and uring_active(); monoio's own driver choice ignored it.
Discovered during the c4a p=1 driver A/B: a run launched with
MOON_NO_URING=1 still showed anon_inode:[io_uring] fds in /proc.

block_on_local now builds the epoll/kqueue LegacyDriver explicitly when
MOON_NO_URING is set (FusionDriver offers no runtime introspection, so
forcing the type is the only reliable switch). Verified empirically on
Linux: with the fix, MOON_NO_URING=1 serves traffic with zero io_uring
fds; unset keeps io_uring active.

This unblocks a same-instance io_uring-vs-epoll A/B on GCloud ARM to
attribute the residual p=1 latency gap vs Redis.

author: Tin Dang
…o runtime

Same-instance driver A/B on GCE c4a (ARM Axion, dedicated, pinned,
best-of-5, redis control): monoio's epoll LegacyDriver beats io_uring by
3.2-3.9% at p=1 AND matches-or-beats it at every pipeline depth
(P1/8/32/128) for KV workloads — epoll GET P128 keeps the 1.20x win vs
Redis. Switching driver closes ~40% of the ARM p=1 gap vs Redis
(0.92 -> 0.95). Other platforms (e.g. OrbStack aarch64) favor io_uring,
so this ships as an opt-in flag, not a default flip — bench per
platform (v2-1 lesson: knees do not transfer across targets).

--io-driver epoll sets a process-wide AtomicBool read by
MonoioRuntimeFactory::block_on_local (safe alternative to mutating
MOON_NO_URING via unsafe set_var in edition 2024); values are
clap-validated. MOON_NO_URING=1 remains the env kill-switch and now
reaches the monoio driver too (previous commit).

Verified on Linux: --io-driver epoll serves traffic with zero io_uring
fds and logs the forced choice; default keeps FusionDriver (io_uring).

author: Tin Dang
…t MOON_NO_URING doc

gcloud-kv-p1-baseline.sh gains:
- --measure-driver: same-instance A/B of moon(io_uring) vs moon(epoll,
  via MOON_NO_URING=1) vs redis control. Same-instance is required —
  Moon's absolute p=1 RPS varies up to 2.5% between c4a instances, the
  same order as the driver effect, so cross-instance driver deltas are
  unreadable.
- MOON_START_ENV passthrough (local -> gcloud ssh -> start_moon) for
  server-process env in diagnostics.
- io_uring self-report after server start: monoio's FusionDriver picks
  a driver silently, so /proc/<pid>/fd is asserted and logged per run.

CLAUDE.md: MOON_NO_URING description was aspirational — before f58ed02
it never reached the monoio driver. Documented the fix, the /proc
verification method, the --io-driver CLI equivalent, and the measured
GCE result (epoll beats io_uring 2-4% at all pipeline depths for KV on
c4a Axion; 1-1.5% on c3 x86; both same-instance A/Bs).

author: Tin Dang
…EFER_TASKRUN

GCE p=1 attribution (same-instance, perf tracepoints + strace): the
stock io_uring ring LOSES to epoll on both c4a ARM (-3.2..3.9%) and c3
x86 (-1.0..1.5%) because io_uring_enter is expensive there, while the
epoll path pays 4 syscalls/op (a speculative recvfrom that EAGAINs
every op at p=1 — readiness stays set after a successful read) vs
Redis's 3. io_uring does 2 syscalls/op — it just needs cheaper enters.

COOP_TASKRUN + SINGLE_ISSUER + DEFER_TASKRUN are the canonical
thread-per-core flags: completions are processed only at enter time on
the ring's owning thread, no IPIs/task_work preemption. Moon's model
satisfies the constraints: each shard creates and enters its ring on
its own thread only; cross-thread signalling is fd-based (flume/
eventfd/UnixStream), never a ring op from a foreign thread.

Fallback ladder: kernels <6.1 reject the flags -> warn + stock
FusionDriver; MOON_URING_PLAIN=1 skips tuning; MOON_NO_URING/
--io-driver epoll still force the legacy poller.

Needs io-uring 0.6 (monoio 0.2.4's uring_builder type) alongside the
existing 0.7 used by the tokio bridge -> renamed dep io-uring-06.

Verified on Linux 6.17: tuned ring initializes (uring fds present, no
fallback warning), serves traffic correctly.

author: Tin Dang
…iments)

ARM p=1 attribution ledger so far: 4 atomic fixes ~0, epoll best-driver
0.93-0.95, tuned uring flags (140ccfb) +0 on c4a. The diag decomposition
says the residual gap vs Redis is kernel-entry + scheduler-wake cost
(el0_svc 1.39us/op vs 0.46, both engines park+wake every op). Syscall
counting alone cannot close it; removing the SLEEP can.

Two env-gated experiments, both default-off (byte-identical behavior
when unset):

1. MOON_URING_SQPOLL=<idle_ms> [+ MOON_URING_SQPOLL_CPU=<n>]: kernel-
   side submission polling. RESULT: functionally broken under monoio --
   the driver is never CQE-woken and reaps only on its 1ms timer tick
   (p50 collapsed to 0.999ms, 1.4k ops/s). Kept as a documented
   diagnostic gate; do not use without a driver-level fix.

2. MOON_URING_SPIN_US=<budget>: poll-mode reaping inside the io_uring
   driver park -- submit pending SQEs immediately, then busy-poll the
   completion queue (shared-memory head/tail, zero syscalls) for the
   budget before falling back to the stock blocking wait. On hit the
   shard thread never sleeps, deleting the scheduler wakeup from the
   per-op path (the ScyllaDB-style thread-per-core poll design).
   Requires patching monoio's driver, so monoio 0.2.4 is vendored at
   vendor/monoio via [patch.crates-io]; the diff vs upstream is
   confined to src/driver/uring/mod.rs (grep "moon patch", no new
   unsafe in the added lines).

Correctness: cross_shard_consistency_red 7/7 + shardslice_live 6/6
green under MOON_URING_SPIN_US=40. OrbStack VM shows spin SLOWER
(virtualized syscalls/wakes are nearly free, so spin is pure overhead
there) -- decisive numbers must come from pinned GCE cores where the
wake path costs real microseconds.

author: Tin Dang
--measure-driver hard-reset MOON_START_ENV per cell (uring cell wiped it
to ""), silently discarding caller-provided experiment env such as
MOON_URING_SPIN_US=40 -- the A/B would re-measure the plain ring while
claiming to test the experiment. Capture the incoming value once and
compose: uring cell = caller env as-is, epoll cell = caller env +
MOON_NO_URING=1.

author: Tin Dang
CQ spin on io_uring is structurally dead for network ops: CQE posting
requires the ring-owner task to run task_work. DEFER_TASKRUN posts only
inside enter(GETEVENTS) (measured: spin burns its full budget, 49us/op
on c4a, 3x regression); a plain ring posts via TWA_SIGNAL kernel kicks
(~9us/op measured); SQPOLL does not run poll task_work on the sqpoll
thread on 6.17 (CQEs never post while spinning, 498 ops/s). The
io_uring spin gate now forces a plain ring so the earlier experiment is
at least well-defined, but the winning primitive is epoll:

epoll readiness is published by softirq with NO task participation, so
a zero-timeout epoll_wait spin observes it ~0.3us after arrival with no
sleep, no scheduler wake, no signal kick. MOON_EPOLL_SPIN_US=<budget>
patches the vendored monoio LegacyDriver park: busy-loop
poll(timeout=0) for the budget, fall back to the stock blocking poll on
miss. Foreign wakes surface during the spin (the waker eventfd is
registered in the same mio poll). Default-off; zero behavior change
when unset.

Pinned-core VM A/B (taskset 0 vs 2, c1 GET p=1): stock epoll 34.3k/29.8k
(p50 31us) vs spin40 47.4k (p50 23us) = +40-60%, p50 -8us. Unpinned VM
numbers are misleading for spin designs (a 100%-busy thread displaces
the client); pinned disjoint cores are the valid condition and match
the GCE instrument. GCE cross-arch verdict pending.

author: Tin Dang
Promote the epoll readiness spin-poll (MOON_EPOLL_SPIN_US, vendored
monoio LegacyDriver patch) to a first-class CLI flag:

  --io-busy-poll-us <N>   busy-poll the shard event loop N microseconds
                          before sleeping (0 = off, the default)

The flag forces the legacy (epoll/kqueue) driver -- busy-poll parks
cannot exist on io_uring since CQEs are not userspace-observable
without task participation -- and hands the budget to the vendored
driver through a programmatic setter (monoio::set_legacy_spin_budget_us,
AtomicU64 override with env fallback; no set_var/unsafe needed).
No-op warning under runtime-tokio.

GCE same-instance A/B (c1 GET/SET p=1, pinned disjoint cores, best-of-5)
with the 40us budget vs Redis:
  ARM  c4a Axion  SET 1.193 / GET 1.206   (was 0.95 losing)
  x86  c3  Intel  SET 1.657 / GET 1.657   (was 1.06)

Cost model: up to N us CPU per idle park (~4%/core at 40us against 1ms
timer parks); unpinned shared-core hosts can see spin as a regression --
documented in CLAUDE.md. Cross-arch replication (n=3/arch) in flight.

Verified: flag parse test, VM end-to-end (io_uring fds absent, spin log
line, serves), cross_shard_consistency_red 7/7 + shardslice_live 6/6
under spin.

author: Tin Dang
…st literals

1) gcloud-kv-p1-baseline.sh gains --measure-shards: shards x busy-poll
   sweep ({1,4} x {0,40us} vs single Redis control) with the p=1 rigor
   (pinned cores, steal gate, fresh-server best-of-N, strict keyspace so
   multi-shard actually scatters). Cells per config: c1/P1 single-conn
   latency (captures the cross-shard-hop story at shards=4 -- the target
   shard normally sleeps; busy-poll removes that wake too), c8/P1,
   c8/P16, c8/P64 with a 3-thread client on disjoint cores. Needs an
   8-vCPU instance (shards pin to 0..3, client to 5-7). start_moon
   generalized via MOON_SHARDS/SERVER_CORES/MOON_EXTRA_FLAGS (defaults
   preserve the shards=1 path); bench_pair gains optional --threads.

2) Fix tokio-feature-gated tests that construct exhaustive ServerConfig
   literals (mq_integration, workspace_integration x2, txn_kv_wiring):
   the io_driver/io_busy_poll_us fields broke compilation ONLY under
   --features runtime-tokio because cargo clippy without --all-targets
   never compiles test targets -- CI-parity gap, caught by the full
   tokio suite run. All-targets check now clean on both feature sets.

author: Tin Dang
…2.10)

New 2.10: same-instance A/B tables (Moon busy-poll vs tuned-uring vs
Redis control) on GCE dedicated 4-vCPU, pinned disjoint cores, best-of-5,
n=3 instances per arch -- ARM c4a 1.193-1.212x, x86 c3 1.646-1.663x.
Supersedes the "loses p=1" rows in 2.7-2.9 when busy-poll is enabled;
documents mechanism (wake elimination), multi-client probe, idle cost,
and the pinned-cores measurement caveat. Executive summary gains the two
p=1 rows; header Last-Updated refreshed.

author: Tin Dang
Cross-arch --measure-shards results at 74de849: the 2.10 busy-poll p=1
win replicates on the 8-vCPU shape (5th ARM + 4th x86 instance);
busy-poll recovers +22-42% of the cross-shard hop (removes the target
shard's wake) but shards=4 still loses every non-pipelined cell to the
SPSC dispatch cost; Redis-server-bound SET P16 shows Moon >=2.0x.
Deep-pipeline cells on the co-located pinned topology are flagged as
client-saturated ceilings, not measurements. Guidance sharpened:
--shards 1 --io-busy-poll-us 40 is the p=1 configuration.

author: Tin Dang
…stance (multi-shard diag)

Lets --measure-diag profile a shards=N busy-poll server: the perf
attribution of the residual cross-shard hop needs the diag mode to
start moon with --shards 4 --io-busy-poll-us 40 on cores 0-3.

author: Tin Dang
Cross-shard dispatch pays a full cross-thread wake per message: flume
try_send -> foreign-waker relay -> eventfd_write syscall (s4 diag on
GCE c4a: eventfd_write 2.01% self + 3.4% CAS/SWP atomics on the conn
shard). When the target shard's driver is already busy-polling
(--io-busy-poll-us), that wake is pure waste — the target is awake and
polling anyway.

New skip-notify handshake, active only with busy-poll AND shards > 1:

- channel::Notify gains a skip_wake advertisement flag. Senders
  (notify_one) elide the entire wake when the target advertises it is
  spinning; a process-global gate keeps even the check off notify_one
  for every other deployment shape. notify_local() bypasses the gate
  for same-thread delivery.
- Vendored monoio legacy driver (grep "moon patch") gains per-thread
  spin-park hooks: advertise(true) at spin entry, advertise(false) at
  every exit, and a probe each spin iteration that checks the shard's
  SPSC ringbufs and re-arms the race2 Notify from the local thread
  (run-queue push, no syscall) when items are pending.
- Lost-wake safety is a Dekker handshake: the sender fences SeqCst
  after its ringbuf push before reading the flag; the driver retracts
  the flag (SeqCst store + fence) and runs one final probe on every
  spin exit. Either the final probe sees the push or the sender sees
  the cleared flag — store-buffering reordering is forbidden by the
  paired fences. Unit-tested (gate off / spinning / local bypass /
  retracted) in channel.rs.
- Observability: spsc_notify_skipped counter (INFO Stats), plus a
  per-shard registration log line.

tokio runtime is untouched: hooks are monoio-only, the gate stays off,
and notify_one behavior is bit-identical. No new unsafe. Smoke-verified
on a live 4-shard busy-poll server (skipped counter engages, cross-shard
MGET/MSET correct); full release suite green on Linux under
MOON_NO_URING=1 MOON_EPOLL_SPIN_US=40 (the skip-notify regime forced for
every multi-shard integration test).

Arithmetic expectation, to be validated on GCE same-instance A/B:
recovers ~1-1.5us of the ~5.3us s4-vs-s1 c1P1 residual; parity at
shards>1 c1 p=1 is NOT expected (irreducible reply hop remains).

author: Tin Dang
Skip-notify's expected delta (~1-1.5us of a ~28us s4 c1P1 op) is a few
percent — inside the ~2.5% instance-to-instance variance — so base and
new commits MUST share one instance. New mode builds both refs
(AB_BASE_REF vs MOON_REF, per-ref cached binaries), runs shards x spin
cells (spin=0 doubles as a no-regression control since skip-notify is
gated off there) with the same pinned/steal-gated/fresh-server-best-of-N
rigor, emits base|new|redis per cell, and ends with an engagement proof
(INFO spsc_notify_skipped after a sustained c1P1 burst). AB_* env
forwarded to the instance.

author: Tin Dang
Multi-shard investigation instrumentation (diagnostic, defaults
unchanged):

- MOON_XSHARD_SPIN_BUDGET / MOON_XSHARD_SPIN_GATE env overrides for the
  C2 reply-side spin constants (read once via OnceLock; 0 budget fully
  disables the spin). Enables same-instance A/Bs of the
  convoy hypothesis — at s4 c8P1 two conns share each shard thread and
  the synchronous reply spin (gate=2 admits both) may block SPSC drain
  of other shards' requests, a circular cross-shard stall that would
  explain the 0.5x c8P1 collapse.
- --measure-ab gains AB_BASE_ENV / AB_NEW_ENV (per-side MOON_START_ENV
  so one binary can be A/B'd across env knobs) and AB_COMBOS
  (configurable clients|pipe cells, e.g. "1|1 8|1 64|1").

author: Tin Dang
…nvoy fix)

The C2 reply-side spin is synchronous on the shard thread. At shards=4
c8P1 every shard hosts 2 connections and the existing gates admit both:
while conn A spins awaiting shard B's reply, its thread can neither run
the sibling conn nor drain incoming SPSC requests from other shards — a
circular cross-shard convoy. Same-instance GCE c4a A/B (env knob
MOON_XSHARD_SPIN_BUDGET=0 vs default, e8ef3d7): s4 c8P1 spin-off
199.8k/200.0k ops/s vs spin-on 72.7k — 2.75x, lifting Moon from 0.45x
to 1.25x Redis. The same A/B confirms the spin must stay for c1
(spin-off loses 13.5% there).

Fix: spin only when the requesting connection is ALONE on its shard
thread. New thread-local SHARD_CONN_COUNT maintained by a ShardConnGuard
RAII at both connection-handler entries (monoio + tokio, fresh +
migrated); xshard_may_spin() now also requires
count <= XSHARD_SPIN_MAX_CONNS (default 1, env override
MOON_XSHARD_SPIN_MAX_CONNS for diagnostic A/Bs). The existing
XSHARD_INFLIGHT gate cannot see a sibling whose readable event is still
parked in the driver — it only counts conns already inside a reply-wait;
the conn count closes that executor-level blind spot.

Known trade-off (documented): a solo hot connection sharing its shard
with idle extra connections (e.g. a monitoring conn) now parks instead
of spinning — the safe direction; busy-poll still covers the target-side
wake. Regression test solo_conn_gate_blocks_spin_with_sibling_connection
in tests/xshard_fastpath_api.rs pins the semantics.

author: Tin Dang
Both cross-shard write arms in drain_spsc_shared called
aof::serialize_command (full command alloc + copy) BEFORE
wal_append_and_fanout, whose S3.5b bypass then discarded the result
whenever WAL v2/v3, replicas, and the AOF pool are all off — pure waste
on every cross-shard write with persistence disabled. Flagged by the
multi-shard serialization trace; suspect for part of the s4 SET-vs-GET
gap (200k vs 266k ops/s at c8P1 spin-off, GCE c4a).

Extract the bypass criterion as wal_fanout_has_work() (inline, derivable
from inputs, no shared state), use it inside wal_append_and_fanout
(behavior unchanged) and as a pre-gate before serialize_command at both
write arms. The blocking-wake path (LPUSH/RPUSH/LMOVE/ZADD/XADD) is
outside the gate and unaffected. Cold SWAPDB site left as is.

Durability/persistence/spsc suites green (coordinator_local_leg 3,
persistence 440, spsc 13).

author: Tin Dang
The c64 cells hit a ~266k ops/s ceiling with the hardcoded 3 client
threads (unpipelined syscall-pair bound) — five identical 266,667
readings across engines/cells in the convoy A/B are the instrument
saturating, not the server. Make the thread count configurable so the
goal measurement can drive from 4 client cores (SHARD_CLIENT_CORES=4-7
AB_BENCH_THREADS=4).

author: Tin Dang
The monoio Phase 2b cross-shard path allocated a fresh flume oneshot per
batch and awaited it through a chunked race2 park (100ms chunks, 30s cap).
The tokio handler had already moved to the loom-tested zero-allocation
ResponseSlotPool; the slotted ShardMessage variants and the target-side
fill arms were runtime-shared and idle on monoio. This unifies monoio on
the same design:

- Phase 2b now sends ShardMessage::PipelineBatchSlotted carrying a
  ResponseSlotPtr into the connection's pre-allocated pool (one slot per
  target shard) — no per-op oneshot alloc, no flume channel internals,
  no lazily boxed recv future on the reply path.
- Reply await is tokio-parity: C2 idle-gated spin over slot.try_take(),
  then a plain infinite future_for(target).await. The chunked 30s
  timeout structure is REMOVED — required by the ResponseSlotPtr
  lifetime contract (the pool lives on the connection task's stack;
  abandoning the await while a slotted message is in flight would turn
  the target shard's eventual slot.fill() into a use-after-free). A
  backpressure reject never pushes the message, so the slot stays EMPTY
  and reuse is safe. Contract documented at the await site.
- The vestigial reply_futures Vec (pre-allocated, cleared, never
  populated) is now the carrier for (meta, target), deleting the
  per-batch oneshot_futures Vec allocation as well.
- spsc_handler: the PipelineBatch + PipelineBatchSlotted arms get the
  wal_fanout_has_work serialize-skip gate (b3fa7be gated only the
  MultiExecute pair), so persistence-off cross-shard writes no longer
  pay the aof::serialize_command alloc on the arm monoio now uses.
- response_slot: try_take() is live under both runtimes; dead_code
  cfg_attr lifted. CROSS_SHARD_RESPONSE_{CHUNK,TIMEOUT}_MS deleted.

No new unsafe blocks: the single deref site remains the existing
SAFETY-commented arm in spsc_handler; ResponseSlotPtr's Send impl and
the slot state machine are unchanged (loom: tests/loom_response_slot.rs;
cross-thread wake proof: tests/spsc_wake_floor_red.rs::swf0).

Coverage: existing monoio suites now exercise the slotted arm end-to-end
(cross-shard consistency, C4-FOLD double-apply, H1 fsync barrier); full
VM suite green on both runtimes.

Part of the multi-shard ≥1.5×-Redis line (tmp/MULTISHARD-REDESIGN.md):
c64P1 already ≥1.65× both arches after the L1 convoy fix; c8P1 is
latency-bound and needs ~4µs off the serial reply path — this is that
lever. GCE same-instance A/B to follow.

author: Tin Dang
a5ec041 added the AB_BENCH_THREADS knob at the measure_ab/measure_shards
usage sites but not to gcloud_run_one's env forwarding, so an OUTER
`--one` invocation silently fell back to 3 client threads (the ~266k
c64P1 instrument ceiling the knob exists to lift). Forward it like the
other AB_* vars so the de-capped goal matrix is reproducible from a
single outer command.

author: Tin Dang
Records the C2 reply-side spin overrides (BUDGET/GATE/MAX_CONNS) in the
CLAUDE.md environment-variable table with their defaults, the budget=0
A/B use that proved the c8P1 convoy, and a warning that raising the
solo-conn ceiling re-creates the multi-conn collapse the L1 fix removed.

author: Tin Dang
Defaults audit outcome: the shipped defaults already encode the measured
evidence for the most common deployment (single app, moderate
connections, durability on) — `--shards 1` (best per-op latency; a
cross-shard hop costs ~10µs), `--appendonly yes` + `everysec` (measured
free at the default shard count on GCE), busy-poll off (regresses on
shared hosts), admin port off. No code-default changes.

What was wrong or missing was the documentation:

- docs/configuration.md claimed `--shards` defaults to `0 (auto)` and
  `--appendonly` to `no`; actual defaults are `1` and `yes`.
  docs/guides/configuration.md had the same appendonly error.
- `--io-driver`, `--io-busy-poll-us`, `--initial-keyspace-hint`,
  `--memory-arenas-cap`, `--aof-fsync-timeout-ms` were absent from the
  flags reference despite busy-poll being the single largest latency
  lever we ship.
- No workload-oriented tuning documentation existed.

New docs/guides/tuning.md (in the site nav under Operations): quick
recipes plus measured guidance per workload — shard-count selection
(1 vs 4 vs auto, hash-tag co-location), busy-poll trade-offs and the
dedicated-core requirement, persistence cost table (everysec free /
always −9% P1 with deep-pipeline tail caveat / multi-shard AOF+WAL disk
volume), memory (elastic budget, keyspace hint, container arena caps),
platform notes (io_uring vs epoll per platform, macOS dev-only,
pinning), client-side checklist (ulimit, pool sizing vs shards,
pipelining), observability cost, and vector quantization-by-dimension.
All numbers from the 2026-07 GCE dedicated-vCPU runs (ARM c4a + x86 c3).

Also refreshed the `--shards` CLI help (was pipeline-only advice;
multi-shard now also wins at 8+ connections post convoy fix).

author: Tin Dang
…L3b)

Records the validated multi-shard result on GCE dedicated-vCPU
instances: after the reply-spin convoy fix (solo-conn gate, 795c4f0)
and the monoio ResponseSlotPool unification (fd13f03), s4 P1 with
busy-poll 40 measures vs Redis:

  c8  GET/SET: ARM 1.57/1.71x — x86 1.9/2.0x (>=1.6x vs Redis's best
               client config)
  c64 GET/SET: 2.50x both arches (>=1.86x best-vs-best)
  c1: 0.91-0.96 = the documented structural hop ceiling (s1 latency)

Supersedes the s4 c>=8 collapse rows in §6.5 (0.44-0.64x), which were
the convoy bug, and updates the guidance: --shards 4 wins from 8
concurrent connections up even without pipelining.

Instrument notes recorded: redis-benchmark --threads 4 lowers Redis's
x86 c8/c64 readings 15-20% vs --threads 3 (Moon thread-insensitive,
ARM unaffected; dedicated Redis-only control run), and unpipelined c64
readings quantize to duration attractors. Raw files under tmp/.

author: Tin Dang
The cross-shard reply fast-path (ResponseSlotPool, L3b + the pre-existing
tokio path) sent a raw `*const ResponseSlot` into a pool that lives on the
connection task's stack. A panic unwinding through the reply dispatch loop
(Phase 2b) or the drain loop drops that stack pool in-place during the
unwind, freeing the slot heap while a target shard still holds the raw
pointer in its SPSC queue — the target's later `slot.fill()` is then a
use-after-free / heap corruption. Confirmed-mechanism in review (async-fn
locals are dropped during panic-unwind; the cross-thread fill race is real);
no client trigger demonstrated, but there is no panic boundary and panic
surfaces exist in the window (Vec indexing, RefCell borrow in the dispatch
backpressure closure). `panic = "abort"` is not an option (mlua + existing
catch_unwind recovery rely on unwinding).

Fix: `ResponseSlotPtr` now carries an `Arc<ResponseSlot>` instead of a raw
pointer, and `ResponseSlotFuture` owns an `Arc` clone instead of borrowing
the pool. The refcount keeps each slot alive until BOTH the connection's
pool AND the in-flight message drop, so abandoning the reply (drop,
panic-unwind, or a future shutdown break) can never dangle a concurrent
`fill()`. This structurally closes the window on both the dispatch and
drain sides, on both runtimes — retroactively fixing the same latent hazard
on tokio (shipped since v0.4.0), where monoio previously used a panic-safe
flume oneshot.

Cost: one refcount bump per batch (<= num_shards), pool allocated once per
connection — no per-op allocation is reintroduced (L3b removed the per-op
flume-oneshot alloc, ~80-120ns; this adds single-digit-ns atomics). Net
`unsafe` REDUCTION: removes the `unsafe impl Send for ResponseSlotPtr` and
four `unsafe { &*response_slot.0 }` derefs in the SPSC fill arms.

TDD: `test_slot_arc_outlives_pool_drop_and_is_fillable` proves via
`Arc::strong_count` that a sent handle survives the pool being dropped and
remains fillable (the raw-pointer design could not express this and would
dangle). Full suite green both runtimes.

Follow-up (now safe to add): a shutdown-aware bound on the reply await to
close the partial-shutdown liveness hang (target thread torn down while a
producer is parked).

author: Tin Dang
Reconcile CHANGELOG [Unreleased] to cover BOTH milestone bodies since
v0.4.1 (previously only v3-4 was documented):
- v3-4 KV write correctness & data-integrity parity (existing).
- p=1 & multi-shard throughput: --io-busy-poll-us / --io-driver, the
  convoy fix + L3b, GET no-copy, lock/atomic strips, corrected default
  docs + tuning guide.
- Dependencies: vendored monoio 0.2.4 supply-chain disclosure (verified
  vs pristine upstream: 4-file bounded patch, zero new unsafe, deps +
  behavior byte-identical when unset).
- Fixed: the Arc cross-shard reply UAF (memory safety).

Also: correct the Cargo.toml [patch.crates-io] comment to reflect the
real 4-file moon-patch scope (was "1 file") + record the upstream sha,
and fix an int_plus_one clippy nit in a metrics_setup test.

author: Tin Dang
…rces

Add a KEYSPACE env knob so bench-compare.sh / bench-resources.sh can
spread ops across a real keyspace (-r) for large-scale runs instead of
redis-benchmark's default single hot key. Empty by default (unchanged
behavior). Also fold the v3-4 milestone completion doc.

author: Tin Dang
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 225 files, which is 75 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

Upgrade to a paid plan to raise the limit.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 414408cb-2f8e-4671-b4c7-92484e34d90b

📥 Commits

Reviewing files that changed from the base of the PR and between c85af24 and 0b873ca.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (225)
  • .add/milestones/v3-3-vector-kv-polish/MILESTONE.md
  • .add/milestones/v3-4-kv-correctness/MILESTONE.md
  • .add/state.json
  • BENCHMARK.md
  • CHANGELOG.md
  • CLAUDE.md
  • Cargo.toml
  • docs/configuration.md
  • docs/guides/configuration.md
  • docs/guides/tuning.md
  • mkdocs.yml
  • scripts/bench-compare.sh
  • scripts/bench-resources.sh
  • scripts/gcloud-kv-p1-baseline.sh
  • scripts/test-commands.sh
  • scripts/test-consistency.sh
  • src/admin/metrics_setup.rs
  • src/client_registry.rs
  • src/command/config.rs
  • src/command/connection.rs
  • src/command/helpers.rs
  • src/command/key.rs
  • src/command/metadata.rs
  • src/command/mod.rs
  • src/command/string/mod.rs
  • src/command/string/string_read.rs
  • src/command/string/string_write.rs
  • src/config.rs
  • src/main.rs
  • src/persistence/replay.rs
  • src/runtime/channel.rs
  • src/runtime/mod.rs
  • src/runtime/monoio_impl.rs
  • src/server/conn/blocking.rs
  • src/server/conn/handler_monoio/dispatch.rs
  • src/server/conn/handler_monoio/mod.rs
  • src/server/conn/handler_sharded/dispatch.rs
  • src/server/conn/handler_sharded/mod.rs
  • src/server/conn/shared.rs
  • src/server/conn/tests.rs
  • src/server/response_slot.rs
  • src/shard/coordinator.rs
  • src/shard/dispatch.rs
  • src/shard/event_loop.rs
  • src/shard/slice.rs
  • src/shard/spsc_handler.rs
  • src/storage/db.rs
  • src/storage/eviction.rs
  • tests/coordinator_local_leg_durability.rs
  • tests/mq_integration.rs
  • tests/msetnx_cross_shard_reject.rs
  • tests/txn_kv_wiring.rs
  • tests/workspace_integration.rs
  • tests/xshard_fastpath_api.rs
  • vendor/monoio/.cargo-ok
  • vendor/monoio/.cargo_vcs_info.json
  • vendor/monoio/Cargo.toml
  • vendor/monoio/Cargo.toml.orig
  • vendor/monoio/LICENSE-APACHE
  • vendor/monoio/LICENSE-MIT
  • vendor/monoio/LICENSE-THIRD-PARTY
  • vendor/monoio/README.md
  • vendor/monoio/src/blocking.rs
  • vendor/monoio/src/buf/io_buf.rs
  • vendor/monoio/src/buf/io_vec_buf.rs
  • vendor/monoio/src/buf/mod.rs
  • vendor/monoio/src/buf/msg.rs
  • vendor/monoio/src/buf/raw_buf.rs
  • vendor/monoio/src/buf/slice.rs
  • vendor/monoio/src/buf/vec_wrapper.rs
  • vendor/monoio/src/builder.rs
  • vendor/monoio/src/driver/legacy/iocp/afd.rs
  • vendor/monoio/src/driver/legacy/iocp/core.rs
  • vendor/monoio/src/driver/legacy/iocp/event.rs
  • vendor/monoio/src/driver/legacy/iocp/mod.rs
  • vendor/monoio/src/driver/legacy/iocp/state.rs
  • vendor/monoio/src/driver/legacy/iocp/waker.rs
  • vendor/monoio/src/driver/legacy/mod.rs
  • vendor/monoio/src/driver/legacy/waker.rs
  • vendor/monoio/src/driver/mod.rs
  • vendor/monoio/src/driver/op.rs
  • vendor/monoio/src/driver/op/accept.rs
  • vendor/monoio/src/driver/op/close.rs
  • vendor/monoio/src/driver/op/connect.rs
  • vendor/monoio/src/driver/op/fsync.rs
  • vendor/monoio/src/driver/op/mkdir.rs
  • vendor/monoio/src/driver/op/open.rs
  • vendor/monoio/src/driver/op/poll.rs
  • vendor/monoio/src/driver/op/read.rs
  • vendor/monoio/src/driver/op/recv.rs
  • vendor/monoio/src/driver/op/rename.rs
  • vendor/monoio/src/driver/op/send.rs
  • vendor/monoio/src/driver/op/splice.rs
  • vendor/monoio/src/driver/op/statx.rs
  • vendor/monoio/src/driver/op/unlink.rs
  • vendor/monoio/src/driver/op/write.rs
  • vendor/monoio/src/driver/poll.rs
  • vendor/monoio/src/driver/pool.rs
  • vendor/monoio/src/driver/ready.rs
  • vendor/monoio/src/driver/scheduled_io.rs
  • vendor/monoio/src/driver/shared_fd.rs
  • vendor/monoio/src/driver/thread.rs
  • vendor/monoio/src/driver/uring/lifecycle.rs
  • vendor/monoio/src/driver/uring/mod.rs
  • vendor/monoio/src/driver/uring/waker.rs
  • vendor/monoio/src/driver/util.rs
  • vendor/monoio/src/fs/create_dir.rs
  • vendor/monoio/src/fs/dir_builder/mod.rs
  • vendor/monoio/src/fs/dir_builder/unix.rs
  • vendor/monoio/src/fs/file.rs
  • vendor/monoio/src/fs/file_type.rs
  • vendor/monoio/src/fs/metadata/mod.rs
  • vendor/monoio/src/fs/metadata/unix.rs
  • vendor/monoio/src/fs/metadata/windows.rs
  • vendor/monoio/src/fs/mod.rs
  • vendor/monoio/src/fs/open_options.rs
  • vendor/monoio/src/fs/permissions.rs
  • vendor/monoio/src/io/as_fd.rs
  • vendor/monoio/src/io/async_buf_read.rs
  • vendor/monoio/src/io/async_buf_read_ext.rs
  • vendor/monoio/src/io/async_read_rent.rs
  • vendor/monoio/src/io/async_read_rent_ext.rs
  • vendor/monoio/src/io/async_rent_cancelable.rs
  • vendor/monoio/src/io/async_rent_cancelable_ext.rs
  • vendor/monoio/src/io/async_write_rent.rs
  • vendor/monoio/src/io/async_write_rent_ext.rs
  • vendor/monoio/src/io/mod.rs
  • vendor/monoio/src/io/sink/mod.rs
  • vendor/monoio/src/io/sink/sink_ext.rs
  • vendor/monoio/src/io/splice.rs
  • vendor/monoio/src/io/stream/iter.rs
  • vendor/monoio/src/io/stream/mod.rs
  • vendor/monoio/src/io/stream/stream_ext.rs
  • vendor/monoio/src/io/util/buf_reader.rs
  • vendor/monoio/src/io/util/buf_writer.rs
  • vendor/monoio/src/io/util/cancel.rs
  • vendor/monoio/src/io/util/copy.rs
  • vendor/monoio/src/io/util/mod.rs
  • vendor/monoio/src/io/util/prefixed_io.rs
  • vendor/monoio/src/io/util/split.rs
  • vendor/monoio/src/lib.rs
  • vendor/monoio/src/macros/debug.rs
  • vendor/monoio/src/macros/join.rs
  • vendor/monoio/src/macros/mod.rs
  • vendor/monoio/src/macros/pin.rs
  • vendor/monoio/src/macros/ready.rs
  • vendor/monoio/src/macros/scoped_tls.rs
  • vendor/monoio/src/macros/select.rs
  • vendor/monoio/src/macros/support.rs
  • vendor/monoio/src/macros/try_join.rs
  • vendor/monoio/src/net/listener_config.rs
  • vendor/monoio/src/net/mod.rs
  • vendor/monoio/src/net/tcp/listener.rs
  • vendor/monoio/src/net/tcp/mod.rs
  • vendor/monoio/src/net/tcp/split.rs
  • vendor/monoio/src/net/tcp/stream.rs
  • vendor/monoio/src/net/tcp/stream_poll.rs
  • vendor/monoio/src/net/tcp/tfo/linux.rs
  • vendor/monoio/src/net/tcp/tfo/macos.rs
  • vendor/monoio/src/net/tcp/tfo/mod.rs
  • vendor/monoio/src/net/udp.rs
  • vendor/monoio/src/net/unix/datagram/mod.rs
  • vendor/monoio/src/net/unix/listener.rs
  • vendor/monoio/src/net/unix/mod.rs
  • vendor/monoio/src/net/unix/pipe.rs
  • vendor/monoio/src/net/unix/seq_packet/listener.rs
  • vendor/monoio/src/net/unix/seq_packet/mod.rs
  • vendor/monoio/src/net/unix/socket_addr.rs
  • vendor/monoio/src/net/unix/split.rs
  • vendor/monoio/src/net/unix/stream.rs
  • vendor/monoio/src/net/unix/stream_poll.rs
  • vendor/monoio/src/net/unix/ucred.rs
  • vendor/monoio/src/runtime.rs
  • vendor/monoio/src/scheduler.rs
  • vendor/monoio/src/task/core.rs
  • vendor/monoio/src/task/harness.rs
  • vendor/monoio/src/task/join.rs
  • vendor/monoio/src/task/mod.rs
  • vendor/monoio/src/task/raw.rs
  • vendor/monoio/src/task/state.rs
  • vendor/monoio/src/task/utils.rs
  • vendor/monoio/src/task/waker.rs
  • vendor/monoio/src/task/waker_fn.rs
  • vendor/monoio/src/time/clock.rs
  • vendor/monoio/src/time/driver/entry.rs
  • vendor/monoio/src/time/driver/handle.rs
  • vendor/monoio/src/time/driver/mod.rs
  • vendor/monoio/src/time/driver/sleep.rs
  • vendor/monoio/src/time/driver/wheel/level.rs
  • vendor/monoio/src/time/driver/wheel/mod.rs
  • vendor/monoio/src/time/driver/wheel/stack.rs
  • vendor/monoio/src/time/error.rs
  • vendor/monoio/src/time/instant.rs
  • vendor/monoio/src/time/interval.rs
  • vendor/monoio/src/time/mod.rs
  • vendor/monoio/src/time/timeout.rs
  • vendor/monoio/src/utils/bind_to_cpu_set.rs
  • vendor/monoio/src/utils/box_into_inner.rs
  • vendor/monoio/src/utils/ctrlc.rs
  • vendor/monoio/src/utils/linked_list.rs
  • vendor/monoio/src/utils/mod.rs
  • vendor/monoio/src/utils/rand.rs
  • vendor/monoio/src/utils/slab.rs
  • vendor/monoio/src/utils/thread_id.rs
  • vendor/monoio/src/utils/uring_detect.rs
  • vendor/monoio/tests/buf_writter.rs
  • vendor/monoio/tests/ctrlc_legacy.rs
  • vendor/monoio/tests/ctrlc_uring.rs
  • vendor/monoio/tests/fs_create_dir.rs
  • vendor/monoio/tests/fs_file.rs
  • vendor/monoio/tests/fs_metadata.rs
  • vendor/monoio/tests/fs_rename.rs
  • vendor/monoio/tests/fs_unlink.rs
  • vendor/monoio/tests/tcp_accept.rs
  • vendor/monoio/tests/tcp_connect.rs
  • vendor/monoio/tests/tcp_echo.rs
  • vendor/monoio/tests/tcp_into_split.rs
  • vendor/monoio/tests/tcp_split.rs
  • vendor/monoio/tests/udp.rs
  • vendor/monoio/tests/uds_cred.rs
  • vendor/monoio/tests/uds_split.rs
  • vendor/monoio/tests/uds_stream.rs
  • vendor/monoio/tests/unix_datagram.rs
  • vendor/monoio/tests/unix_seqpacket.rs
  • vendor/monoio/tests/zero_copy.rs

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/v3-3-p1-hotpath

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Release 0.5.0: KV correctness, p=1 + multi-shard throughput, Arc response-slot UAF fix

🐞 Bug fix ✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Fix Redis-parity KV correctness: wrong-type safety, TTL semantics, overflow guards, atomic MSETNX.
• Boost p=1 and multi-shard throughput via busy-poll I/O, reply convoy fixes, and slot-based
 replies.
• Close cross-shard reply-slot panic-unwind UAF by Arc-owning response slots; add regressions and
 release docs.
Diagram

graph TD
  C((Client)) --> H["Conn handler"] --> CO["Multi-key coordinator"] --> D["Cross-shard dispatch"] --> S["Shard SPSC loop"] --> R["ResponseSlot (Arc)"] --> H
  H --> A[("AOF/WAL append")]
  H --> M["Monoio legacy driver"]
  subgraph Legend
    direction LR
    _c(("Actor")) ~~~ _s["Component"] ~~~ _db[("Storage")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep oneshot-based cross-shard replies (no ResponseSlotPool on monoio)
  • ➕ Simpler ownership model with fewer shared structures
  • ➕ Avoids Arc refcount ops per batch
  • ➖ Per-batch allocation/traffic overhead in the hot path
  • ➖ Does not address reply-spin convoy mechanics as cleanly
  • ➖ Still needs careful drop-safety under panic/shutdown
2. Enforce panic=abort to avoid unwind-time UAF windows
  • ➕ Eliminates unwind-related lifetime hazards globally
  • ➖ Not compatible with existing reliance on unwinding (e.g., mlua/catch_unwind recovery)
  • ➖ Turns recoverable faults into process termination; operationally riskier
3. Avoid vendoring monoio: upstream patch or maintain as a small fork crate
  • ➕ Reduces supply-chain review surface inside this repo
  • ➕ Makes future upgrades less painful if patch lands upstream
  • ➖ Upstreaming can be slow/uncertain; blocks measured p=1 win
  • ➖ Still requires auditing the forked artifact somewhere; CI guardrails needed either way

Recommendation: The chosen approach is the right trade-off for this release: (1) Arc-owning ResponseSlots structurally closes the panic-unwind UAF without relying on runtime policy changes, and it removes unsafe rather than adding it; (2) unifying monoio/tokio on ResponseSlotPool directly targets the multi-shard throughput regression points; (3) vendoring monoio is justified by the measured p=1 win and the patch is bounded and gated. Follow-up worth prioritizing: add CI to diff vendor/monoio against upstream and keep the patch surface minimal.

Files changed (54) +3955 / -413

Enhancement (17) +705 / -295
metrics_setup.rsAdd metric for skipped cross-shard notifies +19/-1

Add metric for skipped cross-shard notifies

• Introduces SPSC notify skipped counter for busy-poll skip-wake path and adjusts a test assertion to avoid equality edge cases.

src/admin/metrics_setup.rs

client_registry.rsRemove Instant::now() from hot-path touch via caller-supplied epoch ms +31/-4

Remove Instant::now() from hot-path touch via caller-supplied epoch ms

• Adds epoch-ms baseline at registration and makes touch() take cached-clock time to avoid per-batch time reads; adds regression tests.

src/client_registry.rs

connection.rsWire coordinator persistence context into connection path +2/-0

Wire coordinator persistence context into connection path

• Threads additional context needed for coordinator durability and runtime behavior (no functional detail shown in snippet).

src/command/connection.rs

metadata.rsUpdate command metadata for new command surface +1/-0

Update command metadata for new command surface

• Extends metadata to include newly supported command(s) for documentation/introspection.

src/command/metadata.rs

mod.rsDispatch MSETNX in command router +4/-1

Dispatch MSETNX in command router

• Adds MSETNX to the main dispatch fast path under the 'm' command bucket.

src/command/mod.rs

main.rsApply I/O driver selection and publish eviction hints at startup +26/-0

Apply I/O driver selection and publish eviction hints at startup

• Forces legacy driver when requested (or when busy-poll is enabled), configures busy-poll budget, and publishes lock-free maxmemory hints after shard count resolution.

src/main.rs

channel.rsAdd Notify skip-wake handshake for busy-poll mode +84/-1

Add Notify skip-wake handshake for busy-poll mode

• Extends Notify with a skip-wake flag and global enable switch, adds local-notify bypass, and adds a gating test to prevent lost wakeups under spin probe mode.

src/runtime/channel.rs

mod.rsAdd process-wide legacy-driver forcing and busy-poll config detection +38/-0

Add process-wide legacy-driver forcing and busy-poll config detection

• Adds an atomic switch to force monoio legacy driver (without env mutation) and a helper to detect busy-poll configuration from flag/env.

src/runtime/mod.rs

monoio_impl.rsHonor legacy-driver forcing and add tuned io_uring builder path +83/-0

Honor legacy-driver forcing and add tuned io_uring builder path

• Makes monoio runtime respect the legacy-driver kill switch and optionally configures tuned io_uring flags / SQPOLL gates, with safe fallback to stock FusionDriver.

src/runtime/monoio_impl.rs

blocking.rsReduce inline GET copies and skip eviction lock when provably unnecessary +51/-37

Reduce inline GET copies and skip eviction lock when provably unnecessary

• Refactors inline GET to frame response directly from borrowed bytes (avoiding intermediate Vec copy) and adds a lock-free eviction pre-gate for inline SET; reduces Bytes cloning in inline SET.

src/server/conn/blocking.rs

dispatch.rsAdjust monoio dispatch wiring for slotted replies +4/-0

Adjust monoio dispatch wiring for slotted replies

• Updates monoio handler dispatch support to align with ResponseSlot-based reply batching (no detailed diff shown here).

src/server/conn/handler_monoio/dispatch.rs

mod.rsUnify monoio cross-shard reply path on ResponseSlotPool and fix spin convoy gate +89/-161

Unify monoio cross-shard reply path on ResponseSlotPool and fix spin convoy gate

• Switches Phase 2b batching to PipelineBatchSlotted using ResponseSlotPool instead of per-batch oneshots; registers per-connection shard guard for solo-spin gating; removes replica lock cost via is_replica mirror.

src/server/conn/handler_monoio/mod.rs

dispatch.rsAlign tokio sharded dispatch with updated slot-based reply path +2/-0

Align tokio sharded dispatch with updated slot-based reply path

• Small dispatch updates to support ResponseSlot Arc ownership and related invariants.

src/server/conn/handler_sharded/dispatch.rs

shared.rsShared connection-path tweaks for new runtime/dispatch behavior +3/-0

Shared connection-path tweaks for new runtime/dispatch behavior

• Minor shared connection logic updates in support of the new throughput and correctness plumbing.

src/server/conn/shared.rs

event_loop.rsRegister busy-poll skip-notify hooks for monoio legacy driver +43/-2

Register busy-poll skip-notify hooks for monoio legacy driver

• Wraps SPSC consumers for monoio spin-probe access and registers driver hooks that advertise spinning and locally re-arm notifies when ring buffers have pending work.

src/shard/event_loop.rs

spsc_handler.rsSkip WAL serialization when fanout is a no-op and remove slot unsafe derefs +114/-88

Skip WAL serialization when fanout is a no-op and remove slot unsafe derefs

• Avoids command serialization allocation when persistence/replication are disabled; updates slotted reply handling to Arc-deref safely (no raw pointer unsafe blocks).

src/shard/spsc_handler.rs

eviction.rsAdd lock-free maxmemory hints and inline write eviction pre-gate +111/-0

Add lock-free maxmemory hints and inline write eviction pre-gate

• Adds atomic mirrors for maxmemory and per-shard budget, a publish helper, and a deterministic test for the skip-eviction decision core.

src/storage/eviction.rs

Bug fix (11) +770 / -94
config.rsPublish maxmemory hints on CONFIG SET maxmemory +5/-1

Publish maxmemory hints on CONFIG SET maxmemory

• Keeps lock-free eviction hint mirrors in sync when runtime maxmemory is changed via CONFIG SET.

src/command/config.rs

helpers.rsAdd expiry_ms_in_range helper for safe TTL storage +13/-0

Add expiry_ms_in_range helper for safe TTL storage

• Adds a shared helper enforcing i64-domain expiry bounds so TTL/introspection commands cannot wrap negative on live keys.

src/command/helpers.rs

key.rsFix EXPIRE/PEXPIRE/EXPIREAT overflow and past-time delete semantics +222/-14

Fix EXPIRE/PEXPIRE/EXPIREAT overflow and past-time delete semantics

• Implements Redis-parity non-positive TTL delete behavior and adds checked arithmetic + i64-range guards; adds extensive regression tests.

src/command/key.rs

string_read.rsFix GETDEL wrong-type data loss via check-before-mutate +16/-3

Fix GETDEL wrong-type data loss via check-before-mutate

• Ensures GETDEL checks key type before removal, returning WRONGTYPE without deleting wrong-type values; adjusts removal path accordingly.

src/command/string/string_read.rs

string_write.rsAdd MSETNX, fix wrong-type SET..GET/GETSET, and harden expiry arithmetic +127/-11

Add MSETNX, fix wrong-type SET..GET/GETSET, and harden expiry arithmetic

• Implements single-shard atomic MSETNX, prevents wrong-type keys from being overwritten/deleted by SET..GET and GETSET, and adds checked expiry calculations with range validation.

src/command/string/string_write.rs

mod.rsAdd solo-conn spin guard and wire coordinator persistence context +12/-5

Add solo-conn spin guard and wire coordinator persistence context

• Registers per-connection shard guard to prevent reply-spin convoys, threads AOF/repl context into coordinator calls, and updates reply-slot usage to Arc clones.

src/server/conn/handler_sharded/mod.rs

response_slot.rsMake response slots Arc-owned and add drop-safety regression tests +65/-43

Make response slots Arc-owned and add drop-safety regression tests

• Changes ResponseSlotPool to store Arc<ResponseSlot> and makes futures own Arc handles; removes raw-pointer lifetime coupling and adds a strong_count-based UAF regression test.

src/server/response_slot.rs

coordinator.rsAdd MSETNX coordination and persist coordinator local-leg writes +201/-1

Add MSETNX coordination and persist coordinator local-leg writes

• Adds coordinator support for MSETNX (reject cross-shard with CROSSSLOT) and persists coordinator local legs for MSET/MSETNX to the owning shard’s AOF, matching single-key durability semantics.

src/shard/coordinator.rs

dispatch.rsReplace raw ResponseSlotPtr with Arc-owned handle +20/-14

Replace raw ResponseSlotPtr with Arc-owned handle

• Makes ResponseSlotPtr carry Arc<ResponseSlot> (auto-Send) and updates debug representation; eliminates localized unsafe Send and closes panic-unwind UAF windows for cross-shard replies.

src/shard/dispatch.rs

slice.rsAdd shard-connection guard and configurable reply-spin budgets +88/-1

Add shard-connection guard and configurable reply-spin budgets

• Introduces per-thread live-connection tracking for solo-spin gating, env-configurable spin budget/gate values, and RAII guard used by both runtimes’ handlers.

src/shard/slice.rs

db.rsMinor DB behavior fix for correctness parity +1/-1

Minor DB behavior fix for correctness parity

• Small adjustment in DB behavior (single-line change) supporting KV correctness expectations.

src/storage/db.rs

Tests (9) +1109 / -2
mod.rsAdd unit tests for MSETNX, overflow guards, and wrong-type safety +166/-0

Add unit tests for MSETNX, overflow guards, and wrong-type safety

• Adds test coverage for MSETNX all-or-nothing behavior, DECRBY overflow, expiry overflow rejection, and wrong-type preservation regressions.

src/command/string/mod.rs

replay.rsAdd replay tests for EXPIRE<=0 delete semantics +50/-0

Add replay tests for EXPIRE<=0 delete semantics

• Adds replay regression tests to ensure WAL replay of non-positive EXPIRE deletes keys, preventing master/replica divergence.

src/persistence/replay.rs

tests.rsAdd connection-level regression tests +58/-0

Add connection-level regression tests

• Adds tests covering updated connection handler behavior and cross-shard fastpath invariants.

src/server/conn/tests.rs

coordinator_local_leg_durability.rsAdd crash/restart integration test for coordinator local-leg AOF durability +482/-0

Add crash/restart integration test for coordinator local-leg AOF durability

• New end-to-end harness validates that multi-key coordinator local legs (MSET/MSETNX) are durably logged and survive SIGKILL + restart across shards.

tests/coordinator_local_leg_durability.rs

mq_integration.rsExtend MQ integration coverage +2/-0

Extend MQ integration coverage

• Small integration test adjustments to maintain parity with new runtime/dispatch behavior.

tests/mq_integration.rs

msetnx_cross_shard_reject.rsAdd integration tests for MSETNX cross-shard rejection and co-located success +311/-0

Add integration tests for MSETNX cross-shard rejection and co-located success

• New live-server RESP harness asserts cross-shard MSETNX is rejected with CROSSSLOT and co-located (hash-tag) MSETNX succeeds atomically.

tests/msetnx_cross_shard_reject.rs

txn_kv_wiring.rsUpdate transaction KV wiring test coverage +2/-0

Update transaction KV wiring test coverage

• Minor test updates ensuring transaction/KV wiring remains correct with new command surface.

tests/txn_kv_wiring.rs

workspace_integration.rsUpdate workspace integration tests for new release behavior +4/-0

Update workspace integration tests for new release behavior

• Small integration updates to keep workspace behavior covered under the updated handlers/runtime settings.

tests/workspace_integration.rs

xshard_fastpath_api.rsTighten cross-shard fastpath API tests for new reply-slot semantics +34/-2

Tighten cross-shard fastpath API tests for new reply-slot semantics

• Adjusts fastpath tests to align with the updated cross-shard reply mechanics and safety invariants.

tests/xshard_fastpath_api.rs

Documentation (8) +538 / -13
MILESTONE.mdUpdate v3-3 milestone scope with KV hot-path review findings +15/-6

Update v3-3 milestone scope with KV hot-path review findings

• Adjusts milestone scope to reflect KV deep-review action items (inline GET copy avoidance, lock discipline, pending_wakers clarification).

.add/milestones/v3-3-vector-kv-polish/MILESTONE.md

MILESTONE.mdAdd v3-4 KV correctness milestone spec +173/-0

Add v3-4 KV correctness milestone spec

• Introduces a detailed milestone document describing correctness fixes, parity goals, and verification items for KV behavior and durability.

.add/milestones/v3-4-kv-correctness/MILESTONE.md

BENCHMARK.mdDocument new p=1 and multi-shard throughput results and methodology notes +100/-1

Document new p=1 and multi-shard throughput results and methodology notes

• Adds new benchmark sections for busy-poll p=1 wins and multi-connection multi-shard wins; clarifies prior shard-count interpretations and updates guidance.

BENCHMARK.md

CHANGELOG.mdAdd 0.5.0 release notes +95/-0

Add 0.5.0 release notes

• Introduces changelog entries covering KV correctness, throughput work, and safety fixes included in v0.5.0.

CHANGELOG.md

CLAUDE.mdCorrect pending_wakers lock rule and reply-wake description +5/-2

Correct pending_wakers lock rule and reply-wake description

• Updates developer notes to match the current cross-shard reply mechanism and event-loop sweep responsibilities.

CLAUDE.md

configuration.mdDocument new I/O driver and busy-poll tuning flags +10/-3

Document new I/O driver and busy-poll tuning flags

• Updates configuration docs to include the new --io-driver and --io-busy-poll-us options and their intended usage.

docs/configuration.md

configuration.mdFix configuration guide link/wording +1/-1

Fix configuration guide link/wording

• Minor guide correction to keep configuration docs consistent with new options.

docs/guides/configuration.md

tuning.mdAdd workload-based tuning guide (shards, busy-poll, durability) +139/-0

Add workload-based tuning guide (shards, busy-poll, durability)

• New tuning guide providing recipes and measured guidance for shard count, busy-polling, persistence trade-offs, and platform notes.

docs/guides/tuning.md

Other (9) +833 / -9
state.jsonUpdate release state tracking metadata +10/-2

Update release state tracking metadata

• Refreshes repository state metadata used for release/milestone tracking.

.add/state.json

Cargo.tomlPatch monoio to vendored fork and add io-uring 0.6 compat dep +16/-0

Patch monoio to vendored fork and add io-uring 0.6 compat dep

• Routes monoio to vendor/monoio via [patch.crates-io] and adds an io-uring 0.6 alias for monoio builder compatibility; documents the bounded patch surface.

Cargo.toml

mkdocs.ymlExpose tuning guide in documentation nav +1/-0

Expose tuning guide in documentation nav

• Adds the new tuning guide page to MkDocs configuration.

mkdocs.yml

bench-compare.shUpdate bench compare script for new measurement workflow +4/-1

Update bench compare script for new measurement workflow

• Tweaks benchmarking helper script behavior/args to support updated comparisons used in the report.

scripts/bench-compare.sh

bench-resources.shAdjust benchmark resource script +4/-1

Adjust benchmark resource script

• Minor updates to benchmark resource setup/collection used by perf runs.

scripts/bench-resources.sh

gcloud-kv-p1-baseline.shAdd GCE p=1 baseline automation script +742/-0

Add GCE p=1 baseline automation script

• Introduces a large automation script for reproducible GCE KV p=1 baselines, including environment and run orchestration.

scripts/gcloud-kv-p1-baseline.sh

test-commands.shExtend command test harness +4/-0

Extend command test harness

• Adds additional command test invocations to cover new/updated correctness behaviors.

scripts/test-commands.sh

test-consistency.shAdd consistency test harness updates +7/-0

Add consistency test harness updates

• Extends consistency test script to include new regression scenarios.

scripts/test-consistency.sh

config.rsAdd --io-driver and --io-busy-poll-us flags; update shard guidance +45/-5

Add --io-driver and --io-busy-poll-us flags; update shard guidance

• Introduces monoio I/O driver selection and busy-poll configuration flags and expands shard-count guidance; adds clap parsing tests.

src/config.rs

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (3) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 37 rules

Grey Divider


Action required

1. Monoio cross-thread AtomicWaker wake 📘 Rule violation ≡ Correctness
Description
The monoio handler now awaits cross-shard replies via ResponseSlotPool and explicitly relies on
cross-thread waking via the slot’s AtomicWaker. This violates the requirement to relay pending
wakers on the runtime thread and use flume::bounded(1) for cross-thread signalling, and may break
on monoio if waking from non-runtime threads is unsupported.
Code

src/server/conn/handler_monoio/mod.rs[R1717-1720]

+            // L3b: await each response slot directly (tokio parity —
+            // handler_sharded/mod.rs). Cross-thread wakes reach this task via
+            // the slot's AtomicWaker + monoio's `sync`-feature waker channel
+            // (proven by tests/spsc_wake_floor_red.rs::swf0 on both drivers).
Relevance

⭐⭐ Medium

No historical evidence about monoio cross-thread AtomicWaker prohibition; similar monoio/shard wake
work exists but no explicit rule enforcement.

PR-#172
PR-#18

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The monoio connection handler comments and logic show it is relying on cross-thread wakes reaching
the task via the slot’s AtomicWaker. The ResponseSlot::fill() implementation performs
self.waker.wake() from the producer (target shard) thread, which is exactly the cross-thread waker
wake pattern this rule forbids for monoio.

Rule 302079: Use Rc-based pending waker relay and flume::bounded(1) for cross-thread signalling with monoio::spawn
src/server/conn/handler_monoio/mod.rs[1717-1720]
src/server/response_slot.rs[58-84]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ResponseSlot` wakes a task via `AtomicWaker` from a different shard thread, and the monoio handler now depends on this cross-thread wake behavior.

## Issue Context
Compliance requires that monoio wakers are not woken directly from non-runtime threads; cross-thread notifications must go through `flume::bounded(1)` and a runtime-thread waker relay.

## Fix Focus Areas
- src/server/conn/handler_monoio/mod.rs[1635-1765]
- src/server/response_slot.rs[58-125]
- src/runtime/channel.rs[169-244]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. moon_binary() ignores MOON_BIN 📘 Rule violation ▣ Testability
Description
New integration tests spawn the server using env!("CARGO_BIN_EXE_moon") instead of requiring
MOON_BIN to be set. This violates the requirement that integration tests explicitly set and use
MOON_BIN rather than relying on default/fallback binary paths.
Code

tests/msetnx_cross_shard_reject.rs[R27-29]

+fn moon_binary() -> std::path::PathBuf {
+    std::path::PathBuf::from(env!("CARGO_BIN_EXE_moon"))
+}
Relevance

⭐⭐ Medium

No historical evidence found enforcing MOON_BIN over CARGO_BIN_EXE for integration tests in this
repo.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Both added integration tests define moon_binary() using env!("CARGO_BIN_EXE_moon") and then
spawn the server with Command::new(moon_binary()), with no MOON_BIN environment lookup or
enforcement.

Rule 992389: Integration tests must set MOON_BIN explicitly for server binaries
tests/msetnx_cross_shard_reject.rs[27-41]
tests/coordinator_local_leg_durability.rs[53-70]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Integration tests that spawn the server do not use `MOON_BIN` and instead rely on `CARGO_BIN_EXE_moon`.

## Issue Context
Compliance requires tests to explicitly use `MOON_BIN` (fail-fast if missing) to avoid silently running an unintended binary.

## Fix Focus Areas
- tests/msetnx_cross_shard_reject.rs[27-52]
- tests/coordinator_local_leg_durability.rs[53-85]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Backlog not updated offline 🐞 Bug ≡ Correctness
Description
wal_fanout_has_work() ignores whether a replication backlog is allocated, so
wal_append_and_fanout() returns early whenever WAL/AOF are off and replica_txs is empty. Because
UnregisterReplica removes replica_txs entries without clearing the backlog, writes during
replica-disconnected windows stop appending to the backlog and can force later PSYNC attempts to
fall back to full resync (missing offsets).
Code

src/shard/spsc_handler.rs[R2429-2436]

+pub(crate) fn wal_fanout_has_work(
+    wal_writer: &Option<WalWriter>,
+    wal_v3_writer: &Option<WalWriterV3>,
+    replica_txs: &[(u64, channel::MpscSender<bytes::Bytes>)],
+    aof_pool: Option<&std::sync::Arc<crate::persistence::aof::AofWriterPool>>,
+) -> bool {
+    wal_writer.is_some() || wal_v3_writer.is_some() || !replica_txs.is_empty() || aof_pool.is_some()
+}
Relevance

⭐⭐ Medium

No closely matching historical review on replication backlog append gating; replication hardening is
valued but acceptance evidence is indirect.

PR-#65

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new predicate and early-return are in the write-path, but they only look at WAL writers, AOF
pool, and live replica sender channels. The disconnect path leaves the backlog allocated but empties
replica_txs, so the early return prevents backlog.append() and replication offset advancement,
which directly undermines the partial-resync condition that requires offsets to be present in the
backlogs.

src/shard/spsc_handler.rs[2429-2453]
src/shard/spsc_handler.rs[1789-1803]
src/replication/handshake.rs[47-75]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`wal_fanout_has_work()` is used to skip WAL/backlog/offset work when it would be a no-op, but the predicate does not account for an *already-allocated* replication backlog (or offset handle). After the last replica disconnects, `replica_txs` becomes empty while the backlog remains allocated, and the early return stops backlog appends and offset advancement, breaking partial resync coverage.

## Issue Context
- `ShardMessage::RegisterReplica` allocates the backlog if needed.
- `ShardMessage::UnregisterReplica` only removes the sender from `replica_txs`; it does not clear the backlog.
- PSYNC partial resync requires the requested offset to be present in all per-shard backlogs.

## Fix Focus Areas
- src/shard/spsc_handler.rs[1789-1803]
- src/shard/spsc_handler.rs[2429-2453]
- src/replication/handshake.rs[47-75]

### Concrete fix direction
Update the predicate so backlog/offset maintenance continues when replication is active even if no replicas are currently connected. Options:
1) Extend `wal_fanout_has_work(...)` to accept an additional boolean/handle:
  - include `repl_state.is_some()` / `repl_offsets.is_some()` as work, OR
  - include a cheap mirror flag like `replication_backlog_allocated: AtomicBool` in `ReplicationState` set by `ensure_backlogs_allocated` / `RegisterReplica`.
2) If you must key off the backlog itself, add a non-blocking/cheap signal rather than taking the backlog mutex on every write.

Add a regression test:
- Register a replica (allocate backlog),
- Unregister it (replica_txs empty but backlog still allocated),
- Perform writes with WAL/AOF off,
- Assert backlog grows / contains offsets needed for `evaluate_psync` to allow partial resync.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. coordinate_msetnx allow lacks justification 📘 Rule violation ✧ Quality
Description
A new #[allow(clippy::too_many_arguments)] was added without a justification comment. This
violates the requirement that new lint suppressions be narrowly scoped and justified rather than
used to silence warnings.
Code

src/shard/coordinator.rs[R926-927]

+#[allow(clippy::too_many_arguments)]
+async fn coordinate_msetnx(
Relevance

⭐⭐⭐ High

Repo enforces justified lint suppressions; prior accepted change required justification for new
#[allow] (PR63).

PR-#63

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The diff introduces a new #[allow(clippy::too_many_arguments)] directly on coordinate_msetnx
with no adjacent justification comment, which is explicitly called out as a compliance concern under
clippy-clean requirements.

Rule 209935: Rust code must be clippy-clean under all supported feature profiles
src/shard/coordinator.rs[918-939]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new clippy suppression was introduced without justification.

## Issue Context
The compliance rule requires new `#[allow(clippy::...)]` uses to be narrowly scoped and justified in-code (or avoided by refactoring).

## Fix Focus Areas
- src/shard/coordinator.rs[918-939]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Skip-wake ordering unsound 🐞 Bug ☼ Reliability
Description
Notify::notify_one() uses a SeqCst fence but then reads skip_wake with Ordering::Relaxed,
which does not participate in the intended synchronization with set_skip_wake(false). Under
--io-busy-poll-us this can (depending on architecture/interleaving) incorrectly elide the only
wake and delay or stall SPSC draining until another unrelated wake occurs.
Code

src/runtime/channel.rs[R208-219]

    pub fn notify_one(&self) {
+        if notify_skip_wake_enabled() {
+            // Dekker handshake with set_skip_wake(false): the SeqCst fence
+            // orders the caller's ringbuf push before this flag load, so
+            // either the spinning target's final probe sees the push or this
+            // load sees the cleared flag — a wake is never lost (classic
+            // store-buffering prevention; both sides fence).
+            std::sync::atomic::fence(std::sync::atomic::Ordering::SeqCst);
+            if self.skip_wake.load(std::sync::atomic::Ordering::Relaxed) {
+                crate::admin::metrics_setup::bump_spsc_notify_skipped();
+                return;
+            }
Relevance

⭐⭐ Medium

No prior reviews on atomic fence+relaxed handshake; wake-path work in PR172 suggests concern but
unclear enforcement.

PR-#172

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new skip-wake fast path explicitly fences but then performs a relaxed read to decide whether to
drop the wake token, and set_skip_wake is driven by monoio busy-poll hooks during shard spinning.
Because the correctness argument is a cross-thread handshake, the ordering on the atomic load
matters for preventing stale reads and missed wakeups.

src/runtime/channel.rs[167-240]
src/shard/event_loop.rs[769-808]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`Notify::notify_one()` uses a `Relaxed` load of `skip_wake` while relying on a Dekker-style handshake described in comments. A `Relaxed` load does not provide the intended synchronization with `set_skip_wake(false)` (which uses `SeqCst` stores/fences), so the sender-side fast path may observe a stale `true` and skip the wake even after the target retracts `skip_wake`, risking a missed wake.

## Issue Context
This logic is exercised only when the process-global skip-wake fast path is enabled and shard busy-poll hooks are registered (monoio legacy driver busy-poll). In that mode, cross-thread notification is intentionally elided, so correctness of the handshake is critical to avoid lost wakeups.

## Fix Focus Areas
- src/runtime/channel.rs[208-240]
- src/shard/event_loop.rs[783-803]

### Concrete fix direction
- Change `self.skip_wake.load(Ordering::Relaxed)` to `Ordering::SeqCst` (simplest) or `Ordering::Acquire`.
- Re-evaluate whether the extra `fence(SeqCst)` is still needed once the load is strengthened; if keeping the fence, ensure the design is correct under Rust/C++ memory model (avoid relying on fences with relaxed operations for synchronization).
- Add a stress test (loom or a targeted concurrency test if available in-repo) that toggles `set_skip_wake(false)` concurrently with `notify_one()` and asserts the receiver is eventually notified.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

6. Unbounded cross-shard await 🐞 Bug ☼ Reliability
Description
In handle_connection_sharded_monoio, Phase 2b now does response_pool.future_for(target).await
with no timeout or shutdown-aware escape path. If a target shard stops draining/filling slots (e.g.,
shard event-loop exits on shutdown without draining pending SPSC messages), the connection task can
hang indefinitely waiting for the slot fill.
Code

src/server/conn/handler_monoio/mod.rs[R1749-1763]

+                let shard_responses = {
+                    let mut spun = None;
+                    if crate::shard::slice::xshard_should_spin(batch_remote_total) {
+                        for _ in 0..crate::shard::slice::xshard_spin_budget() {
+                            if let Some(r) = response_pool.slot_for(target).try_take() {
+                                spun = Some(r);
+                                break;
                            }
+                            core::hint::spin_loop();
                        }
                    }
-                };
-                let shard_responses = match shard_responses {
-                    Ok(r) => r,
-                    Err(err_msg) => {
-                        for (resp_idx, _, _) in &meta {
-                            responses[*resp_idx] =
-                                Frame::Error(Bytes::from_static(err_msg.as_bytes()));
-                        }
-                        continue;
+                    match spun {
+                        Some(r) => r,
+                        None => response_pool.future_for(target).await,
                    }
Relevance

⭐ Low

Team has explicitly deferred “shutdown-aware bound on reply await”; similar shutdown-liveness fixes
were scoped elsewhere (PR95).

PR-#95

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The handler now unconditionally awaits the response slot future when the reply is not already
present, and the shard event loop has a shutdown fast-exit that breaks before draining SPSC. Those
two facts combine into an in-repo, end-to-end hang scenario during shutdown/partial-shutdown.

src/server/conn/handler_monoio/mod.rs[1717-1763]
src/shard/event_loop.rs[1741-1784]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The monoio cross-shard reply wait is now an unbounded `.await` on a `ResponseSlotFuture`. This can hang a connection forever if the target shard never fills the slot (e.g., shutdown path breaks out before draining SPSC queues, a wedged shard, or an in-flight message stranded during partial shutdown).

## Issue Context
- The Arc-ownership fix prevents UAF, but does not address liveness.
- The shard event-loop checks `shutdown.is_cancelled()` and `break`s before draining SPSC consumers, which can strand in-flight `ShardMessage::*Slotted` replies.

## Fix Focus Areas
- src/server/conn/handler_monoio/mod.rs[1717-1763]
- src/shard/event_loop.rs[1741-1784]

### Concrete fix direction
Pick one (or combine):
1) **Bounded await**: race the slot future against a timer + shutdown check (similar to the previous chunked wait). On timeout/shutdown, fill response(s) with an error like `ERR cross-shard response aborted (shutdown)` or `ERR cross-shard response timeout (write may have applied)`.
2) **Drain-on-shutdown**: before breaking the shard loop on shutdown, drain SPSC queues (or at least fill any slotted messages with an explicit error) so origin handlers are woken.

Add a regression test that:
- Dispatches a slotted cross-shard message,
- Triggers shutdown before the target drains,
- Asserts the origin handler does not hang indefinitely and returns an error within a bounded time.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +1717 to +1720
// L3b: await each response slot directly (tokio parity —
// handler_sharded/mod.rs). Cross-thread wakes reach this task via
// the slot's AtomicWaker + monoio's `sync`-feature waker channel
// (proven by tests/spsc_wake_floor_red.rs::swf0 on both drivers).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Monoio cross-thread atomicwaker wake 📘 Rule violation ≡ Correctness

The monoio handler now awaits cross-shard replies via ResponseSlotPool and explicitly relies on
cross-thread waking via the slot’s AtomicWaker. This violates the requirement to relay pending
wakers on the runtime thread and use flume::bounded(1) for cross-thread signalling, and may break
on monoio if waking from non-runtime threads is unsupported.
Agent Prompt
## Issue description
`ResponseSlot` wakes a task via `AtomicWaker` from a different shard thread, and the monoio handler now depends on this cross-thread wake behavior.

## Issue Context
Compliance requires that monoio wakers are not woken directly from non-runtime threads; cross-thread notifications must go through `flume::bounded(1)` and a runtime-thread waker relay.

## Fix Focus Areas
- src/server/conn/handler_monoio/mod.rs[1635-1765]
- src/server/response_slot.rs[58-125]
- src/runtime/channel.rs[169-244]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/shard/coordinator.rs
Comment on lines +926 to +927
#[allow(clippy::too_many_arguments)]
async fn coordinate_msetnx(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. coordinate_msetnx allow lacks justification 📘 Rule violation ✧ Quality

A new #[allow(clippy::too_many_arguments)] was added without a justification comment. This
violates the requirement that new lint suppressions be narrowly scoped and justified rather than
used to silence warnings.
Agent Prompt
## Issue description
A new clippy suppression was introduced without justification.

## Issue Context
The compliance rule requires new `#[allow(clippy::...)]` uses to be narrowly scoped and justified in-code (or avoided by refactoring).

## Fix Focus Areas
- src/shard/coordinator.rs[918-939]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +27 to +29
fn moon_binary() -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_BIN_EXE_moon"))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. moon_binary() ignores moon_bin 📘 Rule violation ▣ Testability

New integration tests spawn the server using env!("CARGO_BIN_EXE_moon") instead of requiring
MOON_BIN to be set. This violates the requirement that integration tests explicitly set and use
MOON_BIN rather than relying on default/fallback binary paths.
Agent Prompt
## Issue description
Integration tests that spawn the server do not use `MOON_BIN` and instead rely on `CARGO_BIN_EXE_moon`.

## Issue Context
Compliance requires tests to explicitly use `MOON_BIN` (fail-fast if missing) to avoid silently running an unintended binary.

## Fix Focus Areas
- tests/msetnx_cross_shard_reject.rs[27-52]
- tests/coordinator_local_leg_durability.rs[53-85]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/runtime/channel.rs
Comment on lines 208 to +219
pub fn notify_one(&self) {
if notify_skip_wake_enabled() {
// Dekker handshake with set_skip_wake(false): the SeqCst fence
// orders the caller's ringbuf push before this flag load, so
// either the spinning target's final probe sees the push or this
// load sees the cleared flag — a wake is never lost (classic
// store-buffering prevention; both sides fence).
std::sync::atomic::fence(std::sync::atomic::Ordering::SeqCst);
if self.skip_wake.load(std::sync::atomic::Ordering::Relaxed) {
crate::admin::metrics_setup::bump_spsc_notify_skipped();
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

4. Skip-wake ordering unsound 🐞 Bug ☼ Reliability

Notify::notify_one() uses a SeqCst fence but then reads skip_wake with Ordering::Relaxed,
which does not participate in the intended synchronization with set_skip_wake(false). Under
--io-busy-poll-us this can (depending on architecture/interleaving) incorrectly elide the only
wake and delay or stall SPSC draining until another unrelated wake occurs.
Agent Prompt
## Issue description
`Notify::notify_one()` uses a `Relaxed` load of `skip_wake` while relying on a Dekker-style handshake described in comments. A `Relaxed` load does not provide the intended synchronization with `set_skip_wake(false)` (which uses `SeqCst` stores/fences), so the sender-side fast path may observe a stale `true` and skip the wake even after the target retracts `skip_wake`, risking a missed wake.

## Issue Context
This logic is exercised only when the process-global skip-wake fast path is enabled and shard busy-poll hooks are registered (monoio legacy driver busy-poll). In that mode, cross-thread notification is intentionally elided, so correctness of the handshake is critical to avoid lost wakeups.

## Fix Focus Areas
- src/runtime/channel.rs[208-240]
- src/shard/event_loop.rs[783-803]

### Concrete fix direction
- Change `self.skip_wake.load(Ordering::Relaxed)` to `Ordering::SeqCst` (simplest) or `Ordering::Acquire`.
- Re-evaluate whether the extra `fence(SeqCst)` is still needed once the load is strengthened; if keeping the fence, ensure the design is correct under Rust/C++ memory model (avoid relying on fences with relaxed operations for synchronization).
- Add a stress test (loom or a targeted concurrency test if available in-repo) that toggles `set_skip_wake(false)` concurrently with `notify_one()` and asserts the receiver is eventually notified.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/shard/spsc_handler.rs
Comment on lines +2429 to +2436
pub(crate) fn wal_fanout_has_work(
wal_writer: &Option<WalWriter>,
wal_v3_writer: &Option<WalWriterV3>,
replica_txs: &[(u64, channel::MpscSender<bytes::Bytes>)],
aof_pool: Option<&std::sync::Arc<crate::persistence::aof::AofWriterPool>>,
) -> bool {
wal_writer.is_some() || wal_v3_writer.is_some() || !replica_txs.is_empty() || aof_pool.is_some()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

6. Backlog not updated offline 🐞 Bug ≡ Correctness

wal_fanout_has_work() ignores whether a replication backlog is allocated, so
wal_append_and_fanout() returns early whenever WAL/AOF are off and replica_txs is empty. Because
UnregisterReplica removes replica_txs entries without clearing the backlog, writes during
replica-disconnected windows stop appending to the backlog and can force later PSYNC attempts to
fall back to full resync (missing offsets).
Agent Prompt
## Issue description
`wal_fanout_has_work()` is used to skip WAL/backlog/offset work when it would be a no-op, but the predicate does not account for an *already-allocated* replication backlog (or offset handle). After the last replica disconnects, `replica_txs` becomes empty while the backlog remains allocated, and the early return stops backlog appends and offset advancement, breaking partial resync coverage.

## Issue Context
- `ShardMessage::RegisterReplica` allocates the backlog if needed.
- `ShardMessage::UnregisterReplica` only removes the sender from `replica_txs`; it does not clear the backlog.
- PSYNC partial resync requires the requested offset to be present in all per-shard backlogs.

## Fix Focus Areas
- src/shard/spsc_handler.rs[1789-1803]
- src/shard/spsc_handler.rs[2429-2453]
- src/replication/handshake.rs[47-75]

### Concrete fix direction
Update the predicate so backlog/offset maintenance continues when replication is active even if no replicas are currently connected. Options:
1) Extend `wal_fanout_has_work(...)` to accept an additional boolean/handle:
   - include `repl_state.is_some()` / `repl_offsets.is_some()` as work, OR
   - include a cheap mirror flag like `replication_backlog_allocated: AtomicBool` in `ReplicationState` set by `ensure_backlogs_allocated` / `RegisterReplica`.
2) If you must key off the backlog itself, add a non-blocking/cheap signal rather than taking the backlog mutex on every write.

Add a regression test:
- Register a replica (allocate backlog),
- Unregister it (replica_txs empty but backlog still allocated),
- Perform writes with WAL/AOF off,
- Assert backlog grows / contains offsets needed for `evaluate_psync` to allow partial resync.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@pilotspacex-byte pilotspacex-byte merged commit 7c5bfe4 into main Jul 4, 2026
17 checks passed
TinDang97 pushed a commit that referenced this pull request Jul 4, 2026
Version 0.4.1 -> 0.5.0 (Cargo.toml + Cargo.lock lockstep).

Bundles everything since v0.4.1 (merged in #207):
- v3-4 KV write correctness & data-integrity parity (supersedes #206).
- p=1 & multi-shard throughput: --io-busy-poll-us poll-mode park (beats
  Redis at p=1 on both GCE arches), C2 reply-spin convoy fix (multi-shard
  wins from c8 up), L3b ResponseSlotPool, corrected default docs + tuning
  guide.
- Vendored monoio 0.2.4 fork (bounded moon-patch, verified vs upstream).
- Memory-safety: Arc-owned ResponseSlotPtr closes a panic-unwind
  cross-shard reply UAF (found in review; also fixes the pre-existing
  tokio path; net -4 unsafe blocks).

Reviews: security supply-chain CLEAN; rust drop-safety finding FIXED
in-PR. Full CI green both runtimes; GCloud A/B confirmed the multi-shard
win held. shardslice cross-shard-read waiver rides (validated, retirable).

- CHANGELOG.md: reconcile [Unreleased] -> [0.5.0]
- RELEASES.md: ledger row (waiver disclosed)

Engine records the cut; tag + publish follow.

author: Tin Dang
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.

2 participants