| author | |
| committer | |
| log | 2a02c7a0d59e25bac07a6ed2948a29438fb05527 |
| tree | e68a1855272d8d64d1934802501eb2d45f0992f3 |
| parent | 2adfd4d107f071f91608bef22c7e91b1a9a93470 |
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, |
| 1285 | 1285 | ||
| ... | @@ -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; |
| 1294 | 1294 | ||
| 1295 | gc.func(group, gc.contextPointer()); | 1295 | assertResult(closure, gc.func(group, gc.contextPointer())); |
| 1296 | 1296 | ||
| 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 | } |
| 1304 | 1304 | ||
| 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; |
| 1359 | 1369 | ||
| 1360 | const gpa = t.allocator; | 1370 | const gpa = t.allocator; |
| 1361 | const gc = GroupClosure.init(gpa, group, context, context_alignment, start) catch | 1371 | 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)); |
| 1363 | 1373 | ||
| 1364 | t.mutex.lock(); | 1374 | t.mutex.lock(); |
| 1365 | 1375 | ||
| ... | @@ -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 | } |
| 1373 | 1383 | ||
| 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 | } |
| 1404 | 1414 | ||
| 1415 | fn 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 | |||
| 1405 | fn groupConcurrent( | 1421 | fn 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; |
| 1413 | 1429 |
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 | } |
| 209 | 209 | ||
| 210 | test "Group cancellation" { | 210 | test "Group cancelation" { |
| 211 | const io = testing.io; | 211 | const io = testing.io; |
| 212 | 212 | ||
| 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 }; |
| 215 | 215 | ||
| 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] }); | ||
| 218 | 223 | ||
| 219 | group.cancel(io); | 224 | group.cancel(io); |
| 220 | 225 | ||
| 221 | try testing.expectEqualSlices(usize, &.{ 1, 1 }, &results); | 226 | try testing.expectEqualSlices(usize, &.{ 1, 1, 1, 1 }, &results); |
| 227 | } | ||
| 228 | |||
| 229 | fn 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 | } |
| 223 | 236 | ||
| 224 | fn sleep(io: Io, result: *usize) void { | 237 | fn sleepUncancelable(io: Io, result: *usize) void { |
| 225 | // TODO when cancellation race bug is fixed, make this timeout much longer so that | 238 | 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 | } |
| 230 | 243 | ||
| 244 | fn 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 | |||
| 231 | test "Group concurrent" { | 252 | test "Group concurrent" { |
| 232 | const io = testing.io; | 253 | const io = testing.io; |
| 233 | 254 |
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 | } |
| 845 | 845 | ||
| 846 | pub fn workerRun(f: *Fetch, prog_name: []const u8) void { | 846 | pub 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(); |
| 849 | 849 | ||
| 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 returned | 852 | 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`. |