fix(server): pipelined batch-tail hardening — remote-pending crash class, D4 migration gate, carried-remainder stall (#438) - #444
Conversation
…ass, D4 migration gate, carried-remainder stall (#438) Writing the red test for #438 D4 (migration races the pipelined batch tail) surfaced three adjacent pre-existing defects in the same frame-loop neighborhood; all four are fixed together because they share one mechanism (the batch tail crossing an early-flush or hand-off boundary) and one test file. 1) Remotely-triggerable whole-process crash (both runtimes, Critical). On --shards >= 2, ONE pipelined write `[GET <remote-key>…, BLPOP]` (or SUBSCRIBE/PSUBSCRIBE/PSYNC) aborted the server: the early-flush arm flushed the remote commands' Frame::Null placeholders (wrong replies), cleared/replaced `responses`, and phase 2's remote-reply drain then indexed out of bounds — shard-thread panic, process abort. Reproduced on pristine main with a 20-line python script; both handlers affected. Fix: early-flush commands defer themselves plus the unconsumed batch tail (re-encoded losslessly into the front of read_buf, carried_input armed) whenever remote-slotted work is pending; phase 2 resolves and the epilogue flushes every prior reply first, then the deferred command runs at the head of a clean batch. The deferral triggers only in the exact combination that panics today — no behavior change for any working workload; local-only batches pay two Vec::is_empty loads. 2) D4: migration gate re-evaluated at the execution point. The affinity sampler latched migration_target mid-batch with a gate check at latch time only; `[…GETs, MULTI, SET]` migrated with the transaction queued, and MigratedConnectionState carries no command_queue/in_multi/subs/tracking — queued txn discarded, EXEC answered "-ERR EXEC without MULTI", tail SUBSCRIBE orphaned. New ConnectionState::migration_eligible() (not in MULTI, no cross-store txn, no subscriptions, no CLIENT TRACKING, not a replica — the last two are new coverage) is evaluated at BOTH the latch and the batch-end execution point; an ineligible batch end keeps the latch armed and the migration runs at the first clean batch end (e.g. right after EXEC). 3) Migrated connections stalled on their carried remainder. A resumed migrated handler received read_buf_remainder but initialized carried_input=false, so its first select awaited a fresh socket read — a pipelined tail crossing a migration sat unanswered until the client sent more bytes. carried_input now initializes to !read_buf.is_empty() (both handlers), and the subscriber-mode loops gained a carry-aware read arm so a deferred tail reaches them without awaiting the socket. 4) Parsed batch tail after SUBSCRIBE/blocking was silently dropped. `[SUBSCRIBE ch, PING]` in one write swallowed the PING (frame iterator discarded the remainder on break). The frame loops are now index-based and any Subscribed/blocking break carries the unconsumed tail forward. Tests (red/green, tests/migration_batch_tail.rs): - multi_batch_tail_survives_migration_convergence — two-phase single-conn design (shard-0 keys then shard-1 keys with the 64-cmd re-migration trigger) guarantees one phase converges on a REMOTE shard regardless of placement; EXEC-exactness asserted both phases. - blocking_tail_with_pending_remote_does_not_crash — the crash-class repro, both key sets, plus liveness PING. - subscribe_batch_tail_survives_migration_convergence — subscription survives the converging batch and still receives PUBLISH. - subscribe_tail_frames_are_not_swallowed — [SUBSCRIBE, PING] one write. - Unit: migration_eligibility_gate pins every blocker + restoration. RED on pristine main: tokio 4/4 fail (incl. the D4 MULTI leg — tokio migrates on macOS), monoio-macOS 3/4 fail (no migration off-Linux). GREEN with fix: 4/4 both runtimes; monoio-Linux leg run on moon-dev VM. Gates: clippy -D warnings both feature sets; fmt; full monoio release suite; tokio suite. Bench waived with rationale: the frame loop's restructure is mem::replace-per-frame instead of Drain::next (same cost), and the new guard is two Vec::is_empty loads per frame, name compares only while a batch already has pending cross-shard work (SPSC hop dominates by ~3 orders of magnitude). Refs #438 (D4 + new findings; F1-F6 + conn-secondary remain open) author: Tin Dang
|
Warning Review limit reached
Next review available in: 49 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. 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 Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThe connection handlers now preserve pipelined command tails across blocking, subscription, and migration transitions. They centralize blocking-command detection, validate migration eligibility, process carried input immediately, and add regression coverage for migration and batch-tail behavior. ChangesConnection pipeline and migration handling
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant ConnectionHandler
participant RemoteReplies
participant ReadBuffer
participant SubscriberMode
Client->>ConnectionHandler: Send pipelined commands
ConnectionHandler->>RemoteReplies: Dispatch pending remote work
ConnectionHandler->>SubscriberMode: Enter subscription handling
SubscriberMode->>ReadBuffer: Preserve unread command tail
RemoteReplies-->>ConnectionHandler: Resolve pending replies
ConnectionHandler->>ReadBuffer: Re-encode deferred frames
ConnectionHandler->>Client: Process carried commands in order
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
PR Summary by QodoHarden pipelined batch tails: remote-pending crash, D4 migration gate, carry fixes
AI Description
Diagram
High-Level Assessment
Files changed (9)
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
tests/migration_batch_tail.rs (1)
357-374: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider covering the pubsub-arm race in this test.
subscribe_tail_frames_are_not_swallowedsends[SUBSCRIBE ch, PING]with no message pending on the channel, so the subscriber-mode read arm always wins the select and the carry is consumed correctly. The failure mode flagged atsrc/server/conn/handler_sharded/mod.rsline 450 andsrc/server/conn/handler_monoio/mod.rsline 520 needs a message queued on the channel at the moment the handler enters subscriber mode.A second connection that publishes to
chimmediately before the pipelined[SUBSCRIBE ch, PING]write would make the race reachable. The test would be timing-dependent, so gate the assertion on a generous deadline rather than a single read.🤖 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 `@tests/migration_batch_tail.rs` around lines 357 - 374, Update subscribe_tail_frames_are_not_swallowed to use a second connection that publishes to migch:t immediately before the tested client sends the pipelined SUBSCRIBE and PING commands, ensuring a channel message is queued when subscriber mode starts. Replace the single immediate reply read with deadline-bounded polling or reads, while preserving assertions that both the subscription confirmation and PING response arrive.
🤖 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/server/conn/handler_monoio/mod.rs`:
- Around line 514-528: Preserve carried_input until the read arm actually
consumes it: in src/server/conn/handler_monoio/mod.rs lines 514-528, read the
flag without taking it before monoio::select!, then clear carried_input inside
the read_result arm; in src/server/conn/handler_sharded/mod.rs lines 447-450,
pass &mut carried_input to run_subscriber_step and clear it inside that
function’s tokio::select! read arm.
---
Nitpick comments:
In `@tests/migration_batch_tail.rs`:
- Around line 357-374: Update subscribe_tail_frames_are_not_swallowed to use a
second connection that publishes to migch:t immediately before the tested client
sends the pipelined SUBSCRIBE and PING commands, ensuring a channel message is
queued when subscriber mode starts. Replace the single immediate reply read with
deadline-bounded polling or reads, while preserving assertions that both the
subscription confirmation and PING response arrive.
🪄 Autofix
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 Plus
Run ID: 14be1fad-a67d-44eb-919f-93442a45c25f
📒 Files selected for processing (9)
CHANGELOG.mdsrc/server/conn/blocking.rssrc/server/conn/core.rssrc/server/conn/handler_monoio/dispatch.rssrc/server/conn/handler_monoio/mod.rssrc/server/conn/handler_sharded/mod.rssrc/server/conn/handler_sharded/pubsub.rssrc/server/conn/tests.rstests/migration_batch_tail.rs
Code Review by Qodo
1.
|
…ins (#438 review) CodeRabbit (PR #444): both subscriber-mode selects consumed carried_input with mem::take BEFORE the select resolved — a round won by the pubsub or shutdown arm ate the flag with the deferred tail still unparsed, re-creating the stall the PR fixes. The flag is now read (not taken) when computing have_carry and cleared in the read arm's BODY, which runs only when that arm wins; a lost round keeps the carry armed and retries. Applied to the monoio subscriber select, the sharded run_subscriber_step (now takes &mut carried_input), and the pre-existing take in the sharded main select, which had the same race against its tracking-push arm. Refs #438 author: Tin Dang
…iene) (#449) Pid-only temp-dir names (`temp_dir().join(format!("...-{}", pid))`) at eight spawn sites across six suites resurrected STALE data dirs: a crashed run leaves its dir behind, the pid is eventually reused, and the next run's server silently reloads the leftover persistence state (the documented CWD/stale-reload trap) — failures then point at whatever assertion tripped over the ghost data, not at the cause. - info_memory_allocator_pagecache, memory_doctor_response, memory_prometheus_kinds: the shared Moon harness now owns a tempfile::TempDir (random unique name; removed on drop AFTER the child is killed — hand-written Drop body runs before field drops). - vector_exact_rerank (3 sites): RAII tempdir; also removes the manual pre/post remove_dir_all pairs, so cleanup now happens even when an assert panics mid-test. - vector_db_isolation (2 sites): unique random dirs via tempfile::Builder + keep(). Deliberately NOT RAII: the restart test shares one dir between two Moon values (kill_keep_dir → same-port respawn), so the existing PathBuf ownership + manual cleanup are kept and only the collision-prone NAME is fixed. - tls_park_keyupdate: RAII tempdir; early-return and end-of-test manual removes deleted. parked_idle_parity deadline-poll fixes (the flakes deferred from the #444/#445 rounds, all environmental classes observed in CI or today's gate runs): - CLIENT KILL registry-removal assert now POLLS with a 10s deadline — the client-side close arrives instantly via shutdown(2) but the registry entry is released only when the killed handler task gets scheduled; the read-once assert fired 3/3 on the starved 2-vCPU runner and passed solo (documented assert-too-soon race). - All 13 TcpStream::connect sites use a connect_retry helper (10s deadline, 50ms backoff): under full-suite load a freshly listening server can still refuse the first attempts, which failed tests at the connect line (seen live today at the F6 test's connect). - Read deadlines 10s → 30s in the two reply helpers; deadline-bound, so green runs spend no extra time. The client_tracking_invalidation multikey second-key push flake is PRODUCT-side (a real RESP3 client would miss the same invalidation) — filed as #448 with the merge-base A/B evidence instead of being papered over here. Gates: fmt + clippy -D warnings (default and tokio,jemalloc); all six converted suites + parked_idle_parity green on macOS monoio (31 tests); tokio leg green on the four suites that run there. Bench gates waived — tests-only change, no src/ code touched. Refs #444 #445 #446, closes nothing (#448 filed for the product flake) author: Tin Dang
Part of #438 (D4, plus three adjacent findings the D4 red test surfaced). F1–F6 + conn-secondary remain open.
What the red test found
Writing the D4 regression test uncovered that the pipelined batch tail is mishandled at every hand-off boundary, including one Critical, remotely-triggerable whole-process crash present on both runtimes:
1. Crash class (Critical). On any
--shards ≥ 2deployment, ONE pipelined write[GET <remote-key>…, BLPOP](or SUBSCRIBE / PSUBSCRIBE / PSYNC) aborted the server: the early-flush arm flushed the remote commands'Frame::Nullplaceholders (wrong replies), cleared/replacedresponses, and phase 2's remote-reply drain indexed out of bounds — shard-thread panic → deliberate process abort. Reproduced on pristine main with a 20-line script.Fix: early-flush commands defer themselves + the unconsumed batch tail (re-encoded losslessly into the front of
read_buf,carried_inputarmed) whenever remote-slotted work is pending. Phase 2 resolves and flushes every prior reply, then the deferred command runs at the head of a clean batch. The deferral fires only in the exact combination that panics today.2. D4 — migration races the batch tail.
migration_targetlatched mid-batch, executed at batch end unchecked;[…GETs, MULTI, SET]migrated with the txn queued → discarded →-ERR EXEC without MULTI. NewConnectionState::migration_eligible()(¬MULTI, ¬cross-txn, ¬subs, ¬tracking, ¬replica — last two are new coverage) evaluated at both latch and batch-end execution; an ineligible batch end keeps the latch and migrates at the first clean one.3. Migrated-connection remainder stall. A resumed migrated handler received
read_buf_remainderbut initializedcarried_input = false— a pipelined tail crossing a migration sat unanswered until the client sent more bytes. Now arms on non-empty initial buffer; subscriber loops gained a carry-aware read arm.4. Swallowed batch tail.
[SUBSCRIBE ch, PING]in one write dropped the PING (frames.drain(..)discarded the remainder on break). Frame loops are index-based; Subscribed/blocking breaks carry the tail forward.Red/green
tests/migration_batch_tail.rs(4 tests) +migration_eligibility_gateunit test:Gates
-D warningsboth feature sets; fmtoom_bypass_closureenv flake under parallel load, 8/8 in isolation); tokio lib 3653/3653mem::replace-per-frame vsDrain::next(same cost); the new guard is twoVec::is_emptyloads per frame, name compares only while the batch already has pending cross-shard work (the SPSC hop dominates by ~3 orders of magnitude)Refs #438
Summary by CodeRabbit
Bug Fixes
Tests