From 554530d846c2f7764fb723d7055209e2ef8eaebc Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Wed, 7 Oct 2026 13:00:05 -0700 Subject: [PATCH] pool: test that the reconnector restarts after it has finished The reconnector task exits once it has restored every connection it was asked for. Before #127 it was spawned at most once per pool, so a connection lost after that first pass stayed missing for good. A pool of 5 on a long-lived server lost its connections one at a time to transient connect failures and ended at PoolExhausted, with nothing retrying. The test lets the reconnector finish its first job, then reports another lost connection the way release() does and expects the pool to refill. Removing the `running = false` reset makes it fail with PoolNotRestored. Co-Authored-By: Claude Opus 5.5 --- src/pool.zig | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/pool.zig b/src/pool.zig index 62ad6bd..b256431 100644 --- a/src/pool.zig +++ b/src/pool.zig @@ -536,6 +536,47 @@ test "Pool: deinit while the reconnector is retrying" { try t.expectEqual(true, elapsed < std.time.ns_per_s); } +// Regression: the reconnector task exits once it has restored everything it +// was asked for, and a connection lost after that must start it again. It used +// to be spawned once per pool, so every later loss stayed missing until the +// pool reported PoolExhausted. +test "Pool: the reconnector restores a connection lost after it finished" { + const io = t.io; + + // the lazily opened connection is the reconnector's first job + var pool = try Pool.init(io, t.allocator, .{ + .size = 2, + .auth = t.authOpts(.{}), + .connect = t.connectOpts(), + .connect_on_init_count = 1, + }); + defer pool.deinit(); + try expectPoolRestored(pool, 2); + + // what release() does when it cannot replace a dirty connection inline + const lost = try pool.acquire(); + lost.deinit(); + t.allocator.destroy(lost); + pool._mutex.lockUncancelable(io); + pool._missing += 1; + pool._mutex.unlock(io); + try pool._reconnector.reconnect(); + + try expectPoolRestored(pool, 2); +} + +fn expectPoolRestored(pool: *Pool, size: usize) !void { + const io = t.io; + const start = std.Io.Timestamp.now(io, .awake); + while (true) { + const s = pool.stats(); + if (s.missing == 0 and s.available == size) return; + const elapsed = start.durationTo(std.Io.Timestamp.now(io, .awake)).toNanoseconds(); + if (elapsed > 5 * std.time.ns_per_s) return error.PoolNotRestored; + try std.Io.sleep(io, .fromNanoseconds(10 * std.time.ns_per_ms), .awake); + } +} + test "Pool: Release" { var pool = try Pool.init(t.io, t.allocator, .{ .size = 2, -- 2.51.2