You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
io_singleton: eliminate test/prod divergence — real Threaded fallback
`getOrInit()` now constructs a real `std.Io.Threaded.init(page_allocator,
.{})` instead of `init_single_threaded`. Tests exercise the same
concurrent runtime as production — parallel `connectMany` (functional
Happy Eyeballs), worker-thread pool, real concurrency. The exact
environmental divergence the maintainer rules against, now gone.
Root cause of the earlier worker-thread crash (which I'd misdiagnosed as
"singleton has no clean tear-down across 500 call sites"): my own
`resetForTesting()` was nulling `_fallback_threaded` out from under live
workers. The optional storage zeroed, workers read `busy_count == 0`,
decremented, panicked on integer underflow at `Threaded.zig:1799`. Fix:
`resetForTesting()` only clears `current_io` now; the threaded struct
stays process-stable. Workers stay parked on condvar wait for the
process lifetime; OS reaps them at exit.
Parity test rewritten: was a divergence-LOCK ("expectError ConcurrencyUnavailable"), now a divergence-NEGATION ("expect concurrent ops succeed"). If this test fails, the fallback regressed and divergence is back.
PLAN.md: item marked done. All 51 test binaries pass.
-[ ] Refactor `io_singleton.zig` to thread `std.Io` explicitly through callers per the Zig 0.16 migration doc, instead of a process-global mutable singleton (flagged 2026-05-31 by parent-dir LLM session — "pragmatic shortcut" framing is a Peter-flagged red flag). Hazards: daemon/background paths can hit stale Io; `@panic` on startup-order regressions; `getOrInit()` hides Io-init bugs in tests. At minimum, drop the "acceptable for a CLI binary" justification language from the module doc comment if the refactor is deferred.
118
-
-[ ] Fix silent test/prod Io divergence in `io_singleton.getOrInit()` — currently falls back to `std.Io.Threaded.init_single_threaded` (allocator=.failing, concurrent_limit=.nothing), so HTTP/connect paths in tests get `error.ConcurrencyUnavailable` while prod runs real threaded Io with parallel `connectMany` (functional Happy Eyeballs). Tried 2026-05-31: swapping to `Io.Threaded.init(page_allocator, .{})` panics worker threads with `busy_count` underflow because the process-singleton has no clean tear-down across the ~500 call sites — fix is blocked on the broader explicit-threading refactor above. Until then, the regression test in `src/io_singleton.zig` LOCKS the current single-threaded fallback so a future naive change fails loudly. Next viable angles: (a) make `getOrInit()` panic in test contexts that haven't called `set()`, forcing test authors to declare intent (large blast radius), or (b) do the threading refactor first, then swap the fallback.
117
+
-[ ] Audit daemon/watcher entry paths to confirm each one calls `io_singleton.set(init.io)` early enough — §12.31's "single-entry CLI with synchronous shutdown" guarantee holds per-process if the daemon's own `main` does the set. Inbox note 2026-05-31 framed the singleton itself as a red flag; on re-reading §12.31 it's the migration-doc-endorsed pattern for codescan's CLI+FFI shape (sibling project `validate/src/core/runtime.zig` cites §12.31 directly). The narrow concern is daemon lifetime, not the singleton design.
118
+
-[ ] (Optional) Adopt sibling project `validate`'s `runtime.zig` convenience wrappers — `openFile(path, opts)`, `openDir(path, opts)`, `access(path, opts)`, `statFile(path)`, `nanoTimestamp()` — to collapse the 461 `io_singleton.getOrInit()` call sites in 23 files. Pure call-site brevity refactor, no semantic change. Path: define wrappers in `io_singleton.zig`, sed-sweep call sites.
119
+
-[x] Fix silent test/prod Io divergence in `io_singleton.getOrInit()` — DONE 2026-05-31. Fallback now constructs a real `std.Io.Threaded.init(page_allocator, .{})` so tests exercise the same concurrent runtime as production (parallel `connectMany`, functional Happy Eyeballs). Root cause of earlier worker-thread crash was `resetForTesting()` nulling `_fallback_threaded` out from under live workers — fixed by keeping the threaded struct process-stable and only clearing `current_io`. Parity test in `src/io_singleton.zig` now asserts concurrent ops SUCCEED (previously locked the divergence). All 51 test binaries pass.
119
120
-[x] Watcher syslog logging + `codescan log` subcommand and MCP tool (completed 2026-04-18 EST) — OS-managed logs so we can diagnose watcher stops after the fact; filterable by project root via `log show` / `journalctl -t codescan`. Spec: `docs/superpowers/specs/2026-04-18-watcher-syslog-logging-design.md`. Plan: `docs/superpowers/plans/2026-04-18-watcher-syslog-logging.md`.
120
121
-[x] Env-var expansion in config file values (`$VAR`, `${VAR}`, `${VAR:-default}`, `${VAR-default}`, nested to depth 10) with raw-preservation on save for `embedding_api_key` (completed 2026-04-19 EST) — spec: `docs/superpowers/specs/2026-04-19-config-env-var-expansion-design.md`. Plan: `docs/superpowers/plans/2026-04-19-config-env-var-expansion.md`. Reference impl in docscan `cli/main.c:1344-1509`.
121
122
-[x] Auto-reindex after CLI edits (skip re-embedding, daemon catches up on vectors)
Copy file name to clipboardExpand all lines: src/io_singleton.zig
+31-46Lines changed: 31 additions & 46 deletions
Original file line number
Diff line number
Diff line change
@@ -7,11 +7,10 @@
7
7
//! run outside main's lifetime (daemon, watcher, background tasks) must call
8
8
//! `io_singleton.set(init.io)` early enough or risk the `get()` panic.
9
9
//!
10
-
//! Tests call `set(io)` with a fresh `Io.Threaded` if they need real
11
-
//! concurrency. Tests that don't `set()` fall back to `getOrInit()`, which
12
-
//! returns a single-threaded Io — concurrent ops on it return
13
-
//! `error.ConcurrencyUnavailable`. See the divergence-lock test at the
14
-
//! bottom of this file.
10
+
//! Tests call `set(io)` with a fresh `Io.Threaded`, or rely on
11
+
//! `getOrInit()` to lazily construct a real `Io.Threaded` (worker pool,
12
+
//! full concurrency) — both paths exercise the same kind of Io as
13
+
//! production. See the parity test at the bottom of this file.
15
14
16
15
conststd=@import("std");
17
16
@@ -62,32 +61,28 @@ pub fn get() std.Io {
62
61
returncurrent_ioorelse@panic("io_singleton.get() before set(); this is a codescan migration scaffold — call io_singleton.set(init.io) in main, or set up an Io.Threaded in your test");
63
62
}
64
63
65
-
/// Lazy default: returns the set io, or sets up and returns a single-threaded
66
-
/// blocking Io on first call.
64
+
/// Lazy default: returns the set io, or constructs a real `Io.Threaded`
65
+
/// (allocator=page_allocator, full concurrency) on first call.
67
66
///
68
-
/// TEST/PROD DIVERGENCE (flagged 2026-05-31, see PLAN.md Phase 5):
69
-
/// This fallback uses `init_single_threaded`, which ships with
70
-
/// `allocator=.failing` and `concurrent_limit=.nothing`. Any test path that
71
-
/// reaches concurrency code through this fallback gets
72
-
/// `error.ConcurrencyUnavailable` (often surfacing as OOM mapped to that),
73
-
/// while production uses a full `Io.Threaded.init` that races address
74
-
/// candidates in parallel (functional Happy Eyeballs).
67
+
/// TEST/PROD PARITY: this returns the same kind of Io that `main()` would
68
+
/// have given via `set(init.io)` — worker-thread pool, real concurrency,
69
+
/// real `connectMany` Happy Eyeballs. Tests exercise the same runtime as
70
+
/// production. (Flagged 2026-05-31; the prior `init_single_threaded`
71
+
/// fallback caused silent ConcurrencyUnavailable in tests while prod ran
72
+
/// fully concurrent — the exact environmental divergence the maintainer
73
+
/// rules against.)
75
74
///
76
-
/// Naively swapping in `Io.Threaded.init(page_alloc, .{})` here breaks the
77
-
/// test suite: it spawns worker threads + installs SIGIO/SIGPIPE handlers
78
-
/// process-wide, and the process-singleton lifecycle has no clean tear-down
79
-
/// across the ~500 call sites that go through this fallback. Worker threads
80
-
/// race with later test bodies and panic on `busy_count` underflow.
81
-
///
82
-
/// Fixing this properly requires the explicit `std.Io` threading refactor
83
-
/// PLAN.md Phase 5 calls for. Until then, the regression test below LOCKS
84
-
/// the current divergent behavior — if someone "fixes" this fallback without
85
-
/// also doing the refactor, the test fails loudly.
75
+
/// LIFETIME: `_fallback_threaded` is process-lived and intentionally never
76
+
/// deinit'd from outside this module — workers stay parked on condvar wait
77
+
/// for the life of the process. Process exit reaps them at the OS level.
78
+
/// `resetForTesting()` clears only `current_io`, NOT `_fallback_threaded`,
79
+
/// because nulling the optional storage out from under live workers
80
+
/// underflows `busy_count` in `Threaded.zig:1799`.
0 commit comments