Skip to content

perf(server): c10k connection-plane wave — memory ratchet fix, loud maxclients, striped registry, deadline-heap blocking (W1-W8+T3) - #421

Merged
TinDang97 merged 17 commits into
mainfrom
feat/c10k-conn-plane
Jul 29, 2026
Merged

perf(server): c10k connection-plane wave — memory ratchet fix, loud maxclients, striped registry, deadline-heap blocking (W1-W8+T3)#421
TinDang97 merged 17 commits into
mainfrom
feat/c10k-conn-plane

Conversation

@TinDang97

@TinDang97 TinDang97 commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

Connection-plane scalability wave from the 2026-07-29 c10k/c10m investigation (tmp/C10K-REVIEW.md). Eight workstreams (W1–W8) plus the Tier-3 subset: fixes the permanent per-connection pipeline memory ratchet, makes maxclients rejection loud and early, deletes dead waker plumbing, stripes the global client registry, replaces the blocking-registry full walk with a deadline heap, rotates SPSC drain start, and load-gates the IP-affinity funnel. The c1M redesign (idle-conn parking, uring buf_ring) is written up as an RFC in .planning/rfcs/c1m-connection-plane.md (submodule commit 356b0a1).

A/B results (moon-dev VM, aarch64, shards=2, monoio release)

Metric main this branch Delta
Idle RSS/conn (10k conns) 56.5 KB 46.6 KB −17.6%
Post-pipeline ratchet RSS/conn (5k conns, one 1024-deep burst) ~217 KB permanent 47.0 KB (stable at +35s) −78%
Idle CPU @10k conns (30s hold) ~0.6% ~0.47% no regression
PING sweep across all 10k conns ok 0.18s, 0 failures liveness intact
RSS return on close-all ok ok (65–70 MB vs 54 MB server baseline) ok

The ratchet fix beat its ≤75 KB/conn expectation: the 16 KiB I/O-buffer shrink floor + batch-vec governor put post-burst footprint below the old pure-idle baseline.

Workstreams

  • W1 — pipeline memory ratchet (01491f09): responses/frames batch vecs shrink back to 64-cap when they exceed 256; I/O buffer shrink floor 64 KiB → 16 KiB; shrink placed at end-of-iteration so idle conns (parked in read()) actually reach it.
  • W2 — future diet (85d5c590): active_cross_txn: Option<Box<CrossStoreTxn>> removes 2.16 KB from every connection future; size_of::<ConnectionState>() <= 768 enforced by test.
  • W3 — maxclients parity (50dfefe8): monoio path now writes -ERR max number of clients reached before close (was a silent drop), gate hoisted before per-conn ctx construction; startup RLIMIT_NOFILE check vs maxclients + reserved fds. Integration test tests/maxclients_reject_parity.rs.
  • W4 — dead plumbing (030c6b1b): vestigial pending_wakers relay (per-conn clone, zero registrants since the flume-oneshot reply design) deleted end-to-end.
  • W5 — registry striping (bed94a34): global client registry → 16 RwLock stripes keyed by id % 16; CLIENT LIST walks one stripe at a time; per-stripe fd-shutdown liveness invariant preserved and documented; TOTAL_CLIENTS atomic.
  • W6 — blocking deadline heap (e3821e4c): BinaryHeap<Reverse<(Instant, usize, Bytes)>> lazy-invalidated index replaces the 100 Hz full-registry walk; only due queues are scanned.
  • W7 — SPSC drain fairness (e59c93e6): thread-local rotation cursor so the drain loop doesn't always start at consumer 0 (peer-shard starvation under sustained load).
  • W8 — affinity load gate (112f8bf7): IP-affinity routing skips a shard when count > 2*(total/n)+16 (per-shard atomic conn counters), bounding the ~2× funnel skew.
  • T3 subset (7a3f921a): loud startup warning that MOON_URING=1 tokio-bridge conns bypass maxclients + registry; CHANGELOG entry; c1M RFC.

