Skip to content

perf(shard): lock-free cross-shard read recovery — idle+batch-gated reply spin + dead-flag cleanup#177

Merged
pilotspacex-byte merged 16 commits into
mainfrom
feat/xshard-read-fastpath
Jun 15, 2026
Merged

perf(shard): lock-free cross-shard read recovery — idle+batch-gated reply spin + dead-flag cleanup#177
pilotspacex-byte merged 16 commits into
mainfrom
feat/xshard-read-fastpath

Conversation

@pilotspacex-byte

@pilotspacex-byte pilotspacex-byte commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Recovers the cross-shard single-key read latency that shardslice-migration (#175) gave up, without re-introducing a cross-thread lock or growing per-key memory — discharging the cross-shard-read half of that PR's RISK-ACCEPTED waiver (expires 2026-08-01).

A cross-shard GET pays two mandatory cross-thread wakes per round-trip (origin→owner request, owner→origin reply). This PR removes the reply-side wake with an idle-gated, batch-depth-gated reply-side spin: when the requesting shard has no other in-flight cross-shard waiters and the batch contains a single cross-shard read, the connection polls its reply slot instead of parking + being woken across threads. The second (origin→owner) wake is irreducible without RCU/snapshot reads, which were declined this milestone to protect moon's low-RSS differentiator — so this is the documented single-connection floor for the safe approach.

Also hard-removes the orphaned --cross-shard-fast-path flag and its dead metric/histogram/functions (zero production callers since the shardslice cutover).

What changed

  • C2 — idle + batch-depth gated reply spin (src/shard/slice.rs, both connection handlers, src/server/response_slot.rs)
    • Thread-local Cell<u32> inflight gate (xshard_may_spin) — single-threaded shard ⇒ no atomic/lock. XshardWaitGuard RAII spans the reply-wait.
    • Batch-depth gate (xshard_should_spin(batch_remote), XSHARD_SPIN_MAX_BATCH_REMOTE=1): only a singleton cross-shard read spins; any pipelined fan-out parks (prevents serializing the pipeline).
    • ResponseSlot take consolidated into one take_if_filled() helper (net −2 unsafe blocks).
  • C4 — dead fast-path surface removed (BREAKING): --cross-shard-fast-path flag, CrossShardFastPath enum, cross_shard_fast_path_enabled, moon_cross_shard_lock_contention_total, moon_dispatch_cross_read_fastpath_latency_us, the record_dispatch_cross_read_fastpath_* fns, and the hardcoded cross_read_fast_dispatches. clap now rejects --cross-shard-fast-path. See CHANGELOG.

Evidence

3-way same-run, best-of-5, quiesced + core-pinned OrbStack VM (absolute VM RPS is untrusted per the milestone's documented instrument exception — relative same-run ratio only):

cell no-mech with-mech (fix) Δ
s4-c1-GET (the win) 20243 24892 +22.9%
s4-P16-GET (pipelined guard) 2479339 2608696 +5.2% (no serialization)
s4-c100-GET (starvation guard) 188679 208189 +10.3%
s1-P16-CTRL (noise floor) 4166667 4109589 −1.4% (flat)

vs the pre-regression tag 3e376a1, the with-mech c1-GET regression is −19.4% ≤ ~20% (the contract's relative target). The absolute "≥30k" line was retired (whole-VM baseline itself drifted 37580→31437→20243 across clean runs) — the durable signal is the same-run +18–23% recovery.

  • Correctness/liveness: cross-shard surface 23/23 monoio + 23/23 tokio on the fix (shardslice_live = real cross-shard reads, cross_shard_consistency_red = data correctness). scripts/test-consistency.sh 197/197 @ shards=1/4/12 unchanged.
  • Safety: audit-unsafe.sh 218/218 (net −2 unsafe), audit-unwrap.sh 0/0, zero new cross-thread lock (shardslice_shape.rs 5/5), RSS flat (49456→48992 kB HWM under 4-shard -r 1M).
  • clippy -D warnings ×2 featuresets + fmt --check clean.

Verify caught + fixed (in-phase)

The first with-mech build regressed pipelined throughput −27.5% on s4-P16 — a synchronous spin serializes a pipelined cross-shard fan-out (exactly the contract's pre-named flag-#1 risk). Fixed by the batch-depth gate so only singleton reads spin; re-measured to +5.2% (table above).

Scope / deferral

Cross-connection read coalescing (C3, the multi-client P1 case) is deferred to follow-up task xshard-read-coalescing (human-approved v2 change request). The dead-code type CoalescedReadBatch is retained as the seam.

Breaking change

--cross-shard-fast-path is removed; passing it now errors. It has been a no-op since the shardslice cutover. Documented in CHANGELOG under [Unreleased] › Removed (BREAKING).

🤖 ADD task xshard-read-fastpath · milestone v2-performance · gate PASS 2026-06-14

Summary by CodeRabbit

Release Notes

  • Breaking Changes

    • Removed --cross-shard-fast-path command-line flag.
  • Performance Improvements

    • Optimized cross-shard single-key read latency using lock-free, adaptive reply-side polling.
  • Documentation

    • Updated project conventions and development documentation for upcoming milestone versions.

… §1+§2

Size the shardslice RISK-ACCEPTED follow-up ("lock-free cross-shard read
acceleration") into a new-major milestone and co-specify its first task
through Specify + Scenarios, with a pre-freeze spike that reshaped the
approach before any contract was frozen.

Milestone v2-performance (bundle, user-confirmed intake): cross-shard read
acceleration (safe / memory-preserving) + FT.SEARCH off-event-loop + WAL
group commit. Low-RSS is promoted to a first-class milestone constraint
(the reason RCU/snapshot reads are Out). Records a measurement exception:
single-client cross-shard latency is NOT reliably measurable on the
OrbStack VM (host->guest vCPU starvation) -> GCloud bare-metal.

Task xshard-read-fastpath §1 (rules) + §2 (scenarios): adaptive idle-gated
reply-side spin + cross-connection read coalescing + hard-remove of the
now-dead --cross-shard-fast-path flag/metric/histograms (zero production
callers since the wave-e2 cutover).

Spin-viability spike (scripts/spike-xshard-read.sh), findings recorded in §1:
- fixed spin=4096 cut s4 c100 GET throughput -48% (materializes
  shard_starvation) -> the spin MUST be idle-gated, not a fixed budget.
- INFO spsc_notify_wakes ratio 1.000 over ~150k cross-shard ops -> the
  eventfd wake fires for every op; the c1 -85% regression is ARCHITECTURAL
  cross-thread wake+reschedule cost, NOT a lost-wake/tick-floor bug.
- the OrbStack VM is an invalid instrument for this metric under host vCPU
  contention (load floored ~1.5, shards parked); the throwaway
  MOON_XSHARD_SPIN prototype in handler_monoio was reverted.

Phase: scenarios. The §3 contract freeze is gated on a GCloud bare-metal
M0 baseline (the VM number is meaningless for this metric).

author: Tin Dang
Close the v1 ADD loop: consolidate the milestone's confirmed competency
deltas into the durable foundation and record the milestone retrospective.

- PROJECT.md → foundation-version 1; record the v1 CLOSE decision + the
  cross-shard-read RISK-ACCEPTED follow-up; add the (now shape-enforced)
  shared-nothing write-path lock-invariant restoration to §Domain.
- CONVENTIONS.md: encode the behavior-preserving-perf red-suite split
  (runtime-red + compile-red API file + green pins) and the §3 freeze
  flag-line requirement.
- hotpath-lock-quickwins TASK §7: mark the three deltas folded.
- v1-shared-nothing MILESTONE: exit criteria annotated MET.
- RETRO.md: milestone retrospective.

author: Tin Dang
Advance xshard-read-fastpath scenarios -> contract and draft the §3
frozen-shape, holding it at DRAFT pending the GCloud bare-metal M0
baseline + the one human bundle approval.

The contract fixes the INTERNAL mechanism only — no wire change (the
197-test consistency suite stays the byte-identical wire contract):

- C1 idle-gate: thread-local `Cell<u32> XSHARD_INFLIGHT` sibling to the
  per-shard SHARD cell (single-threaded shard => no atomic, no lock),
  `xshard_may_spin()` gating on `<= XSHARD_SPIN_GATE`, RAII
  `XshardWaitGuard`. This is the mechanism that lets c1 spin while c100
  parks — the resolution of the spike's starvation tension.
- C2 reply-side adaptive poll wired into the EXISTING wait sites (monoio
  handler_monoio Empty branch, tokio handler_sharded reply await); poll
  is synchronous (no borrow across .await) and falls through to today's
  park path unchanged when the gate is false.
- C3 CoalescedReadBatch: cross-connection read coalescing to one owner
  shard, per-result reply routing; groups ACROSS connections only so
  per-connection read-your-writes order is preserved.
- C4 exact hard-remove surface for the dead --cross-shard-fast-path flag
  + moon_cross_shard_lock_contention_total metric + stale RwLock docs.

Contracted responses cover all six §1 rejects. Two contract-level
lowest-confidence flags surfaced for the freeze: (1) idle-gate counts
cross-shard waiters, not all runnable work (mixed-workload starvation
risk); (2) C3 per-result routing is the most novel surface (consistency
risk, 197-suite is the oracle).

Phase: contract. NOT advancing to tests/build until FROZEN.

author: Tin Dang
Record the quiesced-VM RELATIVE baseline that anchors the "halve the gap"
target, correct the baseline commit (the waiver's label was wrong), and add
the two measurement harnesses.

COMMIT CORRECTION: the shardslice waiver called the pre-regression baseline
`eb5d664 (v1 HEAD)`. Archaeology disproved it — eb5d664 is the ORPHANED
shared-nothing feature tip (the SLOW side, in no branch, not an ancestor of
main). The real pre-regression "before" is v0.3.0 tag `3e376a1` (still has the
cross-thread lock read fast-path). All eb5d664 refs flipped to 3e376a1.

M0 ANCHOR (2026-06-14, monoio, fresh-boot clean VM, pinned cores 0-3 / bench
4-5, fresh-server-per-rep, best-of-5):
- s4-c1-GET 37580 (3e376a1) -> 22252 (HEAD a497602) = -40.8% <- the gap to halve
- s1 LOCAL c1-GET ~40k on BOTH -> regression is cross-shard-specific (anchor valid)
- s4-c100-GET guard ~202k; s4-c1-SET 564->21720 (v0.3.0 WRITES were the slow side;
  shared-nothing improved writes 38x -> confirms reads-only scope)
- M1 target: recover >= half the 15.3k-RPS gap -> c1-GET best-of-N >= ~30k
- Live regression is -40.8%, NOT the historical -85% (current main recovered
  part post-shardslice via PR #173).

INSTRUMENT: GCloud bare-metal was blocked (no open billing account) so the
anchor is a same-instrument RELATIVE ratio on the quiesced VM (overhead cancels
pre/post; absolute us untrusted). A leaked `moon-<hash>-monoio` busy-poller that
survived `pkill -f "moon --port"` floored c1 at 1-6k for hours; the clean number
came only after a Mac reboot. Hygiene rule recorded in the M0 RECORD + auto-memory.

- scripts/baseline-xshard-quiesced.sh: best-of-N pinned baseline harness (deep-settle,
  quiesce-gate, c1-cool/c100-last ordering, hard checkout guard).
- scripts/probe-xshard.sh: local-vs-xshard discriminator (separates contamination
  from real cross-shard cost).

Phase still: contract (DRAFT). Freeze remains gated on the one human bundle
approval; M0 anchor (the other gate) is now satisfied.

author: Tin Dang
Human bundle approval given ("Freeze as-is", Tin Dang 2026-06-14) over the
§1–§4 bundle, lowest-confidence flag surfaced first (the ~30k "halve" target
sits at the mechanism's one-wake-removed ceiling — little margin). Both freeze
gates now satisfied: M0 anchor established (clean-VM best-of-5, c1-GET
37580→22252 = −40.8%) + the one human approval.

The frozen internal shape (no wire change): C1 idle-gate (thread-local
Cell<u32> XSHARD_INFLIGHT + xshard_may_spin + XshardWaitGuard), C2 reply-side
adaptive poll wired into existing monoio/tokio wait sites, C3 CoalescedReadBatch
cross-connection coalescing, C4 hard-remove the dead --cross-shard-fast-path
surface. M1 target locked: c1-GET best-of-N ≥ ~30k (halve the 15.3k gap).

Phase: contract -> tests. Changing any C1–C4 clause or the M1 line is now a
change request back to SPECIFY.

author: Tin Dang
…p shape

Failing-first suite for the frozen §3 contract (behavior-preserving-perf split:
compile-red API pin + runtime-red gate logic + cleanup shape-grep + evidence-gated
oracles). Proven red for the right reason (symbol presence/absence verified):

- tests/xshard_fastpath_api.rs — COMPILE-RED (imports xshard_may_spin /
  XshardWaitGuard / XSHARD_SPIN_GATE / CoalescedReadBatch, all absent until §5):
  idle_gate_allows_spin_when_alone (xrf1), idle_gate_blocks_spin_above_gate
  (xrf1 / reject shard_starvation — the RAII anti-starvation invariant),
  idle_gate_boundary_is_inclusive (the <= gate bound), coalesced_read_batch_type_exists
  (xrf2 — routing correctness deferred to the consistency oracle per contract C3).
- tests/xshard_cleanup_shape.rs — RUNTIME-RED now (6 dead symbols still in src/),
  green after M3: dead_fastpath_surface_removed (xrf3 + reject removed_flag_referenced).

§4 plan records the evidence-gated Musts (xrf1 perf ≥~30k, c100 guard ~202k,
xrf2 P1 recovery, RYW 197/197, RSS) against the M0 anchor + the existing shape
green-pins (reject lock_reintroduced / borrow_across_await).

Phase: tests. On feat/xshard-read-fastpath (compile-red must not red-break main CI).

author: Tin Dang
…path surface

xshard-read-fastpath C4+C1+C2 (staged increment; C3 cross-connection
coalescing deferred behind the M1 checkpoint). Recovers the default-config
cross-shard read regression introduced when shardslice-migration (PR #175)
deleted the cross-thread RwLock read fast-path (RISK-ACCEPTED, waiver
expires 2026-08-01).

C4 — remove the dead pre-shardslice fast-path surface so the new mechanism
is the only cross-shard read path:
  - delete CrossShardFastPath config enum, --cross-shard-fast-path arg, and
    cross_shard_fast_path_enabled()
  - delete the DISPATCH_CROSS_READ_FASTPATH metric, its 3 recorders + getter,
    the INFO field, and the always-0 cross_read_fast_dispatches stat
  (pinned by tests/xshard_cleanup_shape.rs — source grep, zero dead symbols)

C1 — idle-gate primitive (src/shard/slice.rs):
  - XSHARD_INFLIGHT thread-local Cell<u32> (single-threaded shard: no atomic,
    no lock); XshardWaitGuard RAII inc-on-enter / dec-on-drop spanning the
    whole reply-wait (a parked waiter IS in-flight); xshard_may_spin() is true
    when in-flight reply-waiters <= XSHARD_SPIN_GATE (=2)
  - CoalescedReadBatch type defined now (src/shard/dispatch.rs); its producer
    (origin accumulation) + consumer (owner drain) land in C3
  (pinned by tests/xshard_fastpath_api.rs)

C2 — reply-side adaptive spin (both runtimes):
  - ResponseSlot::try_take() — non-blocking FILLED take, registers no waker
  - handler_sharded (tokio) + handler_monoio: when the shard is near-idle
    (gate open) busy-poll the reply for XSHARD_SPIN_BUDGET (=4096) iters to
    skip the reply-side cross-thread wake; on miss fall through to the EXISTING
    park path UNCHANGED. Gate closed => byte-identical to before (the idle gate
    is what prevents the unconditional-spin c100 -48% collapse measured earlier)

Mechanism removes one of the two cross-thread wakes per cross-shard read
round-trip. Compiles + clippy-clean on monoio (default) and tokio,jemalloc;
xshard_fastpath_api + xshard_cleanup_shape green. M1 relative-anchor
checkpoint measured separately on the quiesced VM.

author: Tin Dang
Tests phase exit gate met: §4 red suite (xshard_fastpath_api +
xshard_cleanup_shape) authored and committed; §3 contract FROZEN @ v1.
Marker + state.json advance specify->...->build for the now-running
build phase (C4+C1+C2 landed in 2bfc5bc; C3 staged behind M1).

author: Tin Dang
…ncrement

Verify-phase fixups for xshard-read-fastpath C2 (no behavior change):

1. unsafe/SAFETY: C2's ResponseSlot::try_take() had added a THIRD inline
   `unsafe { (*data.get()).take() }`, tripping §1 Must M4 "zero new unsafe"
   + CLAUDE.md "no new unsafe without approval". Consolidate the single
   UnsafeCell take into one private take_if_filled() helper that BOTH
   poll_take() and try_take() route through. poll_take/try_take now carry
   ZERO inline unsafe; response_slot drops 4 -> 2 unsafe blocks (net -2).
   Logic is byte-identical (same Acquire-check / take / Release-store-EMPTY);
   10 ResponseSlot unit tests + the loom model (which already exercises a
   try_take interleaving) stay green. audit-unsafe: 218/218 SAFETY coverage.

2. fmt: 2bfc5bc was committed clippy+test-clean but WITHOUT cargo fmt --check.
   Apply rustfmt to config.rs (stray blank line from the C4 deletion),
   handler_monoio (reflow the C2 spin block), and the two frozen red-suite
   tests (import ordering + a one-line assert) -- pure whitespace, no
   assertion or frozen contract touched.

M1 was measured on 2bfc5bc's binary; this change is perf-neutral (no logic
change), so the recorded c1-GET recovery stands.

author: Tin Dang
…un list

Add COMMITS (space-sep list, default = baseline + current) so baseline /
no-mechanism / with-mechanism are measured in ONE quiescence window -- the
only trustworthy comparison (cross-run absolute RPS is invalid per the
milestone VM-instrument exception). The anchor block now reports each
non-baseline commit's regression vs the 1.0 reference, plus a C2 RECOVERY
block (mech vs no-mech, same-run) isolating the mechanism's effect with VM
variance cancelled.

Used for the xshard-read-fastpath M1 checkpoint: same-run c1-GET
21468 (a497602 no-mech) -> 25336 (2bfc5bc with-mech) = +18.0% recovery.

author: Tin Dang
…dead bench script

C4 (xshard-read-fastpath) deleted the CrossShardFastPath enum and the
cross_shard_fast_path field from ServerConfig, but ~15 integration tests
constructed full ServerConfig literals naming that field, and
scripts/bench-cross-shard-fastpath.sh drove the removed --cross-shard-fast-path
flag. The xshard_cleanup_shape grep-pin only scans src/, and the earlier verify
runs were scoped to specific test binaries, so the full `cargo test` breakage
went unseen until the dual-runtime CI-parity pass surfaced it.

- Drop the `cross_shard_fast_path: CrossShardFastPath::Auto,` field line from
  every ServerConfig literal in tests/ (mechanical; no assertion changed).
- Drop CrossShardFastPath from the 3 `use moon::config::{...}` imports.
- git rm scripts/bench-cross-shard-fastpath.sh (it benched the removed flag).
- CHANGELOG: record the BREAKING --cross-shard-fast-path removal + the
  idle-gated reply-spin perf recovery.

Completes M3 cleanup repo-wide (not just src/). The tokio CI-parity suite now
compiles cleanly past these literals.

author: Tin Dang
…16 -27%)

Verify caught a real regression the c100-P1 guard didn't cover: at pipeline depth
the synchronous reply-side spin SERIALIZES cross-shard reads behind per-read
thread-blocks and starves throughput -- best-of-7 quiesced s4-P16 GET -26.7%
with-mech vs the s1-P16 noise control flat (+2.8%). This materializes the §3
contract flag #1: the XSHARD_INFLIGHT gate alone cannot see the busy thread,
because a synchronous spin never yields, so co-located waiters never register as
in-flight for each other.

Mitigation (flag #1 reserved): add a batch-depth gate. The spin engages ONLY when
the connection's current batch carries a single cross-shard command
(XSHARD_SPIN_MAX_BATCH_REMOTE=1) -- the pure c1 case -- AND the shard is near-idle.
Both reply-wait sites (monoio oneshot drain, tokio ResponseSlotPool) now call one
xshard_should_spin(batch_remote), so the two regimes the spin must avoid --
pipelined fan-out (batch gate) and concurrent waiters (inflight gate) -- are
enforced in one place.

Effect by regime: c1-P1 batch=1 -> spins (+18% preserved); s4-P16 batch~=12 ->
parks (byte-identical to no-mech); c100-P1 batch=1 but inflight>gate -> parks
(unchanged). Unit-pinned: batch_gate_blocks_spin_for_pipelined_batch +
batch_gate_and_inflight_gate_compose. Re-measurement recorded in §6.

author: Tin Dang
remeasure-xshard-fix.sh validates the batch-depth gate (xshard-read-fastpath
verify): measures no-mech (a497602) / old-mech (2bfc5bc, P16-regressed) /
new-mech (7048e8a, fixed) in one quiescence window, best-of-5, core-pinned,
across 4 cells — s4-c1-GET (the win), s4-c100-GET (starvation guard),
s4-P16-GET (the regression), s1-P16-CTRL (noise floor).

Same-run RELATIVE ratios only (VM absolute RPS is untrusted per the
v2-performance milestone exception). The s1 control cell isolates real
mechanism deltas from VM drift. Reuses the $HOME/moon-baseline file:// clone
for builds and caches binaries in $HOME/moon-baseline-bins.

author: Tin Dang
… + fixed, gate PASS-pending

Advance phase build->verify and write the §6 verify record for the
xshard-read-fastpath task (v2-performance milestone).

Key finding (verify earned its keep): the with-mech build (2bfc5bc) regressed
s4-P16 throughput -27.5% — a synchronous reply spin serializes a pipelined
cross-shard fan-out, and the INFLIGHT-only gate is blind to it (the spin never
yields). This is the exact §3 contract flag-#1 risk, materialized. Fixed by a
batch-depth gate (commit 7048e8a, XSHARD_SPIN_MAX_BATCH_REMOTE=1) so only a
singleton cross-shard read spins; any pipeline parks.

3-way same-run re-measure (no-mech / old-mech / new-mech, best-of-5, quiesced):
  - s4-c1-GET   +22.9% vs no-mech (the win, preserved)
  - s4-P16-GET  -27.5% -> +5.2% vs no-mech (regression cleared)
  - s4-c100-GET +10.3% (no starvation regression)
  - s1-P16-CTRL -1.4% (flat control => the P16 delta is real, not VM drift)

Post-fix dual-runtime re-confirm on 7048e8a: cross-shard surface 23/23 monoio
+ 23/23 tokio (shardslice_live = liveness oracle, cross_shard_consistency_red
= correctness oracle, both green).

Gate: PASS (RECOMMENDED) — pending human sign-off (conservative autonomy +
concurrency/architecture surface; not auto-gated). M2/C3 deferred via the
human-approved v2 change request. No security finding. Adds the pipeline-regime
and rejected-risk-flag competency deltas to §7.

author: Tin Dang
…erion

Record the verify gate PASS for xshard-read-fastpath: phase done, milestone
task checkbox + exit criterion ticked.

The cross-shard c1-GET exit criterion is MET on the contract's RELATIVE form
(same-run +18-23% recovery → regression -19.4% <= ~20% vs 3e376a1). The
absolute ">=30k" sub-line is RETIRED, not met (~25k): the whole-VM baseline
drifted 37580->31437->20243 across three clean runs, so absolute RPS is not a
stable instrument — documented in the §7 OBSERVE delta, recorded transparently
rather than silently checked.

v2-performance stays ACTIVE: two bottleneck tasks remain (ft-search-off-eventloop,
wal-group-commit) plus the deferred xshard-read-coalescing follow-up.

author: Tin Dang
The monoio handler's C2 batch-depth-gate binding exceeded the 100-col limit
(longer `oneshot_futures` + `|(_, meta, _)|` closure). Wrap the initializer
onto its own line — pure whitespace, no semantic change. Restores
`cargo fmt --check` clean (the only red stage in the local CI-parity matrix;
clippy x2 + both full test suites were already green, 0 failed).

author: Tin Dang
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Implements an idle-gated adaptive busy-spin for cross-shard single-key read replies on both the monoio and sharded handlers, using a new thread-local in-flight counter and RAII XshardWaitGuard. Adds ResponseSlot::try_take for non-blocking polling. Removes the now-superseded --cross-shard-fast-path CLI flag, CrossShardFastPath enum, and all associated metrics/config. Closes milestone v1-shared-nothing and opens v2-performance with the completed task recorded.

Changes

xshard-read-fastpath: idle-gated busy-spin + dead flag cleanup

Layer / File(s) Summary
Idle-gate contract: constants, helpers, and XshardWaitGuard
src/shard/slice.rs, src/shard/dispatch.rs
Adds thread-local XSHARD_INFLIGHT, spin constants (XSHARD_SPIN_GATE, XSHARD_SPIN_BUDGET, XSHARD_SPIN_MAX_BATCH_REMOTE), public xshard_may_spin/xshard_should_spin helpers, RAII XshardWaitGuard, and the deferred-coalescing CoalescedReadBatch type stub.
ResponseSlot::try_take and poll_take refactor
src/server/response_slot.rs
Extracts a private take_if_filled helper for the FILLED→EMPTY state transition, refactors poll_take to use it, and adds try_take as a non-blocking busy-poll variant for spinning callers.
Handler wiring: spin path in monoio and sharded handlers
src/server/conn/handler_monoio/mod.rs, src/server/conn/handler_sharded/mod.rs
Wires XshardWaitGuard and the xshard_should_spin decision into both runtimes' cross-shard reply-wait paths; the monoio handler busy-polls reply_rx.try_recv and the sharded handler busy-spins on the response slot via try_take before falling back to parked awaits. Updates per-batch dispatch counters.
Remove CrossShardFastPath enum, config field, and metrics
src/config.rs, src/admin/metrics_setup.rs, src/command/connection.rs
Deletes CrossShardFastPath enum, cross_shard_fast_path config field, cross_shard_fast_path_enabled(), the DISPATCH_CROSS_READ_FASTPATH_TOTAL atomic and its record_*/total_* helpers, and the total_dispatch_cross_read_fastpath INFO stat field.
API pin tests, cleanup shape test, and test harness cleanup
tests/xshard_fastpath_api.rs, tests/xshard_cleanup_shape.rs, tests/integration.rs, tests/*.rs
Adds idle-gate boundary/composition pin tests and a CoalescedReadBatch type assertion; adds a source-grep test enforcing all dead fast-path symbols are absent; removes the cross_shard_fast_path field from ServerConfig initializations across all integration test helpers.
Benchmarking and measurement scripts
scripts/baseline-xshard-quiesced.sh, scripts/probe-xshard.sh, scripts/remeasure-xshard-fix.sh, scripts/spike-xshard-read.sh, scripts/bench-cross-shard-fastpath.sh (deleted)
Adds four new measurement/spike scripts (multi-commit quiesced RPS, single-client GET probe, batch-gate regression validation, focused cross-shard spike); removes the now-dead bench-cross-shard-fastpath.sh.
ADD/project docs: task spec, milestone closure, and process conventions
.add/tasks/xshard-read-fastpath/TASK.md, .add/milestones/v1-shared-nothing/..., .add/milestones/v2-performance/MILESTONE.md, .add/state.json, .add/CONVENTIONS.md, .add/PROJECT.md, CHANGELOG.md
Adds the full xshard-read-fastpath task document (SPECIFY/SCENARIOS/CONTRACT/TESTS/VERIFY/OBSERVE), closes v1-shared-nothing with a RETRO, introduces the v2-performance milestone, advances state.json to v2, updates CONVENTIONS.md with red-suite and ADD-freeze rules, and records CHANGELOG entries for the breaking flag removal and the performance change.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Handler as monoio / sharded Handler
  participant Gate as xshard_should_spin
  participant Slot as ResponseSlot / reply_rx
  participant ShardWorker as Target Shard Worker

  Client->>Handler: cross-shard GET request
  Handler->>ShardWorker: dispatch read (SPSC channel)
  Handler->>Handler: XshardWaitGuard::new() → XSHARD_INFLIGHT++
  Handler->>Gate: xshard_should_spin(batch_remote_total)
  alt inflight ≤ SPIN_GATE AND batch_remote = 1
    loop ≤ XSHARD_SPIN_BUDGET iterations
      Handler->>Slot: try_take() / try_recv()
      ShardWorker-->>Slot: write response
      Slot-->>Handler: Some(frames)
      Handler-->>Client: reply (early return)
    end
  else gate closed or deep pipeline
    Handler->>Slot: await future_for / race2 chunked wait
    Slot-->>Handler: frames
    Handler-->>Client: reply
  end
  Handler->>Handler: XshardWaitGuard::drop() → XSHARD_INFLIGHT--
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • pilotspace/moon#136: Touches the same monoio cross-shard dispatch and reply-wait pipeline in handler_monoio/mod.rs that this PR extends with the adaptive spin path.
  • pilotspace/moon#17: Introduced the ResponseSlot design that this PR refactors (adding take_if_filled and try_take) to support busy-polling callers.

Suggested labels

enhancement

🐇 Hop, hop — the cross-shard wait is slimmed!
No lock to grab, no mutex brimmed,
A spin-guard wakes the sleeping slot,
try_take checks if data's hot,
The dead flag's gone, the path is clear —
v1 is closed, v2 is here! 🌙✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely summarizes the main changes: lock-free cross-shard read recovery via idle+batch-gated reply spin, and removal of dead flag code.
Description check ✅ Passed The PR description comprehensively covers all required sections: Summary (what and why), detailed What changed breakdown, Evidence with performance tables and test results, verification findings, and scope/deferral notes. All checklist items are addressed.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/xshard-read-fastpath

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 and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
scripts/probe-xshard.sh (1)

27-30: 💤 Low value

Consider making commit hashes configurable.

The hardcoded commit hashes 3e376a1 and a497602 reduce flexibility. For a reusable diagnostic, consider accepting them as environment variables with these as defaults:

+BASELINE_COMMIT="${BASELINE_COMMIT:-3e376a1}"
+CURRENT_COMMIT="${CURRENT_COMMIT:-a497602}"
 for r in 1 2 3; do
-  probe "$BINS/moon-3e376a1-monoio" 1 "3e376a1 (v0.3.0 fast) LOCAL"
-  probe "$BINS/moon-3e376a1-monoio" 4 "3e376a1 (v0.3.0 fast) XSHARD"
-  probe "$BINS/moon-a497602-monoio" 1 "a497602 (HEAD slow)  LOCAL"
-  probe "$BINS/moon-a497602-monoio" 4 "a497602 (HEAD slow)  XSHARD"
+  probe "$BINS/moon-${BASELINE_COMMIT}-monoio" 1 "${BASELINE_COMMIT} (v0.3.0 fast) LOCAL"
+  probe "$BINS/moon-${BASELINE_COMMIT}-monoio" 4 "${BASELINE_COMMIT} (v0.3.0 fast) XSHARD"
+  probe "$BINS/moon-${CURRENT_COMMIT}-monoio" 1 "${CURRENT_COMMIT} (HEAD slow)  LOCAL"
+  probe "$BINS/moon-${CURRENT_COMMIT}-monoio" 4 "${CURRENT_COMMIT} (HEAD slow)  XSHARD"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/probe-xshard.sh` around lines 27 - 30, Replace the hardcoded commit
hashes 3e376a1 and a497602 in the probe function calls with environment
variables. Define environment variables at the top of the script (for example,
COMMIT_HASH_1 and COMMIT_HASH_2) with default values set to the current
hardcoded hashes, then substitute these variables into the probe calls. This
allows the script to be reused with different commit hashes without requiring
edits to the script itself.
scripts/remeasure-xshard-fix.sh (1)

24-29: 💤 Low value

Consider cleaning up temporary directories.

The measure function creates a temp directory on line 24 but doesn't remove it after killing the process on line 28. While the OS will eventually clean /tmp, explicitly removing it would be cleaner:

   vals+=("$(bench "$4" "$5" "$6")")
   kill "$pid" 2>/dev/null; wait "$pid" 2>/dev/null
+  rm -rf "$dir"
 done
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/remeasure-xshard-fix.sh` around lines 24 - 29, The measure function
creates a temporary directory stored in the dir variable but does not clean it
up after the process is killed. After the kill and wait commands on line 28, add
an explicit cleanup command to remove the temporary directory stored in the dir
variable to ensure proper resource management and avoid accumulating temporary
files in /tmp.
scripts/baseline-xshard-quiesced.sh (1)

146-147: 💤 Low value

Median calculation handles both odd and even sample counts correctly.

The ternary expression correctly computes the median: for odd N it takes the middle element a[(NR+1)/2], and for even N it averages the two middle elements (a[NR/2]+a[NR/2+1])/2. Note that remeasure-xshard-fix.sh uses a simpler rounding approach instead of averaging; consider aligning them for consistency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/baseline-xshard-quiesced.sh` around lines 146 - 147, The median
calculation in the med variable using the awk ternary expression is logically
correct, but for consistency with the simpler rounding approach used in
remeasure-xshard-fix.sh, update the med variable calculation to match that
file's median computation method instead of using the averaging approach for
even-sized sample counts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/spike-xshard-read.sh`:
- Line 30: Remove the `--disk-offload disable` flag from the Redis server
startup command in the script to prevent unrecognized option errors when
measuring against older commits that don't support this flag. The flag appears
in the command string alongside other flags like `--appendonly no` and should be
deleted entirely from the line to ensure compatibility across different binary
versions being benchmarked.

In `@src/server/conn/handler_sharded/mod.rs`:
- Around line 1649-1660: The busy-spin logic controlled by
xshard_should_spin(batch_remote_total) is currently applied to all remote
operations, including writes, but it should only apply to remote reads. Add an
additional condition to the if statement that gates the spin loop to ensure it
only executes for read operations, not write operations. Since meta already
carries aof_bytes information, check that aof_bytes is zero or empty (indicating
a read-only operation with no AOF overhead) before entering the spin loop. This
ensures the fast path optimization remains limited to singleton remote reads as
originally intended, preventing unnecessary CPU usage on the slower write/AOF
path.

In `@src/shard/slice.rs`:
- Around line 283-315: The XshardWaitGuard struct must be made thread-affine to
prevent it from migrating across threads when held across an await point on a
multithreaded runtime. Replace the `_priv: ()` field with `_priv:
PhantomData<Rc<()>>` to add a !Send marker. This enforces that the guard cannot
move between threads, ensuring the increment and decrement operations on the
thread-local XSHARD_INFLIGHT counter happen on the same thread and preventing
counter corruption as documented in the struct comments.

---

Nitpick comments:
In `@scripts/baseline-xshard-quiesced.sh`:
- Around line 146-147: The median calculation in the med variable using the awk
ternary expression is logically correct, but for consistency with the simpler
rounding approach used in remeasure-xshard-fix.sh, update the med variable
calculation to match that file's median computation method instead of using the
averaging approach for even-sized sample counts.

In `@scripts/probe-xshard.sh`:
- Around line 27-30: Replace the hardcoded commit hashes 3e376a1 and a497602 in
the probe function calls with environment variables. Define environment
variables at the top of the script (for example, COMMIT_HASH_1 and
COMMIT_HASH_2) with default values set to the current hardcoded hashes, then
substitute these variables into the probe calls. This allows the script to be
reused with different commit hashes without requiring edits to the script
itself.

In `@scripts/remeasure-xshard-fix.sh`:
- Around line 24-29: The measure function creates a temporary directory stored
in the dir variable but does not clean it up after the process is killed. After
the kill and wait commands on line 28, add an explicit cleanup command to remove
the temporary directory stored in the dir variable to ensure proper resource
management and avoid accumulating temporary files in /tmp.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f2627f49-9063-465a-9828-5d01b2477c8c

📥 Commits

Reviewing files that changed from the base of the PR and between 05efb80 and 5985c30.

📒 Files selected for processing (39)
  • .add/CONVENTIONS.md
  • .add/PROJECT.md
  • .add/milestones/v1-shared-nothing/MILESTONE.md
  • .add/milestones/v1-shared-nothing/RETRO.md
  • .add/milestones/v2-performance/MILESTONE.md
  • .add/state.json
  • .add/tasks/hotpath-lock-quickwins/TASK.md
  • .add/tasks/xshard-read-fastpath/TASK.md
  • CHANGELOG.md
  • scripts/baseline-xshard-quiesced.sh
  • scripts/bench-cross-shard-fastpath.sh
  • scripts/probe-xshard.sh
  • scripts/remeasure-xshard-fix.sh
  • scripts/spike-xshard-read.sh
  • src/admin/metrics_setup.rs
  • src/command/connection.rs
  • src/config.rs
  • src/server/conn/handler_monoio/mod.rs
  • src/server/conn/handler_sharded/mod.rs
  • src/server/response_slot.rs
  • src/shard/dispatch.rs
  • src/shard/slice.rs
  • tests/ft_search_multi_shard_as_of.rs
  • tests/ft_search_temporal_parity.rs
  • tests/hybrid_filter_backward_compat.rs
  • tests/hybrid_filter_multishard.rs
  • tests/hybrid_filter_tag.rs
  • tests/integration.rs
  • tests/kill_snapshot.rs
  • tests/lunaris_hybrid_ft_search.rs
  • tests/mq_integration.rs
  • tests/pipeline_auto_index.rs
  • tests/replication_test.rs
  • tests/txn_ft_search_snapshot.rs
  • tests/txn_kv_wiring.rs
  • tests/vacuum_commands.rs
  • tests/workspace_integration.rs
  • tests/xshard_cleanup_shape.rs
  • tests/xshard_fastpath_api.rs
💤 Files with no reviewable changes (16)
  • scripts/bench-cross-shard-fastpath.sh
  • tests/replication_test.rs
  • src/command/connection.rs
  • tests/hybrid_filter_tag.rs
  • tests/mq_integration.rs
  • tests/txn_kv_wiring.rs
  • tests/hybrid_filter_multishard.rs
  • tests/ft_search_temporal_parity.rs
  • tests/lunaris_hybrid_ft_search.rs
  • tests/txn_ft_search_snapshot.rs
  • tests/hybrid_filter_backward_compat.rs
  • tests/workspace_integration.rs
  • tests/ft_search_multi_shard_as_of.rs
  • tests/pipeline_auto_index.rs
  • src/admin/metrics_setup.rs
  • src/config.rs

cleanup
local dir; dir="$(mktemp -d /tmp/moon-spike.XXXXXX)" # fresh VM-local dir — dodges diskfull + CWD-reload trap
"$BINARY" --port "$PORT" --shards "$SHARDS" --dir "$dir" \
--appendonly no --disk-offload disable --admin-port 0 >/dev/null 2>&1 &

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if --disk-offload is recognized by the binary
"$BINARY" --help 2>&1 | grep -q 'disk-offload' || echo "WARN: --disk-offload not supported by this binary"

Repository: pilotspace/moon

Length of output: 109


Remove --disk-offload disable flag or expect unrecognized option errors when measuring older commits.

The --disk-offload flag is not supported by the binary being measured. While acceptable for this focused spike, measuring commits from before this feature was introduced will require removing this flag to avoid errors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/spike-xshard-read.sh` at line 30, Remove the `--disk-offload disable`
flag from the Redis server startup command in the script to prevent unrecognized
option errors when measuring against older commits that don't support this flag.
The flag appears in the command string alongside other flags like `--appendonly
no` and should be deleted entirely from the line to ensure compatibility across
different binary versions being benchmarked.

Comment on lines +1649 to +1660
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();
}
}
match spun {
Some(r) => r,
None => response_pool.future_for(target).await,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Gate the busy-spin to remote reads only.

This condition currently spins for any singleton cross-shard batch, including writes, because it only checks batch_remote_total. meta already carries aof_bytes, so a lone remote SET/write will still busy-spin on the executor thread before falling back to future_for(...).await, which widens this fast path beyond the PR’s “singleton read” contract and risks burning CPU on the slower write/AOF path.

Suggested fix
-                            if crate::shard::slice::xshard_should_spin(batch_remote_total) {
+                            let is_single_remote_read =
+                                matches!(meta.as_slice(), [(_, None, _)]);
+                            if is_single_remote_read
+                                && 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;
                                     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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();
}
}
match spun {
Some(r) => r,
None => response_pool.future_for(target).await,
let is_single_remote_read =
matches!(meta.as_slice(), [(_, None, _)]);
if is_single_remote_read
&& 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();
}
}
match spun {
Some(r) => r,
None => response_pool.future_for(target).await,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/conn/handler_sharded/mod.rs` around lines 1649 - 1660, The
busy-spin logic controlled by xshard_should_spin(batch_remote_total) is
currently applied to all remote operations, including writes, but it should only
apply to remote reads. Add an additional condition to the if statement that
gates the spin loop to ensure it only executes for read operations, not write
operations. Since meta already carries aof_bytes information, check that
aof_bytes is zero or empty (indicating a read-only operation with no AOF
overhead) before entering the spin loop. This ensures the fast path optimization
remains limited to singleton remote reads as originally intended, preventing
unnecessary CPU usage on the slower write/AOF path.

Comment thread src/shard/slice.rs
Comment on lines +283 to +315
/// RAII guard that marks the current connection as an in-flight cross-shard
/// reply-waiter for its lifetime: increments `XSHARD_INFLIGHT` on construction,
/// decrements on drop. Wrap every cross-shard reply-wait in one so the idle gate
/// reflects reality even across early returns / cancellation. `!Send` in spirit —
/// the thread-local it guards is per-thread and the guard must not cross threads.
#[must_use = "the guard must be held for the duration of the reply-wait; dropping it early ends the in-flight window"]
pub struct XshardWaitGuard {
// Private field: construct only via `new()`, and a `PhantomData<Rc<()>>`-free
// marker is unnecessary because the type holds no data that could be moved
// across threads; callers keep it on the shard thread's stack.
_priv: (),
}

impl XshardWaitGuard {
/// Enter a cross-shard reply-wait: increment this thread's in-flight count.
#[inline]
pub fn new() -> Self {
XSHARD_INFLIGHT.with(|c| c.set(c.get().saturating_add(1)));
Self { _priv: () }
}
}

impl Default for XshardWaitGuard {
#[inline]
fn default() -> Self {
Self::new()
}
}

impl Drop for XshardWaitGuard {
#[inline]
fn drop(&mut self) {
XSHARD_INFLIGHT.with(|c| c.set(c.get().saturating_sub(1)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Make XshardWaitGuard thread-affine with a !Send marker.

XshardWaitGuard currently implements Send because it only holds (), allowing the guard to migrate across threads if held across an .await on a multithreaded runtime. When the guard drops on a different thread than where it was created, the decrement will operate on a different thread-local XSHARD_INFLIGHT counter than the increment, corrupting both counters and skewing xshard_may_spin() decisions. Adding PhantomData<Rc<()>> enforces thread-affinity by making the type !Send, matching the documented invariant.

Suggested fix
 pub struct XshardWaitGuard {
-    // Private field: construct only via `new()`, and a `PhantomData<Rc<()>>`-free
-    // marker is unnecessary because the type holds no data that could be moved
-    // across threads; callers keep it on the shard thread's stack.
-    _priv: (),
+    // Keep the guard thread-affine: the thread-local counter must be decremented
+    // on the same thread that incremented it.
+    _not_send: PhantomData<Rc<()>>,
 }
 
 impl XshardWaitGuard {
     /// Enter a cross-shard reply-wait: increment this thread's in-flight count.
     #[inline]
     pub fn new() -> Self {
         XSHARD_INFLIGHT.with(|c| c.set(c.get().saturating_add(1)));
-        Self { _priv: () }
+        Self {
+            _not_send: PhantomData,
+        }
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// RAII guard that marks the current connection as an in-flight cross-shard
/// reply-waiter for its lifetime: increments `XSHARD_INFLIGHT` on construction,
/// decrements on drop. Wrap every cross-shard reply-wait in one so the idle gate
/// reflects reality even across early returns / cancellation. `!Send` in spirit —
/// the thread-local it guards is per-thread and the guard must not cross threads.
#[must_use = "the guard must be held for the duration of the reply-wait; dropping it early ends the in-flight window"]
pub struct XshardWaitGuard {
// Private field: construct only via `new()`, and a `PhantomData<Rc<()>>`-free
// marker is unnecessary because the type holds no data that could be moved
// across threads; callers keep it on the shard thread's stack.
_priv: (),
}
impl XshardWaitGuard {
/// Enter a cross-shard reply-wait: increment this thread's in-flight count.
#[inline]
pub fn new() -> Self {
XSHARD_INFLIGHT.with(|c| c.set(c.get().saturating_add(1)));
Self { _priv: () }
}
}
impl Default for XshardWaitGuard {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl Drop for XshardWaitGuard {
#[inline]
fn drop(&mut self) {
XSHARD_INFLIGHT.with(|c| c.set(c.get().saturating_sub(1)));
/// RAII guard that marks the current connection as an in-flight cross-shard
/// reply-waiter for its lifetime: increments `XSHARD_INFLIGHT` on construction,
/// decrements on drop. Wrap every cross-shard reply-wait in one so the idle gate
/// reflects reality even across early returns / cancellation. `!Send` in spirit —
/// the thread-local it guards is per-thread and the guard must not cross threads.
#[must_use = "the guard must be held for the duration of the reply-wait; dropping it early ends the in-flight window"]
pub struct XshardWaitGuard {
// Keep the guard thread-affine: the thread-local counter must be decremented
// on the same thread that incremented it.
_not_send: PhantomData<Rc<()>>,
}
impl XshardWaitGuard {
/// Enter a cross-shard reply-wait: increment this thread's in-flight count.
#[inline]
pub fn new() -> Self {
XSHARD_INFLIGHT.with(|c| c.set(c.get().saturating_add(1)));
Self {
_not_send: PhantomData,
}
}
}
impl Default for XshardWaitGuard {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl Drop for XshardWaitGuard {
#[inline]
fn drop(&mut self) {
XSHARD_INFLIGHT.with(|c| c.set(c.get().saturating_sub(1)));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shard/slice.rs` around lines 283 - 315, The XshardWaitGuard struct must
be made thread-affine to prevent it from migrating across threads when held
across an await point on a multithreaded runtime. Replace the `_priv: ()` field
with `_priv: PhantomData<Rc<()>>` to add a !Send marker. This enforces that the
guard cannot move between threads, ensuring the increment and decrement
operations on the thread-local XSHARD_INFLIGHT counter happen on the same thread
and preventing counter corruption as documented in the struct comments.

@pilotspacex-byte pilotspacex-byte merged commit 797d52d into main Jun 15, 2026
15 checks passed
TinDang97 added a commit that referenced this pull request Jun 15, 2026
The cross-shard idle-gated reply-spin entry under [Unreleased] was the
only v2-performance changelog entry without its originating PR number,
while the sibling WAL group-commit (#178) and FT.SEARCH off-event-loop
(#179) entries both carry one. Add the "(PR #177)" tag so all three v2
stack entries are consistently attributable.

Documentation-only; no code, test, or behavior change.

author: Tin Dang

Co-authored-by: Tin Dang <tin.dang@trustifytechnology.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants