Skip to content

Commit 6a0df32

Browse files
justrachclaude
andcommitted
fix: 2 P2 Codex findings (EINTR-safe sleep, deinit ownership)
- compat.threadSleep: nanosleep now loops across EINTR, consuming the remaining duration from the kernel's out-param. Previously a single signal could collapse a multi-second backoff to near-zero, driving unnecessary wakeups in WAL / background workers under signal-heavy conditions. - runtime: track owns_threaded so deinit() only tears down threaded when init() actually constructed it. setIo()/ensureForTest() publish an externally-owned Io (process.Init's, or global_single_threaded), and calling threaded.deinit() on undefined state there would be UB. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9bd4dcc commit 6a0df32

2 files changed

Lines changed: 18 additions & 3 deletions

File tree

src/compat.zig

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,19 @@ pub fn nanoTimestamp() i128 {
200200
// keeps the old semantics bit-for-bit.
201201

202202
pub fn threadSleep(nanoseconds: u64) void {
203-
const ts = std.c.timespec{
203+
var remaining = std.c.timespec{
204204
.sec = @intCast(nanoseconds / std.time.ns_per_s),
205205
.nsec = @intCast(nanoseconds % std.time.ns_per_s),
206206
};
207-
_ = std.c.nanosleep(&ts, null);
207+
// Loop across EINTR so signal delivery doesn't collapse the sleep —
208+
// WAL and background workers rely on the full duration for backoff.
209+
while (true) {
210+
var rem: std.c.timespec = undefined;
211+
const rc = std.c.nanosleep(&remaining, &rem);
212+
if (rc == 0) return;
213+
if (std.posix.errno(rc) != .INTR) return;
214+
remaining = rem;
215+
}
208216
}
209217

210218
// ─── Random ───────────────────────────────────────────────────────────────

src/runtime.zig

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ pub var io: std.Io = undefined;
2929
/// (which is published with a release store by the winner).
3030
const State = enum(u8) { uninit = 0, initing = 1, ready = 2 };
3131
var state: std.atomic.Value(u8) = std.atomic.Value(u8).init(@intFromEnum(State.uninit));
32+
/// True iff `init()` constructed `threaded` — `setIo()` and `ensureForTest()`
33+
/// publish externally-owned Io and must NOT tear down `threaded`.
34+
var owns_threaded: bool = false;
3235

3336
/// Returns true if the caller won the CAS and should perform init; false if
3437
/// it lost or init already finished (in which case it has already spin-waited
@@ -54,6 +57,7 @@ pub fn init(gpa: std.mem.Allocator) void {
5457
if (!claimOrWait()) return;
5558
threaded = std.Io.Threaded.init(gpa, .{});
5659
io = threaded.io();
60+
owns_threaded = true;
5761
state.store(@intFromEnum(State.ready), .release);
5862
}
5963

@@ -64,12 +68,14 @@ pub fn init(gpa: std.mem.Allocator) void {
6468
pub fn setIo(external: std.Io) void {
6569
if (!claimOrWait()) return;
6670
io = external;
71+
owns_threaded = false;
6772
state.store(@intFromEnum(State.ready), .release);
6873
}
6974

7075
pub fn deinit() void {
7176
if (state.load(.acquire) != @intFromEnum(State.ready)) return;
72-
threaded.deinit();
77+
if (owns_threaded) threaded.deinit();
78+
owns_threaded = false;
7379
state.store(@intFromEnum(State.uninit), .release);
7480
}
7581

@@ -79,5 +85,6 @@ pub fn deinit() void {
7985
pub fn ensureForTest() void {
8086
if (!claimOrWait()) return;
8187
io = std.Io.Threaded.global_single_threaded.io();
88+
owns_threaded = false;
8289
state.store(@intFromEnum(State.ready), .release);
8390
}

0 commit comments

Comments
 (0)