From 3dc21b93a0b52136cfc58c4f6b0d52b38ff56617 Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Mon, 6 Apr 2026 15:41:12 -0500 Subject: [PATCH] fix gcLoop: silently exited after one tick MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gcLoop was using io.sleep on pool_io (Threaded) from a plain std.Thread. the first tick happened to succeed, the second hit an error path, and catch return swallowed it — silently killing the loop. one malloc_trim fired at the 10-min mark and then nothing for 13.5+ hours. fix: switch to std.c.nanosleep directly. plain threads can't safely call into Io scheduler primitives, even on the matching backend, because they aren't registered with that backend's scheduler. drop the io parameter from gcLoop since dp.gc() uses its own bound io internally. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/main.zig | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/main.zig b/src/main.zig index 960a725..ffb9696 100644 --- a/src/main.zig +++ b/src/main.zig @@ -351,7 +351,9 @@ pub fn main() !void { // start GC loop on a plain thread — dp.gc() uses pool_io (Threaded) mutex // and pg.Pool. MUST NOT run as Evented fiber: Threaded futex on Evented // fiber dereferences NULL Thread.current() threadlocal → heap corruption. - const gc_thread = std.Thread.spawn(.{}, gcLoop, .{ &dp, pool_io }) catch |err| { + // sleeps via std.Thread.sleep (NOT io.sleep) — io.sleep on pool_io from + // a non-Io thread fails on the second tick and silently kills the loop. + const gc_thread = std.Thread.spawn(.{}, gcLoop, .{&dp}) catch |err| { log.err("failed to start GC thread: {s}", .{@errorName(err)}); return err; }; @@ -462,15 +464,19 @@ fn runWsServer(server: *websocket.Server(broadcaster.Handler), listener: *Io.net server.runIo(listener, bc); } -fn gcLoop(dp: *event_log_mod.DiskPersist, io: Io) void { - const gc_interval: u64 = 10 * 60; // 10 minutes in seconds +fn gcLoop(dp: *event_log_mod.DiskPersist) void { + const gc_interval_s: u64 = 10 * 60; // 10 minutes while (!shutdown_flag.load(.acquire)) { - // sleep in small increments to check shutdown - var remaining: u64 = gc_interval; - while (remaining > 0 and !shutdown_flag.load(.acquire)) { - const chunk = @min(remaining, 1); - io.sleep(Io.Duration.fromSeconds(@intCast(chunk)), .awake) catch return; - remaining -= chunk; + // sleep in 1s ticks so shutdown is checked frequently. uses + // std.c.nanosleep directly — this is a plain OS thread, so calling + // io.sleep on pool_io would fail on the second tick and silently + // exit the loop via `catch return`. zig 0.16 has no std.Thread.sleep + // and std.posix.nanosleep was removed during the Io migration. + var elapsed: u64 = 0; + while (elapsed < gc_interval_s and !shutdown_flag.load(.acquire)) { + const ts: std.c.timespec = .{ .sec = 1, .nsec = 0 }; + _ = std.c.nanosleep(&ts, null); + elapsed += 1; } if (shutdown_flag.load(.acquire)) return; -- 2.51.2