authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-12-31 01:10:07+00:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-01-03 15:45:11+00:00
logf306a9f84a006a6429f485a6c99ac26723f1e1e4
treea8d87cdaa3e38b4a6f2549a0a42d3d4268c804a8
parentb8a09bcbd955c2d97216ffef23246054aa3fe756
signaturelock-open Commit is signed but in an unrecognized format.

std: rebase fixups and cancelation changes

This commit includes some API changes which I agreed with Andrew as a follow-up to the recent `Io.Group` changes: * `Io.Group.await` *does* propagate cancelation to group tasks; it then waits for them to complete, and *also* returns `error.Canceled`. The assertion that group tasks handle `error.Canceled` "correctly" means this behavior is loosely analagous to how awaiting a future works. The important thing is that the semantics of `Group.await` and `Future.await` are similar, and `error.Canceled` will always be visible to the caller (assuming correct API usage). * `Io.Group.awaitUncancelable` is removed. * `Future.await` calls `recancel` only if the "child" task (the future being awaited) did not acknowledge cancelation. If it did, then it is assumed that the future will propagate `error.Canceled` through `await` as needed.

6 files changed, 86 insertions(+), 39 deletions(-)

lib/compiler/build_runner.zig+1-1
......@@ -849,7 +849,7 @@ fn runStepNames(
849849 defer f.deinit();
850850
851851 f.start();
852 f.waitAndPrintReport();
852 try f.waitAndPrintReport();
853853 }
854854
855855 // Every test has a state
lib/std/Build/Fuzz.zig+2-2
......@@ -513,11 +513,11 @@ fn addEntryPoint(fuzz: *Fuzz, coverage_id: u64, addr: u64) error{ AlreadyReporte
513513 try coverage_map.entry_points.append(fuzz.gpa, @intCast(index));
514514}
515515
516pub fn waitAndPrintReport(fuzz: *Fuzz) void {
516pub fn waitAndPrintReport(fuzz: *Fuzz) Io.Cancelable!void {
517517 assert(fuzz.mode == .limit);
518518 const io = fuzz.io;
519519
520 fuzz.group.awaitUncancelable(io);
520 try fuzz.group.await(io);
521521 fuzz.group = .init;
522522
523523 std.debug.print("======= FUZZING REPORT =======\n", .{});
lib/std/Io.zig+3-12
......@@ -1111,7 +1111,9 @@ pub const Group = struct {
11111111 }
11121112
11131113 /// Blocks until all tasks of the group finish. During this time,
1114 /// cancelation requests propagate to all members of the group.
1114 /// cancelation requests propagate to all members of the group, and
1115 /// will also cause `error.Canceled` to be returned when the group
1116 /// does ultimately finish.
11151117 ///
11161118 /// Idempotent. Not threadsafe.
11171119 ///
......@@ -1124,17 +1126,6 @@ pub const Group = struct {
11241126 assert(g.token.raw == null);
11251127 }
11261128
1127 /// Equivalent to `await` but temporarily blocks cancelation while waiting.
1128 pub fn awaitUncancelable(g: *Group, io: Io) void {
1129 const token = g.token.load(.acquire) orelse return;
1130 const prev = swapCancelProtection(io, .blocked);
1131 defer _ = swapCancelProtection(io, prev);
1132 io.vtable.groupAwait(io.userdata, g, token) catch |err| switch (err) {
1133 error.Canceled => unreachable,
1134 };
1135 assert(g.token.raw == null);
1136 }
1137
11381129 /// Equivalent to `await` but immediately requests cancelation on all
11391130 /// members of the group.
11401131 ///
lib/std/Io/Threaded.zig+76-20
......@@ -182,7 +182,7 @@ const Group = struct {
182182 const Task = struct {
183183 runnable: Runnable,
184184 group: *Io.Group,
185 func: *const fn (context: *const anyopaque) void,
185 func: *const fn (context: *const anyopaque) Io.Cancelable!void,
186186 context_alignment: Alignment,
187187 alloc_len: usize,
188188
......@@ -192,7 +192,7 @@ const Group = struct {
192192 group: Group,
193193 context: []const u8,
194194 context_alignment: Alignment,
195 func: *const fn (context: *const anyopaque) void,
195 func: *const fn (context: *const anyopaque) Io.Cancelable!void,
196196 ) Allocator.Error!*Task {
197197 const max_context_misalignment = context_alignment.toByteUnits() -| @alignOf(Task);
198198 const worst_case_context_offset = context_alignment.forward(@sizeOf(Task) + max_context_misalignment);
......@@ -247,7 +247,20 @@ const Group = struct {
247247 }, .monotonic);
248248 }
249249
250 assertGroupResult(task.func(task.contextPointer()));
250 const result = task.func(task.contextPointer());
251 const cancel_acknowledged = switch (thread.status.load(.monotonic).cancelation) {
252 .none, .canceling => false,
253 .canceled => true,
254 .parked => unreachable,
255 .blocked => unreachable,
256 .blocked_windows_dns => unreachable,
257 .blocked_canceling => unreachable,
258 };
259 if (result) {
260 assert(!cancel_acknowledged); // group task acknowledged cancelation but did not return `error.Canceled`
261 } else |err| switch (err) {
262 error.Canceled => assert(cancel_acknowledged), // group task returned `error.Canceled` but was never canceled
263 }
251264
252265 thread.status.store(.{ .cancelation = .none, .awaitable = .null }, .monotonic);
253266 const old_status = group.status().fetchSub(.{
......@@ -348,7 +361,8 @@ const Future = struct {
348361 pending_awaited = 0b01,
349362 /// Like `pending`, but the future is being canceled. `Future.awaiter` is populated.
350363 pending_canceled = 0b11,
351 /// The future has already completed. `thread` is `null`.
364 /// The future has already completed. `thread` is `.null`, unless the future terminated
365 /// with an acknowledged cancel request, in which case `thread` is `.all_ones`.
352366 done = 0b10,
353367 },
354368 /// When the future begins execution, this is atomically updated from `null` to the thread running the
......@@ -437,10 +451,18 @@ const Future = struct {
437451
438452 future.func(future.contextPointer(), future.resultPointer());
439453
454 const had_acknowledged_cancel = switch (thread.status.load(.monotonic).cancelation) {
455 .none, .canceling => false,
456 .canceled => true,
457 .parked => unreachable,
458 .blocked => unreachable,
459 .blocked_windows_dns => unreachable,
460 .blocked_canceling => unreachable,
461 };
440462 thread.status.store(.{ .cancelation = .none, .awaitable = .null }, .monotonic);
441463 const old_status = future.status.swap(.{
442464 .tag = .done,
443 .thread = .null,
465 .thread = if (had_acknowledged_cancel) .all_ones else .null,
444466 }, .acq_rel); // acquire `future.awaiter`, release results
445467 switch (old_status.tag) {
446468 .pending => {},
......@@ -1712,11 +1734,11 @@ fn groupAsync(
17121734 const t: *Threaded = @ptrCast(@alignCast(userdata));
17131735 const g: Group = .{ .ptr = type_erased };
17141736
1715 if (builtin.single_threaded) return start(context.ptr) catch unreachable;
1737 if (builtin.single_threaded) return groupAsyncEager(start, context.ptr);
17161738
17171739 const gpa = t.allocator;
17181740 const task = Group.Task.create(gpa, g, context, context_alignment, start) catch |err| switch (err) {
1719 error.OutOfMemory => return t.assertGroupResult(start(context.ptr)),
1741 error.OutOfMemory => return groupAsyncEager(start, context.ptr),
17201742 };
17211743
17221744 t.mutex.lock();
......@@ -1726,7 +1748,7 @@ fn groupAsync(
17261748 if (busy_count >= @intFromEnum(t.async_limit)) {
17271749 t.mutex.unlock();
17281750 task.destroy(gpa);
1729 return t.assertGroupResult(start(context.ptr));
1751 return groupAsyncEager(start, context.ptr);
17301752 }
17311753
17321754 t.busy_count = busy_count + 1;
......@@ -1739,7 +1761,7 @@ fn groupAsync(
17391761 t.busy_count = busy_count;
17401762 t.mutex.unlock();
17411763 task.destroy(gpa);
1742 return t.assertGroupResult(start(context.ptr));
1764 return groupAsyncEager(start, context.ptr);
17431765 };
17441766 thread.detach();
17451767 }
......@@ -1757,23 +1779,45 @@ fn groupAsync(
17571779 t.mutex.unlock();
17581780 t.cond.signal();
17591781}
1760
1761fn assertGroupResult(result: Io.Cancelable!void) void {
1762 const cancel_acknowledged = if (Thread.current) |thread|
1763 switch (thread.status.load(.monotonic).cancelation) {
1782fn groupAsyncEager(
1783 start: *const fn (context: *const anyopaque) Io.Cancelable!void,
1784 context: *const anyopaque,
1785) void {
1786 const pre_acknowledged = if (Thread.current) |thread| ack: {
1787 break :ack switch (thread.status.load(.monotonic).cancelation) {
17641788 .none, .canceling => false,
17651789 .canceled => true,
17661790 .parked => unreachable,
17671791 .blocked => unreachable,
17681792 .blocked_windows_dns => unreachable,
17691793 .blocked_canceling => unreachable,
1770 }
1771 else
1772 false;
1794 };
1795 } else false;
1796 const result = start(context);
1797 const post_acknowledged = if (Thread.current) |thread| ack: {
1798 break :ack switch (thread.status.load(.monotonic).cancelation) {
1799 .none, .canceling => false,
1800 .canceled => true,
1801 .parked => unreachable,
1802 .blocked => unreachable,
1803 .blocked_windows_dns => unreachable,
1804 .blocked_canceling => unreachable,
1805 };
1806 } else false;
1807
17731808 if (result) {
1774 assert(!cancel_acknowledged); // group task acknowledged cancelation but did not return `error.Canceled`
1809 if (pre_acknowledged) {
1810 assert(post_acknowledged); // group task called `recancel` but was not canceled
1811 } else {
1812 assert(!post_acknowledged); // group task acknowledged cancelation but did not return `error.Canceled`
1813 }
17751814 } else |err| switch (err) {
1776 error.Canceled => assert(cancel_acknowledged), // group task returned `error.Canceled` but was never canceled
1815 // Don't swallow the cancelation: make it visible to the `Group.async` caller.
1816 error.Canceled => {
1817 assert(!pre_acknowledged); // group task called `recancel` but was not canceled
1818 assert(post_acknowledged); // group task returned `error.Canceled` but was never canceled
1819 recancelInner();
1820 },
17771821 }
17781822}
17791823
......@@ -1920,6 +1964,9 @@ fn groupCancel(userdata: ?*anyopaque, type_erased: *Io.Group, initial_token: *an
19201964fn recancel(userdata: ?*anyopaque) void {
19211965 const t: *Threaded = @ptrCast(@alignCast(userdata));
19221966 _ = t;
1967 recancelInner();
1968}
1969fn recancelInner() void {
19231970 const thread = Thread.current.?; // called `recancel` but was not canceled
19241971 switch (thread.status.fetchXor(.{
19251972 .cancelation = @enumFromInt(0b001),
......@@ -1993,7 +2040,16 @@ fn await(
19932040 future.waitForCancelWithSignaling(t, &num_completed, null);
19942041 },
19952042 }
1996 recancel(t);
2043 // If the future did not acknowledge the cancelation, we need to mark it outstanding
2044 // for us. Because `future.status.tag == .done`, the information about whether there
2045 // was an acknowledged cancelation is encoded in `future.status.thread`.
2046 const final_status = future.status.load(.monotonic);
2047 assert(final_status.tag == .done);
2048 switch (final_status.thread) {
2049 .null => recancelInner(), // cancelation was not acknowledged, so it's ours
2050 .all_ones => {}, // cancelation was acknowledged, so it was this task's job to propagate it
2051 _ => unreachable,
2052 }
19972053 },
19982054 },
19992055 .pending_awaited => unreachable, // `await` raced with `await`
......@@ -11669,7 +11725,7 @@ fn unlockStderr(userdata: ?*anyopaque) void {
1166911725 const t: *Threaded = @ptrCast(@alignCast(userdata));
1167011726 t.stderr_writer.interface.flush() catch |err| switch (err) {
1167111727 error.WriteFailed => switch (t.stderr_writer.err.?) {
11672 error.Canceled => recancel(t),
11728 error.Canceled => recancelInner(),
1167311729 else => {},
1167411730 },
1167511731 };
lib/std/Io/Threaded/test.zig+1-1
......@@ -124,7 +124,7 @@ test "Group.async context alignment" {
124124 var group: std.Io.Group = .init;
125125 var result: ByteArray512 = undefined;
126126 group.async(io, concatByteArraysResultPtr, .{ a, b, &result });
127 group.awaitUncancelable(io);
127 try group.await(io);
128128 try std.testing.expectEqualSlices(u8, &expected.x, &result.x);
129129}
130130
lib/std/Io/test.zig+3-3
......@@ -194,7 +194,7 @@ test "Group" {
194194 group.async(io, count, .{ 1, 10, &results[0] });
195195 group.async(io, count, .{ 20, 30, &results[1] });
196196
197 group.awaitUncancelable(io);
197 try group.await(io);
198198
199199 try testing.expectEqualSlices(usize, &.{ 45, 245 }, &results);
200200}
......@@ -544,9 +544,9 @@ test "tasks spawned in group after Group.cancel are canceled" {
544544 group.concurrent(io, blockUntilCanceled, .{io}) catch {};
545545 group.async(io, blockUntilCanceled, .{io});
546546 }
547 fn blockUntilCanceled(io: Io) void {
547 fn blockUntilCanceled(io: Io) Io.Cancelable!void {
548548 while (true) io.sleep(.fromSeconds(100_000), .awake) catch |err| switch (err) {
549 error.Canceled => return,
549 error.Canceled => |e| return e,
550550 error.UnsupportedClock => @panic("unsupported clock"),
551551 error.Unexpected => @panic("unexpected"),
552552 };