From 46e7dda5ab012ccccf76d4b810420a20adc5a0fe Mon Sep 17 00:00:00 2001 From: Endel Dreyer Date: Fri, 30 Oct 2026 17:10:39 -0300 Subject: [PATCH] iocp: make no_wait collect finished I/O tick(0) broke out of its loop before asking the completion port, so run(.no_wait) only ran timers and what was already queued. epoll and kqueue ask the kernel once, with a zero timeout. A host driving the loop from its own event loop never saw a completion. Assisted-by: Claude Opus 5.5 --- src/backend/iocp.zig | 52 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/backend/iocp.zig b/src/backend/iocp.zig index cc8506ea..c5d1ddf0 100644 --- a/src/backend/iocp.zig +++ b/src/backend/iocp.zig @@ -313,10 +313,12 @@ pub const Loop = struct { } // If we have processed enough event, we break out of the loop. - if (wait_rem == 0) break; + if (wait > 0 and wait_rem == 0) break; // Determine our next timeout based on the timers. const timeout: ?windows.DWORD = timeout: { + if (wait == 0) break :timeout 0; + // If we have a timer, we want to set the timeout to our next timer value. If we // have no timer, we wait forever. const t = self.timers.peek() orelse break :timeout null; @@ -2278,6 +2280,54 @@ test "iocp: socket accept/connect/send/recv/close" { try testing.expect(client_conn_closed); } +test "iocp: no_wait collects what the kernel has finished" { + const testing = std.testing; + + var loop = try Loop.init(.{}); + defer loop.deinit(); + + const address = try net.Address.parseIp4("127.0.0.1", 3132); + const socket = try windows.WSASocketW(windows.ws2_32.AF.INET, windows.ws2_32.SOCK.DGRAM, windows.ws2_32.IPPROTO.UDP, null, 0, windows.ws2_32.WSA_FLAG_OVERLAPPED); + defer iocpClose(socket); + try iocpBind(asSocket(socket), &address.any, address.getOsSockLen()); + + var received: ?ReadError!usize = null; + var buf: [8]u8 = undefined; + var c: Completion = .{ + .op = .{ .recvfrom = .{ .fd = socket, .buffer = .{ .slice = &buf } } }, + .userdata = &received, + .callback = (struct { + fn callback(ud: ?*anyopaque, _: *Loop, _: *Completion, r: Result) CallbackAction { + const out: *?ReadError!usize = @ptrCast(@alignCast(ud.?)); + out.* = r.recvfrom; + return .disarm; + } + }).callback, + }; + loop.add(&c); + try loop.run(.no_wait); + try testing.expect(received == null); + + const sender = try windows.WSASocketW(windows.ws2_32.AF.INET, windows.ws2_32.SOCK.DGRAM, windows.ws2_32.IPPROTO.UDP, null, 0, windows.ws2_32.WSA_FLAG_OVERLAPPED); + defer iocpClose(sender); + const Os = struct { + extern "kernel32" fn Sleep(ms: u32) callconv(.winapi) void; + extern "ws2_32" fn sendto(s: windows.ws2_32.SOCKET, buf: [*]const u8, len: i32, flags: i32, to: *const posix.sockaddr, to_len: i32) callconv(.winapi) i32; + }; + try testing.expectEqual(4, Os.sendto(asSocket(sender), "ping", 4, 0, &address.any, @intCast(address.getOsSockLen()))); + + // Loopback delivery isn't synchronous, so this gives it time, without + // ever letting the loop block. + for (0..1000) |_| { + try loop.run(.no_wait); + if (received != null) break; + Os.Sleep(1); + } + try testing.expect(received != null); + try testing.expectEqual(4, try received.?); + try testing.expectEqualStrings("ping", buf[0..4]); +} + test "iocp: recv cancellation" { const mem = std.mem; const testing = std.testing;