Skip to content

fix(conn): stop timeout N from silently disabling the c1M connection park (c10k D1) - #427

Merged
TinDang97 merged 2 commits into
mainfrom
fix/c10k-timeout-park-d1
Aug 6, 2026
Merged

fix(conn): stop timeout N from silently disabling the c1M connection park (c10k D1)#427
TinDang97 merged 2 commits into
mainfrom
fix/c10k-timeout-park-d1

Conversation

@TinDang97

@TinDang97 TinDang97 commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Problem (c10k hardening finding D1)

Setting timeout N — the only slowloris knob moon ships — silently disabled the entire c1M connection park.

timeout was enforced by a select! arm racing the idle read against sleep(timeout), and that arm sat first in the read loop's if/else chain (handler_monoio/mod.rs). Taking it made the stage-1 downshift, the stage-2 park and task-exit parking all structurally unreachable: every connection silently reverted from the parked footprint to its full working set. The regression was invisible — no error, no log, no failing test — and it fired in exactly the deployments that hardened against slowloris.

Fix

Enforcement moves off the per-connection timer and onto the existing 1 Hz shard chore, via client_registry::kill_idle_clients(shard, timeout_secs, now_ms). The registry already carried last_cmd_ms, kill_flag and kill_fd; the sweep reuses them and closes idle clients with shutdown(SHUT_RDWR), which works on parked connections precisely because it does not need the handler task to be alive. The read loop keeps no timeout arm at all, so the park engages unconditionally.

Redis parity (clientsCronHandleTimeout): blocked, subscriber and replica clients are exempt.

Prerequisite bug, included here

ClientFlags::blocked was hardcoded false at every call site. A naive sweep would therefore have closed BLPOP key 0 clients as idle. This PR adds ClientLiveState::set_blocked() and drives it around the blocking await on both runtimes. ClientFlags also gains replica (bit 3) for the exemption — deliberately not added to to_flag_str, so CLIENT LIST output stays byte-identical.

Observability

INFO clients gains a parked_clients gauge (RAII-counted in the park watcher). This is what makes the claim CI-assertable instead of RSS-inferred.

Verification

RED/GREEN. Restoring the timeout arm fails the regression test:

park_still_engages_with_timeout_set:
  with `timeout 60` set, the idle connection must still task-park
  (parked_clients=0) — this is the D1 regression

Linux memory A/B (moon-dev VM, 500 idle conns, --conn-park-secs 2, 12s settle, /proc/<pid>/status VmRSS):

leg timeout RSS delta per-conn parked_clients
before (D1 arm present) 60 24,056 kB 49,266 B 0
after (registry sweep) 60 4,800 kB 9,830 B 500
after (control) 0 5,396 kB 11,051 B 500

A second run gave 49,201 / 12,443 / 10,772 B — same shape. 4–5× per-connection reduction, and the timeout=60 leg now sits inside the noise band of the timeout=0 control (it beat the control in one run, which reads as equivalent, not cheaper).

Absolute numbers are scale-specific: 500 conns on one shard amortizes fixed overhead only 500 ways, hence ~10 kB/conn rather than the 3.25 kB c1M headline. Run-to-run allocator noise is ±1–2 kB/conn; the before→after gap is ~5× that.

Tests. New tests/idle_timeout_sweep.rs (5 e2e):

  • park_still_engages_with_timeout_set — the D1 regression test
  • idle_connection_is_closed_at_timeout — the policy still fires
  • active_connection_is_never_closed
  • blocked_client_is_exempt_from_timeout — covers the prerequisite bug
  • subscriber_is_exempt_from_timeout

