Skip to content

fix(server): graceful-shutdown connection drain + guard-first parked teardown (#438 F1/F2) - #445

Merged
TinDang97 merged 1 commit into
mainfrom
fix/438-f1-f2-shutdown-drain
Aug 7, 2026
Merged

fix(server): graceful-shutdown connection drain + guard-first parked teardown (#438 F1/F2)#445
TinDang97 merged 1 commit into
mainfrom
fix/438-f1-f2-shutdown-drain

Conversation

@TinDang97

@TinDang97 TinDang97 commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Lands F1 (conn#8) and F2 (conn#9 / sec L1) from the #438 c10k hardening tail — together, as the review prescribed: F1's drain refactor is exactly what makes F2's latent drop-order hazard a routine path. Writing F1's red test then caught a third bug in the same class (tokio-only, pre-existing).

F1 — graceful shutdown never drained connection tasks

On SIGTERM each shard's event loop ran its persistence teardown and returned from run; dropping the runtime killed every pending connection task mid-poll. In-flight replies were truncated and the blocking/subscriber shutdown arms (-ERR server shutting down) never executed.

Measured pre-fix (Linux io_uring): 19/50 BLPOP-blocked clients lost their shutdown reply. macOS/kqueue passed only by scheduler luck.

Fix — a bounded drain in the shutdown arm, before persistence teardown:

  • monoio: stage-1/2 idle-park reads are plain awaits the token cannot wake → the drain fires their cancellers (cancel_all_parked, re-fired per 2 ms tick to close the mid-batch re-park race); the woken handler checks the token and exits through the normal flush+FIN epilogue. Tracking-conn selects gain a token arm.
  • tokio: the main/subscriber/blocking selects already had token arms — they just never got polled; the drain keeps the LocalSet polling until they exit.
  • Stop condition: per-shard-thread ConnTaskGuard live-task counter (incremented pre-spawn, RAII-decremented at exit). Ceiling SHUTDOWN_DRAIN_MAX = 5 s so a wedged peer cannot hold up shutdown (on expiry: pre-fix behaviour).

Bonus fix — tokio central-listener connections died with the MAIN runtime's io driver

The first CI run of the new drain test failed under tokio with ~half the clients losing replies despite the drain running correctly (live 50→0 in 16 ms). Write-path tracing showed their final writes failing with "A Tokio 1.x context was found, but it is being shutdown": connections accepted by the central listener are io-bound at creation to the main runtime's driver, so once main's runtime drops, their io is dead no matter what the shard drains — and SO_REUSEPORT splits accepts roughly evenly between the central and per-shard listeners on Linux. Forwarded streams are now re-registered with the owning shard's runtime driver at spawn (into_std + from_std in shard context). The monoio path was already immune (it forwards std streams and converts on the shard). Post-fix: 50/50 replies under tokio.

F2 — guard-first {registry_guard, stream} teardown

kill_clients' fd-liveness invariant (deregister strictly before fd close) held in handler tasks via local-before-parameter drop order, but the task-exit park watcher co-owned both as future upvars (drop order = capture order, no guarantee). Now wrapped in ParkedSession with a hand-written guard-first Drop; invariant + owners restated at the kill site. Generic over the guard type so the drop-order unit tests run under the tokio feature set CI executes.

Red/green

  • tests/shutdown_drain.rs: 50-conn BLPOP drain (shards 1 + 2), parked-idle FIN, subscriber FIN — each also asserts bounded process exit. Pre-fix: Linux io_uring 19/50 lost (monoio), Linux tokio ~25/50 lost → post-fix 50/50 on both runtimes.
  • Unit: cancel_all_parked age/idempotency, ParkedSession drop order + into_parts, ConnTaskGuard counting.

Gates

  • fmt + clippy -D warnings (default + tokio,jemalloc) ✅
  • macOS monoio release full suite ✅ (one unrelated parallel-suite flake, solo-green)
  • Linux monoio release: shutdown_drain 4/4, parked_idle_parity 6/6, 50-conn stress 50/50 ✅
  • Linux tokio: shutdown_drain 4/4, parked_idle_parity 6/6, 50-conn probe 50/50, full suite ✅ (one pre-existing load flake, solo-green)
  • Bench waived — no hot-path cost: drain + cancellers run only in the shutdown arm; ConnTaskGuard is one thread-local Cell op per connection lifetime; the tokio stream re-registration is one into_std/from_std pair per connection accept (epoll deregister+register, off the per-op path).

Refs #438

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@TinDang97, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 28 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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e84d3b54-e6ad-45a8-bf99-cc8e01f80fb5

📥 Commits

Reviewing files that changed from the base of the PR and between 49bc762 and f867d7b.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • src/shard/conn_accept.rs
  • src/shard/event_loop.rs
  • tests/shutdown_drain.rs
📝 Walkthrough

Walkthrough

The change adds live connection-task tracking, parked-read cancellation, and a five-second graceful-shutdown drain. Parked sessions now deregister clients before closing streams. Unix integration tests cover blocked, idle, and subscribed connections.

Changes

Connection shutdown lifecycle

Layer / File(s) Summary
Connection task and parked-session ownership
src/client_registry.rs, src/shard/conn_accept.rs, CHANGELOG.md
Connection handlers now use RAII task tracking. ParkedSession owns registry guards and streams, and drops guards before streams. Tests cover ownership transfer, destruction order, and task counts.
Parked-read shutdown cancellation
src/server/conn/handler_monoio/idle_park.rs, src/server/conn/handler_monoio/mod.rs
Monoio cancels all parked reads during shutdown. Tracking and idle-read handlers follow the normal shutdown cleanup path.
Bounded shutdown drain
src/shard/event_loop.rs, tests/shutdown_drain.rs, CHANGELOG.md
Tokio and Monoio drain live connection tasks for up to five seconds before persistence teardown. Tests cover blocked BLPOP, idle, and subscriber connections across one and two shards.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ShardEventLoop
  participant MonoioIdlePark
  participant ConnectionHandlers
  participant PersistenceTeardown
  ShardEventLoop->>MonoioIdlePark: cancel_all_parked()
  MonoioIdlePark->>ConnectionHandlers: cancel parked reads
  ConnectionHandlers-->>ShardEventLoop: live connection tasks finish
  ShardEventLoop->>PersistenceTeardown: begin teardown after drain or deadline
Loading

Possibly related issues

Possibly related PRs

  • pilotspace/moon#421 — Adds the Monoio idle-parking mechanism extended here with shutdown cancellation.
  • pilotspace/moon#424 — Shares parked-connection lifecycle and registry ownership changes.
  • pilotspace/moon#427 — Shares parked-connection teardown and shutdown handling changes.

Suggested reviewers: pilotspacex-byte, tindangtts

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly identifies the two primary fixes: graceful-shutdown connection draining and guard-first parked teardown.
Description check ✅ Passed The description covers the change summary, validation gates, performance impact, test results, and design notes, although it uses alternative headings.
✨ 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 fix/438-f1-f2-shutdown-drain

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

❤️ Share

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

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Fix graceful shutdown by draining connection tasks and enforcing guard-first teardown

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Drain per-shard connection tasks on SIGTERM/SIGINT before tearing down persistence/runtime.
• Wake monoio parked reads during drain and ensure handlers exit via normal flush+FIN epilogue.
• Make parked watcher teardown drop registry guard before stream to preserve fd-liveness invariant.
Diagram

graph TD
  shard[Shard event loop] --> drain[Shutdown drain loop] --> guard[Live conn task counter]
  drain --> idlepark[Monoio idle-park cancels]
  guard --> handler[Conn tasks (handlers/watchers)] --> parked[ParkedSession (guard-first Drop)] --> registry[Client registry / kill_clients]
  tests[tests/shutdown_drain.rs] --> shard
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Track JoinHandles and await explicit joins
  • ➕ Directly waits for specific tasks instead of polling a counter
  • ➕ Easier to attribute which tasks remain live at drain timeout
  • ➖ Harder with task migrations/resumes and multiple task types
  • ➖ More memory/bookkeeping overhead; handle ownership gets tricky across watcher↔handler handoffs
  • ➖ Monoio/tokio APIs differ; may complicate dual-runtime parity
2. Force-close all sockets immediately (no drain)
  • ➕ Simpler shutdown path and less timing-sensitive logic
  • ➕ Avoids needing parked-read cancel machinery
  • ➖ Regresses correctness: truncated replies and missing shutdown errors
  • ➖ Reintroduces observed lost BLPOP shutdown replies and unsafe fd reuse hazards

Recommendation: Keep the PR’s approach: a bounded, per-shard drain keyed off a pre-spawn-visible task counter is a good fit for shard-pinned tasks and works across both runtimes. Pairing it with explicit parked-read cancellation (monoio) and a refactor-proof guard-first drop wrapper (ParkedSession) addresses both the correctness issue (missing shutdown arms) and the latent fd-liveness hazard without requiring invasive handle plumbing.

Files changed (7) +592 / -13

Bug fix (5) +320 / -13
client_registry.rsClarify fd-liveness invariant ownership and ParkedSession requirement +17/-8

Clarify fd-liveness invariant ownership and ParkedSession requirement

• Updates the kill_clients invariant commentary to explicitly call out that task-parked connections cannot rely on local-before-parameter drop ordering. Documents that parked watcher owners must preserve guard-before-stream via ParkedSession.

src/client_registry.rs

idle_park.rsAdd shutdown-drain cancel_all_parked and unit coverage +41/-0

Add shutdown-drain cancel_all_parked and unit coverage

• Introduces cancel_all_parked() to cancel any currently parked stage-1/2 reads regardless of age, enabling shutdown to wake plain-await parked IO. Adds a unit test verifying it ignores age, skips unparked slots, and is idempotent.

src/server/conn/handler_monoio/idle_park.rs

mod.rsEnsure shutdown token breaks tracking/park paths cleanly +14/-0

Ensure shutdown token breaks tracking/park paths cleanly

• Adds a shutdown token arm to the tracking select so the drain can reach tracking connections. Ensures sweep-cancel errors during shutdown cause clean exit via the flush+FIN epilogue rather than re-parking or watcher parking.

src/server/conn/handler_monoio/mod.rs

conn_accept.rsTrack live connection tasks and enforce parked watcher drop ordering +182/-5

Track live connection tasks and enforce parked watcher drop ordering

• Adds a thread-local live connection task counter with an RAII ConnTaskGuard incremented before spawn and decremented at task exit. Wraps parked watcher ownership in ParkedSession with guard-first Drop, updates watcher spawn to use it, and adds unit tests for drop order, into_parts handoff, and task counting behavior.

src/shard/conn_accept.rs

event_loop.rsAdd bounded per-shard shutdown drain before persistence teardown +66/-0

Add bounded per-shard shutdown drain before persistence teardown

• Introduces SHUTDOWN_DRAIN_MAX (5s) and runs a drain loop on shutdown before tearing down persistence and returning from run. Tokio drain polls until live connection tasks reach zero; monoio additionally cancels all parked reads every tick to wake otherwise-unwakeable awaits.

src/shard/event_loop.rs

Tests (1) +251 / -0
shutdown_drain.rsIntegration tests for shutdown drain, clean FIN, and bounded exit +251/-0

Integration tests for shutdown drain, clean FIN, and bounded exit

• Adds unix-only integration tests that SIGTERM the server and assert: 50 BLPOP-blocked clients all receive a shutdown reply (shards 1 and 2), parked-idle connections see a clean FIN, subscribers also close cleanly, and the process exits within a deadline.

tests/shutdown_drain.rs

Documentation (1) +21 / -0
CHANGELOG.mdDocument graceful shutdown drain and parked teardown ordering fix +21/-0

Document graceful shutdown drain and parked teardown ordering fix

• Adds release notes describing the prior shutdown behavior (tasks dropped mid-poll) and the new bounded drain phase. Documents the parked-connection drop-order hazard and the new guard-first teardown wrapper.

CHANGELOG.md

@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: 2

🤖 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 1168-1179: Remove the unwrap calls and their clippy suppressions
from ParkedSession::into_parts and ParkedSession::park_readable. Encode or
validate the populated-session state through the API so into_parts returns an
explicit state result and park_readable handles an absent stream without
panicking, while preserving normal behavior for initialized sessions.

In `@src/shard/event_loop.rs`:
- Around line 2103-2132: Seal Monoio accept and migration intake as soon as
shutdown cancellation begins: update the local_accept_rx and conn_rx try_recv
loops to stop receiving new work, and add the missing cancellation handling to
the local accept task. In the MigrateConnection path of the Monoio source
handler, drain queued migration payloads through guarded handlers before
checking conn_accept::live_conn_tasks(), and reject or stop new migration
handoffs after cancellation. Add a multishard SIGTERM regression test combining
migration with connection intake.
🪄 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: 7728647e-733b-49fb-81df-c41e3c3cde1c

📥 Commits

Reviewing files that changed from the base of the PR and between 8e2fe8f and 49bc762.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • src/client_registry.rs
  • src/server/conn/handler_monoio/idle_park.rs
  • src/server/conn/handler_monoio/mod.rs
  • src/shard/conn_accept.rs
  • src/shard/event_loop.rs
  • tests/shutdown_drain.rs

Comment thread src/shard/conn_accept.rs Outdated
Comment thread src/shard/event_loop.rs
Comment on lines +2103 to +2132
// F1 (#438): bounded connection drain BEFORE persistence
// teardown — the `break` below returns from `run`, and
// dropping the monoio runtime kills every connection task
// still pending, truncating in-flight replies and
// skipping the blocking/subscriber shutdown arms (found
// live: 19/50 BLPOP clients lost their shutdown reply on
// Linux io_uring). Stage-1/2 idle-park reads are plain
// awaits the token cannot wake, so fire their cancellers;
// the woken handlers see the cancelled token and exit
// through the flush+FIN epilogue. Re-fired every tick to
// close the mid-batch re-park race. Deadline-bounded: a
// wedged peer must not hold up shutdown — its task is
// then dropped, the pre-F1 behaviour.
{
let drain_deadline = std::time::Instant::now() + SHUTDOWN_DRAIN_MAX;
loop {
crate::server::conn::handler_monoio::idle_park::cancel_all_parked();
let live = conn_accept::live_conn_tasks();
if live == 0 {
break;
}
if std::time::Instant::now() >= drain_deadline {
tracing::warn!(
"Shard {shard_id}: shutdown drain timed out with {live} connection task(s) still live; dropping them"
);
break;
}
monoio::time::sleep(std::time::Duration::from_millis(2)).await;
}
}

@coderabbitai coderabbitai Bot Aug 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Seal Monoio input before the shutdown drain.

The local_accept_rx.try_recv() and conn_rx.try_recv() loops run before this shutdown check. The local accept task has no cancellation arm. Under sustained accepts, those loops can keep receiving work and prevent the five-second drain from starting.

A source handler can also return MigrateConnection before its later shutdown check in src/server/conn/handler_monoio/mod.rs Lines 3028-3065. This branch does not drain queued migration payloads before checking live_conn_tasks(). A target shard can tear down while a transferred socket is still queued.

Stop accept intake when cancellation starts. Drain queued migrations into guarded handlers before testing the live count. Prevent new migration handoffs after cancellation. Add a multishard SIGTERM regression test that combines migration and connection intake.

🤖 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 2103 - 2132, Seal Monoio accept and
migration intake as soon as shutdown cancellation begins: update the
local_accept_rx and conn_rx try_recv loops to stop receiving new work, and add
the missing cancellation handling to the local accept task. In the
MigrateConnection path of the Monoio source handler, drain queued migration
payloads through guarded handlers before checking
conn_accept::live_conn_tasks(), and reject or stop new migration handoffs after
cancellation. Add a multishard SIGTERM regression test combining migration with
connection intake.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Part-skip, part-tracked: (1) "prevents the drain from starting" doesn't hold — the accept-drain loops are non-blocking try_recv with bounded channel capacity, and the shutdown check runs every event-loop iteration, so the drain starts within one iteration regardless of accept pressure. Connections accepted during the drain window queue unspawned and are dropped at teardown — same client-visible outcome as pre-F1 (and as a plain listener close). (2) Sealing intake (cancellation arm on the dedicated accept task + closing listeners at drain start) and the queued MigrateConnection fd payload are real follow-ups — the fd-leak half is exactly issue #438 F4 ("migration fd lifecycle: raw i32 payload with no Drop leaks the fd on shutdown/undelivered"), which is next in the campaign queue and will cover the drain interaction.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@TinDang97, agreed. I withdraw the claim that the try_recv loops can prevent the shutdown drain from starting. The loops are non-blocking, and the bounded channels ensure that the shutdown check runs on the next event-loop iteration.

The remaining concerns are follow-up work, not blockers for this PR. #438 F4 will address migration FD ownership and the shutdown interaction. Closing accept intake during drain is also appropriate for that follow-up.

🐇


✏️ Learnings added
Learnt from: TinDang97
Repo: pilotspace/moon PR: 445
File: src/shard/event_loop.rs:2103-2132
Timestamp: 2026-08-07T12:02:37.736Z
Learning: In `src/shard/event_loop.rs`, the Monoio `local_accept_rx.try_recv()` and `conn_rx.try_recv()` loops are non-blocking and consume bounded channels. They do not prevent the shutdown check from starting the connection drain on the next event-loop iteration. The shutdown behavior for connections accepted during the drain is to leave them unspawned and drop them during teardown.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@qodo-code-review

qodo-code-review Bot commented Aug 7, 2026

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Remediation recommended

1. Drain loop scans too often ✓ Resolved 🐞 Bug ➹ Performance
Description
During monoio shutdown, the drain loop calls cancel_all_parked() every 2ms until all connection
tasks exit or the 5s deadline elapses, causing repeated full scans of the per-thread idle-park
registry. With a large registry and a long/timeout-bound drain, this can create a significant but
shutdown-only CPU spike and slow shutdown progress.
Code

src/shard/event_loop.rs[R2118-2121]

+                        loop {
+                            crate::server::conn::handler_monoio::idle_park::cancel_all_parked();
+                            let live = conn_accept::live_conn_tasks();
+                            if live == 0 {
Relevance

●●● Strong

Repo recently accepted reducing repeated full registry scans in shard loop; similar performance
concern here.

PR-#427

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The monoio shutdown drain loop explicitly calls cancel_all_parked() once per 2ms tick, and
cancel_all_parked() iterates over all entries in the per-thread REGISTRY, making shutdown work
proportional to registry size multiplied by drain ticks.

src/shard/event_loop.rs[2100-2132]
src/server/conn/handler_monoio/idle_park.rs[167-191]

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

### Issue description
The monoio shutdown drain invokes `idle_park::cancel_all_parked()` every 2ms, and `cancel_all_parked()` linearly iterates the entire idle-park registry (including unparked entries). This can do a large amount of work during shutdown when many connections exist and the drain lasts close to the deadline.

### Issue Context
This is a shutdown-path-only performance risk, but it scales with the total number of registered connections and the number of drain ticks (up to ~2500 iterations over 5s at 2ms/tick).

### Fix Focus Areas
- src/shard/event_loop.rs[2100-2132]
- src/server/conn/handler_monoio/idle_park.rs[167-191]

### Suggested fix directions
Pick one (or combine):
1. **Adaptive retry/backoff:** Call `cancel_all_parked()` once immediately, then re-fire less frequently (e.g., every 20–50ms, or exponential backoff) while `live_conn_tasks() > 0`.
2. **Only re-fire when needed:** Use `cancel_all_parked()`'s return value; if it returns `0`, skip subsequent calls for a longer interval.
3. **Track parked set:** Maintain a separate collection/count of currently parked slots so cancellation can iterate only parked entries instead of all registered connections.

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


2. Unit tests in conn_accept.rs ✗ Dismissed 📘 Rule violation ▣ Testability
Description
src/shard/conn_accept.rs adds a #[cfg(test)] mod ... block even though the shard module is
split via src/shard/mod.rs. This violates the requirement that split-module unit tests live in the
corresponding mod.rs.
Code

src/shard/conn_accept.rs[R1573-1576]

+#[cfg(test)]
+mod conn_accept_tests {
+    use super::*;
+    use std::cell::RefCell;
Relevance

●● Moderate

No clear repo precedent on relocating split-module unit tests into mod.rs; could go either way.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 302093 requires unit tests for split modules to be declared only in the module’s
mod.rs. The shard module is split (has src/shard/mod.rs declaring pub mod conn_accept;), yet
tests were added inside conn_accept.rs instead of mod.rs.

Rule 302093: Keep test code for split Rust modules in mod.rs
src/shard/mod.rs[1-10]
src/shard/conn_accept.rs[1573-1635]

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

## Issue description
A `#[cfg(test)] mod conn_accept_tests` module was added to `src/shard/conn_accept.rs`, but the `shard` module is a directory module with `src/shard/mod.rs`. Tests for split modules must live in `mod.rs`.

## Issue Context
Keeping tests centralized in `mod.rs` avoids scattering unit-test code across leaf modules and aligns with the repository’s split-module conventions.

## Fix Focus Areas
- src/shard/conn_accept.rs[1573-1635]
- src/shard/mod.rs[1-41]

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


3. ParkedSession unwrap lacks justification ✓ Resolved 📘 Rule violation ✧ Quality
Description
ParkedSession uses .unwrap() with #[allow(clippy::unwrap_used)] but does not provide the
required standalone justification comment immediately above the allow attribute. This violates the
unwrap-annotation rule and weakens the unwrap audit trail for production code.
Code

src/shard/conn_accept.rs[R1168-1170]

+    pub(crate) fn into_parts(mut self) -> (G, S) {
+        #[allow(clippy::unwrap_used)] // Some until into_parts; new() is the only constructor
+        (self.guard.take().unwrap(), self.stream.take().unwrap())
Relevance

●● Moderate

Team often accepts inline justifications on allow attributes; unclear they’ll change formatting-only
unwrap audit rule.

PR-#213

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 302083 requires a justification comment on the line directly preceding the
#[allow(clippy::unwrap_used)] attribute. The code places the justification as an inline comment on
the same line as the attribute instead, so the required comment+allow pairing is missing per the
rule’s formatting requirement.

Rule 302083: Annotate safe unwrap calls with allow and justification
src/shard/conn_accept.rs[1168-1179]

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

## Issue description
In `ParkedSession::{into_parts, park_readable}`, `.unwrap()` is used with `#[allow(clippy::unwrap_used)]` but the required justification comment is not on the line immediately preceding the allow attribute.

## Issue Context
The unwrap audit convention requires a one-line comment directly above the `#[allow(clippy::unwrap_used)]` so reviewers can quickly validate the safety invariant.

## Fix Focus Areas
- src/shard/conn_accept.rs[1168-1179]

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



Informational

4. moon_binary() uses cargo fallback 📘 Rule violation ▣ Testability
Description
The new integration test starts the server without requiring MOON_BIN, falling back to
env!("CARGO_BIN_EXE_moon"). This violates the requirement that integration tests set MOON_BIN
explicitly instead of guessing a binary path.
Code

tests/shutdown_drain.rs[R29-37]

+fn moon_binary() -> Option<std::path::PathBuf> {
+    if let Ok(p) = std::env::var("MOON_BIN") {
+        return Some(std::path::PathBuf::from(p));
+    }
+    let cargo_bin = std::path::PathBuf::from(env!("CARGO_BIN_EXE_moon"));
+    if cargo_bin.exists() {
+        return Some(cargo_bin);
+    }
+    None
Relevance

● Weak

Close precedent: multiple PRs rejected requiring explicit MOON_BIN and disallowing cargo/guess
fallbacks in tests.

PR-#216
PR-#421
PR-#427

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 992389 requires integration tests to set MOON_BIN explicitly and not rely on
fallback binary discovery. The added moon_binary() helper returns a Cargo-produced path when
MOON_BIN is unset, which is exactly the disallowed fallback behavior.

Rule 992389: Integration tests must set MOON_BIN explicitly for server binaries
tests/shutdown_drain.rs[29-38]

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

## Issue description
`tests/shutdown_drain.rs` falls back to `env!("CARGO_BIN_EXE_moon")` when `MOON_BIN` is unset. The compliance rule requires integration tests to set `MOON_BIN` explicitly and avoid fallback helpers.

## Issue Context
Relying on implicit Cargo build outputs makes tests environment-dependent and breaks the “explicit binary under test” contract.

## Fix Focus Areas
- tests/shutdown_drain.rs[29-38]

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


5. Unannotated unwrap() in tests 📘 Rule violation ✧ Quality
Description
The newly added integration test uses .unwrap() without the required
#[allow(clippy::unwrap_used)] + adjacent justification comment. This breaks the unwrap-audit
convention and can hide accidental panics in test logic.
Code

tests/shutdown_drain.rs[R55-57]

+    let tmp_dir = tempfile::tempdir().expect("tempdir");
+    let dir_str = tmp_dir.path().to_str().unwrap().to_string();
+    let shards = shards.to_string();
Relevance

● Weak

Close precedent: repo rejected enforcing unwrap-annotation policy in tests; unannotated unwrap
findings not acted upon.

PR-#211
PR-#427

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 302083 requires every .unwrap() to be covered by a justification comment
immediately above a #[allow(clippy::unwrap_used)] attached to the scope/item. The new test uses
.unwrap() directly (e.g., to_str().unwrap()), without the required annotation/comment pair.

Rule 302083: Annotate safe unwrap calls with allow and justification
tests/shutdown_drain.rs[53-58]
tests/shutdown_drain.rs[127-132]

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

## Issue description
`tests/shutdown_drain.rs` contains `.unwrap()` calls that are not accompanied by the required justification comment and `#[allow(clippy::unwrap_used)]` attribute.

## Issue Context
The project enforces an unwrap audit convention to prevent unreviewed panics from creeping in (including in tests).

## Fix Focus Areas
- tests/shutdown_drain.rs[53-58]
- tests/shutdown_drain.rs[127-132]

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


Grey Divider

Context used
✅ Compliance rules (platform): 55 rules

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread src/shard/conn_accept.rs
Comment thread src/shard/conn_accept.rs Outdated
Comment thread src/shard/event_loop.rs
@TinDang97
TinDang97 force-pushed the fix/438-f1-f2-shutdown-drain branch from 49bc762 to e938c9f Compare August 7, 2026 11:48
…teardown (#438 F1/F2)

F1 (conn#8) — graceful shutdown never drained connection tasks. On
SIGTERM each shard's event loop observed the cancelled token, ran its
persistence teardown and returned from `run`; dropping the runtime then
killed every pending connection task mid-poll. In-flight replies were
truncated and the blocking/subscriber shutdown arms ("-ERR server
shutting down") never executed. Measured pre-fix on Linux io_uring:
19/50 BLPOP-blocked clients lost their shutdown reply and saw a bare
EOF; macOS/kqueue passed only by scheduler luck (small replies complete
inline, conn tasks happened to run before the event-loop task).

The shutdown arm now runs a bounded drain BEFORE persistence teardown:

- monoio: stage-1/2 idle-park reads are plain awaits the token cannot
  wake, so the drain fires their cancellers (`cancel_all_parked`,
  re-fired every 2 ms tick to close the mid-batch re-park race). The
  woken handler sees the sweep-cancel errno, checks the token, and
  exits through the normal flush+FIN epilogue instead of re-parking or
  task-parking. Tracking-connection selects gain a token arm (they park
  in a select, not a cancel-registered read).
- tokio: the main/subscriber/blocking selects already had token arms —
  they just never got polled. The drain keeps the LocalSet polling
  until they exit.
- Both: a per-shard-thread `ConnTaskGuard` counter (incremented at
  spawn, RAII-decremented at task exit) is the drain's stop condition;
  ceiling 5 s (`SHUTDOWN_DRAIN_MAX`) so a wedged peer cannot hold up
  shutdown — on expiry remaining tasks are dropped, the pre-fix
  behaviour.

F2 (conn#9 / sec L1) — kill_clients' fd-liveness invariant (registry
deregister strictly before fd close) held in handler tasks via
local-before-parameter drop order, but the task-exit park watcher
co-owned {registry_guard, stream} as future upvars, whose drop order is
merely capture order — and F1's drain makes dropping that future a
routine path, not a teardown-only one. Both are now wrapped in
`ParkedSession`, whose hand-written Drop deregisters before closing the
fd; the invariant and its owners are restated at the kill site. The
wrapper holds plain fields and relies on language-guaranteed
declaration-order field drop (RFC 1857) — no Options/unwraps, and the
wake path destructures the parts back out — with the order stated as a
contract in the struct docs and pinned by a unit test. Generic over the
guard type so those tests run under the tokio feature set CI actually
executes. Review round: the drain's canceller rescan fast-ticks (2 ms)
only for its first 5 iterations — enough to close the one legal
re-park interleaving — then backs off to 50 ms, capping the
shutdown-only O(registry) scan cost at high connection counts.

Bonus find (tokio-only, same class): connections accepted by the
CENTRAL listener were io-bound to the MAIN runtime's driver (tokio io
resources bind at creation), so their reads/writes died with main's
runtime regardless of shard-side draining — with SO_REUSEPORT splitting
accepts roughly evenly between the central and per-shard listeners,
~half of all tokio connections failed their final writes with "A Tokio
1.x context was found, but it is being shutdown" (caught by the new
drain test in CI; diagnosed via write-path tracing). Forwarded streams
are now re-registered with the owning shard's runtime driver at spawn
(`into_std` + `from_std` in shard context) — the monoio path already
did this by forwarding std streams and converting on the shard.

Red/green: tests/shutdown_drain.rs (4 tests: 50-conn BLPOP drain at
shards 1 and 2, parked-idle FIN, subscriber FIN — each also asserts
bounded process exit). Pre-fix Linux io_uring: 19/50 replies lost;
post-fix: 50/50 delivered, all suites green (Linux monoio release,
Linux tokio CI-parity, macOS monoio release). Unit tests:
cancel_all_parked, ParkedSession drop order/into_parts, ConnTaskGuard.

Bench gates waived — no hot-path cost: the drain loop and cancellers
run only inside the shutdown arm; ConnTaskGuard is one thread-local
Cell increment/decrement per connection LIFETIME (not per operation);
the stage-1/2 cancel arms add one `is_cancelled()` atomic load on the
already-cold cancel path; the tracking-select token arm is a
pending-until-cancelled future identical in cost to the existing
subscriber arm.

Known flake (pre-existing, not this change): parked_idle_parity timing
asserts fail under heavy suite load on the shared VM (fires with 2
vCPUs, passes solo) — deadline-poll fix deferred to the test-hygiene
sweep.

Refs #438 (F1 = conn#8, F2 = conn#9 = sec L1)
author: Tin Dang
@TinDang97
TinDang97 force-pushed the fix/438-f1-f2-shutdown-drain branch from e938c9f to f867d7b Compare August 7, 2026 12:02
@TinDang97
TinDang97 merged commit 2bf488f into main Aug 7, 2026
10 checks passed
@TinDang97
TinDang97 deleted the fix/438-f1-f2-shutdown-drain branch August 7, 2026 12:06
TinDang97 added a commit that referenced this pull request Aug 7, 2026
… gates (#438 F3–F6 + conn-secondary) (#446)

* fix(server): accept-loop HOL block, migration fd ownership, park auth gates (#438 F3-F6 + conn-secondary)

F3 (routing F10) — the central accept loop delivered every connection
with a blocking send on the routed shard's bounded (4096) conn channel:
one wedged shard froze accepts for ALL shards. The monoio legs were
worse than reviewed — `flume::Sender::send` there is synchronous, so a
full channel stalled the entire listener THREAD (plain + TLS legs
alike). All five delivery sites (tokio plain/TLS, monoio plain/TLS x2
select shapes) now `try_send` and rotate to the next shard with room
(`try_route_conn`); the affinity hint is best-effort and any live shard
serves the client. Only when EVERY channel refuses does the loop fall
back to the pre-F3 blocking send — server-wide saturation, where
back-pressure is the correct behaviour — with a warn. Unit-tested
(rotation, disconnected-skip, all-full payload return, target-first).

F4 (routing F12) — MigrateConnectionPayload carried the migrated
socket as a raw i32 with no Drop: a migration message still queued in
an SPSC ring at shutdown, or drained into pending_migrations but never
spawned, leaked the fd and stranded the client on a connection no task
would ever serve (silent, until the fd table filled). The payload now
owns the socket as an OwnedFd end to end — producer (tokio into_std;
monoio dup wrapped at birth) → ring → pending_migrations →
spawn_migrated_* — so every undelivered path closes the socket and the
client gets a FIN. This also discharges the F1-drain interaction
flagged on PR #445: queued migrations dropped by a shutting-down shard
now close deterministically. Three `unsafe from_raw_fd` blocks became
safe ownership conversions (net -3 unsafe). Unit test pins the drop
semantics; a revert to a raw fd no longer compiles. Resumed-parked
conns stay deliberately can_migrate:false — the resume wrapper has no
MigrateConnection arm, and migration sampling exists for HOT conns
while this path is only reachable by idling past --conn-park-secs;
rationale documented at the spawn site.

F5 (sec L3) — both migrated-connection spawn sites built the target
shard's ConnectionContext with requirepass: None. Session auth was
unaffected (it travels in MigratedConnectionState), but AUTH issued on
a migrated conn wrongly answered "no password is set", and any future
auth-re-derivation from the context would have failed open. The real
requirepass is now read from runtime config at spawn.

F6 (sec L2) — unauthenticated connections could task-park: on an
auth-enabled server an attacker could open maxclients silent sockets,
never AUTH, and hold each at the parked ~3.3 KB watcher footprint
indefinitely and invisibly. The stage-2 parkable predicate now requires
conn.authenticated — pre-AUTH conns keep their full handler task
(costly enough to surface in monitoring, still reaped by `timeout N`);
no-auth servers see no change (authenticated=true from accept).
Red/green on macOS monoio: parked_clients=2 pre-fix, =1 post-fix
(authed idle sibling doubles as the positive control that the park
machinery stayed live).

conn-secondary —
* is_sweep_cancel matched any bare errno 125, so an ECANCELED from a
  non-sweep source read as "sweep cancel -> re-park": for a dead fd
  that is a permanent park->wake->park spin at 100% CPU (the exact E11
  failure class the errno check was built to prevent). The idle-park
  slot now records provenance (`swept`, set by sweep/drain before
  firing, re-armed false at every park) and the park arms require it
  via consume-once `was_swept_cancel`; blocking.rs keeps the plain
  errno check where a stray 125 only re-arms a level-triggered poll
  (no spin possible). Unit-tested (provenance required, consume-once,
  stale-flag cleared on re-park, drain marks too).
* client_registry::register double-counted TOTAL_CLIENTS and the shard
  gauges when inserting over a still-present id (the kept_registration
  miss-arm fail-safe racing an entry back into existence): permanent
  +1 skew feeding shard_overloaded routing. register() now balances
  against the replaced entry, and the miss arm (an invariant
  violation, not a code path) warns loudly. Unit-tested.
* `timeout` read-once: already fixed by the D1 chore-sweep rework —
  both shard chores re-read runtime_config.timeout every second and
  CONFIG SET timeout is live (verified, comment at the monoio chore
  says exactly this). No change.

Gates: fmt + clippy -D warnings (default and tokio,jemalloc), new unit
tests green under both feature sets, parked_idle_parity 7/7 +
shutdown_drain 4/4 + tls_park_keyupdate on macOS monoio AND tokio
(F6 leg degenerates to parked=0 under tokio as documented), full
suites on macOS monoio (28 suites; one cross_shard_consistency_red
load flake, solo-green — same documented family as cdg6e), Linux VM
monoio (lib 4519/4519; client_tracking_invalidation push-delivery
flake fires on UNMODIFIED main too — 3x-vs-3x merge-base A/B, 1/3 main
vs 2/3 branch, same missing-second-key signature; pre-existing, queued
for the test-hygiene sweep) and Linux VM tokio CI-parity (exit 0,
190 suites).
Bench gates waived — no hot-path cost: try_route_conn replaces a
blocking send with try_send attempts on the accept path (not the
command path); the OwnedFd is layout-identical to i32 with a Drop that
only runs on the cold migration/teardown path; the park-arm provenance
check is two Cell ops on the already-cold cancelled-read path; the
parkable predicate gains one bool test evaluated once per park
decision.

Refs #438 (F3 = routing F10, F4 = routing F12, F5 = sec L3,
F6 = sec L2, conn-secondary)
author: Tin Dang

* fix(server): review round for #446 — live-shard fallback, deterministic tests, F6 identity check

Review findings (Qodo + CodeRabbit, converging on one real defect):

1. Fallback could drop a conn a live shard would take (both bots,
   CONFIRMED): when try_route_conn exhausted the rotation, the blocking
   fallback targeted the ORIGINALLY routed shard — if that receiver was
   disconnected while another shard was merely full, the fallback
   errored instantly and the accepted conn was dropped. The helper now
   reports the first LIVE-but-full shard from the rotation
   (Err((Option<usize>, payload))); callers back-pressure on THAT
   shard, and only drop (with an error log) when every receiver is
   gone — i.e. shutdown, where dropping is correct. New unit tests:
   disconnected-target-with-live-full-shard, all-disconnected.

2. Flaky global-state unit tests (both bots): the TOTAL_CLIENTS bracket
   could false-fail if an unrelated test registered between its two
   loads, and a failed assert skipped cleanup; it now retries with a
   fresh id (bug reads +1 on EVERY attempt; fix needs one undisturbed
   attempt) and deregisters before the verdict. The F4 fd-drop test
   probed the raw fd number, which the parallel runner could reuse
   between close and probe; it now observes the close through the
   PEER's read() -> EOF — no fd-number reasoning at all.

3. F6 test identified WHICH conn parked (CodeRabbit): parked_clients=1
   also holds if the silent conn parked and the authed one didn't. The
   test now closes the authed conn and requires the gauge to drain to
   0 while the silent conn stays open.

Skipped with rationale (replied on-thread):
- "Split oversized files" — pre-existing sizes, out of scope for a
  hardening PR; noted for a future refactor wave.
- New-unsafe flag on the monoio dup wrap — consolidates two prior
  unsafe sites at the same location; net unsafe count for the PR is
  -3, SAFETY comment present.

Gates: fmt + clippy -D warnings (both feature sets), listener/registry/
migrate-fd unit tests green both feature sets, parked_idle_parity
monoio 3/4 full-green runs (the one bad run failed on connect/read
setup races in OTHER tests — the documented harness flake class queued
for the test-hygiene sweep; the new assertions never tripped) and
tokio 7/7.

Refs #438, #446
author: Tin Dang
TinDang97 added a commit that referenced this pull request Aug 7, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant