authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-28 18:40:48-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-30 22:03:14-08:00
log3320e6a1ae453d40dd78ff3abf6c8543bec2555d
treed067055e1fd09de5a21f2d0aff8b04475a2ab6e1
parentd770e14e001daaea9eb921c1630af69c518468a2

std.Io.Threaded.batchWait better fix for any_done

It is legal to call batchWait with already completed operations in the ring. In such case, we need to avoid waiting in the syscall. The any_done flag was a poor way of tracking state we already have: whether the completion queue is empty. This problem affects the posix poll implementation as well. Thanks again to jacobly for finding the problem.

1 files changed, 17 insertions(+), 8 deletions(-)

lib/std/Io/Threaded.zig+17-8
......@@ -2560,17 +2560,31 @@ fn batchWait(userdata: ?*anyopaque, b: *Io.Batch, timeout: Io.Timeout) Io.Batch.
25602560 const deadline = timeout.toDeadline(t_io) catch return error.UnsupportedClock;
25612561 const max_poll_ms = std.math.maxInt(i32);
25622562 while (true) {
2563 const timeout_ms: i32 = if (deadline) |d| t: {
2563 const timeout_ms: i32 = t: {
2564 if (b.user.complete_head != complete_tail) {
2565 // It is legal to call batchWait with already completed
2566 // operations in the ring. In such case, we need to avoid
2567 // blocking in the poll syscall, but we can still take this
2568 // opportunity to find additional ready operations.
2569 break :t 0;
2570 }
2571 const d = deadline orelse break :t -1;
25642572 const duration = d.durationFromNow(t_io) catch return error.UnsupportedClock;
25652573 if (duration.raw.nanoseconds <= 0) return error.Timeout;
25662574 break :t @intCast(@min(max_poll_ms, duration.raw.toMilliseconds()));
2567 } else -1;
2575 };
25682576 const syscall = try Syscall.start();
25692577 const rc = posix.system.poll(&poll_buffer, poll_i, timeout_ms);
25702578 syscall.finish();
25712579 switch (posix.errno(rc)) {
25722580 .SUCCESS => {
25732581 if (rc == 0) {
2582 if (b.user.complete_head != complete_tail) {
2583 // Since there are already completions available in the
2584 // queue, this is neither a timeout nor a case for
2585 // retrying.
2586 return;
2587 }
25742588 // Although spurious timeouts are OK, when no deadline is
25752589 // passed we must not return `error.Timeout`.
25762590 if (deadline == null) continue;
......@@ -2677,8 +2691,6 @@ fn batchWaitWindows(t: *Threaded, b: *Io.Batch, timeout: Io.Timeout) Io.Batch.Wa
26772691 b.user.complete_tail = complete_tail;
26782692 }
26792693
2680 var any_done = false;
2681
26822694 while (submit_head != submit_tail) : (submit_head = submit_head.next(len)) {
26832695 const op = ring[submit_head.index(len)];
26842696 const operation = &operations[op];
......@@ -2688,7 +2700,6 @@ fn batchWaitWindows(t: *Threaded, b: *Io.Batch, timeout: Io.Timeout) Io.Batch.Wa
26882700 .noop => |*o| {
26892701 _ = o.status.unstarted;
26902702 o.status = .{ .result = {} };
2691 any_done = true;
26922703 submitComplete(ring, &complete_tail, op);
26932704 },
26942705 .file_read_streaming => |*o| {
......@@ -2696,7 +2707,6 @@ fn batchWaitWindows(t: *Threaded, b: *Io.Batch, timeout: Io.Timeout) Io.Batch.Wa
26962707 switch (try ntReadFile(o.file.handle, o.data, &metadata.iosb)) {
26972708 .status => {
26982709 o.status = .{ .result = ntReadFileResult(&metadata.iosb) };
2699 any_done = true;
27002710 submitComplete(ring, &complete_tail, op);
27012711 },
27022712 .pending => {
......@@ -2725,11 +2735,10 @@ fn batchWaitWindows(t: *Threaded, b: *Io.Batch, timeout: Io.Timeout) Io.Batch.Wa
27252735 o.status = .{ .result = ntReadFileResult(&metadata.iosb) };
27262736 },
27272737 }
2728 any_done = true;
27292738 metadata.pending = false;
27302739 submitComplete(ring, &complete_tail, op);
27312740 }
2732 if (any_done) return;
2741 if (b.user.complete_head != complete_tail) return;
27332742 if (!any_pending) return;
27342743 const alertable_syscall = try AlertableSyscall.start();
27352744 const delay_rc = windows.ntdll.NtDelayExecution(windows.TRUE, &delay_interval);