From 99524bf0547d1f31eb1776f1fdf8839fa0ae2e18 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 01:12:48 +0000 Subject: [PATCH 1/8] Initial plan From 7fed499a02673df6af15dbfeeb261b26311f50f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 01:20:18 +0000 Subject: [PATCH 2/8] fix: clean up stale h2 pool slots Co-authored-by: ejpir <16493171+ejpir@users.noreply.github.com> --- src/http/http2/pool.zig | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/http/http2/pool.zig b/src/http/http2/pool.zig index 5ee77f7..372a444 100644 --- a/src/http/http2/pool.zig +++ b/src/http/http2/pool.zig @@ -50,6 +50,8 @@ pub const H2ConnectionPool = struct { /// Initialize pool with backend servers pub fn init(backends: []const BackendServer, allocator: std.mem.Allocator) Self { + std.debug.assert(backends.len <= MAX_BACKENDS); + var pool = Self{ .slots = undefined, .slot_state = undefined, @@ -79,6 +81,7 @@ pub const H2ConnectionPool = struct { /// 5. Unlock mutex on return pub fn getOrCreate(self: *Self, backend_idx: u32, io: Io) !*H2Connection { std.debug.assert(backend_idx < self.backends.len); + std.debug.assert(backend_idx < MAX_BACKENDS); // Lock per-backend mutex to prevent concurrent creation race // Critical: without this, multiple coroutines see "empty" before any assigns @@ -89,10 +92,14 @@ pub const H2ConnectionPool = struct { for (&self.slot_state[backend_idx], &self.slots[backend_idx], 0..) |*state, *slot, i| { if (state.* == .available) { if (slot.*) |conn| { - // Skip stale connections - DON'T destroy here! - // Other requests may still hold references. Let release() handle cleanup. + // Destroy stale connections so slots don't remain permanently unavailable. + // Available slots must have ref_count == 0. if (self.isConnectionStale(conn)) { - log.debug("Connection stale, skipping: backend={d} slot={d}", .{ backend_idx, i }); + log.debug("Connection stale, destroying: backend={d} slot={d}", .{ backend_idx, i }); + std.debug.assert(conn.ref_count.load(.acquire) == 0); + slot.* = null; + state.* = .empty; + self.destroyConnection(conn, io); continue; } From 50d600321c95acfbf075d8ceae8ec97c27233480 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 01:21:44 +0000 Subject: [PATCH 3/8] chore: clarify pool invariants Co-authored-by: ejpir <16493171+ejpir@users.noreply.github.com> --- src/http/http2/pool.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/http/http2/pool.zig b/src/http/http2/pool.zig index 372a444..d5a1d84 100644 --- a/src/http/http2/pool.zig +++ b/src/http/http2/pool.zig @@ -81,7 +81,6 @@ pub const H2ConnectionPool = struct { /// 5. Unlock mutex on return pub fn getOrCreate(self: *Self, backend_idx: u32, io: Io) !*H2Connection { std.debug.assert(backend_idx < self.backends.len); - std.debug.assert(backend_idx < MAX_BACKENDS); // Lock per-backend mutex to prevent concurrent creation race // Critical: without this, multiple coroutines see "empty" before any assigns @@ -96,6 +95,8 @@ pub const H2ConnectionPool = struct { // Available slots must have ref_count == 0. if (self.isConnectionStale(conn)) { log.debug("Connection stale, destroying: backend={d} slot={d}", .{ backend_idx, i }); + // backend_mutex serializes getOrCreate calls; release() only + // marks a slot available when ref_count drops to 0. std.debug.assert(conn.ref_count.load(.acquire) == 0); slot.* = null; state.* = .empty; From 2766e0b207b6ab39177b5bc2225653889a3bf5f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 01:22:30 +0000 Subject: [PATCH 4/8] chore: adjust refcount load ordering Co-authored-by: ejpir <16493171+ejpir@users.noreply.github.com> --- src/http/http2/pool.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/http/http2/pool.zig b/src/http/http2/pool.zig index d5a1d84..38f67b8 100644 --- a/src/http/http2/pool.zig +++ b/src/http/http2/pool.zig @@ -97,7 +97,7 @@ pub const H2ConnectionPool = struct { log.debug("Connection stale, destroying: backend={d} slot={d}", .{ backend_idx, i }); // backend_mutex serializes getOrCreate calls; release() only // marks a slot available when ref_count drops to 0. - std.debug.assert(conn.ref_count.load(.acquire) == 0); + std.debug.assert(conn.ref_count.load(.monotonic) == 0); slot.* = null; state.* = .empty; self.destroyConnection(conn, io); From 48f5e148e848c2cfa1bdff36956b6f6fd2ecd8c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 01:23:27 +0000 Subject: [PATCH 5/8] chore: restore acquire ordering Co-authored-by: ejpir <16493171+ejpir@users.noreply.github.com> --- src/http/http2/pool.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/http/http2/pool.zig b/src/http/http2/pool.zig index 38f67b8..d5a1d84 100644 --- a/src/http/http2/pool.zig +++ b/src/http/http2/pool.zig @@ -97,7 +97,7 @@ pub const H2ConnectionPool = struct { log.debug("Connection stale, destroying: backend={d} slot={d}", .{ backend_idx, i }); // backend_mutex serializes getOrCreate calls; release() only // marks a slot available when ref_count drops to 0. - std.debug.assert(conn.ref_count.load(.monotonic) == 0); + std.debug.assert(conn.ref_count.load(.acquire) == 0); slot.* = null; state.* = .empty; self.destroyConnection(conn, io); From 43be1e2415e3d8d0e31d19719d36022c13122899 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 01:24:35 +0000 Subject: [PATCH 6/8] chore: strengthen refcount assert Co-authored-by: ejpir <16493171+ejpir@users.noreply.github.com> --- src/http/http2/pool.zig | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/http/http2/pool.zig b/src/http/http2/pool.zig index d5a1d84..13d437d 100644 --- a/src/http/http2/pool.zig +++ b/src/http/http2/pool.zig @@ -95,9 +95,10 @@ pub const H2ConnectionPool = struct { // Available slots must have ref_count == 0. if (self.isConnectionStale(conn)) { log.debug("Connection stale, destroying: backend={d} slot={d}", .{ backend_idx, i }); - // backend_mutex serializes getOrCreate calls; release() only - // marks a slot available when ref_count drops to 0. - std.debug.assert(conn.ref_count.load(.acquire) == 0); + // backend_mutex serializes getOrCreate calls; release() transitions + // to .available only after ref_count hits 0 (acq_rel), so there is + // no concurrent increment while we hold the mutex. + std.debug.assert(conn.ref_count.load(.seq_cst) == 0); slot.* = null; state.* = .empty; self.destroyConnection(conn, io); From c92a42a4e89865ae12e5c261fb140d36e1e82eb9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 01:25:12 +0000 Subject: [PATCH 7/8] chore: clarify refcount ordering Co-authored-by: ejpir <16493171+ejpir@users.noreply.github.com> --- src/http/http2/pool.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/http/http2/pool.zig b/src/http/http2/pool.zig index 13d437d..d8ec156 100644 --- a/src/http/http2/pool.zig +++ b/src/http/http2/pool.zig @@ -95,10 +95,10 @@ pub const H2ConnectionPool = struct { // Available slots must have ref_count == 0. if (self.isConnectionStale(conn)) { log.debug("Connection stale, destroying: backend={d} slot={d}", .{ backend_idx, i }); - // backend_mutex serializes getOrCreate calls; release() transitions - // to .available only after ref_count hits 0 (acq_rel), so there is + // backend_mutex serializes getOrCreate calls; release() uses acq_rel + // when transitioning to .available after ref_count hits 0, so there is // no concurrent increment while we hold the mutex. - std.debug.assert(conn.ref_count.load(.seq_cst) == 0); + std.debug.assert(conn.ref_count.load(.acquire) == 0); slot.* = null; state.* = .empty; self.destroyConnection(conn, io); From b62b6f873d0816594301e001ecd84102746b71ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Dec 2025 01:26:13 +0000 Subject: [PATCH 8/8] chore: align refcount comment Co-authored-by: ejpir <16493171+ejpir@users.noreply.github.com> --- src/http/http2/pool.zig | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/http/http2/pool.zig b/src/http/http2/pool.zig index d8ec156..cdb6595 100644 --- a/src/http/http2/pool.zig +++ b/src/http/http2/pool.zig @@ -95,10 +95,9 @@ pub const H2ConnectionPool = struct { // Available slots must have ref_count == 0. if (self.isConnectionStale(conn)) { log.debug("Connection stale, destroying: backend={d} slot={d}", .{ backend_idx, i }); - // backend_mutex serializes getOrCreate calls; release() uses acq_rel - // when transitioning to .available after ref_count hits 0, so there is - // no concurrent increment while we hold the mutex. - std.debug.assert(conn.ref_count.load(.acquire) == 0); + // backend_mutex serializes getOrCreate calls; release() marks a slot + // available only after its acq_rel fetchSub drops ref_count to 0. + std.debug.assert(conn.ref_count.load(.monotonic) == 0); slot.* = null; state.* = .empty; self.destroyConnection(conn, io);