Plus 8 unit tests in client_registry, each on its own synthetic shard id (the registry is process-global and cargo runs tests in parallel; the sweep's existing entry.shard filter provides the isolation).

Gates. fmt --check; clippy --all-targets zero warnings on default and runtime-tokio,jemalloc; lib tests 4452 monoio / 3617 tokio; idle_timeout_sweep 5/5 on both runtimes; existing park suites unchanged — parked_idle_parity (5), idle_downshift_parity (2), tls_idle_downshift_parity (3).

Notes

Summary by CodeRabbit

  • New Features
    • Added parked_clients to INFO clients to improve visibility into parked connections.
  • Bug Fixes
    • Idle connections are now closed reliably based on the configured timeout, including parked connections.
    • Blocking (BL*), subscribers, and replica connections are exempt from idle timeouts.
    • CLIENT LIST / CLIENT INFO now correctly reflect the blocked state.
  • Tests
    • Added/expanded integration coverage for idle timeout behavior, parked connections, blocked clients, subscribers, and replicas.
  • Documentation
    • Refined the timeout N entry in the Unreleased changelog with more operational details.

@coderabbitai

coderabbitai Bot commented Jul 30, 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: 20 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: 1948842c-6767-4e29-a71a-8363512f9240

📥 Commits

Reviewing files that changed from the base of the PR and between 4c25c9d and afbcf9c.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • src/client_registry.rs
  • src/command/connection.rs
  • src/server/conn/handler_monoio/dispatch.rs
  • src/server/conn/handler_monoio/mod.rs
  • src/server/conn/handler_sharded/dispatch.rs
  • src/server/conn/handler_sharded/mod.rs
  • src/shard/conn_accept.rs
  • src/shard/event_loop.rs
  • tests/idle_timeout_sweep.rs
📝 Walkthrough

Walkthrough

Idle timeout enforcement moves from per-connection read loops to shard-level registry sweeps for both runtimes. Blocked and replica states are tracked, parked connections are counted and exposed through INFO clients, and integration tests cover timeout and exemption behavior.

Changes

Idle timeout lifecycle

Layer / File(s) Summary
Registry state and idle sweep
src/client_registry.rs
Client flags track blocked and replica state; parked clients are counted; striped sweeps close eligible connections while exempting subscribers, blocked clients, and replicas.
Handler timeout and blocking flow
src/server/conn/handler_sharded/*, src/server/conn/handler_monoio/*
Per-connection timeout branches are removed, and blocking commands use RAII guards for registry blocked state.
Sweep scheduling and parked visibility
src/shard/event_loop.rs, src/shard/conn_accept.rs, src/command/connection.rs
Both runtimes invoke idle sweeps on shard ticks, parked watcher accounting handles drops and killed connections, and INFO clients reports parked_clients.
Integration validation and release notes
tests/idle_timeout_sweep.rs, CHANGELOG.md, .gitignore
Integration tests cover idle, active, parked, blocked, and subscriber connections; release notes and the Linux VM target ignore are updated.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Handler
  participant ShardEventLoop
  participant ClientRegistry
  participant ParkedWatcher
  Client->>Handler: Send commands or remain idle
  Handler->>ClientRegistry: Track activity and blocked state
  ShardEventLoop->>ClientRegistry: Run kill_idle_clients on 1-second tick
  ClientRegistry->>ParkedWatcher: Mark and force-close eligible idle connection
  ParkedWatcher->>Client: Close killed parked connection
Loading

Possibly related PRs

Suggested reviewers: pilotspacex-byte

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: moving timeout enforcement so timeout N no longer breaks connection parking.
Description check ✅ Passed The description is thorough and covers the problem, fix, verification, and notes, though it doesn't follow the exact template headings.
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.
✨ 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/c10k-timeout-park-d1

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 idle timeout so timeout N no longer disables connection parking (c10k D1)

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Enforce timeout N via per-shard registry sweep, keeping c1M connection parking reachable.
• Add Redis-parity exemptions (blocked/subscriber/replica) and fix blocked flag wiring.
• Expose INFO clients.parked_clients and add regression tests for parking + timeout behavior.
Diagram

graph TD
A["Shard chore (1Hz)"] --> B["client_registry::kill_idle_clients"] --> C["shutdown(fd)"]
D["Conn handler loop"] --> E["ClientLiveState (flags/last_cmd)"] --> B
D --> G["Task-exit park watcher"] --> H["parked_clients gauge"] --> I["INFO clients"]
D --> F["Blocking await"] --> E

subgraph Legend
  direction LR
  _svc(["Service/loop"]) ~~~ _mod["Module/state"] ~~~ _obs["Metric output"]
end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep per-connection timeout but reorder control-flow
  • ➕ More precise close time (no 1Hz sweep jitter)
  • ➕ Avoids iterating registry stripes each second
  • ➖ Still risks reintroducing control-flow shadowing of park stages
  • ➖ Harder to ensure parity across runtimes and live CONFIG updates
  • ➖ Doesn't naturally reach task-exit parked connections unless special-cased
2. Central timer wheel scheduling per connection
  • ➕ Precise deadlines without per-connection sleep futures
  • ➕ Can be more efficient than full sweeps at high client counts
  • ➖ Significantly more complex and stateful (update/cancel on activity)
  • ➖ More moving parts and potential correctness hazards than Redis-like cron
  • ➖ Still needs careful integration with parked/task-exit states
3. Close-on-read-path only (cooperative kill flag)
  • ➕ Minimal syscall usage; avoids shutdown(fd) in sweep
  • ➕ Simpler in terms of cross-platform support
  • ➖ Cannot close connections that are parked in read() or task-exit parked
  • ➖ Makes timeout weaker against slowloris-style idle sockets

Recommendation: The chosen approach (Redis-style periodic sweep in the shard chore) is the best trade-off here: it removes the structural interaction between timeout and parking, applies uniformly to both runtimes, supports live CONFIG SET timeout by re-reading config each tick, and can close sockets even when the handler is parked or has exited (task-exit parking). The 1-second enforcement jitter is acceptable and matches upstream Redis behavior.

Files changed (11) +694 / -52

Enhancement (2) +32 / -0
connection.rsExpose parked_clients in INFO clients +9/-0

Expose parked_clients in INFO clients

• Extends 'INFO clients' output with a 'parked_clients' line sourced from the client registry gauge for direct operational visibility and CI assertions.

src/command/connection.rs

conn_accept.rsRAII-count task-exit parked connections for parked_clients gauge +23/-0

RAII-count task-exit parked connections for parked_clients gauge

• Adds a monoio/unix-only RAII counter in the parked watcher so 'parked_clients' remains accurate even if the watcher future is dropped during runtime teardown.

src/shard/conn_accept.rs

Bug fix (6) +369 / -52
client_registry.rsAdd idle-timeout sweep, replica flag, blocked-bit setter, and parked gauge +296/-1

Add idle-timeout sweep, replica flag, blocked-bit setter, and parked gauge

• Introduces 'kill_idle_clients(shard, timeout_secs, now_ms)' to enforce 'timeout N' via the registry, including exemptions for subscribers, blocked clients, and replication links. Adds 'ClientLiveState::set_blocked()' to correctly drive the blocked flag, adds a replica-only exemption bit, and implements a process-global 'parked_clients' gauge with unit tests.

src/client_registry.rs

dispatch.rsPropagate replica flag and wire blocked tracking for monoio blocking commands +11/-0

Propagate replica flag and wire blocked tracking for monoio blocking commands

• Ensures 'ClientFlags' includes 'replica: conn.saw_replconf' at touch sites and marks clients blocked across the blocking command await so the timeout sweep can exempt them correctly.

src/server/conn/handler_monoio/dispatch.rs

mod.rsRemove per-connection idle timeout select arm to restore parking +23/-29

Remove per-connection idle timeout select arm to restore parking

• Deletes the 'select!'-based read timeout arm that shadowed all parking stages when 'timeout' was set, and updates the blocking path call to pass 'client_live' for blocked-bit toggling.

src/server/conn/handler_monoio/mod.rs

dispatch.rsPropagate replica flag into registry touch for tokio/sharded runtime +4/-0

Propagate replica flag into registry touch for tokio/sharded runtime

• Updates admin touch sites to include the replica exemption bit, keeping timeout sweep behavior aligned with Redis.

src/server/conn/handler_sharded/dispatch.rs

mod.rsRemove tokio per-connection timeout wrapper and mark blocked during awaits +17/-22

Remove tokio per-connection timeout wrapper and mark blocked during awaits

• Eliminates the 'tokio::time::timeout' wrapper (which read config only at connection setup and lacked replica exemptions) and toggles the blocked bit around blocking command waits to match Redis timeout exemptions.

src/server/conn/handler_sharded/mod.rs

event_loop.rsInvoke idle-timeout sweep from shard 1Hz chores on both runtimes +18/-0

Invoke idle-timeout sweep from shard 1Hz chores on both runtimes

• Calls 'client_registry::kill_idle_clients' from the existing 1-second shard chore loops, re-reading 'runtime_config.timeout' each tick so live config changes apply to active connections.

src/shard/event_loop.rs

Tests (1) +263 / -0
idle_timeout_sweep.rsAdd end-to-end regression tests for timeout sweep and parking +263/-0

Add end-to-end regression tests for timeout sweep and parking

• Adds an integration test suite that verifies (1) parking still engages with 'timeout' set (via 'INFO clients.parked_clients' where supported), (2) idle connections are closed around the configured deadline, and (3) blocked and subscriber clients are exempt from timeout closure.

tests/idle_timeout_sweep.rs

Documentation (1) +29 / -0
CHANGELOG.mdDocument D1 timeout/parking fix and new parked_clients gauge +29/-0

Document D1 timeout/parking fix and new parked_clients gauge

• Adds Unreleased entries describing the timeout enforcement move to a shard sweep, Redis-parity exemptions, and the new 'INFO clients.parked_clients' metric for observability and testability.

CHANGELOG.md

Other (1) +1 / -0
.gitignoreIgnore additional tokio build target directory +1/-0

Ignore additional tokio build target directory

• Adds '/target-tokio2/' to ignored build artifacts to prevent accidental check-ins.

.gitignore

@qodo-code-review

qodo-code-review Bot commented Jul 30, 2026

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Idle sweep scales with shards ✓ Resolved 🐞 Bug ➹ Performance
Description
kill_idle_clients() scans every registry stripe and every entry, then filters by entry.shard;
because it’s called once per shard every second, total work becomes O(num_shards * total_clients)
per second and repeatedly holds read locks that can delay register/deregister (write locks). This
contradicts the comment that “work is distributed” and can become a significant CPU/lock-contention
cost at c10k/c1M scales.
Code

src/client_registry.rs[R460-483]

+    for lock in REGISTRY.iter() {
+        let guard = lock.read();
+        for entry in guard.values() {
+            if entry.shard != shard || entry.live.is_killed() {
+                continue;
+            }
+            let flags = ClientFlags::from_bits(entry.live.flags.load(Ordering::Relaxed));
+            if flags.subscriber || flags.blocked || flags.replica {
+                continue;
+            }
+            // `last_cmd_ms` is ms-since-connect of the last completed batch,
+            // so a connection that has never completed one reads 0 and is
+            // idle since it connected — which is what we want for a client
+            // that opens a socket and never speaks.
+            let idle_ms = now_epoch_ms
+                .saturating_sub(entry.live.connected_at_epoch_ms)
+                .saturating_sub(entry.live.last_cmd_ms.load(Ordering::Relaxed));
+            if idle_ms >= timeout_ms {
+                entry.live.kill_flag.store(true, Ordering::Relaxed);
+                force_close_fd(entry.live.kill_fd);
+                count += 1;
+            }
+        }
+    }
Relevance

●● Moderate

Perf scan-bounding suggestions mixed; SCAN O(COUNT) change rejected in PR#380, similar
sweep-bounding suggestion in PR#259 unresolved.

PR-#380
PR-#259

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The registry is striped by client id modulo STRIPES, so entries from all shards are mixed in each
stripe; kill_idle_clients() still walks all stripes and all entries, only skipping non-matching
shards after reading them. Since each shard’s 1 Hz chore calls kill_idle_clients(shard_id, ...),
the same global scan work is repeated per shard every second.

src/client_registry.rs[19-38]
src/client_registry.rs[427-485]
src/shard/event_loop.rs[1699-1710]
src/shard/event_loop.rs[2458-2472]

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

## Issue description
`client_registry::kill_idle_clients()` currently iterates every `REGISTRY` stripe and all entries, then filters by `entry.shard`. Since it is invoked once per shard every second, this multiplies the full scan by `num_shards`, causing avoidable CPU work and repeated read-lock holding that can delay accept/close paths needing stripe write locks.

## Issue Context
The registry is striped by `id % STRIPES` (not by shard), so scanning all stripes is a global scan regardless of the `entry.shard` filter.

## Fix Focus Areas
- Prefer one global sweep per second (e.g., only shard 0 runs it) OR change the sweep ownership model so each shard only scans a disjoint subset of stripes/entries.
- Keep the existing exemptions (subscriber/blocked/replica) and fd-liveness invariants intact.

## Fix Focus Areas (code pointers)
- src/client_registry.rs[19-38]
- src/client_registry.rs[427-485]
- src/shard/event_loop.rs[1699-1710]
- src/shard/event_loop.rs[2458-2472]

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



Remediation recommended

2. dir.to_str().unwrap() lacks allow 📘 Rule violation ✧ Quality
Description
The new test uses .unwrap() without the required #[allow(clippy::unwrap_used)] and adjacent
justification comment. This violates the policy requiring explicit annotation/justification for each
.unwrap() in diffs (including tests).
Code

tests/idle_timeout_sweep.rs[R41-43]

+            "--dir",
+            dir.to_str().unwrap(),
+            "--disk-free-min-pct",
Relevance

●● Moderate

No clear historical enforcement for unwrap_used allows in tests; some uses exist but not
consistently reviewed.

PR-#339
PR-#421

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 302083 requires every .unwrap() in changed Rust code (including tests) to be
accompanied by an immediately-adjacent #[allow(clippy::unwrap_used)] and a one-line justification
comment directly above it. The added dir.to_str().unwrap() has no such allow/comment in scope.

Rule 302083: Annotate safe unwrap calls with allow and justification
tests/idle_timeout_sweep.rs[34-49]

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/idle_timeout_sweep.rs` contains an `.unwrap()` call that is not covered by a `#[allow(clippy::unwrap_used)]` with an immediately preceding single-line justification comment.

## Issue Context
The unwrap is used to convert `dir` to `&str` for `Command::args([...])`. This can be avoided by using `Command::arg(dir)`/`OsStr` directly, or kept with the required allow+comment pair.

## Fix Focus Areas
- tests/idle_timeout_sweep.rs[34-54]

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


3. Timeout spawns parked handlers 🐞 Bug ➹ Performance
Description
When a task-exit parked connection hits timeout, kill_idle_clients() sets kill_flag and calls
shutdown(SHUT_RDWR), which wakes the parked-idle watcher; the watcher always spawns a full resumed
handler on wake. If many task-exit parked connections time out together, this can create a burst of
short-lived handler tasks created solely to observe kill_flag and exit.
Code

src/client_registry.rs[R474-481]

+            let idle_ms = now_epoch_ms
+                .saturating_sub(entry.live.connected_at_epoch_ms)
+                .saturating_sub(entry.live.last_cmd_ms.load(Ordering::Relaxed));
+            if idle_ms >= timeout_ms {
+                entry.live.kill_flag.store(true, Ordering::Relaxed);
+                force_close_fd(entry.live.kill_fd);
+                count += 1;
+            }
Relevance

●● Moderate

No historical evidence found about avoiding wake→spawn bursts for parked connections on idle
shutdown paths.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The timeout sweep triggers shutdown(SHUT_RDWR) for idle connections, which makes the parked
watcher’s park_readable() select arm fire; on any wake the watcher calls
spawn_resumed_parked_conn(...) rather than closing in-place. This ties idle-timeout closure of
task-exit parked sockets to handler task spawning overhead.

src/client_registry.rs[474-481]
src/shard/conn_accept.rs[1072-1106]
src/shard/conn_accept.rs[1110-1152]

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

## Issue description
For task-exit parked connections, idle-timeout closing currently forces a wake via `shutdown(SHUT_RDWR)`, and the parked watcher unconditionally spawns a resumed handler on wake. If the wake was triggered by an out-of-band kill (idle timeout / CLIENT KILL), spawning a full handler is unnecessary overhead.

## Issue Context
`kill_idle_clients()` uses `force_close_fd()` (shutdown) to wake parked reads/watchers. The parked watcher resumes by calling `spawn_resumed_parked_conn(...)` without checking whether the connection was marked killed.

## Fix Focus Areas
- In `spawn_parked_idle_watcher`, after `woke == true`, check whether the connection is killed (e.g., `crate::client_registry::is_killed(client_id)` or a lock-free live handle if available).
- If killed, directly drop `registry_guard` + `stream` and record the close metric, instead of spawning `spawn_resumed_parked_conn`.

## Fix Focus Areas (code pointers)
- src/client_registry.rs[474-481]
- src/shard/conn_accept.rs[1072-1106]
- src/shard/conn_accept.rs[1110-1187]

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



Informational

4. idle_timeout_sweep uses find_moon_binary() 📘 Rule violation ▣ Testability
Description
The new integration test spawns the server via common::find_moon_binary(), which can fall back to
target/{release,debug}/moon instead of requiring MOON_BIN. This violates the requirement that
integration tests explicitly set and use MOON_BIN, preventing accidental use of the wrong
binary/profile/target-dir in CI and local runs.
Code

tests/idle_timeout_sweep.rs[R34-36]

+fn spawn(dir: &std::path::Path, port: u16, timeout_secs: &str) -> Child {
+    Command::new(common::find_moon_binary())
+        .args([
Relevance

● Weak

Team previously rejected “require MOON_BIN” and kept find_moon_binary fallbacks in tests (PR#421,
PR#216); harness standardized it (PR#376).

PR-#421
PR-#376
PR-#216

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 992389 requires integration tests that spawn a moon server to set and use
MOON_BIN explicitly and to avoid helpers that fall back to target/release/moon. The test calls
common::find_moon_binary() when constructing the Command, indicating it may run without an
explicitly-set MOON_BIN.

Rule 992389: Integration tests must set MOON_BIN explicitly for server binaries
tests/idle_timeout_sweep.rs[34-36]
tests/idle_timeout_sweep.rs[104-107]

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/idle_timeout_sweep.rs` starts the server using `common::find_moon_binary()`, which can fall back to guessed paths when `MOON_BIN` is not set. The compliance rule requires integration tests to require and use `MOON_BIN` explicitly.

## Issue Context
This test currently calls `common::find_moon_binary()` (which may use fallbacks) in both `spawn()` and `server()`.

## Fix Focus Areas
- tests/idle_timeout_sweep.rs[34-117]

ⓘ 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 tests/idle_timeout_sweep.rs
Comment thread src/client_registry.rs Outdated
Comment thread src/client_registry.rs

@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

🧹 Nitpick comments (1)
tests/idle_timeout_sweep.rs (1)

104-117: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor duplication: dir path recomputed instead of reused.

server() builds the same moon-{tag}-{port} path twice — once inside the spawn_listening closure (Line 111) and again afterward (Line 115) — relying on both computations staying in sync. Consider capturing the value once (e.g., via a RefCell/return from the closure or restructuring spawn_listening to hand back the dir) to avoid the duplicate format!+join logic drifting apart later.

♻️ Illustrative approach
-    let (child, port) = common::spawn_listening(|port| {
-        let dir = std::env::temp_dir().join(format!("moon-{tag}-{port}"));
-        let _ = std::fs::create_dir_all(&dir);
-        spawn(&dir, port, timeout_secs)
-    });
-    let dir = std::env::temp_dir().join(format!("moon-{tag}-{port}"));
+    let dir_cell = std::cell::RefCell::new(std::path::PathBuf::new());
+    let (child, port) = common::spawn_listening(|port| {
+        let dir = std::env::temp_dir().join(format!("moon-{tag}-{port}"));
+        let _ = std::fs::create_dir_all(&dir);
+        *dir_cell.borrow_mut() = dir.clone();
+        spawn(&dir, port, timeout_secs)
+    });
+    let dir = dir_cell.into_inner();
🤖 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/idle_timeout_sweep.rs` around lines 104 - 117, Update server so the
temporary directory path is computed once and reused by both the spawn_listening
closure and the returned Server; restructure the closure or spawn_listening
interaction as needed to retain that single dir value, while preserving the
existing spawn and Server behavior.
🤖 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/client_registry.rs`:
- Around line 404-486: Update kill_idle_clients in
src/client_registry.rs:404-486 to use a shard-partitioned registry or per-shard
secondary index so each sweep iterates only that shard’s clients, rather than
scanning every stripe and filtering entry.shard. Preserve the existing timeout,
exemption, and close behavior. The calls in src/shard/event_loop.rs:1702-1710
and src/shard/event_loop.rs:2464-2472 require no direct change; they should
automatically become proportional to the invoking shard’s client set.

In `@src/server/conn/handler_sharded/mod.rs`:
- Around line 999-1006: Replace the manual blocked-state toggles with a shared
RAII guard that clears the flag on drop, adding the guard API near
ClientLiveState in src/client_registry.rs. Update both blocking
paths—src/server/conn/handler_sharded/mod.rs lines 999-1006 and
src/server/conn/handler_monoio/dispatch.rs lines 1656-1674—to use the guard
around their awaited blocking-command calls; both sites require the replacement.

---

Nitpick comments:
In `@tests/idle_timeout_sweep.rs`:
- Around line 104-117: Update server so the temporary directory path is computed
once and reused by both the spawn_listening closure and the returned Server;
restructure the closure or spawn_listening interaction as needed to retain that
single dir value, while preserving the existing spawn and Server behavior.
🪄 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 Plus

Run ID: 965b922a-614b-4312-9211-a9ff08836a8d

📥 Commits

Reviewing files that changed from the base of the PR and between a7b6174 and 7477238.

📒 Files selected for processing (11)
  • .gitignore
  • CHANGELOG.md
  • src/client_registry.rs
  • src/command/connection.rs
  • src/server/conn/handler_monoio/dispatch.rs
  • src/server/conn/handler_monoio/mod.rs
  • src/server/conn/handler_sharded/dispatch.rs
  • src/server/conn/handler_sharded/mod.rs
  • src/shard/conn_accept.rs
  • src/shard/event_loop.rs
  • tests/idle_timeout_sweep.rs

Comment thread src/client_registry.rs
Comment thread src/server/conn/handler_sharded/mod.rs Outdated
TinDang97 added a commit that referenced this pull request Jul 30, 2026
…427 review)

Three findings from the automated reviewers on #427, all real.

1. The idle sweep scaled with shard count. The registry is striped by
   `id % STRIPES`, NOT by shard, so filtering by `entry.shard` after walking
   every stripe meant each shard scanned the WHOLE registry once per second:
   O(num_shards x total_clients) per second, 12x the necessary work at
   --shards 12, all of it holding stripe read locks that contend with the
   write locks register/deregister need on accept and close. Each shard now
   sweeps a disjoint subset of stripes (i % num_shards == shard) and does not
   filter by entry.shard at all — one full pass per second regardless of
   shard count, genuinely distributed. Killing is safe from any thread
   (kill_flag is atomic, force_close_fd is a shutdown(2)); this is exactly
   what CLIENT KILL already does cross-shard.

   Unit tests keep their per-test synthetic shard isolation through a
   test-only entry point that walks all stripes and filters by shard, since
   production no longer does.

2. `blocked` was set and cleared as two statements around the blocking await.
   A panic or a dropped future (migration, cancellation) skipped the clear,
   and a client stuck with `blocked` set is exempt from `timeout` FOREVER —
   the same immortal-connection shape D1 exists to prevent. Replaced with an
   RAII `BlockedGuard` on both runtimes.

3. A parked connection closed by the sweep or by CLIENT KILL rehydrated a
   full handler task purely so its first read could observe kill_flag and
   exit. A fleet-wide timeout expiry wakes them in a burst. The watcher now
   checks is_killed on wake and closes directly, keeping the close accounting
   it already owns.

New tests: stripe_partition_is_disjoint_and_total (every stripe swept exactly
once per round across 1/2/3/4/12/16/24 shards),
idle_client_is_reaped_by_exactly_one_shard (never by all, never by none),
blocked_guard_clears_on_drop.

Gates (Linux VM): fmt; clippy -D warnings on default and
runtime-tokio,jemalloc; lib 4477 monoio / 3639 tokio; idle_timeout_sweep 5/5;
parked_idle_parity 5/5 and tls_idle_downshift_parity 3/3 — the latter two
cover the parked-kill path changed in (3), including
parked_connection_visible_and_killable.

author: Tin Dang

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

🤖 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/client_registry.rs`:
- Around line 164-191: Add a Loom model test covering the cross-thread atomic
behavior of ClientLiveState, including set_blocked and kill_flag, alongside the
existing ResponseSlot Loom coverage. Exercise concurrent updates and reads under
loom::model so the blocked and killed states remain correctly synchronized
without changing BlockedGuard behavior.
🪄 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 Plus

Run ID: 2ad3b4a7-c4a5-4a8d-b933-80cc8e3a868f

📥 Commits

Reviewing files that changed from the base of the PR and between 7477238 and 2fb7184.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • src/client_registry.rs
  • src/server/conn/handler_monoio/dispatch.rs
  • src/server/conn/handler_sharded/mod.rs
  • src/shard/conn_accept.rs
  • src/shard/event_loop.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md

Comment thread src/client_registry.rs
…n park (c10k D1)

The idle-timeout was enforced by a per-connection `select!` arm racing the
read against `sleep(timeout)`, and that arm sat FIRST in the read loop's
if/else chain:

    if let Some(dur) = idle_timeout { ... continue }
    else if conn.tracking_rx.is_some() { ... }
    else if downshifted { ...stage-2 park / task-exit park... }
    else if let Some(reg) = idle_reg { ...stage-1 downshift... }

Setting `timeout` therefore made the stage-1 downshift, the stage-2 park and
task-exit parking all structurally unreachable. Every connection silently
reverted from the parked footprint to its full working set — in exactly the
deployments that use the only slowloris knob moon ships. The v0.8.3/v0.8.4
headline feature was off and nothing said so; the code comment at the park
arm even stated the exclusion as if it were intentional.

Two further defects in the same arm: it read `runtime_config.timeout` once at
connection setup, so `CONFIG SET timeout` never reached a live connection; and
it had no exemption for replication links, which Redis exempts.

Enforcement moves to `client_registry::kill_idle_clients`, run once a second
by each shard's chore over the connections that shard owns. The registry
already had every piece: `last_cmd_ms` (touched unconditionally at the end of
every batch on both runtimes), `ClientEntry.shard`, and `force_close_fd` —
the CLIENT KILL mechanism, already documented to tear down a connection parked
in `read()`. So one sweep now covers plain and TLS, parked and unparked, and
task-exit-parked connections that no longer have a task to hold a `select!` —
with no per-connection timer and no new allocation. Redis enforces `timeout`
from serverCron the same way, so enforcement being up to one sweep interval
late matches upstream rather than diverging from it.

Prerequisite fix, included: `ClientFlags::blocked` was hardcoded `false` at
every `touch` call site, so the bit was never set. It was dead code behind
CLIENT LIST's `b` flag, and — the reason it matters here — a sweep would have
seen a client parked in `BLPOP key 0` as idle and closed it. Redis exempts
blocked clients, and so did moon implicitly (a blocked client never reached
the old arm). `set_blocked` now brackets the blocking await on both runtimes.
A `replica` bit is added for the same reason; it is exemption-only and is
deliberately not surfaced by `to_flag_str`, so CLIENT LIST output is
unchanged.

Also adds `parked_clients` to `INFO clients` — a real operational signal (how
much of the fleet is actually parked) and, for tests, a direct assertion that
parking engaged instead of inferring it from process RSS. Counted via an RAII
guard so the gauge stays accurate even when the watcher future is dropped
outright at runtime teardown.

Tests (red/green, verified failing first):
  - 8 unit tests for the sweep policy: idle-closed vs active-spared, timeout 0
    disabled, blocked/subscriber/replica exemptions, unblocking restores
    eligibility, set_blocked preserves other flag bits, per-shard isolation,
    idempotence. Each test uses its own synthetic shard id because the
    registry is process-global and cargo runs tests in parallel.
  - tests/idle_timeout_sweep.rs, 5 e2e cases against a real server. The D1
    case asserts `parked_clients >= 1` with `--timeout 60` set; restoring the
    old arm fails it with `parked_clients=0`.

Gates: cargo fmt --check; clippy --all-targets on monoio and on
runtime-tokio,jemalloc (zero warnings); lib 4452 monoio / 3617 tokio; the new
e2e green on both runtimes; parked_idle_parity, idle_downshift_parity and
tls_idle_downshift_parity all still green.

author: Tin Dang
…427 review)

Three findings from the automated reviewers on #427, all real.

1. The idle sweep scaled with shard count. The registry is striped by
   `id % STRIPES`, NOT by shard, so filtering by `entry.shard` after walking
   every stripe meant each shard scanned the WHOLE registry once per second:
   O(num_shards x total_clients) per second, 12x the necessary work at
   --shards 12, all of it holding stripe read locks that contend with the
   write locks register/deregister need on accept and close. Each shard now
   sweeps a disjoint subset of stripes (i % num_shards == shard) and does not
   filter by entry.shard at all — one full pass per second regardless of
   shard count, genuinely distributed. Killing is safe from any thread
   (kill_flag is atomic, force_close_fd is a shutdown(2)); this is exactly
   what CLIENT KILL already does cross-shard.

   Unit tests keep their per-test synthetic shard isolation through a
   test-only entry point that walks all stripes and filters by shard, since
   production no longer does.

2. `blocked` was set and cleared as two statements around the blocking await.
   A panic or a dropped future (migration, cancellation) skipped the clear,
   and a client stuck with `blocked` set is exempt from `timeout` FOREVER —
   the same immortal-connection shape D1 exists to prevent. Replaced with an
   RAII `BlockedGuard` on both runtimes.

3. A parked connection closed by the sweep or by CLIENT KILL rehydrated a
   full handler task purely so its first read could observe kill_flag and
   exit. A fleet-wide timeout expiry wakes them in a burst. The watcher now
   checks is_killed on wake and closes directly, keeping the close accounting
   it already owns.

New tests: stripe_partition_is_disjoint_and_total (every stripe swept exactly
once per round across 1/2/3/4/12/16/24 shards),
idle_client_is_reaped_by_exactly_one_shard (never by all, never by none),
blocked_guard_clears_on_drop.

Gates (Linux VM): fmt; clippy -D warnings on default and
runtime-tokio,jemalloc; lib 4477 monoio / 3639 tokio; idle_timeout_sweep 5/5;
parked_idle_parity 5/5 and tls_idle_downshift_parity 3/3 — the latter two
cover the parked-kill path changed in (3), including
parked_connection_visible_and_killable.

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