authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-02-27 17:26:34-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-02-28 05:07:54+01:00
log02142a54d22c20b07009780d7c79d90950d3b2c8
tree91d0f80adbe515e475d71d0350cb95e07e7246ad
parent99229ceb55be717aa1995f1a0105bc6b96a910f6

std.Io: remove cancelation propagation assertions

While the general guidance remains useful, it is not the case that error.Canceled will always pass across the Group task function boundary. Remove the too-aggressive assertions and add unit test coverage. Closes #30096 Closes #31340 Closes #31358

6 files changed, 89 insertions(+), 139 deletions(-)

lib/std/Io.zig+16-22
......@@ -124,7 +124,7 @@ pub const VTable = struct {
124124 /// Copied and then passed to `start`.
125125 context: []const u8,
126126 context_alignment: std.mem.Alignment,
127 start: *const fn (context: *const anyopaque) Cancelable!void,
127 start: *const fn (context: *const anyopaque) void,
128128 ) void,
129129 /// Thread-safe.
130130 groupConcurrent: *const fn (
......@@ -135,7 +135,7 @@ pub const VTable = struct {
135135 /// Copied and then passed to `start`.
136136 context: []const u8,
137137 context_alignment: std.mem.Alignment,
138 start: *const fn (context: *const anyopaque) Cancelable!void,
138 start: *const fn (context: *const anyopaque) void,
139139 ) ConcurrentError!void,
140140 groupAwait: *const fn (?*anyopaque, *Group, token: *anyopaque) Cancelable!void,
141141 groupCancel: *const fn (?*anyopaque, *Group, token: *anyopaque) void,
......@@ -1169,19 +1169,18 @@ pub const Group = struct {
11691169 /// instead of becoming associated with a `Future`.
11701170 ///
11711171 /// The return type of `function` must be coercible to `Cancelable!void`.
1172 /// `function` returning `error.Canceled` does nothing because it is an
1173 /// cancelation propagation boundary.
11721174 ///
11731175 /// Once this function is called, there are resources associated with the
11741176 /// group. To release those resources, `Group.await` or `Group.cancel` must
11751177 /// eventually be called.
1176 ///
1177 /// If `error.Canceled` is returned from any operation this task performs,
1178 /// it is asserted that `function` returns `error.Canceled`.
11791178 pub fn async(g: *Group, io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) void {
11801179 const Args = @TypeOf(args);
11811180 const TypeErased = struct {
1182 fn start(context: *const anyopaque) Cancelable!void {
1181 fn start(context: *const anyopaque) void {
11831182 const args_casted: *const Args = @ptrCast(@alignCast(context));
1184 return @call(.auto, function, args_casted.*);
1183 _ = @as(Cancelable!void, @call(.auto, function, args_casted.*)) catch {};
11851184 }
11861185 };
11871186 io.vtable.groupAsync(io.userdata, g, @ptrCast(&args), .of(Args), TypeErased.start);
......@@ -1191,19 +1190,18 @@ pub const Group = struct {
11911190 /// `Group` instead of becoming associated with a `Future`.
11921191 ///
11931192 /// The return type of `function` must be coercible to `Cancelable!void`.
1193 /// `function` returning `error.Canceled` does nothing because it is an
1194 /// cancelation propagation boundary.
11941195 ///
11951196 /// Once this function is called, there are resources associated with the
11961197 /// group. To release those resources, `Group.await` or `Group.cancel` must
11971198 /// eventually be called.
1198 ///
1199 /// If `error.Canceled` is returned from any operation this task performs,
1200 /// it is asserted that `function` returns `error.Canceled`.
12011199 pub fn concurrent(g: *Group, io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) ConcurrentError!void {
12021200 const Args = @TypeOf(args);
12031201 const TypeErased = struct {
1204 fn start(context: *const anyopaque) Cancelable!void {
1202 fn start(context: *const anyopaque) void {
12051203 const args_casted: *const Args = @ptrCast(@alignCast(context));
1206 return @call(.auto, function, args_casted.*);
1204 _ = @as(Cancelable!void, @call(.auto, function, args_casted.*)) catch {};
12071205 }
12081206 };
12091207 return io.vtable.groupConcurrent(io.userdata, g, @ptrCast(&args), .of(Args), TypeErased.start);
......@@ -1352,15 +1350,13 @@ pub fn Select(comptime U: type) type {
13521350 const Context = struct {
13531351 select: *S,
13541352 args: @TypeOf(args),
1355 fn start(type_erased_context: *const anyopaque) Cancelable!void {
1353 fn start(type_erased_context: *const anyopaque) void {
13561354 const context: *const @This() = @ptrCast(@alignCast(type_erased_context));
1357 const raw_result = @call(.auto, function, context.args);
1358 const elem = @unionInit(U, @tagName(field), raw_result);
1355 const result = @call(.auto, function, context.args);
1356 const elem = @unionInit(U, @tagName(field), result);
13591357 context.select.queue.putOneUncancelable(context.select.io, elem) catch |err| switch (err) {
13601358 error.Closed => {},
13611359 };
1362 if (@typeInfo(@TypeOf(raw_result)) == .error_union)
1363 _ = raw_result catch |err| if (err == error.Canceled) return error.Canceled;
13641360 }
13651361 };
13661362 const context: Context = .{ .select = s, .args = args };
......@@ -1391,15 +1387,13 @@ pub fn Select(comptime U: type) type {
13911387 const Context = struct {
13921388 select: *S,
13931389 args: @TypeOf(args),
1394 fn start(type_erased_context: *const anyopaque) Cancelable!void {
1390 fn start(type_erased_context: *const anyopaque) void {
13951391 const context: *const @This() = @ptrCast(@alignCast(type_erased_context));
1396 const raw_result = @call(.auto, function, context.args);
1397 const elem = @unionInit(U, @tagName(field), raw_result);
1392 const result = @call(.auto, function, context.args);
1393 const elem = @unionInit(U, @tagName(field), result);
13981394 context.select.queue.putOneUncancelable(context.select.io, elem) catch |err| switch (err) {
13991395 error.Closed => {},
14001396 };
1401 if (@typeInfo(@TypeOf(raw_result)) == .error_union)
1402 _ = raw_result catch |err| if (err == error.Canceled) return error.Canceled;
14031397 }
14041398 };
14051399 const context: Context = .{ .select = s, .args = args };
lib/std/Io/Dispatch.zig+5-26
......@@ -1331,7 +1331,7 @@ const Group = struct {
13311331 evented: *Evented,
13321332 group: Group,
13331333 fiber: *Fiber,
1334 start: *const fn (context: *const anyopaque) Io.Cancelable!void,
1334 start: *const fn (context: *const anyopaque) void,
13351335
13361336 fn fromFiber(fiber: *Fiber) *Group.AsyncClosure {
13371337 return @ptrFromInt(Fiber.max_context_align.max(.of(Group.AsyncClosure)).backward(
......@@ -1370,11 +1370,7 @@ const Group = struct {
13701370 const ev = closure.evented;
13711371 const fiber = closure.fiber;
13721372 message.handle(ev);
1373 if (closure.start(closure.contextPointer())) {
1374 assert(!fiber.cancel_protection.acknowledged); // group task acknowledged cancelation but did not return `error.Canceled`
1375 } else |err| switch (err) {
1376 error.Canceled => assert(fiber.cancel_protection.acknowledged), // group task returned `error.Canceled` but was never canceled
1377 }
1373 closure.start(closure.contextPointer());
13781374 if (closure.group.removeFiber(ev, fiber)) |awaiter| ev.queue.async(awaiter, &Fiber.@"resume");
13791375 ev.yield(.destroy);
13801376 unreachable; // switched to dead fiber
......@@ -1387,28 +1383,11 @@ fn groupAsync(
13871383 type_erased: *Io.Group,
13881384 context: []const u8,
13891385 context_alignment: Alignment,
1390 start: *const fn (context: *const anyopaque) Io.Cancelable!void,
1386 start: *const fn (context: *const anyopaque) void,
13911387) void {
13921388 const ev: *Evented = @ptrCast(@alignCast(userdata));
13931389 return groupConcurrent(ev, type_erased, context, context_alignment, start) catch {
1394 const fiber = Thread.current().currentFiber();
1395 const pre_acknowledged = fiber.cancel_protection.acknowledged;
1396 const result = start(context.ptr);
1397 const post_acknowledged = fiber.cancel_protection.acknowledged;
1398 if (result) {
1399 if (pre_acknowledged) {
1400 assert(post_acknowledged); // group task called `recancel` but was not canceled
1401 } else {
1402 assert(!post_acknowledged); // group task acknowledged cancelation but did not return `error.Canceled`
1403 }
1404 } else |err| switch (err) {
1405 // Don't swallow the cancelation: make it visible to the `Group.async` caller.
1406 error.Canceled => {
1407 assert(!pre_acknowledged); // group task called `recancel` but was not canceled
1408 assert(post_acknowledged); // group task returned `error.Canceled` but was never canceled
1409 fiber.cancel_protection.recancel();
1410 },
1411 }
1390 start(context.ptr);
14121391 };
14131392}
14141393
......@@ -1417,7 +1396,7 @@ fn groupConcurrent(
14171396 type_erased: *Io.Group,
14181397 context: []const u8,
14191398 context_alignment: Alignment,
1420 start: *const fn (context: *const anyopaque) Io.Cancelable!void,
1399 start: *const fn (context: *const anyopaque) void,
14211400) Io.ConcurrentError!void {
14221401 assert(context_alignment.compare(.lte, Fiber.max_context_align)); // TODO
14231402 assert(context.len <= Fiber.max_context_size); // TODO
lib/std/Io/Kqueue.zig+2-2
......@@ -766,7 +766,7 @@ fn groupAsync(
766766 type_erased: *Io.Group,
767767 context: []const u8,
768768 context_alignment: Alignment,
769 start: *const fn (context: *const anyopaque) Io.Cancelable!void,
769 start: *const fn (context: *const anyopaque) void,
770770) void {
771771 const k: *Kqueue = @ptrCast(@alignCast(userdata));
772772 _ = k;
......@@ -782,7 +782,7 @@ fn groupConcurrent(
782782 type_erased: *Io.Group,
783783 context: []const u8,
784784 context_alignment: Alignment,
785 start: *const fn (context: *const anyopaque) Io.Cancelable!void,
785 start: *const fn (context: *const anyopaque) void,
786786) Io.ConcurrentError!void {
787787 const k: *Kqueue = @ptrCast(@alignCast(userdata));
788788 _ = k;
lib/std/Io/Threaded.zig+7-58
......@@ -412,7 +412,7 @@ const Group = struct {
412412 const Task = struct {
413413 runnable: Runnable,
414414 group: *Io.Group,
415 func: *const fn (context: *const anyopaque) Io.Cancelable!void,
415 func: *const fn (context: *const anyopaque) void,
416416 context_alignment: Alignment,
417417 alloc_len: usize,
418418
......@@ -422,7 +422,7 @@ const Group = struct {
422422 group: Group,
423423 context: []const u8,
424424 context_alignment: Alignment,
425 func: *const fn (context: *const anyopaque) Io.Cancelable!void,
425 func: *const fn (context: *const anyopaque) void,
426426 ) Allocator.Error!*Task {
427427 const max_context_misalignment = context_alignment.toByteUnits() -| @alignOf(Task);
428428 const worst_case_context_offset = context_alignment.forward(@sizeOf(Task) + max_context_misalignment);
......@@ -477,21 +477,7 @@ const Group = struct {
477477 }, .monotonic);
478478 }
479479
480 const result = task.func(task.contextPointer());
481 const cancel_acknowledged = switch (thread.status.load(.monotonic).cancelation) {
482 .none, .canceling => false,
483 .canceled => true,
484 .parked => unreachable,
485 .blocked => unreachable,
486 .blocked_alertable => unreachable,
487 .blocked_alertable_canceling => unreachable,
488 .blocked_canceling => unreachable,
489 };
490 if (result) {
491 assert(!cancel_acknowledged); // group task acknowledged cancelation but did not return `error.Canceled`
492 } else |err| switch (err) {
493 error.Canceled => assert(cancel_acknowledged), // group task returned `error.Canceled` but was never canceled
494 }
480 task.func(task.contextPointer());
495481
496482 thread.status.store(.{ .cancelation = .none, .awaitable = .null }, .monotonic);
497483 const old_status = group.status().fetchSub(.{
......@@ -2272,7 +2258,7 @@ fn groupAsync(
22722258 type_erased: *Io.Group,
22732259 context: []const u8,
22742260 context_alignment: Alignment,
2275 start: *const fn (context: *const anyopaque) Io.Cancelable!void,
2261 start: *const fn (context: *const anyopaque) void,
22762262) void {
22772263 const t: *Threaded = @ptrCast(@alignCast(userdata));
22782264 const g: Group = .{ .ptr = type_erased };
......@@ -2323,47 +2309,10 @@ fn groupAsync(
23232309 condSignal(&t.cond);
23242310}
23252311fn groupAsyncEager(
2326 start: *const fn (context: *const anyopaque) Io.Cancelable!void,
2312 start: *const fn (context: *const anyopaque) void,
23272313 context: *const anyopaque,
23282314) void {
2329 const pre_acknowledged = if (Thread.current) |thread| ack: {
2330 break :ack switch (thread.status.load(.monotonic).cancelation) {
2331 .none, .canceling => false,
2332 .canceled => true,
2333 .parked => unreachable,
2334 .blocked => unreachable,
2335 .blocked_alertable => unreachable,
2336 .blocked_alertable_canceling => unreachable,
2337 .blocked_canceling => unreachable,
2338 };
2339 } else false;
2340 const result = start(context);
2341 const post_acknowledged = if (Thread.current) |thread| ack: {
2342 break :ack switch (thread.status.load(.monotonic).cancelation) {
2343 .none, .canceling => false,
2344 .canceled => true,
2345 .parked => unreachable,
2346 .blocked => unreachable,
2347 .blocked_alertable => unreachable,
2348 .blocked_alertable_canceling => unreachable,
2349 .blocked_canceling => unreachable,
2350 };
2351 } else false;
2352
2353 if (result) {
2354 if (pre_acknowledged) {
2355 assert(post_acknowledged); // group task called `recancel` but was not canceled
2356 } else {
2357 assert(!post_acknowledged); // group task acknowledged cancelation but did not return `error.Canceled`
2358 }
2359 } else |err| switch (err) {
2360 // Don't swallow the cancelation: make it visible to the `Group.async` caller.
2361 error.Canceled => {
2362 assert(!pre_acknowledged); // group task called `recancel` but was not canceled
2363 assert(post_acknowledged); // group task returned `error.Canceled` but was never canceled
2364 recancelInner();
2365 },
2366 }
2315 start(context);
23672316}
23682317
23692318fn groupConcurrent(
......@@ -2371,7 +2320,7 @@ fn groupConcurrent(
23712320 type_erased: *Io.Group,
23722321 context: []const u8,
23732322 context_alignment: Alignment,
2374 start: *const fn (context: *const anyopaque) Io.Cancelable!void,
2323 start: *const fn (context: *const anyopaque) void,
23752324) Io.ConcurrentError!void {
23762325 if (builtin.single_threaded) return error.ConcurrencyUnavailable;
23772326
lib/std/Io/Uring.zig+5-26
......@@ -1738,7 +1738,7 @@ const Group = struct {
17381738 evented: *Evented,
17391739 group: Group,
17401740 fiber: *Fiber,
1741 start: *const fn (context: *const anyopaque) Io.Cancelable!void,
1741 start: *const fn (context: *const anyopaque) void,
17421742
17431743 fn fromFiber(fiber: *Fiber) *Group.AsyncClosure {
17441744 return @ptrFromInt(Fiber.max_context_align.max(.of(Group.AsyncClosure)).backward(
......@@ -1784,11 +1784,7 @@ const Group = struct {
17841784 const fiber = closure.fiber;
17851785 message.handle(ev);
17861786 assert(fiber.status.queue_next == null);
1787 if (closure.start(closure.contextPointer())) {
1788 assert(!fiber.cancel_protection.acknowledged); // group task acknowledged cancelation but did not return `error.Canceled`
1789 } else |err| switch (err) {
1790 error.Canceled => assert(fiber.cancel_protection.acknowledged), // group task returned `error.Canceled` but was never canceled
1791 }
1787 closure.start(closure.contextPointer());
17921788 ev.yield(closure.group.removeFiber(ev, fiber), .destroy);
17931789 unreachable; // switched to dead fiber
17941790 }
......@@ -1800,28 +1796,11 @@ fn groupAsync(
18001796 type_erased: *Io.Group,
18011797 context: []const u8,
18021798 context_alignment: Alignment,
1803 start: *const fn (context: *const anyopaque) Io.Cancelable!void,
1799 start: *const fn (context: *const anyopaque) void,
18041800) void {
18051801 const ev: *Evented = @ptrCast(@alignCast(userdata));
18061802 return groupConcurrent(ev, type_erased, context, context_alignment, start) catch {
1807 const fiber = Thread.current().currentFiber();
1808 const pre_acknowledged = fiber.cancel_protection.acknowledged;
1809 const result = start(context.ptr);
1810 const post_acknowledged = fiber.cancel_protection.acknowledged;
1811 if (result) {
1812 if (pre_acknowledged) {
1813 assert(post_acknowledged); // group task called `recancel` but was not canceled
1814 } else {
1815 assert(!post_acknowledged); // group task acknowledged cancelation but did not return `error.Canceled`
1816 }
1817 } else |err| switch (err) {
1818 // Don't swallow the cancelation: make it visible to the `Group.async` caller.
1819 error.Canceled => {
1820 assert(!pre_acknowledged); // group task called `recancel` but was not canceled
1821 assert(post_acknowledged); // group task returned `error.Canceled` but was never canceled
1822 fiber.cancel_protection.recancel();
1823 },
1824 }
1803 start(context.ptr);
18251804 };
18261805}
18271806
......@@ -1830,7 +1809,7 @@ fn groupConcurrent(
18301809 type_erased: *Io.Group,
18311810 context: []const u8,
18321811 context_alignment: Alignment,
1833 start: *const fn (context: *const anyopaque) Io.Cancelable!void,
1812 start: *const fn (context: *const anyopaque) void,
18341813) Io.ConcurrentError!void {
18351814 assert(context_alignment.compare(.lte, Fiber.max_context_align)); // TODO
18361815 assert(context.len <= Fiber.max_context_size); // TODO
lib/std/Io/test.zig+54-5
......@@ -255,8 +255,6 @@ test "Group.cancel" {
255255}
256256
257257test "Group.concurrent" {
258 if (builtin.os.tag == .linux and !builtin.link_libc) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30096
259
260258 const io = testing.io;
261259
262260 var group: Io.Group = .init;
......@@ -265,14 +263,14 @@ test "Group.concurrent" {
265263
266264 group.concurrent(io, count, .{ 1, 10, &results[0] }) catch |err| switch (err) {
267265 error.ConcurrencyUnavailable => {
268 try testing.expect(builtin.single_threaded);
266 try expect(builtin.single_threaded);
269267 return;
270268 },
271269 };
272270
273271 group.concurrent(io, count, .{ 20, 30, &results[1] }) catch |err| switch (err) {
274272 error.ConcurrencyUnavailable => {
275 try testing.expect(builtin.single_threaded);
273 try expect(builtin.single_threaded);
276274 return;
277275 },
278276 };
......@@ -282,6 +280,57 @@ test "Group.concurrent" {
282280 try testing.expectEqualSlices(usize, &.{ 45, 245 }, &results);
283281}
284282
283test "Group materializes error.Cancel" {
284 const S = struct {
285 fn task() Io.Cancelable!void {
286 return error.Canceled;
287 }
288 };
289
290 const io = testing.io;
291
292 var group: Io.Group = .init;
293
294 group.async(io, S.task, .{});
295 group.concurrent(io, S.task, .{}) catch |err| switch (err) {
296 error.ConcurrencyUnavailable => {
297 try expect(builtin.single_threaded);
298 return;
299 },
300 };
301
302 try group.await(io);
303}
304
305test "Group task receives cancelation unknowingly" {
306 const S = struct {
307 io: Io,
308 err: ?Io.Cancelable!void,
309
310 fn task(s: *@This()) void {
311 foo(s);
312 }
313
314 fn foo(s: *@This()) void {
315 s.err = s.io.sleep(.fromSeconds(300), .awake);
316 }
317 };
318
319 const io = testing.io;
320
321 var group: Io.Group = .init;
322 var result: S = .{ .io = io, .err = null };
323 group.concurrent(io, S.task, .{&result}) catch |err| switch (err) {
324 error.ConcurrencyUnavailable => {
325 try expect(builtin.single_threaded);
326 return;
327 },
328 };
329 group.cancel(io);
330
331 try expectError(error.Canceled, result.err.?);
332}
333
285334fn testQueue(comptime len: usize) !void {
286335 const io = testing.io;
287336 var buf: [len]usize = undefined;
......@@ -541,7 +590,7 @@ test "random" {
541590 io.random(@ptrCast(&b));
542591 io.random(@ptrCast(&c));
543592
544 try std.testing.expect(a ^ b ^ c != 0);
593 try expect(a ^ b ^ c != 0);
545594}
546595
547596test "randomSecure" {