diff --git a/CHANGELOG.md b/CHANGELOG.md index b9a32777..efc4355a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed — Connection counter double-decrement (2026-04-12) + +- **monoio handler: `maxclients reached` after first disconnect.** `handle_connection_sharded_monoio` called `record_connection_closed()` unconditionally at its exit, while the caller (`conn_accept.rs`) also called it in the non-migration branch. The `AtomicU64` counter wrapped to `u64::MAX` on the second `fetch_sub`, causing every subsequent `try_accept_connection` to reject against `maxclients`. Removed the handler-level decrement to restore symmetry with the `try_accept_connection` increment owned by the caller. Verified: 10 sequential SETs now succeed; `redis-benchmark SET p=16 c=50` reports real throughput (1.25M+ req/s) instead of rejection errors. + ### Added — Graph Engine Integration (v0.1.4, 2026-04-11) - **Property graph engine** (`src/graph/`, feature-gated under `graph`): segment-aligned CSR storage with SlotMap generational indices, ArcSwap lock-free reads, Roaring validity bitmaps, and Rabbit Order compaction for cache locality. 8,500+ LOC, 319 tests. diff --git a/src/server/conn/handler_monoio.rs b/src/server/conn/handler_monoio.rs index 95bfd22a..ef228a27 100644 --- a/src/server/conn/handler_monoio.rs +++ b/src/server/conn/handler_monoio.rs @@ -2327,6 +2327,11 @@ pub(crate) async fn handle_connection_sharded_monoio< } } - crate::admin::metrics_setup::record_connection_closed(); + // NOTE: connection close is recorded by the caller (conn_accept.rs) to + // preserve symmetry with `try_accept_connection`, which owns the + // increment. Decrementing here too produces a double-decrement on the + // AtomicU64 counter — it wraps to u64::MAX on the second subtraction + // and all subsequent `try_accept_connection` comparisons against + // `maxclients` reject new connections. (MonoioHandlerResult::Done, None) } diff --git a/src/shard/conn_accept.rs b/src/shard/conn_accept.rs index abec51f2..69e3a8ee 100644 --- a/src/shard/conn_accept.rs +++ b/src/shard/conn_accept.rs @@ -700,6 +700,11 @@ pub(crate) fn spawn_migrated_monoio_connection( fd, e ); + // Source shard's `try_accept_connection` already counted this + // client; its wrapper skipped the decrement because the migration + // succeeded from the source's perspective. Since we cannot hand + // ownership to the handler, we own the balancing decrement here. + crate::admin::metrics_setup::record_connection_closed(); return; // std_stream Drop closes FD } match monoio::net::TcpStream::from_std(std_stream) { @@ -765,6 +770,12 @@ pub(crate) fn spawn_migrated_monoio_connection( Some(&state), ) .await; + // Migrated connection: the source shard's wrapper skipped the + // decrement (because `_migrated == true`), so this target-shard + // spawn site owns the balancing decrement. Without this the + // AtomicU64 `CONNECTED_CLIENTS` counter would leak upward on + // every migration and eventually trip `maxclients`. + crate::admin::metrics_setup::record_connection_closed(); }); } Err(e) => { @@ -773,6 +784,10 @@ pub(crate) fn spawn_migrated_monoio_connection( shard_id, e ); + // Same rationale as the set_nonblocking failure above: + // the source-side increment stands but no handler will run, + // so we own the balancing decrement. + crate::admin::metrics_setup::record_connection_closed(); } } }