Round 2 (same branch): roadmap continuation — P4a + P3 shipped

  • --uring-entries N (fdcd5d90): per-shard io_uring SQ size (monoio default 1024; also the legacy driver's event batch). Zero vendored-monoio changes — RuntimeBuilder::with_entries was already public. Clamps at monoio's 256 floor with a warning; loud no-op under tokio. Smoke-tested live (4096 → startup log + PONG).
  • Cold-future boxing (39203484): -Zprint-type-sizes showed the 58-variant connection future sized entirely by cold suspend points. Box::pin of TXN.ABORT rollback, leaked-txn teardown, and the FT.* body (behind their name-check fast paths, both handlers; never on the per-command filter chain) drops the per-conn monoio task allocation 9280 → 5952 B plain TCP / 13248 → 9920 B TLS. The hot cross-shard dispatch await is deliberately left inline.
  • New measurements folded into the RFC: TLS idle 91.8 KB/conn (vs 20–40 estimated), slope linear to 28k conns, parked-idle feasibility map (io_uring multishot = LARGE + UB blocker in monoio's lifecycle; legacy-driver parking = MEDIUM; poll-io feature lead).
  • Round-2 idle A/B: 46.6 → 43.5 KB/conn @10k. Campaign total: 56.5 → 43.5 KB/conn (−23%), ratchet gone.
  • Gates: tokio 3609 / VM monoio 4462 pass; fmt + both clippy matrices clean.

Round 3: W11 idle-buffer downshift

  • Two-stage idle park (b80c2bf9): connections parked in read() ≥1s are cancelled by the shard's existing 1s chore (thread-local Canceller registry; per park = two Cell stores + an Rc clone, zero hot-path allocs/timers) and re-park on a 512 B probe buffer, shedding the 8 KiB rent buffer + empty scratch. Cancel-and-await on the same pinned future is loss-free on both drivers (io_uring AsyncCancel, legacy READ_CANCELED) — a completion racing the cancel still delivers its bytes. TLS + tokio keep prior behavior via the IdleParkRead capability trait.
  • Idle RSS/conn 43.5 → 19.9 KB (10k-conn VM A/B; post-wake steady 27.8 KB). Campaign total: 56.5 → 19.9 KB/idle-conn (−65%).
  • GCE hardware proof (moon-bench-arm t2a-standard-8, same-instance A/B, 10k conns; 4K pages so absolutes differ from the 16K-page VM): pre-W11 idle 33.1 KB/conn flat forever vs W11 17.9 KB/conn (−46%) after 30 s idle — and back to 17.9 after a wake-sweep (downshift re-arms). Post-1024-pipeline leg settles at 19.4 KB/conn (was ~217 KB permanent pre-campaign). PING sweeps across all 10k conns: 0.11 s, 0 failures, both binaries; RSS returns to baseline on close.
  • Perf-neutral, verified on GCE (moon-bench-arm, pinned cores, 3 alternating rounds): GET p=1 −0.4%, busy-poll GET p=1 −0.2%, p=16 flat. (An OrbStack A/B showed a phantom −30% p=1 — VM noise, refuted by the pinned GCE run.)
  • Parity suite tests/idle_downshift_parity.rs (probe wake / 700 B pipeline / 4 KiB value / undisturbed active sibling) green on both kqueue and io_uring cancel paths.
  • TLS diet (P4b) root-caused for the RFC: monoio-io-wrapper's 2×16 KiB eager per-stream buffers = 32 KB of the measured 91.8 KB/TLS conn; fix deferred as a vendoring decision.

Round 4: P4b TLS diet — vendored wrapper stack

  • monoio-rustls 0.4.0 + monoio-io-wrapper 0.1.1 vendored (d2d01449; moon-patch style like vendor/monoio, wired via [patch.crates-io]): the wrapper's two eagerly-allocated, never-freed 16 KiB per-stream buffers are now lazy + released on idle downshift, and the TLS stream implements a loss-free CancelableAsyncReadRent — so W11's idle sweep now covers TLS connections end-to-end (SUPPORTS_IDLE_PARK = true + an on_idle_downshift hook that sheds both wrapper buffers; they reallocate lazily on the next I/O).
  • Correctness trap found in design: the wrapper's do_io stashes read errors for status replay; a stashed ECANCELED would resurface on the NEXT plain read and tear down a healthy connection. The cancelable path therefore returns errors WITHOUT stashing (unit-tested in the vendored crate, 5 tests).
  • E9 A/B (VM, 3000 idle TLS conns, shards=2): pre-P4b 87.5 KB/conn flat forever → P4b 47.4 KB/conn after 30 s idle (−46%, −40.1 KB), re-downshifts to 48.7 after a wake-sweep; PING sweeps bad=0; RSS returns on close. Matches the RFC's −30–40 KB estimate; the remaining floor is the rustls session (~15–20 KB) + 9.9 KB task future (Proposal 1 territory).
  • Parity suite tests/tls_idle_downshift_parity.rs: one continuous TLS session across three downshift/wake cycles (probe wake, 700 B pipeline, 4 KiB value), rustls client with a throwaway self-signed cert — green on kqueue and io_uring.
  • CI repair (d2f3e7cf): round 2's uring_entries config field broke three tokio-only integration suites (E0063) that no lib-suite gate compiles — this is what had the tokio Check job red. Fixed + the items_after_test_module clippy nit; all three suites verified green on the VM tokio matrix.

Test plan

  • Local tokio lib suite: 3607 passed / 0 failed
  • VM monoio release suite: 4460 passed / 0 failed
  • cargo fmt --check; clippy default + tokio,jemalloc matrices: 0 warnings
  • tests/maxclients_reject_parity.rs (reject message + slot-frees-on-disconnect)
  • E5 ratchet + E1b idle A/Bs on VM (tables above; harness in tmp/c10k/)

author: Tin Dang

Summary by CodeRabbit

  • New Features
    • Added --uring-entries to tune io_uring queue capacity for Monoio deployments.
    • Idle connections now reduce resource usage while remaining responsive when traffic resumes.
  • Bug Fixes
    • Connections rejected by maxclients now receive a clear error response.
    • Improved fairness when processing cross-shard requests.
    • Overloaded shards now fall back to balanced connection routing.
    • Blocked clients with expired deadlines are cleaned up more efficiently.
  • Performance
    • Reduced memory retention during large pipelines and long-lived connections.

TinDang97 added 10 commits July 29, 2026 12:28
…tate module (c10k W4)

The pending_wakers waker relay (Rc<RefCell<Vec<Waker>>>) had ZERO
registrants anywhere in the tree — the only waker push in the codebase is
runtime/cancel.rs on a different structure. It was retired as the
reply-wake mechanism when swf0 proved cross-thread oneshot wakes work
under monoio's `sync` feature (M2), but the plumbing survived: allocated
per shard, cloned per accepted connection (2 spawn paths + 2 migration
fail-open paths), threaded through 4 call sites, and drained twice per
event-loop iteration for nothing.

Also deletes src/server/conn_state.rs (209 lines): a duplicate
ConnectionState/ConnectionContext pair marked "Phase 44: Defined only",
never adopted, zero references.

Found by the 2026-07-29 c10k connection-plane review
(tmp/C10K-REVIEW.md); this is workstream W4 of tmp/C10K-PLAN.md.

Both runtimes compile clean (cargo check default + runtime-tokio,jemalloc).
CLAUDE.md monoio-waker note updated to match.

author: Tin Dang
Empirically confirmed (tmp/C10K-REVIEW.md E5): one 1024-deep pipeline
permanently ratcheted a connection from 56.5 KB to ~217 KB RSS until
disconnect — 5000 such conns pinned 1.09 GB, stable after 35s idle.
~147 KB of that is the `responses`/`frames` Vec<Frame> pair growing to
the 1024-frame batch cap (72 B/Frame) and only ever `.clear()`ing.

Fixes, monoio handler:
- Clear + shrink both batch scratch vecs at END of the batch iteration
  (before parking in read()), via new `util::shrink_batch_vec` — shrink
  fires only above 256-frame capacity, so p99 small batches never pay a
  realloc; a burst-then-idle conn now parks at steady-state capacity.
  (Shrinking at the top-of-loop clear sites is too late: an idle conn
  parks in read() before ever reaching them.)
- I/O buffer shrink floor lowered 64 KiB → 16 KiB (util::
  IO_BUF_SHRINK_TRIGGER): 16–64 KiB high-waters previously ratcheted
  until disconnect.
- Subscriber mode: rent the loop-level tmp_buf via mem::take instead of
  allocating + zeroing a fresh 8 KiB per select iteration (the
  `__memset_zva64` flame entry, 2.5% at c10k). The buffer is only lost
  when the pubsub arm wins (dropped read op owns it — io_uring cancel
  semantics); command traffic now allocates nothing.

Fixes, tokio handler:
- Same 16 KiB shrink floor for read/write buffers.
- Bump arena capped: reset() retains the largest chunk, so one huge
  batch pinned its high-water until disconnect; >64 KiB arenas are now
  rebuilt at 4 KiB.

Red/green: 3 new unit tests for shrink_batch_vec thresholds; conn suite
42/42 green; both runtimes compile clean. RSS A/B re-run of E5/E1
gates the branch before merge.

Workstream W1 of tmp/C10K-PLAN.md.

author: Tin Dang
…0k W2)

CrossStoreTxn is ~2.2 KB of inline SmallVec capacity (kv_undo
SmallVec<[UndoRecord;16]> alone is 1160 B). Stored unboxed as
Option<CrossStoreTxn> in ConnectionState, it sat inside EVERY
connection's ~21 KB task future — transacting or not — as part of the
measured 56.5 KB/idle-conn footprint (tmp/C10K-REVIEW.md §2).

Option<Box<CrossStoreTxn>> moves that to the heap for the rare
transacting connection only; TXN.BEGIN (cold path, not in the
no-alloc dispatch list) pays one allocation. Call-site churn is
minimal: field access auto-derefs, Option<&CrossStoreTxn> consumers
switch as_ref() → as_deref(), by-value abort/commit sites deref the
box.

New regression guard: connection_state_stays_small asserts
size_of::<ConnectionState>() ≤ 768 B (was ~2.7 KB).

txn (46) + transaction (39) suites green; both runtimes compile clean.
Workstream W2 of tmp/C10K-PLAN.md.

author: Tin Dang
…heck (c10k W3)

Redis parity break found by the 2026-07-29 c10k review: the monoio accept
paths rejected over-cap connections with a warn! and a silent close — a
client at the cap (default: exactly 10000) saw an unexplained EOF. The
tokio plain-TCP path already wrote the error; monoio (default runtime)
and both TLS paths did not.

- monoio: the maxclients CAS now runs BEFORE the per-connection
  clone-fest and ConnectionContext construction — a rejected connection
  previously paid 3 O(num_shards) Vec clones, ~25 Arc refcount bumps and
  a spawned handler task before the gate ran. On rejection moon now
  writes `-ERR max number of clients reached\r\n` best-effort, then
  closes. TLS keeps slot-before-handshake semantics (#17); the in-task
  gates are removed (slot taken pre-spawn, released at task end as
  before — rejected conns were never counted, so accounting is
  unchanged).
- tokio TLS: same best-effort error write on rejection (plaintext write
  surfaces client-side as a loud handshake failure, matching Redis's
  raw-fd behavior).
- startup (unix): getrlimit(RLIMIT_NOFILE) vs maxclients + reserved fds
  (rlimit_reserved_fds: 64 + 16/shard for listeners/WAL/AOF/spill/uring).
  Soft limit is raised toward the hard limit when short (logged); if the
  hard limit still cannot fit, a loud warning states the real client
  ceiling. Previously moon never looked at RLIMIT_NOFILE and a 10k
  target on a 1024-fd shell died in silent EMFILE accept-backoff loops.

Red/green: new tests/maxclients_reject_parity.rs —
rejected_connection_receives_err_max_clients (red before: silent EOF;
green after) and slot_frees_on_disconnect (guards the accounting
restructure). Unit test for rlimit_reserved_fds. Both runtimes compile.

Workstream W3 of tmp/C10K-PLAN.md.

author: Tin Dang
… W6)

BlockingRegistry::expire_timed_out ran at 100 Hz per shard and walked
EVERY queue of EVERY blocked waiter — two full passes (scan + retain) —
even when nothing was due: ~2M entry visits/s/shard at 10k blocked
BLPOP clients, quadratic when many waiters shared a key
(tmp/C10K-REVIEW.md defect #3). There was no deadline-ordered structure
at all.

Now a min-heap of (deadline, db_index, key) indexes every waiter that
HAS a deadline. The sweep pops due entries and scans only the queues
they name; block-forever waiters (BLPOP key 0) never enter the heap and
are never visited. Stale heap entries (waiter served or cancelled before
its deadline) pop to a no-op at their original deadline — bounded by
registration rate x timeout, the same envelope as the queues themselves.
Steady state with nothing due is a single peek.

expire_timed_out now returns the visited-entry count as the test/
observability surface.

Red/green: 4 new unit tests — sweep_skips_undeadlined_waiters (100
forever-waiters + 1 due => exactly 1 visit), shared-key partial expiry
preserves FIFO, served-waiter stale-entry no-op, multi-key waiter
expires from all queues. Full blocking suite 8/8.

Workstream W6 of tmp/C10K-PLAN.md.

author: Tin Dang
…(c10k W7)

drain_spsc_shared always began at consumers[0] with a shared 256-message
budget per cycle. Whenever the budget ran out before the tail was
reached — sustained cross-shard pressure from a hot peer — low-index
peers monopolized every cycle and high-index peers starved indefinitely
(R-5 from the 2026-07-06 conn-plane review, re-confirmed by the
2026-07-29 c10k review, tmp/C10K-REVIEW.md defect #4).

The drain now starts at a per-thread rotating index (thread-local Cell,
same pattern as the existing DRAIN_SCRATCH), so the budget cut-off
truncates a DIFFERENT tail each cycle and every peer is head-most once
per n cycles. SnapshotBegin early-exit semantics unchanged.

The visit order is extracted as rotated_indices(start, n) with unit
tests: full coverage per cycle, rotating head, n=0 safety. spsc suite
20/20, shard suite 191/191.

Workstream W7 of tmp/C10K-PLAN.md. (The give-up error path was already
clean — both slotted dispatch paths return `ERR cross-shard dispatch
backpressure` to the client — so W7 is rotation only.)

author: Tin Dang
The registry was one process-wide write-preferring RwLock<HashMap>:
every accept and every close serialized on it across all shard threads
(the only cross-shard serialization point on the connect path), and a
CLIENT LIST formatted all N clients under the read lock — blocking every
pending register/deregister on all shards for the duration (at 1M conns
that is a ~128 MB format under a global lock). CLIENT KILL scanned O(N)
even for KILL ID on an id-keyed map. (tmp/C10K-REVIEW.md defect #2.)

Now 16 stripes keyed by id % 16:
- register/deregister/update/is_killed: single-stripe locks (contention
  /16, and never blocked by a listing of the other 15 stripes).
- CLIENT LIST: walks one stripe at a time, pre-sized from a lock-free
  total counter. Cross-stripe ordering is not insertion order — Redis
  makes no CLIENT LIST ordering guarantee.
- CLIENT KILL ID: O(1) single-stripe lookup. ADDR/USER filters walk
  stripes one at a time; a conn registering into an already-visited
  stripe mid-walk is missed — the same inherent race the single-lock
  version had at command granularity.
- The R-3 fd-shutdown liveness invariant is preserved per stripe: an
  entry visible under its stripe's READ lock blocks that entry's
  deregister (same stripe's WRITE lock), so kill_fd is still open.

New cross-stripe test (list sees all stripes; KILL ID exactly one
victim; KILL USER reaches every stripe). client_registry suite 12/12.

Workstream W5 of tmp/C10K-PLAN.md.

author: Tin Dang
The AffinityTracker pins ALL central-accepted connections from an IP to
the shard where one of its connections migrated (key locality) or first
subscribed (pub/sub locality) — with zero load feedback. A saturated
shard was never removed from rotation, giving ~2x worst-case connection
skew from few client IPs, and FD migration compounded it
(tmp/C10K-REVIEW.md defect #5).

- client_registry now maintains readable per-shard connection counters
  (OnceLock<Box<[AtomicUsize]>>, initialized from startup with the
  resolved shard count; inert when uninitialized — tests/embedded).
  The metrics gauge could not serve this: gauges are write-only and
  gated on METRICS_INITIALIZED.
- All three listener affinity lookups filter the hint through
  shard_overloaded(): a shard above 2x the mean conn count (+16 floor,
  so tiny fleets never flap) falls back to round-robin instead of
  receiving further funneled connections. Affinity resumes once the
  shard drains below threshold.
- maybe_evict's sort stays as-is — review found it amortized ~60
  ops/insert past cap (fires once per 4096 inserts), not a real cost.

4 unit tests on the pure threshold predicate (balanced, at-threshold,
funneled, small-fleet floor, zero-shard safety).

Workstream W8 of tmp/C10K-PLAN.md.

author: Tin Dang
…g (c10k T3)

The experimental tokio->io_uring bridge accepts connections that bypass
maxclients and the client registry (CLIENT LIST/KILL blind). Full
accounting integration is deliberately unscheduled — the bridge is
documented as broken under sustained load — but the bypass now announces
itself at startup instead of being silent. c1M roadmap (parked-idle
conns, task-future diet, uring-entries knob, TLS caps) captured in
.planning/rfcs/c1m-connection-plane.md.

author: Tin Dang
rlimit math kept in rlim_t domain (kills useless u64::from lints);
batch-vec shrink helpers cfg-gated monoio-or-test (tokio-only build
saw them as dead code); cargo fmt over the new test/impl code.

author: Tin Dang
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR updates client admission and registry accounting, deadline-based blocked-client expiration, boxed cross-store transactions, io_uring configuration, connection memory management, Monoio idle handling, vendored TLS I/O, SPSC drain fairness, and removal of pending-waker plumbing.

Changes

Runtime connection updates

Layer / File(s) Summary
Heap-driven blocked-client expiration
src/blocking/mod.rs, src/shard/timers.rs
Deadline-indexed waiters are expired through a heap, with stale-entry handling, queue visit counts, and FIFO and multi-key tests.
Striped registry and connection admission
src/client_registry.rs, src/main.rs, src/shard/conn_accept.rs, tests/maxclients_reject_parity.rs
Client operations use striped locks and shard counters; admission checks configure file limits, send explicit errors, and tests cover rejection and slot reuse.
Load-gated affinity routing
src/server/listener.rs
Affinity-selected shards are skipped when overloaded, with round-robin fallback.
Boxed cross-store transaction state
src/server/conn/*
Cross-store transactions and related futures are boxed, call sites use dereferenced values, FT snapshot resolution uses as_deref(), and connection size is tested.
Connection memory management
src/server/conn/util.rs, src/server/conn/handler_monoio/*, src/server/conn/handler_sharded/*
Buffers, batch vectors, and arenas receive reuse, shrinking, and high-water-capacity controls.
Monoio idle-read downshift
src/server/conn/handler_monoio/idle_park.rs, src/server/conn/handler_monoio/mod.rs, src/shard/event_loop.rs, tests/*idle_downshift*
Idle reads are canceled and downshifted through a shard sweep, with protocol, TLS, and active-sibling integration coverage.
io_uring runtime configuration
src/config.rs, src/main.rs, src/runtime/*, tests/*
A CLI entry-size override is parsed, clamped, applied to Monoio builders, and reported by runtime.
Cross-thread signaling and drain fairness
CLAUDE.md, src/shard/event_loop.rs, src/shard/conn_accept.rs, src/shard/spsc_handler.rs
Pending-waker relay plumbing is removed, and SPSC drain traversal rotates its starting consumer.
Vendored Monoio buffer and TLS streams
Cargo.toml, vendor/monoio-io-wrapper/*, vendor/monoio-rustls/*
Vendored safe and unsafe I/O buffers and Rustls client/server streams implement Monoio-compatible reads, writes, cancellation, handshakes, and idle-buffer release.
Module cleanup and release notes
src/server/mod.rs, CHANGELOG.md
The obsolete connection-state export is removed, the embedded module is feature-gated, and the unreleased changelog is expanded.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Listener
  participant ClientRegistry
  participant Shard
  Client->>Listener: connect
  Listener->>ClientRegistry: check shard load and capacity
  ClientRegistry-->>Listener: accept or reject
  Listener->>Shard: route accepted connection
  Shard-->>Client: PONG or maxclients error
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is specific and matches the main changes: memory ratchets, maxclients, striped registry, deadline-heap blocking, and related follow-ups.
Description check ✅ Passed The description is detailed and relevant, but it doesn't follow the template fully because Checklist, Performance Impact, and Notes sections are not clearly provided.
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 feat/c10k-conn-plane

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

Scale the c10k connection plane and eliminate memory ratchets

🐞 Bug fix ✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Bounds per-connection memory after bursts and boxes rarely used transaction state.
• Scales admission, registries, blocking timeouts, and cross-shard fairness for c10k loads.
• Adds maxclients parity, load-aware routing, regression tests, and operational warnings.
Diagram

graph TD
  C["Client"] --> L["Listener"] --> A{"Capacity gate"} -->|accepted| R["Shard routing"] --> H["Connection loop"]
  A -->|ERR rejected| C
  H --> G["Striped registry"]
  H --> B["Deadline heap"]
  R --> S["Rotating drain"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Per-shard client registries
  • ➕ Eliminates shared registry locks from connection accept and close paths.
  • ➕ Naturally co-locates connection ownership and shard load accounting.
  • ➖ CLIENT LIST and broad CLIENT KILL require cross-shard aggregation.
  • ➖ Preserving raw-fd shutdown liveness becomes a distributed coordination concern.
  • ➖ Requires a larger protocol and ownership redesign than this scalability wave.
2. Hashed timer wheel
  • ➕ Provides near-constant-time timeout insertion and expiration at fixed precision.
  • ➕ Avoids stale binary-heap entries under high cancellation rates.
  • ➖ Adds bucket management, rollover, and deadline-precision complexity.
  • ➖ Offers limited benefit until timed waiter churn becomes materially larger.
  • ➖ Would require more invasive validation of multi-key waiter cancellation.

Recommendation: Retain the PR’s incremental approach for c10k: fixed registry striping and lazy deadline-heap invalidation materially reduce contention and sweep cost without redesigning connection ownership. Consider per-shard registries and a timer wheel only as part of the planned c1M architecture, where their additional coordination complexity can be addressed holistically.

Files changed (21) +915 / -134

Enhancement (4) +394 / -49
mod.rsDrive blocked-client expiration from a deadline heap +151/-7

Drive blocked-client expiration from a deadline heap

• Indexes finite blocking deadlines in a lazy-invalidated min-heap so timer sweeps visit only due queues. Adds coverage for indefinite waits, partial expiry, stale entries, FIFO preservation, and multi-key waits.

src/blocking/mod.rs

client_registry.rsStripe client tracking and expose shard load +233/-40

Stripe client tracking and expose shard load

• Replaces the global registry lock with sixteen stripes, adds atomic total and per-shard counts, and makes kill-by-ID a direct lookup. Adds tests for cross-stripe operations and affinity overload thresholds.

src/client_registry.rs

core.rsBox active cross-store transaction state +4/-1

Box active cross-store transaction state

• Moves the rarely used CrossStoreTxn allocation out of every connection future, reducing baseline ConnectionState size by roughly 2.2 KB.

src/server/conn/core.rs

timers.rsIntegrate heap-driven blocked-client expiry +6/-1

Integrate heap-driven blocked-client expiry

• Invokes the deadline-indexed expiration routine while intentionally discarding its observability-only visit count.

src/shard/timers.rs

Bug fix (7) +308 / -47
main.rsValidate file-descriptor capacity during startup +69/-0

Validate file-descriptor capacity during startup

• Attempts to raise the Unix RLIMIT_NOFILE soft limit for maxclients plus reserved descriptors and warns when the hard limit is insufficient. Initializes the per-shard connection counters used by routing.

src/main.rs

mod.rsReclaim monoio connection buffers after pipeline bursts +30/-11

Reclaim monoio connection buffers after pipeline bursts

• Reuses the main read buffer in subscriber mode, lowers I/O shrink thresholds, and shrinks oversized frame and response vectors before idle reads. Removes the obsolete pending-waker parameter.

src/server/conn/handler_monoio/mod.rs

mod.rsCap tokio connection arenas and I/O buffers +16/-4

Cap tokio connection arenas and I/O buffers

• Recreates oversized bump arenas and lowers the I/O buffer shrink threshold, preventing burst high-water allocations from persisting until disconnect. Adapts shutdown cleanup to boxed transactions.

src/server/conn/handler_sharded/mod.rs

util.rsAdd reusable connection-buffer capacity governors +71/-0

Add reusable connection-buffer capacity governors

• Introduces batch-vector steady-state and shrink thresholds plus a 16 KiB I/O shrink trigger. Unit tests verify oversized vectors shrink without disturbing normal capacities.

src/server/conn/util.rs

listener.rsLoad-gate IP-affinity shard routing +20/-6

Load-gate IP-affinity shard routing

• Rejects affinity hints targeting shards above the overload threshold and falls back to round-robin routing across listener variants.

src/server/listener.rs

conn_accept.rsReject maxclients early with an explicit error +44/-25

Reject maxclients early with an explicit error

• Moves monoio admission before connection-context cloning and task creation, reserves slots consistently across TLS and migration paths, and writes the Redis-compatible rejection message. Removes pending-waker propagation.

src/shard/conn_accept.rs

spsc_handler.rsRotate SPSC drain order for peer fairness +58/-1

Rotate SPSC drain order for peer fairness

• Uses a thread-local rotating start index so the fixed message budget cannot permanently starve higher-index consumers. Adds deterministic rotation and empty-input tests.

src/shard/spsc_handler.rs

Refactor (6) +29 / -37
ft.rsDereference boxed transactions for monoio search +3/-3

Dereference boxed transactions for monoio search

• Adapts full-text transaction snapshot lookups to the boxed active transaction representation.

src/server/conn/handler_monoio/ft.rs

txn.rsAllocate monoio transaction state only on demand +3/-3

Allocate monoio transaction state only on demand

• Boxes transaction state at TXN.BEGIN and moves the inner transaction into abort handling.

src/server/conn/handler_monoio/txn.rs

ft.rsDereference boxed transactions for tokio search +3/-3

Dereference boxed transactions for tokio search

• Updates full-text snapshot resolution to accept references to boxed cross-store transactions.

src/server/conn/handler_sharded/ft.rs

txn.rsAllocate tokio transaction state only on demand +3/-3

Allocate tokio transaction state only on demand

• Boxes transaction state at TXN.BEGIN and unwraps it when abort processing takes ownership.

src/server/conn/handler_sharded/txn.rs

mod.rsRemove the unused duplicate connection-state module +0/-1

Remove the unused duplicate connection-state module

• Stops exporting the deleted server::conn_state implementation, leaving the adopted connection model under server::conn.

src/server/mod.rs

event_loop.rsDelete dead waker sweeps and warn on bridge accounting gaps +17/-24

Delete dead waker sweeps and warn on bridge accounting gaps

• Removes the zero-registrant pending-waker relay from the shard loop and spawn calls. Emits a startup warning when the experimental io_uring bridge bypasses maxclients and client registry accounting.

src/shard/event_loop.rs

Tests (2) +140 / -0
tests.rsGuard the connection-state memory budget +13/-0

Guard the connection-state memory budget

• Adds a regression test requiring ConnectionState to remain at or below 768 bytes.

src/server/conn/tests.rs

maxclients_reject_parity.rsVerify explicit maxclients rejection and slot release +127/-0

Verify explicit maxclients rejection and slot release

• Adds process-level tests proving over-capacity clients receive the Redis-compatible error, existing clients remain healthy, and disconnected slots become reusable.

tests/maxclients_reject_parity.rs

Documentation (2) +44 / -1
CHANGELOG.mdDocument the c10k connection-plane scalability wave +43/-0

Document the c10k connection-plane scalability wave

• Records memory, admission, registry, blocking, fairness, affinity, and experimental io_uring behavior changes under Unreleased.

CHANGELOG.md

CLAUDE.mdRemove obsolete pending-waker guidance +1/-1

Remove obsolete pending-waker guidance

• Clarifies that the unused monoio waker relay was deleted and recommends flume oneshots for cross-thread signaling.

CLAUDE.md

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

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

Context used
✅ Compliance rules (platform): 55 rules

Grey Divider


Remediation recommended

1. rlimit SAFETY rationale incomplete 📘 Rule violation ≡ Correctness
Description
The new unsafe block's SAFETY comment does not state where each FFI precondition is established or
why violating it would cause undefined behavior, as required by project policy. The PR summary also
omits the policy-required Unsafe added: N blocks disclosure.
Code

src/main.rs[R627-630]

+        // SAFETY: getrlimit/setrlimit with a valid resource constant and a
+        // pointer to a properly initialized rlimit struct — plain libc FFI
+        // with no aliasing or lifetime concerns.
+        unsafe {
Relevance

⭐⭐⭐ High

PR 214 accepted strict UNSAFE_POLICY enforcement; PR 71 also prompted policy coverage review.

PR-#214
PR-#71

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 297369 requires new unsafe usage to satisfy UNSAFE_POLICY.md. The policy requires each SAFETY
comment to name exact preconditions, their source, and the UB consequence, while the added comment
only generally asserts that the resource and pointer are valid; the policy also requires an
unsafe-block count in the PR summary.

Rule 297369: Enforce unsafe code usage against UNSAFE_POLICY.md
src/main.rs[627-642]
UNSAFE_POLICY.md[21-25]
UNSAFE_POLICY.md[41-57]

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 new libc FFI block does not include the complete SAFETY rationale required by `UNSAFE_POLICY.md`, and the PR summary does not disclose the added unsafe block.

## Issue Context
Document the exact preconditions for both `getrlimit` and `setrlimit`, identify how the local values and libc constants establish them, and explain the undefined behavior risk. Also add the required `Unsafe added: 1 blocks` summary entry with a one-line justification.

## Fix Focus Areas
- src/main.rs[627-642]

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


2. Shared-key expiry becomes quadratic 🐞 Bug ➹ Performance
Description
Each due heap item identifies only a queue, so expire_timed_out rescans every remaining waiter for
that key on successive staggered expirations. A hot key with many blocked clients can therefore
accumulate O(n²) timer work and stall its shard despite the intended O(due) behavior.
Code

src/blocking/mod.rs[R188-192]

+            let std::cmp::Reverse((_, db_index, key)) = self.deadlines.pop().unwrap();
+            let queue_key = (db_index, key);
+            // A missing queue is a STALE heap entry (waiter served/cancelled
+            // before its deadline) — the lazy-invalidation no-op.
+            let Some(queue) = self.waiters.get_mut(&queue_key) else {
Relevance

⭐⭐⭐ High

PRs 230 and 251 accepted shard-stall performance findings affecting hot paths.

PR-#230
PR-#251

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Registration creates one heap item per timed waiter but stores only its deadline and queue key.
Expiration pops each item, retrieves the complete VecDeque for that key, and scans every entry;
deadlines d1 < d2 < ... on one shared key consequently visit n, n-1, and so on across successive
timer invocations.

src/blocking/mod.rs[109-129]
src/blocking/mod.rs[182-207]

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

## Issue description
Deadline heap entries identify only a queue, causing repeated full-queue scans as staggered waiters expire on the same key. Index each deadline by waiter identity or another direct-removal mechanism so expiration work scales with due waiters rather than queue length.

## Issue Context
Preserve FIFO ordering for non-expired waiters and lazy cancellation safety. Add a shared-key stress test that checks visit/removal work across staggered deadlines.

## Fix Focus Areas
- src/blocking/mod.rs[69-79]
- src/blocking/mod.rs[109-129]
- src/blocking/mod.rs[169-225]
- src/blocking/mod.rs[385-402]

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


3. Served deadlines retain memory 🐞 Bug ➹ Performance
Description
Serving or cancelling a waiter removes it from the waiter maps but leaves its heap entry and cloned
key until the original deadline. Promptly served requests with long timeouts therefore retain memory
at registration-rate × timeout even when no clients remain blocked.
Code

src/blocking/mod.rs[R115-117]

+        if let Some(deadline) = entry.deadline {
+            self.deadlines
+                .push(std::cmp::Reverse((deadline, db_index, key)));
Relevance

⭐⭐⭐ High

PRs 218 and 349 show strong acceptance of bounded-memory and truthful-retention fixes.

PR-#218
PR-#349

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The deadline heap takes ownership of a key for every timed registration. remove_wait, which wakeup
paths call after serving a waiter, removes only wait_keys and waiters; the heap entry remains
until expire_timed_out reaches its deadline.

src/blocking/mod.rs[109-159]
src/blocking/mod.rs[182-193]
src/blocking/wakeup.rs[19-102]

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

## Issue description
Timed waiters leave stale heap entries after being served or cancelled, retaining tuples and key storage until their original deadlines. Add eager removal, compact validity indexing, or bounded heap rebuilding so stale-entry memory tracks active waiters rather than the timeout horizon.

## Issue Context
The design must preserve efficient deadline lookup and safe cancellation. Add a test that repeatedly registers and serves long-timeout waiters and verifies stale storage is reclaimed or bounded.

## Fix Focus Areas
- src/blocking/mod.rs[69-79]
- src/blocking/mod.rs[109-159]
- src/blocking/mod.rs[182-193]
- src/blocking/wakeup.rs[19-102]

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


View more (1)
4. Rlimit failures stay silent 🐞 Bug ◔ Observability
Description
The startup check ignores failures from both getrlimit and setrlimit; when want == need but
raising the soft limit fails, the hard-limit warning is also skipped. The server then starts with an
insufficient effective descriptor ceiling and can still encounter unexplained EMFILE failures
under load.
Code

src/main.rs[R642-651]

+                    if libc::setrlimit(libc::RLIMIT_NOFILE, &raised) == 0 {
+                        tracing::info!(
+                            "Raised RLIMIT_NOFILE soft limit {} -> {} to fit maxclients {} (+{} reserved fds)",
+                            rl.rlim_cur,
+                            want,
+                            config.maxclients,
+                            reserved
+                        );
+                    }
+                    if want < need {
Relevance

⭐⭐ Medium

Silent-failure feedback was accepted in PR 291 but rejected in PRs 217 and 387.

PR-#291
PR-#217
PR-#387

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The code enters its logic only when getrlimit succeeds, logs only when setrlimit succeeds, and
warns only when the hard limit is numerically below need. There is no diagnostic branch for either
syscall failure, so a denied soft-limit raise with a sufficient hard limit is completely silent.

src/main.rs[620-665]

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 new startup RLIMIT check silently ignores syscall failures and may report neither a successful raise nor an insufficient effective ceiling. Handle both failure paths and report the actual usable client ceiling.

## Issue Context
On `setrlimit` failure, retain the original soft limit when calculating the effective ceiling. Choose an explicit policy such as warning, reducing the effective limit, or failing startup, and include the OS error in diagnostics.

## Fix Focus Areas
- src/main.rs[620-665]
- src/main.rs[2096-2103]

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



Informational

5. find_moon_binary() bypasses explicit MOON_BIN 📘 Rule violation ▣ Testability
Description
The new integration test starts the server through common::find_moon_binary(), which can fall back
to Cargo or default target paths when MOON_BIN is unset. This can execute an unintended or stale
server binary during testing.
Code

tests/maxclients_reject_parity.rs[16]

+    Command::new(common::find_moon_binary())
Relevance

⭐ Low

PR 216 rejected explicit-MOON_BIN feedback; PR 376 standardized find_moon_binary to avoid stale
binaries.

PR-#216
PR-#376

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 992389 expressly prohibits integration tests from relying on find_moon_binary() fallbacks.
The new test calls that helper, and its implementation falls back to CARGO_BIN_EXE_moon,
target/release/moon, and target/debug/moon.

Rule 992389: Integration tests must set MOON_BIN explicitly for server binaries
tests/maxclients_reject_parity.rs[15-16]
tests/common/mod.rs[133-160]

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 integration test resolves the server through `common::find_moon_binary()`, allowing fallback binaries when `MOON_BIN` is absent.

## Issue Context
PR Compliance ID 992389 requires integration tests that spawn Moon to obtain the binary explicitly from `MOON_BIN` and fail fast when it is unset.

## Fix Focus Areas
- tests/maxclients_reject_parity.rs[15-16]

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


Grey Divider

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

Qodo Logo

Comment thread src/main.rs
Comment on lines +627 to +630
// SAFETY: getrlimit/setrlimit with a valid resource constant and a
// pointer to a properly initialized rlimit struct — plain libc FFI
// with no aliasing or lifetime concerns.
unsafe {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. rlimit safety rationale incomplete 📘 Rule violation ≡ Correctness

The new unsafe block's SAFETY comment does not state where each FFI precondition is established or
why violating it would cause undefined behavior, as required by project policy. The PR summary also
omits the policy-required Unsafe added: N blocks disclosure.
Agent Prompt
## Issue description
The new libc FFI block does not include the complete SAFETY rationale required by `UNSAFE_POLICY.md`, and the PR summary does not disclose the added unsafe block.

## Issue Context
Document the exact preconditions for both `getrlimit` and `setrlimit`, identify how the local values and libc constants establish them, and explain the undefined behavior risk. Also add the required `Unsafe added: 1 blocks` summary entry with a one-line justification.

## Fix Focus Areas
- src/main.rs[627-642]

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

Comment thread src/blocking/mod.rs
Comment on lines +188 to +192
let std::cmp::Reverse((_, db_index, key)) = self.deadlines.pop().unwrap();
let queue_key = (db_index, key);
// A missing queue is a STALE heap entry (waiter served/cancelled
// before its deadline) — the lazy-invalidation no-op.
let Some(queue) = self.waiters.get_mut(&queue_key) else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

3. Shared-key expiry becomes quadratic 🐞 Bug ➹ Performance

Each due heap item identifies only a queue, so expire_timed_out rescans every remaining waiter for
that key on successive staggered expirations. A hot key with many blocked clients can therefore
accumulate O(n²) timer work and stall its shard despite the intended O(due) behavior.
Agent Prompt
## Issue description
Deadline heap entries identify only a queue, causing repeated full-queue scans as staggered waiters expire on the same key. Index each deadline by waiter identity or another direct-removal mechanism so expiration work scales with due waiters rather than queue length.

## Issue Context
Preserve FIFO ordering for non-expired waiters and lazy cancellation safety. Add a shared-key stress test that checks visit/removal work across staggered deadlines.

## Fix Focus Areas
- src/blocking/mod.rs[69-79]
- src/blocking/mod.rs[109-129]
- src/blocking/mod.rs[169-225]
- src/blocking/mod.rs[385-402]

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

Comment thread src/blocking/mod.rs
Comment on lines +115 to +117
if let Some(deadline) = entry.deadline {
self.deadlines
.push(std::cmp::Reverse((deadline, db_index, key)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

4. Served deadlines retain memory 🐞 Bug ➹ Performance

Serving or cancelling a waiter removes it from the waiter maps but leaves its heap entry and cloned
key until the original deadline. Promptly served requests with long timeouts therefore retain memory
at registration-rate × timeout even when no clients remain blocked.
Agent Prompt
## Issue description
Timed waiters leave stale heap entries after being served or cancelled, retaining tuples and key storage until their original deadlines. Add eager removal, compact validity indexing, or bounded heap rebuilding so stale-entry memory tracks active waiters rather than the timeout horizon.

## Issue Context
The design must preserve efficient deadline lookup and safe cancellation. Add a test that repeatedly registers and serves long-timeout waiters and verifies stale storage is reclaimed or bounded.

## Fix Focus Areas
- src/blocking/mod.rs[69-79]
- src/blocking/mod.rs[109-159]
- src/blocking/mod.rs[182-193]
- src/blocking/wakeup.rs[19-102]

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

Comment thread src/main.rs
Comment on lines +642 to +651
if libc::setrlimit(libc::RLIMIT_NOFILE, &raised) == 0 {
tracing::info!(
"Raised RLIMIT_NOFILE soft limit {} -> {} to fit maxclients {} (+{} reserved fds)",
rl.rlim_cur,
want,
config.maxclients,
reserved
);
}
if want < need {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

5. Rlimit failures stay silent 🐞 Bug ◔ Observability

The startup check ignores failures from both getrlimit and setrlimit; when want == need but
raising the soft limit fails, the hard-limit warning is also skipped. The server then starts with an
insufficient effective descriptor ceiling and can still encounter unexplained EMFILE failures
under load.
Agent Prompt
## Issue description
The new startup RLIMIT check silently ignores syscall failures and may report neither a successful raise nor an insufficient effective ceiling. Handle both failure paths and report the actual usable client ceiling.

## Issue Context
On `setrlimit` failure, retain the original soft limit when calculating the effective ceiling. Choose an explicit policy such as warning, reducing the effective limit, or failing startup, and include the OS error in diagnostics.

## Fix Focus Areas
- src/main.rs[620-665]
- src/main.rs[2096-2103]

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

@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/blocking/mod.rs`:
- Around line 182-210: Update the deadline sweep around the `self.deadlines`
loop to collect due `(db_index, key)` pairs in a `HashSet`, while still draining
due heap entries and preserving timeout IDs. After collection, scan each
corresponding `self.waiters` queue once so later or block-forever waiters are
not repeatedly rescanned; retain stale-entry handling and queue removal
behavior. Add a regression test covering mixed deadlines for multiple waiters
sharing one key.

In `@src/main.rs`:
- Around line 638-663: Update the RLIMIT_NOFILE handling around the setrlimit
call to explicitly detect a nonzero return and emit a warning containing the
failure details and requested limit, including when want >= need. Preserve the
existing success log and hard-limit warning behavior, while ensuring every
failed setrlimit attempt is surfaced to operators.
🪄 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: 6dc668ff-3ea8-44fd-adcf-fef104ae43eb

📥 Commits

Reviewing files that changed from the base of the PR and between ea7bd12 and 6fe1844.

📒 Files selected for processing (22)
  • CHANGELOG.md
  • CLAUDE.md
  • src/blocking/mod.rs
  • src/client_registry.rs
  • src/main.rs
  • src/server/conn/core.rs
  • src/server/conn/handler_monoio/ft.rs
  • src/server/conn/handler_monoio/mod.rs
  • src/server/conn/handler_monoio/txn.rs
  • src/server/conn/handler_sharded/ft.rs
  • src/server/conn/handler_sharded/mod.rs
  • src/server/conn/handler_sharded/txn.rs
  • src/server/conn/tests.rs
  • src/server/conn/util.rs
  • src/server/conn_state.rs
  • src/server/listener.rs
  • src/server/mod.rs
  • src/shard/conn_accept.rs
  • src/shard/event_loop.rs
  • src/shard/spsc_handler.rs
  • src/shard/timers.rs
  • tests/maxclients_reject_parity.rs
💤 Files with no reviewable changes (2)
  • src/server/conn_state.rs
  • src/server/mod.rs

Comment thread src/blocking/mod.rs
Comment on lines +182 to +210
while self
.deadlines
.peek()
.is_some_and(|std::cmp::Reverse((d, _, _))| *d <= now)
{
#[allow(clippy::unwrap_used)] // peek above just proved non-empty
let std::cmp::Reverse((_, db_index, key)) = self.deadlines.pop().unwrap();
let queue_key = (db_index, key);
// A missing queue is a STALE heap entry (waiter served/cancelled
// before its deadline) — the lazy-invalidation no-op.
let Some(queue) = self.waiters.get_mut(&queue_key) else {
continue;
};
let mut i = 0;
while i < queue.len() {
visited += 1;
let is_expired = queue[i].deadline.map_or(false, |d| d <= now);
if is_expired {
#[allow(clippy::unwrap_used)] // i < queue.len() by loop guard
let entry = queue.remove(i).unwrap();
timed_out_ids.push(entry.wait_id);
timed_out.push((entry.wait_id, entry.reply_tx));
} else {
i += 1;
}
}
if queue.is_empty() {
self.waiters.remove(&queue_key);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Deduplicate due queue keys before scanning.

The heap has one entry per waiter, not per queue. If many waiters on one key time out while later/block-forever waiters remain, every due heap entry rescans those survivors. This can turn a sweep into O(due entries × remaining queue length), stalling the 100 Hz shard loop. Collect due (db_index, key) values in a HashSet and scan each queue once; add a mixed-deadline same-key regression test. This also makes the O(due + stale) claim in src/shard/timers.rs accurate.

🤖 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/blocking/mod.rs` around lines 182 - 210, Update the deadline sweep around
the `self.deadlines` loop to collect due `(db_index, key)` pairs in a `HashSet`,
while still draining due heap entries and preserving timeout IDs. After
collection, scan each corresponding `self.waiters` queue once so later or
block-forever waiters are not repeatedly rescanned; retain stale-entry handling
and queue removal behavior. Add a regression test covering mixed deadlines for
multiple waiters sharing one key.

Comment thread src/main.rs
Comment on lines +638 to +663
if config.maxclients > 0 && rl.rlim_cur < need {
let want = need.min(rl.rlim_max);
let mut raised = rl;
raised.rlim_cur = want;
if libc::setrlimit(libc::RLIMIT_NOFILE, &raised) == 0 {
tracing::info!(
"Raised RLIMIT_NOFILE soft limit {} -> {} to fit maxclients {} (+{} reserved fds)",
rl.rlim_cur,
want,
config.maxclients,
reserved
);
}
if want < need {
tracing::warn!(
"RLIMIT_NOFILE hard limit {} cannot fit maxclients {} (+{} reserved fds): \
accepts beyond ~{} clients will fail with EMFILE. Raise `ulimit -n` \
or lower --maxclients.",
rl.rlim_max,
config.maxclients,
reserved,
want.saturating_sub(reserved)
);
}
}
}

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 | 🟡 Minor | ⚡ Quick win

Silent no-op when setrlimit itself fails.

If setrlimit fails but want >= need (i.e., the raise should have succeeded within the hard limit — e.g. blocked by a seccomp/LSM policy), neither the info! nor the warn! fires. The operator gets zero indication that the fd-limit safety net silently didn't take effect, and later hits opaque EMFILE accept failures.

🔧 Proposed fix to surface a setrlimit failure
                     let mut raised = rl;
                     raised.rlim_cur = want;
-                    if libc::setrlimit(libc::RLIMIT_NOFILE, &raised) == 0 {
+                    if libc::setrlimit(libc::RLIMIT_NOFILE, &raised) == 0 {
                         tracing::info!(
                             "Raised RLIMIT_NOFILE soft limit {} -> {} to fit maxclients {} (+{} reserved fds)",
                             rl.rlim_cur,
                             want,
                             config.maxclients,
                             reserved
                         );
+                    } else {
+                        tracing::warn!(
+                            "setrlimit(RLIMIT_NOFILE, {}) failed: {}; maxclients {} may hit EMFILE under load",
+                            want,
+                            std::io::Error::last_os_error(),
+                            config.maxclients
+                        );
                     }
📝 Committable suggestion

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

Suggested change
if config.maxclients > 0 && rl.rlim_cur < need {
let want = need.min(rl.rlim_max);
let mut raised = rl;
raised.rlim_cur = want;
if libc::setrlimit(libc::RLIMIT_NOFILE, &raised) == 0 {
tracing::info!(
"Raised RLIMIT_NOFILE soft limit {} -> {} to fit maxclients {} (+{} reserved fds)",
rl.rlim_cur,
want,
config.maxclients,
reserved
);
}
if want < need {
tracing::warn!(
"RLIMIT_NOFILE hard limit {} cannot fit maxclients {} (+{} reserved fds): \
accepts beyond ~{} clients will fail with EMFILE. Raise `ulimit -n` \
or lower --maxclients.",
rl.rlim_max,
config.maxclients,
reserved,
want.saturating_sub(reserved)
);
}
}
}
if config.maxclients > 0 && rl.rlim_cur < need {
let want = need.min(rl.rlim_max);
let mut raised = rl;
raised.rlim_cur = want;
if libc::setrlimit(libc::RLIMIT_NOFILE, &raised) == 0 {
tracing::info!(
"Raised RLIMIT_NOFILE soft limit {} -> {} to fit maxclients {} (+{} reserved fds)",
rl.rlim_cur,
want,
config.maxclients,
reserved
);
} else {
tracing::warn!(
"setrlimit(RLIMIT_NOFILE, {}) failed: {}; maxclients {} may hit EMFILE under load",
want,
std::io::Error::last_os_error(),
config.maxclients
);
}
if want < need {
tracing::warn!(
"RLIMIT_NOFILE hard limit {} cannot fit maxclients {} (+{} reserved fds): \
accepts beyond ~{} clients will fail with EMFILE. Raise `ulimit -n` \
or lower --maxclients.",
rl.rlim_max,
config.maxclients,
reserved,
want.saturating_sub(reserved)
);
}
}
}
🤖 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/main.rs` around lines 638 - 663, Update the RLIMIT_NOFILE handling around
the setrlimit call to explicitly detect a nonzero return and emit a warning
containing the failure details and requested limit, including when want >= need.
Preserve the existing success log and hard-limit warning behavior, while
ensuring every failed setrlimit attempt is surfaced to operators.

Expose monoio's already-public RuntimeBuilder::with_entries through moon
config: `--uring-entries N` sets the io_uring submission-queue size per
shard ring (monoio default 1024; CQ = 2x by kernel), and sizes the legacy
epoll/kqueue driver's mio event batch on that path. 1024 in-flight ops
per shard is small for 10k+ conns/shard under bursty pipelines
(.planning/rfcs/c1m-connection-plane.md Proposal 4).

Plumbing mirrors the --io-driver epoll precedent: a process-global
AtomicU32 in runtime/mod.rs set once in main BEFORE any shard thread
spawns, consumed by all three monoio build paths (legacy, tuned
io_uring, fusion fallback). Values below monoio's 256 floor clamp up
with a warning; unset keeps monoio's default; no effect under the tokio
runtime (loud warn). Zero vendored-monoio changes needed —
with_entries() was already public and forwarded on every builder path.

Tests: config parse (default None / value / reject non-numeric) +
runtime static default/set/clamp.

author: Tin Dang
…re (c10k P3)

-Zprint-type-sizes on the 58-variant connection state machine showed its
size is set entirely by two COLD suspend points: awaiting
try_handle_txn_abort (5568 B awaitee -> 8319 B variant) and the
disconnect-teardown abort_cross_store_txn_routed (5376 B -> 8127 B),
with try_handle_ft_command third (2816 B -> 5567 B). Box::pin those
three behind their existing name-check fast paths, in both the monoio
and sharded handlers:

- TXN.ABORT rollback + leaked-txn disconnect teardown: alloc only when
  an abort actually executes
- FT.* body split into ft_command_inner and boxed after the "FT."
  prefix check: alloc only per executed FT command (noise next to the
  search itself), never on the per-command filter chain

Measured (nightly -Zprint-type-sizes A/B, macOS aarch64 release-fast):
per-conn monoio task allocation 9280 -> 5952 B plain TCP (-36%),
13248 -> 9920 B TLS, 9472 -> 6144 B migrated; handler future
8640 -> 5312 B. Meets the RFC Proposal 3 target (<=8 KB) without
touching the hot KV dispatch path (the next-largest variant is the
hot cross-shard dispatch await, deliberately left inline).

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: 3

🤖 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/config.rs`:
- Around line 298-305: Add an explicit upper bound or memory-budget validation
for Config::uring_entries alongside the existing MIN_URING_ENTRIES clamp,
document the accepted maximum in the CLI help, and reject oversized values
during startup configuration validation. Ensure the validated value is used by
both Monoio and legacy driver setup so neither io_uring::Builder::build nor
mio::Events allocation can receive an unsafe size, and add coverage for
oversized values.

In `@src/runtime/mod.rs`:
- Around line 390-407: Update uring_entries_default_set_and_clamp to serialize
access to the process-global URING_ENTRIES state and restore it to 0 after
assertions using a test-only guard. Ensure cleanup runs even when an assertion
fails, while preserving the existing value and clamping checks.
- Around line 48-73: Align the zero-value behavior between the `URING_ENTRIES`
storage, `set_uring_entries`, and CLI/help/log handling: either reject/document
zero as invalid floor input, or preserve the documented unset contract by
skipping storage for zero. Update the boundary test to cover `--uring-entries 0`
and ensure it verifies the chosen behavior consistently.
🪄 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: 087e6634-1cda-40a3-9523-70563250d329

📥 Commits

Reviewing files that changed from the base of the PR and between 6fe1844 and dc94c4b.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • src/config.rs
  • src/main.rs
  • src/runtime/mod.rs
  • src/runtime/monoio_impl.rs
  • src/server/conn/handler_monoio/ft.rs
  • src/server/conn/handler_monoio/mod.rs
  • src/server/conn/handler_monoio/txn.rs
  • src/server/conn/handler_sharded/ft.rs
  • src/server/conn/handler_sharded/mod.rs
  • src/server/conn/handler_sharded/txn.rs
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/server/conn/handler_sharded/txn.rs
  • src/server/conn/handler_sharded/mod.rs
  • src/server/conn/handler_sharded/ft.rs
  • src/server/conn/handler_monoio/mod.rs
  • CHANGELOG.md
  • src/server/conn/handler_monoio/txn.rs
  • src/main.rs

Comment thread src/config.rs
Comment on lines +298 to +305
/// io_uring submission-queue entries per shard ring (default: monoio's
/// 1024; CQ is sized 2x by the kernel). Raise for high-connection shards
/// under bursty pipelines — 1024 in-flight ops per shard is small at 10k+
/// conns/shard. Values below 256 are raised to 256 (monoio's floor).
/// Also sizes the legacy (epoll/kqueue) driver's event batch. Applies to
/// the monoio runtime only.
#[arg(long = "uring-entries")]
pub uring_entries: Option<u32>,

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)src/config\.rs$|monoio_impl\.rs$|Cargo\.toml$|Cargo\.lock$' || true

echo "== config relevant lines =="
if [ -f src/config.rs ]; then
  nl -ba src/config.rs | sed -n '260,330p'
fi

echo "== monoio_impl relevant lines =="
if [ -f src/runtime/monoio_impl.rs ]; then
  nl -ba src/runtime/monoio_impl.rs | sed -n '120,175p'
fi

echo "== uring_entries search =="
rg -n "uring_entries|set_uring_entries|io_uring|mio::Events|Builder::build|build\(entries\)" src . --glob '*.rs' --glob 'Cargo.toml' --glob 'Cargo.lock' || true

echo "== Cargo monoio refs =="
rg -n 'name = "monoio"|version = ' Cargo.toml Cargo.lock | sed -n '1,140p'

Repository: pilotspace/moon

Length of output: 393


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== config relevant lines =="
awk '{printf "%6d: %s\n", NR, $0}' src/config.rs | sed -n '260,330p'

echo "== monoio_impl relevant lines =="
awk '{printf "%6d: %s\n", NR, $0}' src/runtime/monoio_impl.rs | sed -n '120,175p'

echo "== uring_entries search =="
rg -n "uring_entries|set_uring_entries|RUNTIME|io_uring|mio::Events|Builder::build|build\\(entries\\)" src Cargo.toml Cargo.lock fuzz Cargo.lock fuzz/Cargo.toml vendor/monoio 2>/dev/null || true

echo "== Cargo monoio refs =="
rg -n 'monoio|version = ' Cargo.toml Cargo.lock vendor/monoio/Cargo.toml 2>/dev/null | sed -n '1,200p'

Repository: pilotspace/moon

Length of output: 40401


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== src/runtime/mod.rs uring_entries definitions =="
awk '{printf "%6d: %s\n", NR, $0}' src/runtime/mod.rs | sed -n '40,75p'
awk '{printf "%6d: %s\n", NR, $0}' src/runtime/mod.rs | sed -n '393,410p'

echo "== src/main.rs uring_entries callsite =="
awk '{printf "%6d: %s\n", NR, $0}' src/main.rs | sed -n '1068,1085p'
rg -n "configure_shards_from_config|async_shard_runner|spawn.*monoio|start.*monoio|set_uring_entries" src/main.rs src/runtime/mod.rs src | sed -n '1,240p'

echo "== monoio builder implementation =="
awk '{printf "%6d: %s\n", NR, $0}' vendor/monoio/src/builder.rs | sed -n '120,165p'
awk '{printf "%6d: %s\n", NR, $0}' vendor/monoio/src/runtime.rs | sed -n '190,235p'
awk '{printf "%6d: %s\n", NR, $0}' vendor/monoio/src/driver/uring/mod.rs | sed -n '96,145p'
awk '{printf "%6d: %s\n", NR, $0}' vendor/monoio/src/driver/poll.rs | sed -n '12,24p'
awk '{printf "%6d: %s\n", NR, $0}' vendor/monoio/src/driver/legacy/mod.rs | sed -n '215,235p'

echo "== all tests touching uring_entries in src =="
rg -n "uring_entries|with_entries|io_uring entries|entries" src --glob '*test*' --glob '*.rs' | sed -n '1,200p'

Repository: pilotspace/moon

Length of output: 28859


🌐 Web query:

io-uring 0.6 Builder build entries parameter allocation Mio Events capacity

💡 Result:

In the io-uring Rust crate (version 0.6 and later), the Builder::build method is used to construct an IoUring instance with a specified number of entries [1]. When you call Builder::build(entries: u32), the entries parameter sets the size of the submission queue (SQ) and the completion queue (CQ) [1]. Unless you have previously called setup_cqsize(entries: u32) to explicitly configure the completion queue size, the entries value passed to build will be used for both [1]. The number of entries must be a power of two, and the value may be rounded up by the kernel if it is not already [1]. Regarding Mio events and capacity, the io-uring crate itself is a low-level interface to the Linux io_uring syscalls [2][3]. It does not directly manage Mio events or their capacity. Integration with event loops like Mio or the Tokio runtime occurs at a higher level [4][5]. In such integrations (like Tokio's io_uring driver), the io_uring file descriptor is registered with the Mio poll/event system to allow the runtime to wait for readiness notifications [4][5]. The capacity of the io_uring itself—the number of SQ and CQ entries—is determined during the initial construction of the IoUring instance via the Builder or IoUring::new methods, not by Mio's event capacity [1][6].

Citations:


Document and enforce an upper bound for --uring-entries.

This flag only clamps small values to MIN_URING_ENTRIES, so the process-wide setting can be u32::MAX and is passed to every Monoio runtime builder. That reaches io_uring::Builder::build(entries) and sizes the legacy driver’s mio::Events capacity via entries as usize, which can fail startup with a per-shard resource/allocation problem. Add a documented maximum or memory-budget validation, return a startup error instead of panicking on the fallback path, and cover oversized values.

🤖 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/config.rs` around lines 298 - 305, Add an explicit upper bound or
memory-budget validation for Config::uring_entries alongside the existing
MIN_URING_ENTRIES clamp, document the accepted maximum in the CLI help, and
reject oversized values during startup configuration validation. Ensure the
validated value is used by both Monoio and legacy driver setup so neither
io_uring::Builder::build nor mio::Events allocation can receive an unsafe size,
and add coverage for oversized values.

Comment thread src/runtime/mod.rs
Comment on lines +48 to +73
/// Process-wide io_uring SQ-entries override (`--uring-entries N`), set ONCE
/// from main BEFORE any shard thread spawns — same contract as
/// [`force_legacy_driver`]. 0 = unset (monoio's default 1024). Also sizes the
/// legacy driver's mio event batch.
static URING_ENTRIES: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);

/// monoio's `RuntimeBuilder::with_entries` floor; values below are raised so
/// the operator-visible behavior matches what the ring actually gets.
pub const MIN_URING_ENTRIES: u32 = 256;

/// Set the per-shard ring size. Values below [`MIN_URING_ENTRIES`] are clamped
/// up (mirrors monoio's internal floor). Call before shard threads spawn.
pub fn set_uring_entries(entries: u32) {
URING_ENTRIES.store(
entries.max(MIN_URING_ENTRIES),
std::sync::atomic::Ordering::Release,
);
}

/// The configured ring size, or `None` when the operator left the default.
pub fn uring_entries() -> Option<u32> {
match URING_ENTRIES.load(std::sync::atomic::Ordering::Acquire) {
0 => None,
n => Some(n),
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)src/(runtime/mod|main)\.rs$|Cargo\.toml$' || true

echo "== runtime mod outline =="
ast-grep outline src/runtime/mod.rs --view compact || true

echo "== relevant src/runtime/mod.rs =="
sed -n '1,120p' src/runtime/mod.rs

echo "== main usages =="
rg -n "set_uring_entries|uring_entries|uring-entries|MIN_URING_ENTRIES|ring size|entries" src/main.rs src/runtime/mod.rs tests 2>/dev/null || true

echo "== target files if tests exist =="
fd -i '.*tests?.*' . -t f | sed -n '1,80p'

Repository: pilotspace/moon

Length of output: 21273


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== src/main.rs relevant sections =="
sed -n '1030,1105p' src/main.rs

echo "== config definitions/options around ureg_uring_entries =="
rg -n "uring_entries|--uring-entries|ring size|floor|clap|Clap|Arg" src/main.rs src -g '*.rs' | sed -n '1,120p'

echo "== runtime tests =="
sed -n '360,415p' src/runtime/mod.rs

echo "== read-only behavioral semantic verifier =="
python3 - <<'PY'
from dataclasses import dataclass

MIN = 256
class UringEntries:
    def __init__(self):
        self.value = 0
    def set_uring_entries(self, entries):
        self.value = entries if entries >= MIN else MIN
    def uring_entries(self):
        return None if self.value == 0 else self.value

c = UringEntries()
for input_val in [0, 1, 255, 256]:
    c.value = 0
    c.set_uring_entries(input_val)
    print({"input": input_val, "stored": c.value, "read": c.uring_entries(), "log_effective": f"{input_val} is {'below floor' if input_val < MIN else 'at/above floor'}", "log_prints_warning": "yes" if input_val < MIN else "no"})
PY

Repository: pilotspace/moon

Length of output: 17023


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== src/config.rs uring_entries definitions =="
sed -n '280,330p' src/config.rs

echo "== src/config.rs test around uring_entries_flag =="
sed -n '2000,2035p' src/config.rs

echo "== zero-range/value_parser occurrences involving uring_entries =="
rg -n "uring-entries|uring_entries|value_parser.*u32|0|default_value_t" src/config.rs src/main.rs -C 3

echo "== behavioral parser/zero contract probe =="
python3 - <<'PY'
from pathlib import Path
src = Path("src/config.rs").read_text()
idx = src.index("pub struct ServerConfig")
try:
    idx2 = src.index("pub struct ServerConfig", idx + 1)
except ValueError:
    idx2 = src.index("pub mod", idx)
struct = src[idx:idx2]
for needle in ["#[arg(", "uring_entries: Option", "value_parser", "0..", "1.."]:
    print(f"{needle!r}: {needle in struct}")
print("uring_entries_help_lines:")
for i,line in enumerate(src.splitlines(),1):
    if "--uring-entries" in line or "uring_entries: Option<u32>" in line:
        print(f"{i}: {line}")
print("tests:")
for i,line inenumerate(src.splitlines(),1):
    if "--uring-entries" in line or "uring_entries" in line:
        print(f"{i}: {line}")
PY

Repository: pilotspace/moon

Length of output: 50372


Align the zero-value contract.

--uring-entries 0 is parsed as Some(0) and set_uring_entries(0) stores MIN_URING_ENTRIES, so the help/log path treats it as a sub-floor value instead of “unset”. Reject 0/document it as the floor input, or keep “unset” semantics by treating 0 as skipped input and make the boundary test cover --uring-entries 0.

🤖 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/runtime/mod.rs` around lines 48 - 73, Align the zero-value behavior
between the `URING_ENTRIES` storage, `set_uring_entries`, and CLI/help/log
handling: either reject/document zero as invalid floor input, or preserve the
documented unset contract by skipping storage for zero. Update the boundary test
to cover `--uring-entries 0` and ensure it verifies the chosen behavior
consistently.

Comment thread src/runtime/mod.rs Outdated
Comment on lines +390 to +407
#[cfg(test)]
mod uring_entries_tests {
// Shared process-global: run assertions in one test to avoid ordering
// races between parallel test threads.
#[test]
fn uring_entries_default_set_and_clamp() {
assert_eq!(super::uring_entries(), None, "unset must read as None");
super::set_uring_entries(4096);
assert_eq!(super::uring_entries(), Some(4096));
super::set_uring_entries(64);
assert_eq!(
super::uring_entries(),
Some(super::MIN_URING_ENTRIES),
"sub-floor values clamp up to monoio's minimum"
);
}
}

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 | 🟡 Minor | ⚡ Quick win

Restore the process-global test state.

This test leaves URING_ENTRIES set to 256. Any sibling test that checks the unset state or constructs a runtime afterward can observe the mutated value; keeping the assertions in one test does not isolate the static from the rest of the test harness. Serialize access and restore the value to 0 with a test-only guard.

🤖 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/runtime/mod.rs` around lines 390 - 407, Update
uring_entries_default_set_and_clamp to serialize access to the process-global
URING_ENTRIES state and restore it to 0 after assertions using a test-only
guard. Ensure cleanup runs even when an assertion fails, while preserving the
existing value and clamping checks.

…rking set (c10k W11)

A connection parked in read() pinned its full working set forever: an
8 KiB rent buffer submitted to the kernel plus the empty 8 KiB read/write
scratch — ~24 KiB of the measured 43.5 KB idle RSS/conn, and the floor
the c1M RFC said only connection parking could reclaim.

Two-stage park, no timers or allocs on the hot path:

- Stage 1 (full buffers): cancel-capable streams read via monoio's
  cancelable_read, with the park timestamp (shard cached clock) in a
  shard-thread-local IdleSlot registry. One Rc alloc per CONNECTION at
  setup; per park just two Cell stores + a CancelHandle Rc clone.
- The shard event loop's existing 1s chore sweeps the registry and fires
  the Canceller of any read parked >=1s. Cancel-and-await is loss-free
  on both drivers (io_uring AsyncCancel; legacy READ_CANCELED): a
  completion that raced the cancel still delivers its bytes.
- Stage 2: the woken handler drops the rent buffer, releases the empty
  scratch buffers, and re-parks on a 512 B probe buffer with no further
  chore involvement — one cancellation per idle period, not per second.
- Real data restores the full working set lazily (pre-park sizing) and
  re-arms stage 1. Bursts >512 B spill into the next full-size read.

TLS streams opt out via IdleParkRead::SUPPORTS_IDLE_PARK (monoio_rustls
has no cancelable read) and keep the pre-W11 path byte-for-byte; the
tokio runtime is untouched.

Measured (moon-dev VM, 2 shards, 10k idle conns): 43.5 -> 19.9 KB/conn
after 30s idle (-54%; campaign total 56.5 -> 19.9, -65%); post-wake
steady state 27.8 KB/conn (lazy regrowth also sheds the old always-8KiB
read/write scratch); PING sweep over all 10k downshifted conns 0.07s,
0 failures; RSS returns on close. Parity suite (probe-size wake, >512B
pipeline, 4 KiB value round trip, undisturbed active sibling) green on
BOTH cancel paths: macOS kqueue legacy driver and Linux io_uring.

author: Tin Dang
… red tokio Check

Round-2 fallout (--uring-entries, fdcd5d9): three tokio-only integration
suites build ServerConfig struct literals and were never rebuilt by the
lib-suite gates, so the missing `uring_entries` field only surfaced in
CI's full-matrix Check job (E0063 in mq_integration, txn_kv_wiring,
workspace_integration ×2). Adds `uring_entries: None` to all four
literals.

Also moves the round-2 `uring_entries_tests` module to the end of
src/runtime/mod.rs (clippy `items_after_test_module`, visible only under
--all-targets).

Verified: all three suites compile and pass under
--no-default-features --features runtime-tokio,jemalloc (VM;
txn_kv_wiring's crash-recovery leg needs disk headroom — the macOS host
root volume sits under the 5% diskfull floor).

Lesson for the gate checklist: a config-struct field addition must be
followed by `cargo clippy --all-targets` on BOTH matrices — lib suites
do not compile integration tests.

author: Tin Dang
…d 40 KB (round 4)

Round 4 of the c10k campaign, per the c1M RFC sequencing: the TLS diet.
Root cause (round 3): monoio-io-wrapper eagerly allocates two fixed
16 KiB Box<[u8]> buffers per TLS stream and never frees them, and
monoio-rustls has no cancelable read, so W11's idle downshift could not
cover TLS connections at all — idle TLS RSS sat flat at ~87.5 KB/conn.

Changes (vendored crates follow the existing vendor/monoio "moon patch"
comment style; wired via [patch.crates-io]):

- vendor/monoio-io-wrapper: Buffer is lazy (allocated on first use) and
  releasable (release_if_empty, only while drained; reallocates
  transparently on next use). New do_io_cancelable whose errors —
  including ECANCELED — are returned directly and NOT stashed for status
  replay: a stashed cancel error would resurface on the next plain read
  and tear down a healthy connection (unit-tested in-crate, 5 tests).
- vendor/monoio-rustls: Stream::release_idle_buffers() and a
  CancelableAsyncReadRent impl that forwards the cancel handle into the
  wrapper's socket read. Loss-free under cancel-and-await: bytes from a
  completion racing the cancel persist in the wrapper buffer + rustls
  deframer and are consumed by the next read.
- src/server/conn/handler_monoio/idle_park.rs: TLS streams now set
  SUPPORTS_IDLE_PARK = true and implement the new on_idle_downshift hook
  (releases both wrapper buffers). Lazy-alloc alone would be worthless —
  the handshake itself fills both buffers, so the win requires the
  idle-park cancel to fire the release.
- tests/tls_idle_downshift_parity.rs: wire parity on one continuous TLS
  session across three downshift/wake cycles (probe wake, 700 B
  pipeline, 4 KiB value round trip), rustls client with throwaway cert;
  green on kqueue (macOS) and io_uring (VM).

E9 A/B (moon-dev VM, 3000 idle TLS conns, shards=2):
pre-P4b 87.5 KB/conn flat forever → P4b 47.4 KB/conn after 30 s idle
(−46%), re-downshifts to 48.7 after a wake-sweep; sweeps bad=0; RSS
returns on close. Matches the RFC's −30–40 KB estimate. Remaining TLS
floor (rustls session ~15–20 KB + 9.9 KB task future) is Proposal 1
territory.

Gates: tokio lib suite 3609 pass; VM monoio lib suite pass; fmt + both
clippy matrices clean; unsafe/unwrap audits pass.

refs: tmp/C10K-REVIEW.md round 4, .planning/rfcs/c1m-connection-plane.md
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: 2

🧹 Nitpick comments (1)
vendor/monoio-io-wrapper/src/unsafe_io.rs (1)

162-191: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Confirm unsafe_io remains opt-in in CI, and document the vendor unsafe exemption

No repo feature currently enables monoio-rustls/unsafe_io, but this package depends on path overrides for monoio-rustls and monoio-io-wrapper, so ensure CI/build manifests don’t enable it. Also add a short comment/exemption note near the vendored source so the project’s unsafe-code audit doesn’t flag RawBuf’s upstream unsafe impl IoBuf/IoBufMut.

🤖 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 `@vendor/monoio-io-wrapper/src/unsafe_io.rs` around lines 162 - 191, Keep the
monoio-rustls/unsafe_io feature disabled across CI and build manifests,
including path-override configurations for monoio-rustls and monoio-io-wrapper.
Add a concise vendor exemption comment near RawBuf and its unsafe IoBuf/IoBufMut
implementations documenting that this upstream unsafe code is intentionally
retained and audited as an exemption.

Source: Coding guidelines

🤖 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 `@vendor/monoio-rustls/src/stream.rs`:
- Line 289: Format the cancelable_read and cancelable_readv function
declarations in the vendored crate so they comply with rustfmt’s default line
width. Run cargo fmt for the crate and verify cargo fmt --check passes.
- Around line 197-220: Remove the unconditional write_io call from
read_io_cancelable’s process_new_packets error path, leaving it to return the
InvalidData error directly. This must preserve split-stream safety by ensuring
the cancelable read half never mutates the session or write buffer; do not alter
the existing read_io behavior.

---

Nitpick comments:
In `@vendor/monoio-io-wrapper/src/unsafe_io.rs`:
- Around line 162-191: Keep the monoio-rustls/unsafe_io feature disabled across
CI and build manifests, including path-override configurations for monoio-rustls
and monoio-io-wrapper. Add a concise vendor exemption comment near RawBuf and
its unsafe IoBuf/IoBufMut implementations documenting that this upstream unsafe
code is intentionally retained and audited as an exemption.
🪄 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: 7839b4e6-3d78-4170-8ddf-912782852e40

📥 Commits

Reviewing files that changed from the base of the PR and between 6ef45e5 and d2d0144.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (24)
  • CHANGELOG.md
  • Cargo.toml
  • src/runtime/mod.rs
  • src/server/conn/handler_monoio/idle_park.rs
  • src/server/conn/handler_monoio/mod.rs
  • tests/mq_integration.rs
  • tests/tls_idle_downshift_parity.rs
  • tests/txn_kv_wiring.rs
  • tests/workspace_integration.rs
  • vendor/monoio-io-wrapper/Cargo.toml
  • vendor/monoio-io-wrapper/Cargo.toml.orig
  • vendor/monoio-io-wrapper/README.adoc
  • vendor/monoio-io-wrapper/src/lib.rs
  • vendor/monoio-io-wrapper/src/safe_io.rs
  • vendor/monoio-io-wrapper/src/unsafe_io.rs
  • vendor/monoio-rustls/Cargo.toml
  • vendor/monoio-rustls/Cargo.toml.orig
  • vendor/monoio-rustls/README.md
  • vendor/monoio-rustls/examples/connect.rs
  • vendor/monoio-rustls/src/client.rs
  • vendor/monoio-rustls/src/error.rs
  • vendor/monoio-rustls/src/lib.rs
  • vendor/monoio-rustls/src/server.rs
  • vendor/monoio-rustls/src/stream.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/runtime/mod.rs
  • CHANGELOG.md
  • src/server/conn/handler_monoio/mod.rs

Comment on lines +197 to +220
pub(crate) async fn read_io_cancelable(&mut self, c: CancelHandle) -> io::Result<usize>
where
IO: CancelableAsyncReadRent,
{
let n = loop {
match self.session.read_tls(&mut self.r_buffer) {
Ok(n) => {
break n;
}
Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => {
self.r_buffer.do_io_cancelable(&mut self.io, c.clone()).await?;
continue;
}
Err(err) => return Err(err),
}
};

let state = match self.session.process_new_packets() {
Ok(state) => state,
Err(err) => {
let _ = self.write_io().await;
return Err(io::Error::new(io::ErrorKind::InvalidData, err));
}
};

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 | ⚡ Quick win

read_io_cancelable unconditionally calls write_io(), but CancelableAsyncReadRent is implemented for split-capable streams.

read_io takes a splitted flag precisely to avoid touching the write side from the read half (see the upstream comment at Lines 110-113). read_io_cancelable has no such guard, yet Stream is Split (Line 36) and the CancelableAsyncReadRent impl at Line 284 is unconditional — so a read half can drive write_io(), mutating session and w_buffer concurrently with the write half. The doc comment says "Unsplit streams only", but nothing enforces it.

Since this is the error path and the upstream contract already requires the user to shut down manually, the simplest fix is to drop the write attempt (equivalent to splitted = true), or thread a splitted flag through.

🛡️ Proposed fix
         let state = match self.session.process_new_packets() {
             Ok(state) => state,
             Err(err) => {
-                let _ = self.write_io().await;
+                // Do not touch the write side here: this path is reachable
+                // from a split read half (see `read_io`'s `splitted` guard).
+                // Callers must shut the stream down on error.
                 return Err(io::Error::new(io::ErrorKind::InvalidData, err));
             }
         };
📝 Committable suggestion

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

Suggested change
pub(crate) async fn read_io_cancelable(&mut self, c: CancelHandle) -> io::Result<usize>
where
IO: CancelableAsyncReadRent,
{
let n = loop {
match self.session.read_tls(&mut self.r_buffer) {
Ok(n) => {
break n;
}
Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => {
self.r_buffer.do_io_cancelable(&mut self.io, c.clone()).await?;
continue;
}
Err(err) => return Err(err),
}
};
let state = match self.session.process_new_packets() {
Ok(state) => state,
Err(err) => {
let _ = self.write_io().await;
return Err(io::Error::new(io::ErrorKind::InvalidData, err));
}
};
pub(crate) async fn read_io_cancelable(&mut self, c: CancelHandle) -> io::Result<usize>
where
IO: CancelableAsyncReadRent,
{
let n = loop {
match self.session.read_tls(&mut self.r_buffer) {
Ok(n) => {
break n;
}
Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => {
self.r_buffer.do_io_cancelable(&mut self.io, c.clone()).await?;
continue;
}
Err(err) => return Err(err),
}
};
let state = match self.session.process_new_packets() {
Ok(state) => state,
Err(err) => {
// Do not touch the write side here: this path is reachable
// from a split read half. Callers must shut the stream down on error.
return Err(io::Error::new(io::ErrorKind::InvalidData, err));
}
};
🤖 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 `@vendor/monoio-rustls/src/stream.rs` around lines 197 - 220, Remove the
unconditional write_io call from read_io_cancelable’s process_new_packets error
path, leaving it to return the InvalidData error directly. This must preserve
split-stream safety by ensuring the cancelable read half never mutates the
session or write buffer; do not alter the existing read_io behavior.

IO: AsyncReadRent + AsyncWriteRent + CancelableAsyncReadRent,
C: DerefMut + Deref<Target = ConnectionCommon<SD>>,
{
async fn cancelable_read<T: IoBufMut>(&mut self, mut buf: T, c: CancelHandle) -> BufResult<usize, T> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

These signatures exceed rustfmt's default max_width and will fail the formatting gate.

Both cancelable_read and cancelable_readv declarations run past 100 columns, so cargo fmt --check will reject them. Run cargo fmt over the vendored crate (or wrap manually as below).

Based on learnings: "Before pushing, run the documented CI matrix: formatting, both clippy configurations with -D warnings, release tests, MSRV build, and applicable safety, unwrap, fuzz, and CodeQL checks."

🎨 Proposed formatting
-    async fn cancelable_read<T: IoBufMut>(&mut self, mut buf: T, c: CancelHandle) -> BufResult<usize, T> {
+    async fn cancelable_read<T: IoBufMut>(
+        &mut self,
+        mut buf: T,
+        c: CancelHandle,
+    ) -> BufResult<usize, T> {
-    async fn cancelable_readv<T: IoVecBufMut>(&mut self, mut buf: T, c: CancelHandle) -> BufResult<usize, T> {
+    async fn cancelable_readv<T: IoVecBufMut>(
+        &mut self,
+        mut buf: T,
+        c: CancelHandle,
+    ) -> BufResult<usize, T> {

Also applies to: 312-312

🤖 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 `@vendor/monoio-rustls/src/stream.rs` at line 289, Format the cancelable_read
and cancelable_readv function declarations in the vendored crate so they comply
with rustfmt’s default line width. Run cargo fmt for the crate and verify cargo
fmt --check passes.

Source: Learnings

@TinDang97
TinDang97 merged commit 287a63f into main Jul 29, 2026
19 of 20 checks passed
@TinDang97
TinDang97 deleted the feat/c10k-conn-plane branch July 29, 2026 12:49
TinDang97 added a commit that referenced this pull request Jul 29, 2026
…conns −94%) (#423)

Patch release rolling up the connection-plane campaign: PR #421
(rounds 1-4: W1-W8+T3, P3 future diet, P4a --uring-entries, W11 idle
downshift, P4b TLS diet) and PR #422 (round 5: P1 task-exit parking),
plus the SDK reconnect fix.

Headline: idle-connection memory 56.5 → 3.25 KB/conn (−94%);
1 M idle connections ≈ 3.3 GB. Long-idle plain-TCP conns exit their
handler task entirely (--conn-park-secs, default 60 s), leaving a tiny
readiness watcher; wake is wire-invisible and re-parks indefinitely.
TLS idle 87.5 → 47.4 KB/conn via vendored monoio-rustls/io-wrapper
lazy+releasable buffers. Pipeline memory ratchet fixed (~217 KB
permanent → 47 KB). Operability: loud maxclients -ERR + RLIMIT_NOFILE
check, 16-way striped client registry, deadline-heap blocking sweep,
SPSC drain rotation, affinity-funnel load gate.

Validation: same-binary flag A/Bs on the Linux VM at 10 k conns for
every memory wave; GCE t2a hardware proof (idle −46%, pinned p=1/p=16
perf-neutral); wire-parity suites across park/wake cycles green on
kqueue AND io_uring; parked conns visible in CLIENT LIST and killable.
This release touches no crash/persistence path; the crash-matrix +
soak gate is dispatched on the RC to hold the ritual
(soak-first-then-tag).

Rolls CHANGELOG [Unreleased] into [0.8.3], bumps Cargo.toml/lock,
adds the RELEASES.md row, updates the README milestone table.

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