fix(server): connection-plane robustness hardening (Track B)#230
Conversation
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
|
Warning Review limit reached
Next review available in: 45 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (6)
📝 WalkthroughWalkthroughThis PR bundles connection-plane durability and robustness fixes: shard-wide FLUSHDB/FLUSHALL broadcast, WAL v3 XactCommitV2 db-aware crash replay, migration fail-open handling, connected-client/backpressure metrics, lazy FunctionRegistry init, configurable TCP backlog, CLIENT KILL force-close, a max inline command size, bounded cross-shard spsc_send retries, and accept-loop backoff. ChangesConnection-plane durability and lifecycle wave
Estimated code review effort: 4 (Complex) | ~75 minutes Connection-plane robustness hardening (Track B)
Estimated code review effort: 4 (Complex) | ~80 minutes Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
d6d633a to
531038b
Compare
Five fixes from a deep-dive review of the connection-plane hardening PR: 1. CLIENT KILL self-kill stays cooperative (R-3 regression): kill_clients now takes the executing connection's id and skips the fd shutdown(2) for a matching self entry — flag only — so the +count reply is still flushed before the handler closes (Redis replies first on self-kill). Red/green: test_self_kill_does_not_force_close_socket_fd. 2. R-1 give-up fails loud at side-effect-bearing senders: a dropped MqTxnMaterialize leg turns TXN.COMMIT's reply into an explicit "MOONERR TXN.COMMIT partial" error (the commit is already WAL-durable; only foreign MQ materialization was lost — previously a silent +OK with lost messages) and logs at error!; a dropped GraphRollback logs at error! (leaked remote graph intents), both on both runtimes. 3. PushOutcome is #[must_use]: no call site can silently drop a cross-shard message by accident. All reply-carrying sites (which fail loud via the closed reply channel) now discard explicitly (let _ =). 4. spsc_send latency: a bounded ≤64-iteration spin before the first timed backoff preserves the old ~10µs-class recovery when the target ring is only transiently full — the 100µs timer sleep may round up to ~1ms on coarse runtime timers. Wedged-target budget unchanged. 5. Accept backoff cap 1s → 100ms: the sleep runs inside select! arms where the shutdown branch cannot preempt it, so the cap bounds shutdown latency during an fd-exhaustion storm (≤10 wakeups/s). Both runtimes: fmt clean, clippy clean, full lib suites green. Refs: PR #230 author: Tin Dang
Implements the recommended fixes from the connection-plane architecture
review (multi-connection × multi-db), prioritized for long-term
durability:
D-1 (durability, HIGH): cross-store TXN crash replay hardcoded db 0
(wal_v3/replay.rs "TODO: support multi-db"). CrossStoreTxn now records
the BEGIN-time db index; commits write a new XactCommitV2 WAL record
(0x53) carrying it in the header, and replay restores KV ops into that
db (out-of-range → db 0 with loud warn; v1 records keep db-0 replay).
Red/green: test_xact_commit_v2_replays_into_selected_db + fallback test.
R-6 (lifecycle): connection migration failed closed — the source shard
dropped its socket before the SPSC hand-off was confirmed, losing the
client exactly under overload. Both runtimes now keep the original
stream until the push succeeds (bounded retry 8×100µs) and on give-up
resume serving locally with migration disabled, restoring state from
the undelivered message. Also fixes the tokio loss path's
connected_clients counter leak.
Observability: moon_xshard_backpressure_drops_total{target_shard}
counter + warn at the R-1 give-up point; per-shard
moon_shard_connected_clients{shard} gauge at registry
register/deregister (SO_REUSEPORT imbalance / affinity funnels).
P-1 (footprint): FunctionRegistry + LuaEvictionCtx now lazy per
connection (built on first FUNCTION/FCALL/FCALL_RO) instead of eager —
zero cost for the >99% of connections that never call Functions.
S-5: --tcp-backlog flag replaces the hardcoded listen(1024)
(default unchanged; process-wide, set before any listener binds).
Verified: fmt + clippy clean both runtimes; macOS monoio, tokio, and
Linux (OrbStack) checks green; full lib suites green on both runtimes.
Refs: PR #230, tmp/CONN-DEEP-REVIEW.md
author: Tin Dang
|
Two follow-up commits from a self-review + architecture pass: b1a9f10 — review fixes on the original 4 hardenings:
8f3f44d — durability & lifecycle wave (from the arch review):
Full matrix locally green: fmt, clippy ×2, monoio 3773 / tokio 3118 lib tests, Linux (OrbStack) check. |
FLUSHDB and FLUSHALL are keyless commands, so key-based routing executed them only on the shard that owned the issuing connection. On a `--shards 4` server a client FLUSHALL therefore left ~3/4 of the keyspace intact (red/green measured 49/64 keys surviving; DBSIZE scatter-gathers, so the survivors were directly visible — and served stale reads as ghost keys). Fix: after the local shard executes the flush, the originating handler calls the new `coordinate_flush_broadcast` (src/shard/coordinator.rs), which sends the original FLUSH frame to every peer shard as a single-command `ShardMessage::MultiExecute` leg. Reusing MultiExecute means each remote shard gets, for free, the existing remote-arm behavior: dispatch execution, per-shard AOF/WAL persistence via `wal_append_and_fanout` (fail-loud `AOF_APPEND_LOST_ERR`), and `auto_flush_indexes`. The broadcast collects every leg's ack; any failed / unreachable leg turns the client reply into a loud `MOONERR FLUSH partial: shard N leg failed or unreachable — local shard flushed; retry the FLUSH command` plus a `tracing::error!`, never a silent partial flush. Hooked in both sharded handler stacks: - handler_sharded (tokio): after the R3 auto_flush_indexes block. - handler_monoio: after the with_shard closure exits (the broadcast awaits cross-shard acks, so it cannot run inside the closure), guarded on a successful local response. Red/green TDD (tests/flush_cross_shard_scatter.rs, spawns a real `--shards 4` server, honors MOON_BIN): - RED (pre-fix HEAD binary): 0/3 — FLUSHALL 49/64 survived, FLUSHDB 20/32 survived, db-1 FLUSHDB left db-1 keys on remote shards. - GREEN (fixed binary): 3/3 — DBSIZE 0 on all shards, ghost-key GETs empty, db-0 keys survive a db-1 FLUSHDB. (The first red run false-passed: the harness passed a nonexistent `--persistence-dir` flag so the server never started and the tests skipped; fixed to `--dir` before trusting either color.) Documented limits (CHANGELOG): the flush is not atomic across shards (same relaxed semantics as SWAPDB); FLUSH inside MULTI/EXEC still executes local-only (follow-up); FLUSHALL still clears only the selected db per the documented v0.1.5 single-active-DB behavior — all-dbs semantics needs dispatch+replay parity and is a separate follow-up. Refs: tmp/CONN-DEEP-REVIEW.md D-2 (Tier 1), PR #230 author: Tin Dang
Deep review of Moon's connection/event-loop plane surfaced four robustness
gaps that bite at high parallel connection counts (1K-10K clients). All four
are fixed here, each with red/green tests; both runtimes build, full lib
suites pass (tokio 3115, monoio 3770), clippy/fmt clean.
R-2 — inline-command length cap (src/protocol/inline.rs, frame.rs)
The RESP-less inline path had no maximum line length, so a client that
never sends CRLF (raw non-RESP bytes) grew the connection read buffer
without bound — a per-connection memory-exhaustion vector. Added
ParseConfig::max_inline_size (default 64 KB, mirrors Redis
PROTO_INLINE_MAX_SIZE); parse_inline now rejects an over-cap line (complete
or incomplete) with "Protocol error: too big inline request".
R-1 — bounded cross-shard spsc_send (src/shard/coordinator.rs)
The single dispatch primitive behind every scatter-gather command
(MGET/MSET/DEL/EXISTS/SCAN/KEYS/DBSIZE, vector scatter, FT.INFO, graph
traverse) retried a full SPSC ring in an UNBOUNDED loop { try_push;
yield/sleep } — on tokio yield_now() reschedules with no backoff,
busy-spinning a full core, and a wedged target could block graceful
shutdown forever. Replaced with a bounded retry (CROSS_SHARD_PUSH_MAX_RETRIES
x CROSS_SHARD_PUSH_BACKOFF, same budget as push_with_backpressure) returning
PushOutcome. On give-up the message is dropped; reply-carrying callers
observe the closed reply channel and synthesize a per-shard error via the
path they already handle — no call-site signature change.
R-4 — accept-loop backoff on fd exhaustion (src/server/accept_backoff.rs,
listener.rs, shard/event_loop.rs)
All six socket-accept loops (tokio and monoio; sharded, non-sharded, TLS)
retried accept() errors with zero backoff, so EMFILE/ENFILE under a
connection storm pinned a shard core at 100% and flooded the log. Added
AcceptBackoff: capped exponential backoff (1ms -> 1s) on resource-exhaustion
errors only, plus rate-limited logging; benign per-connection errors
(ECONNABORTED) log without sleeping. The io_uring multishot-accept resubmit
(opt-in MOON_URING=1 bridge) is documented as a scoped follow-up
(synchronous CQE handler, no async context to sleep in).
R-3 — CLIENT KILL force-closes idle connections (src/client_registry.rs,
handlers, conn_accept.rs)
CLIENT KILL only set a cooperative kill_flag checked once per batch, so a
connection parked in read().await (idle, default timeout 0) was never torn
down until it next sent bytes. The registry now stores each connection's
socket fd and kill_clients also shutdown(2)s it, so the parked read returns
Ok(0)/Err immediately (existing disconnect path). The raw-fd close is
race-free: kill_clients holds the registry read lock, and a connection's
RegistryGuard deregisters (needs the write lock) strictly before its stream
drops — so a visible entry always has a live fd. No-op on non-unix.
Review report: tmp/CONN-DEEP-REVIEW.md (full 5-dimension findings, incl.
Tier-0 multi-tenant isolation and Tier-2 head-of-line-blocking tracks not in
this PR).
author: Tin Dang
Five fixes from a deep-dive review of the connection-plane hardening PR: 1. CLIENT KILL self-kill stays cooperative (R-3 regression): kill_clients now takes the executing connection's id and skips the fd shutdown(2) for a matching self entry — flag only — so the +count reply is still flushed before the handler closes (Redis replies first on self-kill). Red/green: test_self_kill_does_not_force_close_socket_fd. 2. R-1 give-up fails loud at side-effect-bearing senders: a dropped MqTxnMaterialize leg turns TXN.COMMIT's reply into an explicit "MOONERR TXN.COMMIT partial" error (the commit is already WAL-durable; only foreign MQ materialization was lost — previously a silent +OK with lost messages) and logs at error!; a dropped GraphRollback logs at error! (leaked remote graph intents), both on both runtimes. 3. PushOutcome is #[must_use]: no call site can silently drop a cross-shard message by accident. All reply-carrying sites (which fail loud via the closed reply channel) now discard explicitly (let _ =). 4. spsc_send latency: a bounded ≤64-iteration spin before the first timed backoff preserves the old ~10µs-class recovery when the target ring is only transiently full — the 100µs timer sleep may round up to ~1ms on coarse runtime timers. Wedged-target budget unchanged. 5. Accept backoff cap 1s → 100ms: the sleep runs inside select! arms where the shutdown branch cannot preempt it, so the cap bounds shutdown latency during an fd-exhaustion storm (≤10 wakeups/s). Both runtimes: fmt clean, clippy clean, full lib suites green. Refs: PR #230 author: Tin Dang
Implements the recommended fixes from the connection-plane architecture
review (multi-connection × multi-db), prioritized for long-term
durability:
D-1 (durability, HIGH): cross-store TXN crash replay hardcoded db 0
(wal_v3/replay.rs "TODO: support multi-db"). CrossStoreTxn now records
the BEGIN-time db index; commits write a new XactCommitV2 WAL record
(0x53) carrying it in the header, and replay restores KV ops into that
db (out-of-range → db 0 with loud warn; v1 records keep db-0 replay).
Red/green: test_xact_commit_v2_replays_into_selected_db + fallback test.
R-6 (lifecycle): connection migration failed closed — the source shard
dropped its socket before the SPSC hand-off was confirmed, losing the
client exactly under overload. Both runtimes now keep the original
stream until the push succeeds (bounded retry 8×100µs) and on give-up
resume serving locally with migration disabled, restoring state from
the undelivered message. Also fixes the tokio loss path's
connected_clients counter leak.
Observability: moon_xshard_backpressure_drops_total{target_shard}
counter + warn at the R-1 give-up point; per-shard
moon_shard_connected_clients{shard} gauge at registry
register/deregister (SO_REUSEPORT imbalance / affinity funnels).
P-1 (footprint): FunctionRegistry + LuaEvictionCtx now lazy per
connection (built on first FUNCTION/FCALL/FCALL_RO) instead of eager —
zero cost for the >99% of connections that never call Functions.
S-5: --tcp-backlog flag replaces the hardcoded listen(1024)
(default unchanged; process-wide, set before any listener binds).
Verified: fmt + clippy clean both runtimes; macOS monoio, tokio, and
Linux (OrbStack) checks green; full lib suites green on both runtimes.
Refs: PR #230, tmp/CONN-DEEP-REVIEW.md
author: Tin Dang
FLUSHDB and FLUSHALL are keyless commands, so key-based routing executed them only on the shard that owned the issuing connection. On a `--shards 4` server a client FLUSHALL therefore left ~3/4 of the keyspace intact (red/green measured 49/64 keys surviving; DBSIZE scatter-gathers, so the survivors were directly visible — and served stale reads as ghost keys). Fix: after the local shard executes the flush, the originating handler calls the new `coordinate_flush_broadcast` (src/shard/coordinator.rs), which sends the original FLUSH frame to every peer shard as a single-command `ShardMessage::MultiExecute` leg. Reusing MultiExecute means each remote shard gets, for free, the existing remote-arm behavior: dispatch execution, per-shard AOF/WAL persistence via `wal_append_and_fanout` (fail-loud `AOF_APPEND_LOST_ERR`), and `auto_flush_indexes`. The broadcast collects every leg's ack; any failed / unreachable leg turns the client reply into a loud `MOONERR FLUSH partial: shard N leg failed or unreachable — local shard flushed; retry the FLUSH command` plus a `tracing::error!`, never a silent partial flush. Hooked in both sharded handler stacks: - handler_sharded (tokio): after the R3 auto_flush_indexes block. - handler_monoio: after the with_shard closure exits (the broadcast awaits cross-shard acks, so it cannot run inside the closure), guarded on a successful local response. Red/green TDD (tests/flush_cross_shard_scatter.rs, spawns a real `--shards 4` server, honors MOON_BIN): - RED (pre-fix HEAD binary): 0/3 — FLUSHALL 49/64 survived, FLUSHDB 20/32 survived, db-1 FLUSHDB left db-1 keys on remote shards. - GREEN (fixed binary): 3/3 — DBSIZE 0 on all shards, ghost-key GETs empty, db-0 keys survive a db-1 FLUSHDB. (The first red run false-passed: the harness passed a nonexistent `--persistence-dir` flag so the server never started and the tests skipped; fixed to `--dir` before trusting either color.) Documented limits (CHANGELOG): the flush is not atomic across shards (same relaxed semantics as SWAPDB); FLUSH inside MULTI/EXEC still executes local-only (follow-up); FLUSHALL still clears only the selected db per the documented v0.1.5 single-active-DB behavior — all-dbs semantics needs dispatch+replay parity and is a separate follow-up. Refs: tmp/CONN-DEEP-REVIEW.md D-2 (Tier 1), PR #230 author: Tin Dang
7497ff4 to
f1a01a5
Compare
Main (RSS/CPU wave 5) tightened scripts/audit-unsafe.sh: the literal
`SAFETY:` token must sit within the 3 lines directly above the
`unsafe {` line. Three Track B blocks had multi-line SAFETY comments
whose marker line drifted out of range after the rebase; restructured
the comments (context prose first, SAFETY invariant last) with no
semantic change. audit-unsafe + audit-unwrap + fmt + check green on
both runtimes.
author: Tin Dang
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
src/server/conn/handler_sharded/write.rs (1)
140-162: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winDon't silently drop the workspace-cleanup hop.
If this send backpressures, the workspace record is already removed but the key sweep never runs, and this branch still returns
OK. Please branch onPushOutcomehere and surface/retry the failure before acknowledgingWS.DROP.Suggested fix
- let _ = crate::shard::coordinator::spsc_send( - &ctx.dispatch_tx, - ctx.shard_id, - cleanup_owner, - msg, - &ctx.spsc_notifiers, - ) - .await; - match reply_rx.recv().await { - Ok(count) => { - tracing::debug!( - "WS.DROP cleanup: deleted {} keys on shard {}", - count, - cleanup_owner - ); - } - Err(_) => { - tracing::warn!( - "WS.DROP cleanup: reply channel closed for shard {}", - cleanup_owner - ); - } - } + if !matches!( + crate::shard::coordinator::spsc_send( + &ctx.dispatch_tx, + ctx.shard_id, + cleanup_owner, + msg, + &ctx.spsc_notifiers, + ) + .await, + crate::shard::dispatch::PushOutcome::Pushed + ) { + responses.push(Frame::Error(Bytes::from_static( + b"ERR workspace cleanup backpressure", + ))); + return true; + } + match reply_rx.recv().await { + Ok(count) => { + tracing::debug!( + "WS.DROP cleanup: deleted {} keys on shard {}", + count, + cleanup_owner + ); + } + Err(_) => { + tracing::warn!( + "WS.DROP cleanup: reply channel closed for shard {}", + cleanup_owner + ); + } + }🤖 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/write.rs` around lines 140 - 162, In the workspace-drop cleanup path inside the write handler, do not ignore the result of shard::coordinator::spsc_send when sending the cleanup hop. Update the WS.DROP branch in the sharded write flow to inspect PushOutcome before awaiting reply_rx.recv(), and if the send is backpressured or otherwise fails, surface or retry the failure instead of continuing to return OK. Use the existing cleanup_owner, reply_rx, and spsc_send call site in the write handler to locate the branch and make sure acknowledgement only happens after the cleanup hop is successfully queued.src/server/conn/handler_monoio/write.rs (1)
145-153: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winDon't silently drop the workspace-cleanup hop.
If this send backpressures, the workspace record is already removed but the key sweep never runs, and this branch still returns
OK. Please branch onPushOutcomehere and surface/retry the failure before acknowledgingWS.DROP.Suggested fix
- let _ = crate::shard::coordinator::spsc_send( - &ctx.dispatch_tx, - ctx.shard_id, - owner, - msg, - &ctx.spsc_notifiers, - ) - .await; - let _ = reply_rx.recv().await; + if !matches!( + crate::shard::coordinator::spsc_send( + &ctx.dispatch_tx, + ctx.shard_id, + owner, + msg, + &ctx.spsc_notifiers, + ) + .await, + crate::shard::dispatch::PushOutcome::Pushed + ) { + responses.push(Frame::Error(Bytes::from_static( + b"ERR workspace cleanup backpressure", + ))); + return true; + } + let _ = reply_rx.recv().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_monoio/write.rs` around lines 145 - 153, The workspace-cleanup path in the monoio write handler is swallowing the result of the coordinator hop, so a backpressured send can still end in an OK reply without running the key sweep. Update the branch in the write handler around spsc_send and reply_rx.recv to inspect the returned PushOutcome, and only acknowledge WS.DROP after the cleanup hop succeeds; on failure, surface or retry the error instead of discarding it.src/shard/event_loop.rs (2)
322-347: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick winHoist per-error
format!()out of the accept-error hot path.
ctxis recomputed withformat!()on every accept error inside the loop, even thoughshard_id_copynever changes. As per coding guidelines,format!()must not be used on hot paths in shard event loops (src/shard/event_loop.rsis explicitly listed). Compute it once before the loop.As per coding guidelines, "Do not use
Box::new(),Vec::new(),String::new(),Arc::new(),clone(),format!(), orto_string()on hot paths in command dispatch, protocol parsing, shard event loops, or I/O drivers."♻️ Proposed fix
monoio::spawn(async move { let mut accept_backoff = crate::server::accept_backoff::AcceptBackoff::new(); + let ctx = format!("Shard {shard_id_copy}: per-shard accept error"); loop { match listener.accept().await { Ok((stream, _addr)) => { accept_backoff.reset(); ... } Err(e) => { // R-4: capped backoff + rate-limited log so an // fd-exhaustion storm can't hot-spin this shard. - let ctx = format!("Shard {shard_id_copy}: per-shard accept error"); accept_backoff.record_error(&ctx, &e).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/shard/event_loop.rs` around lines 322 - 347, The accept-error path in the shard event loop is recomputing a formatted context string on every loop iteration, which violates the hot-path guidelines. In the monoio::spawn accept loop, hoist the per-shard context creation out of the loop in the surrounding closure before calling listener.accept(), and then pass that precomputed value into AcceptBackoff::record_error. Use the existing shard_id_copy and the accept_backoff/record_error flow to locate and update the code without changing the loop’s behavior.Source: Coding guidelines
1065-1136: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick winSame per-error
format!()allocation on the tokio per-shard accept branch.
ctxis rebuilt viaformat!()inside theErrarm of thetokio::select!accept branch on every accept failure. Hoist it out, computed once alongsideper_shard_accept_backoff, sinceshard_idis constant for the life of the loop.As per coding guidelines, "Do not use
Box::new(),Vec::new(),String::new(),Arc::new(),clone(),format!(), orto_string()on hot paths in command dispatch, protocol parsing, shard event loops, or I/O drivers."♻️ Proposed fix
#[cfg(feature = "runtime-tokio")] let mut per_shard_accept_backoff = crate::server::accept_backoff::AcceptBackoff::new(); + #[cfg(feature = "runtime-tokio")] + let per_shard_accept_ctx = format!("Shard {shard_id}: per-shard accept error"); ... Err(e) => { - let ctx = format!("Shard {shard_id}: per-shard accept error"); - per_shard_accept_backoff.record_error(&ctx, &e).await; + per_shard_accept_backoff.record_error(&per_shard_accept_ctx, &e).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/shard/event_loop.rs` around lines 1065 - 1136, The tokio per-shard accept error path in the shard event loop rebuilds the same context string on every failure, which violates the hot-path allocation guideline. Hoist the accept error context out of the `tokio::select!` branch in `event_loop.rs`, near `per_shard_accept_backoff`, so it is computed once per shard/loop instead of inside the `Err` arm. Keep the existing `record_error` call in the `per_shard_accept_backoff` handling, but pass the precomputed context value there. Use the `per_shard_accept_backoff` setup and the per-shard accept branch as the main anchors when updating `event_loop`.Source: Coding guidelines
src/server/conn/handler_sharded/txn.rs (1)
151-151: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winMQ commit should use
txn.db_index
conn.selected_dbcan change afterTXN.BEGIN, buttxn.db_indexis the transaction’s BEGIN-time db used for KV WAL/replay. Using the live connection db here can materialize self-intents andMqTxnMaterializeinto a different database than the transaction’s KV state. Update both this local apply and the foreign-shard payload to usetxn.db_index.🤖 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/txn.rs` at line 151, The MQ commit path is using the live connection database instead of the transaction’s BEGIN-time database, which can desync materialization from KV WAL/replay. Update the local apply in txn::handle_commit and the foreign-shard payload in the sharded transaction flow to use txn.db_index rather than conn.selected_db, so self-intents and MqTxnMaterialize are applied against the same database as the transaction state.
🤖 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 `@src/shard/conn_accept.rs`:
- Around line 782-845: The fail-open migration fallback is dropping
already-buffered bytes because both resume paths call
handle_connection_sharded_monoio with BytesMut::new(). Update the local-recovery
logic in conn_accept.rs to pass the saved remainder from
state.read_buf_remainder into the initial_read_buf argument when retrying
locally, including both the branch after the initial monoio handoff and the
branch after a full SPSC push failure. Keep the fix centered around
handle_connection_sharded_monoio and the MigrateConnectionPayload/state recovery
path.
---
Outside diff comments:
In `@src/server/conn/handler_monoio/write.rs`:
- Around line 145-153: The workspace-cleanup path in the monoio write handler is
swallowing the result of the coordinator hop, so a backpressured send can still
end in an OK reply without running the key sweep. Update the branch in the write
handler around spsc_send and reply_rx.recv to inspect the returned PushOutcome,
and only acknowledge WS.DROP after the cleanup hop succeeds; on failure, surface
or retry the error instead of discarding it.
In `@src/server/conn/handler_sharded/txn.rs`:
- Line 151: The MQ commit path is using the live connection database instead of
the transaction’s BEGIN-time database, which can desync materialization from KV
WAL/replay. Update the local apply in txn::handle_commit and the foreign-shard
payload in the sharded transaction flow to use txn.db_index rather than
conn.selected_db, so self-intents and MqTxnMaterialize are applied against the
same database as the transaction state.
In `@src/server/conn/handler_sharded/write.rs`:
- Around line 140-162: In the workspace-drop cleanup path inside the write
handler, do not ignore the result of shard::coordinator::spsc_send when sending
the cleanup hop. Update the WS.DROP branch in the sharded write flow to inspect
PushOutcome before awaiting reply_rx.recv(), and if the send is backpressured or
otherwise fails, surface or retry the failure instead of continuing to return
OK. Use the existing cleanup_owner, reply_rx, and spsc_send call site in the
write handler to locate the branch and make sure acknowledgement only happens
after the cleanup hop is successfully queued.
In `@src/shard/event_loop.rs`:
- Around line 322-347: The accept-error path in the shard event loop is
recomputing a formatted context string on every loop iteration, which violates
the hot-path guidelines. In the monoio::spawn accept loop, hoist the per-shard
context creation out of the loop in the surrounding closure before calling
listener.accept(), and then pass that precomputed value into
AcceptBackoff::record_error. Use the existing shard_id_copy and the
accept_backoff/record_error flow to locate and update the code without changing
the loop’s behavior.
- Around line 1065-1136: The tokio per-shard accept error path in the shard
event loop rebuilds the same context string on every failure, which violates the
hot-path allocation guideline. Hoist the accept error context out of the
`tokio::select!` branch in `event_loop.rs`, near `per_shard_accept_backoff`, so
it is computed once per shard/loop instead of inside the `Err` arm. Keep the
existing `record_error` call in the `per_shard_accept_backoff` handling, but
pass the precomputed context value there. Use the `per_shard_accept_backoff`
setup and the per-shard accept branch as the main anchors when updating
`event_loop`.
🪄 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: 5f3d749b-4bec-4a6e-977d-1c30e35da9a6
📒 Files selected for processing (37)
.gitignoreCHANGELOG.mdfuzz/fuzz_targets/inline_parse.rsfuzz/fuzz_targets/resp_parse.rsfuzz/fuzz_targets/resp_parse_differential.rssrc/admin/metrics_setup.rssrc/client_registry.rssrc/config.rssrc/main.rssrc/persistence/wal_v3/record.rssrc/persistence/wal_v3/replay.rssrc/protocol/frame.rssrc/protocol/inline.rssrc/protocol/parse.rssrc/server/accept_backoff.rssrc/server/conn/core.rssrc/server/conn/handler_monoio/dispatch.rssrc/server/conn/handler_monoio/mod.rssrc/server/conn/handler_monoio/txn.rssrc/server/conn/handler_monoio/write.rssrc/server/conn/handler_sharded/dispatch.rssrc/server/conn/handler_sharded/mod.rssrc/server/conn/handler_sharded/txn.rssrc/server/conn/handler_sharded/write.rssrc/server/conn/shared.rssrc/server/listener.rssrc/server/mod.rssrc/shard/conn_accept.rssrc/shard/coordinator.rssrc/shard/dispatch.rssrc/shard/event_loop.rssrc/shard/scatter_aggregate.rssrc/shard/scatter_hybrid.rssrc/shard/uring_handler.rssrc/transaction/abort.rssrc/transaction/mod.rstests/flush_cross_shard_scatter.rs
| let _ = handle_connection_sharded_monoio( | ||
| stream, peer2, &conn_ctx, sd2, cid, | ||
| false, // can_migrate | ||
| BytesMut::new(), pw2, | ||
| Some(&state), kill_fd, | ||
| ) | ||
| .await; | ||
| } else { | ||
| let msg = ShardMessage::MigrateConnection(Box::new( | ||
| let mut pending_msg = Some(ShardMessage::MigrateConnection(Box::new( | ||
| crate::shard::dispatch::MigrateConnectionPayload { | ||
| fd: dup_fd, | ||
| state, | ||
| }, | ||
| )); | ||
| ))); | ||
| let target_idx = ChannelMesh::target_index(shard_id, target_shard); | ||
| let push_result = { | ||
| let mut producers = dtx2.borrow_mut(); | ||
| producers[target_idx].try_push(msg) | ||
| }; | ||
| match push_result { | ||
| Ok(()) => { | ||
| _migrated = true; | ||
| notifiers2[target_shard].notify_one(); | ||
| tracing::info!( | ||
| "Shard {}: migrated connection {} to shard {} (monoio)", | ||
| shard_id, cid, target_shard | ||
| ); | ||
| // Bounded retry: migration is elective — a briefly-full | ||
| // ring is worth a few 100µs waits, but a target that | ||
| // stays full gets the fail-open path, never a lost conn. | ||
| for attempt in 0..8u32 { | ||
| if attempt > 0 { | ||
| monoio::time::sleep(std::time::Duration::from_micros(100)).await; | ||
| } | ||
| Err(returned_msg) => { | ||
| if let ShardMessage::MigrateConnection(payload) = returned_msg { | ||
| // SAFETY: fd is a valid dup'd socket from libc::dup above. | ||
| // SPSC push failed, so ownership returns to us for cleanup. | ||
| drop(unsafe { | ||
| std::os::unix::io::OwnedFd::from_raw_fd(payload.fd) | ||
| }); | ||
| #[allow(clippy::unwrap_used)] | ||
| // pending_msg is always re-filled on Err below | ||
| let msg = pending_msg.take().unwrap(); | ||
| let push_result = { | ||
| let mut producers = dtx2.borrow_mut(); | ||
| producers[target_idx].try_push(msg) | ||
| }; | ||
| match push_result { | ||
| Ok(()) => { | ||
| _migrated = true; | ||
| notifiers2[target_shard].notify_one(); | ||
| tracing::info!( | ||
| "Shard {}: migrated connection {} to shard {} (monoio)", | ||
| shard_id, cid, target_shard | ||
| ); | ||
| break; | ||
| } | ||
| tracing::warn!( | ||
| "Shard {}: migration SPSC full, connection {} lost (monoio)", | ||
| shard_id, cid | ||
| ); | ||
| Err(returned_msg) => pending_msg = Some(returned_msg), | ||
| } | ||
| } | ||
| if _migrated { | ||
| drop(stream); // hand-off confirmed: target owns dup_fd | ||
| } else if let Some(ShardMessage::MigrateConnection(payload)) = pending_msg { | ||
| // SAFETY: dup_fd is a valid dup'd socket from libc::dup | ||
| // above; the push never succeeded so we still own it. | ||
| drop(unsafe { | ||
| std::os::unix::io::OwnedFd::from_raw_fd(payload.fd) | ||
| }); | ||
| tracing::warn!( | ||
| "Shard {}: migration SPSC full, keeping connection {} local (monoio)", | ||
| shard_id, cid | ||
| ); | ||
| // Fail-open (R-6): resume serving on this shard with | ||
| // the state recovered from the undelivered message. | ||
| let _ = handle_connection_sharded_monoio( | ||
| stream, peer2, &conn_ctx, sd2, cid, | ||
| false, // can_migrate: pin locally, no retry loop | ||
| BytesMut::new(), pw2, | ||
| Some(&payload.state), kill_fd, | ||
| ) | ||
| .await; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the relevant handler and state definitions.
rg -n --no-heading \
"read_buf_remainder|take_migration_read_buf|initial_read_buf|MigrateConnection|handle_connection_sharded_monoio|ConnectionState::new" \
src/shard/conn_accept.rs src -g '*.rs'
# Show the relevant slice around the reviewed lines.
sed -n '740,880p' src/shard/conn_accept.rs
# Inspect the connection handler/state definitions that consume initial_read_buf.
fd -a 'handler_monoio.rs' src
fd -a 'connection_state.rs' srcRepository: pilotspace/moon
Length of output: 16492
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the handler setup that consumes initial_read_buf in monoio.
sed -n '106,170p' src/server/conn/handler_monoio/mod.rs
# Show the migration result construction where read_buf_remainder is captured.
sed -n '1938,1968p' src/server/conn/handler_monoio/mod.rs
# Show the spawn path that reconstructs migrated connections.
sed -n '400,445p' src/shard/conn_accept.rsRepository: pilotspace/moon
Length of output: 6009
Preserve the buffered remainder on fail-open migration
Both fail-open paths restart the handler with BytesMut::new(), so any bytes already read into state.read_buf_remainder are dropped and the connection can desync. Thread that remainder into initial_read_buf when resuming locally in both branches.
🤖 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/conn_accept.rs` around lines 782 - 845, The fail-open migration
fallback is dropping already-buffered bytes because both resume paths call
handle_connection_sharded_monoio with BytesMut::new(). Update the local-recovery
logic in conn_accept.rs to pass the saved remainder from
state.read_buf_remainder into the initial_read_buf argument when retrying
locally, including both the branch after the initial monoio handoff and the
branch after a full SPSC push failure. Keep the fix centered around
handle_connection_sharded_monoio and the MigrateConnectionPayload/state recovery
path.
S-5 added `tcp_backlog` to ServerConfig; three integration-test files build the struct with exhaustive literals (no ..Default spread) and failed to compile in CI (E0063). Added `tcp_backlog: 1024` (the default) to the 4 literals. Verified with `cargo test --no-run` on both runtimes (0 errors) — the local gate previously compiled only --lib, which is how this slipped past. author: Tin Dang
is_resource_exhaustion (accept-loop backoff, PR #230) matched libc::EMFILE/ENFILE/ENOBUFS/ENOMEM without a cfg guard; libc is not linked on Windows targets, so every main-push Windows check has failed E0433 since the merge (PR CI never caught it -- the Windows job is skipped on PRs). Split the function: unix keeps the errno match, Windows matches WSAEMFILE (10024) / WSAENOBUFS (10055) and ErrorKind::OutOfMemory. author: Tin Dang
is_resource_exhaustion (accept-loop backoff, PR #230) matched libc::EMFILE/ENFILE/ENOBUFS/ENOMEM without a cfg guard; libc is not linked on Windows targets, so every main-push Windows check has failed E0433 since the merge (PR CI never caught it -- the Windows job is skipped on PRs). Split the function: unix keeps the errno match, Windows matches WSAEMFILE (10024) / WSAENOBUFS (10055) and ErrorKind::OutOfMemory. author: Tin Dang
#242) * perf(persistence): coalesce each AOF group-commit batch into one write The monoio AOF writer was write-syscall-bound under appendfsync always with pipelining. strace -c on aof-writer-0 during an 8s always-P16 SET window (GCE c3-standard-8, 2 shards): write: 127,812 calls, 1.25s (~1 syscall per record, 9us each) fdatasync: 2,144 calls, 0.20s The writer spent 6x more time issuing write(2) than fsyncing: both monoio paths wrote each record individually on the raw unbuffered File -- the TopLevel path one write per message via commit_group_commit_batch, the PerShard framed path a header+body write PAIR per record. Redis batches ~120 records into one write via aof_buf (its main thread did 32,791 writes total for MORE throughput), which is where the remaining 0.85x always-P16 gap lived: each ~60-record batch paid ~540us of serialized write syscalls before its fsync could start. Fix, both monoio paths: - commit_group_commit_batch (TopLevel): multi-message batches coalesce into one contiguous buffer and ONE sink.write_all; single-message batches keep the zero-copy direct write. - PerShard framed loop: records are framed into a reusable batch_buf ([u64 lsn][u32 len] header + body, channel order) and written with ONE write_all; buffer high-water capped at 1MB. - tokio writer loops already amortize via BufWriter -- untouched. Failure semantics unchanged: a failed batch write acks every AppendSync waiter WriteFailed (none Synced) and engages the torn-stream latch; the single per-batch fsync still runs strictly after all bytes are written, so fsync-before-ack (H1) is preserved. The everysec path shares the same write code and gets the same syscall reduction. wal_group_commit sink tests updated to pin the coalesced contract: one write per batch, bytes concatenated in channel order, fsync after it, whole-batch write failure (no torn half-batch acks). Validation: - wal_group_commit 9/9 green (contract tests updated). - crash_matrix_per_shard_aof --ignored 3/3 green (SIGKILL durability contract intact, including appendfsync always). - fmt, clippy x2, cargo test --lib x2 (monoio 3868 / tokio 3143), unwrap ratchet green. - GCE always-P16 A/B vs Redis to be posted on the PR before merge. author: Tin Dang * fix(server): cfg-gate accept-backoff libc errnos for Windows is_resource_exhaustion (accept-loop backoff, PR #230) matched libc::EMFILE/ENFILE/ENOBUFS/ENOMEM without a cfg guard; libc is not linked on Windows targets, so every main-push Windows check has failed E0433 since the merge (PR CI never caught it -- the Windows job is skipped on PRs). Split the function: unix keeps the errno match, Windows matches WSAEMFILE (10024) / WSAENOBUFS (10055) and ErrorKind::OutOfMemory. author: Tin Dang --------- Co-authored-by: Tin Dang <tindang.ht97@gmail.com>
Follow-up to the accept_backoff Windows fix in PR #242: the module's resource_exhaustion_classification unit test also referenced libc::EMFILE/ENFILE/ECONNABORTED, so the main-push Windows check kept failing E0433 on the test build. cfg-split the test the same way as the function under test: unix keeps the libc errnos, Windows uses WSAEMFILE (10024) / WSAENOBUFS (10055) / WSAECONNABORTED (10053). The Windows job runs only on main pushes (skipped on PRs), which is why neither the original break (PR #230) nor the partial fix could be validated pre-merge. author: Tin Dang Co-authored-by: Tin Dang <tindang.ht97@gmail.com>
Connection-plane robustness hardening (Track B)
A self-review of Moon's connection/event-loop plane (part of a broader 5-dimension deep review — see
tmp/CONN-DEEP-REVIEW.md) surfaced four robustness gaps that bite at high parallel connection counts (1K–10K clients). All four are fixed here, each with red/green tests. Both runtimes build; full lib suites pass (tokio 3115, monoio 3770); clippy/fmt clean.R-2 — inline-command length cap
src/protocol/inline.rs,src/protocol/frame.rsThe RESP-less inline (telnet) path had no maximum line length, so a client that never sends
CRLF(raw non-RESP bytes) grew the connection read buffer without bound — a per-connection memory-exhaustion vector. AddedParseConfig::max_inline_size(default 64 KB, mirrors RedisPROTO_INLINE_MAX_SIZE);parse_inlinenow rejects an over-cap line (complete or incomplete) withProtocol error: too big inline request.R-1 — bounded cross-shard
spsc_sendsrc/shard/coordinator.rsThe single dispatch primitive behind every scatter-gather command (MGET/MSET/DEL/EXISTS/SCAN/KEYS/DBSIZE, vector scatter, FT.INFO, graph traverse) retried a full SPSC ring in an unbounded
loop { try_push; yield/sleep }— on tokioyield_now()reschedules with no backoff, busy-spinning a full core, and a wedged target could block graceful shutdown forever. Replaced with a bounded retry (CROSS_SHARD_PUSH_MAX_RETRIES×CROSS_SHARD_PUSH_BACKOFF, same budget aspush_with_backpressure) returningPushOutcome. On give-up the message is dropped; reply-carrying callers observe the closed reply channel and synthesize a per-shard error via the path they already handle — no call-site signature change.R-4 — accept-loop backoff on fd exhaustion
src/server/accept_backoff.rs(new),src/server/listener.rs,src/shard/event_loop.rsAll six socket-accept loops (tokio and monoio; sharded, non-sharded, TLS) retried
accept()errors with zero backoff, soEMFILE/ENFILEunder a connection storm pinned a shard core at 100% and flooded the log. AddedAcceptBackoff: capped exponential backoff (1ms → 1s) on resource-exhaustion errors only, plus rate-limited logging; benign per-connection errors (ECONNABORTED) log without sleeping. The io_uring multishot-accept resubmit (opt-inMOON_URING=1bridge) is documented as a scoped follow-up (synchronous CQE handler, no async context to sleep in).R-3 — CLIENT KILL force-closes idle connections
src/client_registry.rs, connection handlers,src/shard/conn_accept.rsCLIENT KILLonly set a cooperativekill_flagchecked once per batch, so a connection parked inread().await(idle, default timeout 0) was never torn down until it next sent bytes. The registry now stores each connection's socket fd andkill_clientsalsoshutdown(2)s it, so the parked read returnsOk(0)/Errimmediately (existing disconnect path). The raw-fd close is race-free:kill_clientsholds the registry read lock, and a connection'sRegistryGuardderegisters (needs the write lock) strictly before itsstreamdrops — so a visible entry always has a live fd. No-op on non-unix.Testing: each fix ships red/green unit tests (inline cap boundaries; bounded
spsc_sendgive-up + success;AcceptBackoffclassification/backoff curve;CLIENT KILLpeer-EOF viaUnixStream::pair). Full CI matrix run locally via OrbStack (fmt, clippy ×2, tokio + monoio lib suites).Deep-review context (Tier-0 multi-tenant isolation and Tier-2 head-of-line-blocking tracks, not in this PR) is in
tmp/CONN-DEEP-REVIEW.md.Summary by CodeRabbit
New Features
FLUSHDB/FLUSHALLclears data consistently across shards.Bug Fixes
CLIENT KILLso targeted connections close more reliably.Performance & Reliability