authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-12-29 18:24:40-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-12-29 22:51:06-08:00
log2a02c7a0d59e25bac07a6ed2948a29438fb05527
treee68a1855272d8d64d1934802501eb2d45f0992f3
parent2adfd4d107f071f91608bef22c7e91b1a9a93470

std.Io.Group: async and concurrent support Cancelable results

Now, the return type of functions spawned with `Group.async` and `Group.concurrent` may be anything that coerces to `Io.Cancelable!void`. Before this commit, group tasks were the only exception to the rule "error.Canceled should never be swallowed". Now, there is no exception, and it is enforced with an assertion upon closure completion. Finally, fixes a case of swallowing error.Canceled in the compiler, solving a TODO. There are three ways to handle `error.Canceled`. In order of most common: 1. Propagate it 2. After receiving it, io.recancel() and then don't propagate it 3. Make it unreachable with io.swapCancelProtection()

4 files changed, 61 insertions(+), 24 deletions(-)

lib/std/Io.zig+7-7
...@@ -631,7 +631,7 @@ pub const VTable = struct {...@@ -631,7 +631,7 @@ pub const VTable = struct {
631 /// Copied and then passed to `start`.631 /// Copied and then passed to `start`.
632 context: []const u8,632 context: []const u8,
633 context_alignment: std.mem.Alignment,633 context_alignment: std.mem.Alignment,
634 start: *const fn (*Group, context: *const anyopaque) void,634 start: *const fn (*Group, context: *const anyopaque) Cancelable!void,
635 ) void,635 ) void,
636 /// Thread-safe.636 /// Thread-safe.
637 groupConcurrent: *const fn (637 groupConcurrent: *const fn (
...@@ -642,7 +642,7 @@ pub const VTable = struct {...@@ -642,7 +642,7 @@ pub const VTable = struct {
642 /// Copied and then passed to `start`.642 /// Copied and then passed to `start`.
643 context: []const u8,643 context: []const u8,
644 context_alignment: std.mem.Alignment,644 context_alignment: std.mem.Alignment,
645 start: *const fn (*Group, context: *const anyopaque) void,645 start: *const fn (*Group, context: *const anyopaque) Cancelable!void,
646 ) ConcurrentError!void,646 ) ConcurrentError!void,
647 groupAwait: *const fn (?*anyopaque, *Group, token: *anyopaque) Cancelable!void,647 groupAwait: *const fn (?*anyopaque, *Group, token: *anyopaque) Cancelable!void,
648 groupCancel: *const fn (?*anyopaque, *Group, token: *anyopaque) void,648 groupCancel: *const fn (?*anyopaque, *Group, token: *anyopaque) void,
...@@ -1082,10 +1082,10 @@ pub const Group = struct {...@@ -1082,10 +1082,10 @@ pub const Group = struct {
1082 pub fn async(g: *Group, io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) void {1082 pub fn async(g: *Group, io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) void {
1083 const Args = @TypeOf(args);1083 const Args = @TypeOf(args);
1084 const TypeErased = struct {1084 const TypeErased = struct {
1085 fn start(group: *Group, context: *const anyopaque) void {1085 fn start(group: *Group, context: *const anyopaque) Cancelable!void {
1086 _ = group;1086 _ = group;
1087 const args_casted: *const Args = @ptrCast(@alignCast(context));1087 const args_casted: *const Args = @ptrCast(@alignCast(context));
1088 @call(.auto, function, args_casted.*);1088 return @call(.auto, function, args_casted.*);
1089 }1089 }
1090 };1090 };
1091 io.vtable.groupAsync(io.userdata, g, @ptrCast(&args), .of(Args), TypeErased.start);1091 io.vtable.groupAsync(io.userdata, g, @ptrCast(&args), .of(Args), TypeErased.start);
...@@ -1110,10 +1110,10 @@ pub const Group = struct {...@@ -1110,10 +1110,10 @@ pub const Group = struct {
1110 pub fn concurrent(g: *Group, io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) ConcurrentError!void {1110 pub fn concurrent(g: *Group, io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) ConcurrentError!void {
1111 const Args = @TypeOf(args);1111 const Args = @TypeOf(args);
1112 const TypeErased = struct {1112 const TypeErased = struct {
1113 fn start(group: *Group, context: *const anyopaque) void {1113 fn start(group: *Group, context: *const anyopaque) Cancelable!void {
1114 _ = group;1114 _ = group;
1115 const args_casted: *const Args = @ptrCast(@alignCast(context));1115 const args_casted: *const Args = @ptrCast(@alignCast(context));
1116 @call(.auto, function, args_casted.*);1116 return @call(.auto, function, args_casted.*);
1117 }1117 }
1118 };1118 };
1119 return io.vtable.groupConcurrent(io.userdata, g, @ptrCast(&args), .of(Args), TypeErased.start);1119 return io.vtable.groupConcurrent(io.userdata, g, @ptrCast(&args), .of(Args), TypeErased.start);
...@@ -1265,7 +1265,7 @@ pub fn Select(comptime U: type) type {...@@ -1265,7 +1265,7 @@ pub fn Select(comptime U: type) type {
1265 ) void {1265 ) void {
1266 const Args = @TypeOf(args);1266 const Args = @TypeOf(args);
1267 const TypeErased = struct {1267 const TypeErased = struct {
1268 fn start(group: *Group, context: *const anyopaque) void {1268 fn start(group: *Group, context: *const anyopaque) Cancelable!void {
1269 const args_casted: *const Args = @ptrCast(@alignCast(context));1269 const args_casted: *const Args = @ptrCast(@alignCast(context));
1270 const unerased_select: *S = @fieldParentPtr("group", group);1270 const unerased_select: *S = @fieldParentPtr("group", group);
1271 const elem = @unionInit(U, @tagName(field), @call(.auto, function, args_casted.*));1271 const elem = @unionInit(U, @tagName(field), @call(.auto, function, args_casted.*));
lib/std/Io/Threaded.zig+25-9
...@@ -1279,7 +1279,7 @@ const GroupClosure = struct {...@@ -1279,7 +1279,7 @@ const GroupClosure = struct {
1279 group: *Io.Group,1279 group: *Io.Group,
1280 /// Points to sibling `GroupClosure`. Used for walking the group to cancel all.1280 /// Points to sibling `GroupClosure`. Used for walking the group to cancel all.
1281 node: std.SinglyLinkedList.Node,1281 node: std.SinglyLinkedList.Node,
1282 func: *const fn (*Io.Group, context: *anyopaque) void,1282 func: *const fn (*Io.Group, context: *anyopaque) Io.Cancelable!void,
1283 context_alignment: Alignment,1283 context_alignment: Alignment,
1284 alloc_len: usize,1284 alloc_len: usize,
12851285
...@@ -1292,7 +1292,7 @@ const GroupClosure = struct {...@@ -1292,7 +1292,7 @@ const GroupClosure = struct {
1292 current_thread.current_closure = closure;1292 current_thread.current_closure = closure;
1293 current_thread.cancel_protection = .unblocked;1293 current_thread.cancel_protection = .unblocked;
12941294
1295 gc.func(group, gc.contextPointer());1295 assertResult(closure, gc.func(group, gc.contextPointer()));
12961296
1297 current_thread.current_closure = null;1297 current_thread.current_closure = null;
1298 current_thread.cancel_protection = undefined;1298 current_thread.cancel_protection = undefined;
...@@ -1302,6 +1302,16 @@ const GroupClosure = struct {...@@ -1302,6 +1302,16 @@ const GroupClosure = struct {
1302 if (prev_state == (sync_one_pending | sync_is_waiting)) event.set(ioBasic(t));1302 if (prev_state == (sync_one_pending | sync_is_waiting)) event.set(ioBasic(t));
1303 }1303 }
13041304
1305 fn assertResult(closure: *Closure, result: Io.Cancelable!void) void {
1306 if (result) |_| switch (closure.cancel_status.unpack()) {
1307 .none, .requested => {},
1308 .acknowledged => unreachable, // task illegally swallowed error.Canceled
1309 .signal_id => unreachable,
1310 } else |err| switch (err) {
1311 error.Canceled => assert(closure.cancel_status == .acknowledged),
1312 }
1313 }
1314
1305 fn contextPointer(gc: *GroupClosure) [*]u8 {1315 fn contextPointer(gc: *GroupClosure) [*]u8 {
1306 const base: [*]u8 = @ptrCast(gc);1316 const base: [*]u8 = @ptrCast(gc);
1307 const context_offset = gc.context_alignment.forward(@intFromPtr(gc) + @sizeOf(GroupClosure)) - @intFromPtr(gc);1317 const context_offset = gc.context_alignment.forward(@intFromPtr(gc) + @sizeOf(GroupClosure)) - @intFromPtr(gc);
...@@ -1314,7 +1324,7 @@ const GroupClosure = struct {...@@ -1314,7 +1324,7 @@ const GroupClosure = struct {
1314 group: *Io.Group,1324 group: *Io.Group,
1315 context: []const u8,1325 context: []const u8,
1316 context_alignment: Alignment,1326 context_alignment: Alignment,
1317 func: *const fn (*Io.Group, context: *const anyopaque) void,1327 func: *const fn (*Io.Group, context: *const anyopaque) Io.Cancelable!void,
1318 ) Allocator.Error!*GroupClosure {1328 ) Allocator.Error!*GroupClosure {
1319 const max_context_misalignment = context_alignment.toByteUnits() -| @alignOf(GroupClosure);1329 const max_context_misalignment = context_alignment.toByteUnits() -| @alignOf(GroupClosure);
1320 const worst_case_context_offset = context_alignment.forward(@sizeOf(GroupClosure) + max_context_misalignment);1330 const worst_case_context_offset = context_alignment.forward(@sizeOf(GroupClosure) + max_context_misalignment);
...@@ -1352,14 +1362,14 @@ fn groupAsync(...@@ -1352,14 +1362,14 @@ fn groupAsync(
1352 group: *Io.Group,1362 group: *Io.Group,
1353 context: []const u8,1363 context: []const u8,
1354 context_alignment: Alignment,1364 context_alignment: Alignment,
1355 start: *const fn (*Io.Group, context: *const anyopaque) void,1365 start: *const fn (*Io.Group, context: *const anyopaque) Io.Cancelable!void,
1356) void {1366) void {
1357 const t: *Threaded = @ptrCast(@alignCast(userdata));1367 const t: *Threaded = @ptrCast(@alignCast(userdata));
1358 if (builtin.single_threaded) return start(group, context.ptr);1368 if (builtin.single_threaded) return start(group, context.ptr) catch unreachable;
13591369
1360 const gpa = t.allocator;1370 const gpa = t.allocator;
1361 const gc = GroupClosure.init(gpa, group, context, context_alignment, start) catch1371 const gc = GroupClosure.init(gpa, group, context, context_alignment, start) catch
1362 return start(group, context.ptr);1372 return t.assertGroupResult(start(group, context.ptr));
13631373
1364 t.mutex.lock();1374 t.mutex.lock();
13651375
...@@ -1368,7 +1378,7 @@ fn groupAsync(...@@ -1368,7 +1378,7 @@ fn groupAsync(
1368 if (busy_count >= @intFromEnum(t.async_limit)) {1378 if (busy_count >= @intFromEnum(t.async_limit)) {
1369 t.mutex.unlock();1379 t.mutex.unlock();
1370 gc.deinit(gpa);1380 gc.deinit(gpa);
1371 return start(group, context.ptr);1381 return t.assertGroupResult(start(group, context.ptr));
1372 }1382 }
13731383
1374 t.busy_count = busy_count + 1;1384 t.busy_count = busy_count + 1;
...@@ -1381,7 +1391,7 @@ fn groupAsync(...@@ -1381,7 +1391,7 @@ fn groupAsync(
1381 t.busy_count = busy_count;1391 t.busy_count = busy_count;
1382 t.mutex.unlock();1392 t.mutex.unlock();
1383 gc.deinit(gpa);1393 gc.deinit(gpa);
1384 return start(group, context.ptr);1394 return t.assertGroupResult(start(group, context.ptr));
1385 };1395 };
1386 thread.detach();1396 thread.detach();
1387 }1397 }
...@@ -1402,12 +1412,18 @@ fn groupAsync(...@@ -1402,12 +1412,18 @@ fn groupAsync(
1402 t.cond.signal();1412 t.cond.signal();
1403}1413}
14041414
1415fn assertGroupResult(t: *Threaded, result: Io.Cancelable!void) void {
1416 const current_thread: *Thread = .getCurrent(t);
1417 const current_closure = current_thread.current_closure orelse return;
1418 GroupClosure.assertResult(current_closure, result);
1419}
1420
1405fn groupConcurrent(1421fn groupConcurrent(
1406 userdata: ?*anyopaque,1422 userdata: ?*anyopaque,
1407 group: *Io.Group,1423 group: *Io.Group,
1408 context: []const u8,1424 context: []const u8,
1409 context_alignment: Alignment,1425 context_alignment: Alignment,
1410 start: *const fn (*Io.Group, context: *const anyopaque) void,1426 start: *const fn (*Io.Group, context: *const anyopaque) Io.Cancelable!void,
1411) Io.ConcurrentError!void {1427) Io.ConcurrentError!void {
1412 if (builtin.single_threaded) return error.ConcurrencyUnavailable;1428 if (builtin.single_threaded) return error.ConcurrencyUnavailable;
14131429
lib/std/Io/test.zig+27-6
...@@ -207,27 +207,48 @@ fn count(a: usize, b: usize, result: *usize) void {...@@ -207,27 +207,48 @@ fn count(a: usize, b: usize, result: *usize) void {
207 result.* = sum;207 result.* = sum;
208}208}
209209
210test "Group cancellation" {210test "Group cancelation" {
211 const io = testing.io;211 const io = testing.io;
212212
213 var group: Io.Group = .init;213 var group: Io.Group = .init;
214 var results: [2]usize = undefined;214 var results: [4]usize = .{ 0, 0, 0, 0 };
215215
216 // TODO when robust cancelation is available, make the sleep timeouts much
217 // longer so that it causes the unit test to be failed if not canceled.
218 // https://codeberg.org/ziglang/zig/issues/30049
216 group.async(io, sleep, .{ io, &results[0] });219 group.async(io, sleep, .{ io, &results[0] });
217 group.async(io, sleep, .{ io, &results[1] });220 group.async(io, sleep, .{ io, &results[1] });
221 group.async(io, sleepUncancelable, .{ io, &results[2] });
222 group.async(io, sleepRecancel, .{ io, &results[3] });
218223
219 group.cancel(io);224 group.cancel(io);
220225
221 try testing.expectEqualSlices(usize, &.{ 1, 1 }, &results);226 try testing.expectEqualSlices(usize, &.{ 1, 1, 1, 1 }, &results);
227}
228
229fn sleep(io: Io, result: *usize) error{Canceled}!void {
230 defer result.* = 1;
231 io.sleep(.fromMilliseconds(1), .awake) catch |err| switch (err) {
232 error.Canceled => |e| return e,
233 else => {},
234 };
222}235}
223236
224fn sleep(io: Io, result: *usize) void {237fn sleepUncancelable(io: Io, result: *usize) void {
225 // TODO when cancellation race bug is fixed, make this timeout much longer so that238 const old_prot = io.swapCancelProtection(.blocked);
226 // it causes the unit test to be failed if not canceled.239 defer _ = io.swapCancelProtection(old_prot);
227 io.sleep(.fromMilliseconds(1), .awake) catch {};240 io.sleep(.fromMilliseconds(1), .awake) catch {};
228 result.* = 1;241 result.* = 1;
229}242}
230243
244fn sleepRecancel(io: Io, result: *usize) void {
245 io.sleep(.fromMilliseconds(1), .awake) catch |err| switch (err) {
246 error.Canceled => io.recancel(),
247 else => {},
248 };
249 result.* = 1;
250}
251
231test "Group concurrent" {252test "Group concurrent" {
232 const io = testing.io;253 const io = testing.io;
233254
src/Package/Fetch.zig+2-2
...@@ -843,13 +843,13 @@ pub fn relativePathDigest(pkg_root: Cache.Path, cache_root: Cache.Directory) Pac...@@ -843,13 +843,13 @@ pub fn relativePathDigest(pkg_root: Cache.Path, cache_root: Cache.Directory) Pac
843 return .initPath(pkg_root.sub_path, pkg_root.root_dir.eql(cache_root));843 return .initPath(pkg_root.sub_path, pkg_root.root_dir.eql(cache_root));
844}844}
845845
846pub fn workerRun(f: *Fetch, prog_name: []const u8) void {846pub fn workerRun(f: *Fetch, prog_name: []const u8) Io.Cancelable!void {
847 const prog_node = f.prog_node.start(prog_name, 0);847 const prog_node = f.prog_node.start(prog_name, 0);
848 defer prog_node.end();848 defer prog_node.end();
849849
850 run(f) catch |err| switch (err) {850 run(f) catch |err| switch (err) {
851 error.OutOfMemory => f.oom_flag = true,851 error.OutOfMemory => f.oom_flag = true,
852 error.Canceled => {}, // TODO make groupAsync functions be cancelable and assert proper value was returned852 error.Canceled => |e| return e,
853 error.FetchFailed => {853 error.FetchFailed => {
854 // Nothing to do because the errors are already reported in `error_bundle`,854 // Nothing to do because the errors are already reported in `error_bundle`,
855 // and a reference is kept to the `Fetch` task inside `all_fetches`.855 // and a reference is kept to the `Fetch` task inside `all_fetches`.