diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 88813e8ae85eb8aca53a01d4f0a6b3302e8fa43d..d9bf914aa4018da8dd5670eef03bc48a8a1a0117 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -124,7 +124,7 @@ pub const VTable = struct { /// Copied and then passed to `start`. context: []const u8, context_alignment: std.mem.Alignment, - start: *const fn (context: *const anyopaque) Cancelable!void, + start: *const fn (context: *const anyopaque) void, ) void, /// Thread-safe. groupConcurrent: *const fn ( @@ -135,7 +135,7 @@ pub const VTable = struct { /// Copied and then passed to `start`. context: []const u8, context_alignment: std.mem.Alignment, - start: *const fn (context: *const anyopaque) Cancelable!void, + start: *const fn (context: *const anyopaque) void, ) ConcurrentError!void, groupAwait: *const fn (?*anyopaque, *Group, token: *anyopaque) Cancelable!void, groupCancel: *const fn (?*anyopaque, *Group, token: *anyopaque) void, @@ -1169,19 +1169,18 @@ pub const Group = struct { /// instead of becoming associated with a `Future`. /// /// The return type of `function` must be coercible to `Cancelable!void`. + /// `function` returning `error.Canceled` does nothing because it is an + /// cancelation propagation boundary. /// /// Once this function is called, there are resources associated with the /// group. To release those resources, `Group.await` or `Group.cancel` must /// eventually be called. - /// - /// If `error.Canceled` is returned from any operation this task performs, - /// it is asserted that `function` returns `error.Canceled`. pub fn async(g: *Group, io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) void { const Args = @TypeOf(args); const TypeErased = struct { - fn start(context: *const anyopaque) Cancelable!void { + fn start(context: *const anyopaque) void { const args_casted: *const Args = @ptrCast(@alignCast(context)); - return @call(.auto, function, args_casted.*); + _ = @as(Cancelable!void, @call(.auto, function, args_casted.*)) catch {}; } }; io.vtable.groupAsync(io.userdata, g, @ptrCast(&args), .of(Args), TypeErased.start); @@ -1191,19 +1190,18 @@ pub const Group = struct { /// `Group` instead of becoming associated with a `Future`. /// /// The return type of `function` must be coercible to `Cancelable!void`. + /// `function` returning `error.Canceled` does nothing because it is an + /// cancelation propagation boundary. /// /// Once this function is called, there are resources associated with the /// group. To release those resources, `Group.await` or `Group.cancel` must /// eventually be called. - /// - /// If `error.Canceled` is returned from any operation this task performs, - /// it is asserted that `function` returns `error.Canceled`. pub fn concurrent(g: *Group, io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) ConcurrentError!void { const Args = @TypeOf(args); const TypeErased = struct { - fn start(context: *const anyopaque) Cancelable!void { + fn start(context: *const anyopaque) void { const args_casted: *const Args = @ptrCast(@alignCast(context)); - return @call(.auto, function, args_casted.*); + _ = @as(Cancelable!void, @call(.auto, function, args_casted.*)) catch {}; } }; return io.vtable.groupConcurrent(io.userdata, g, @ptrCast(&args), .of(Args), TypeErased.start); @@ -1352,15 +1350,13 @@ pub fn Select(comptime U: type) type { const Context = struct { select: *S, args: @TypeOf(args), - fn start(type_erased_context: *const anyopaque) Cancelable!void { + fn start(type_erased_context: *const anyopaque) void { const context: *const @This() = @ptrCast(@alignCast(type_erased_context)); - const raw_result = @call(.auto, function, context.args); - const elem = @unionInit(U, @tagName(field), raw_result); + const result = @call(.auto, function, context.args); + const elem = @unionInit(U, @tagName(field), result); context.select.queue.putOneUncancelable(context.select.io, elem) catch |err| switch (err) { error.Closed => {}, }; - if (@typeInfo(@TypeOf(raw_result)) == .error_union) - _ = raw_result catch |err| if (err == error.Canceled) return error.Canceled; } }; const context: Context = .{ .select = s, .args = args }; @@ -1391,15 +1387,13 @@ pub fn Select(comptime U: type) type { const Context = struct { select: *S, args: @TypeOf(args), - fn start(type_erased_context: *const anyopaque) Cancelable!void { + fn start(type_erased_context: *const anyopaque) void { const context: *const @This() = @ptrCast(@alignCast(type_erased_context)); - const raw_result = @call(.auto, function, context.args); - const elem = @unionInit(U, @tagName(field), raw_result); + const result = @call(.auto, function, context.args); + const elem = @unionInit(U, @tagName(field), result); context.select.queue.putOneUncancelable(context.select.io, elem) catch |err| switch (err) { error.Closed => {}, }; - if (@typeInfo(@TypeOf(raw_result)) == .error_union) - _ = raw_result catch |err| if (err == error.Canceled) return error.Canceled; } }; const context: Context = .{ .select = s, .args = args }; diff --git a/lib/std/Io/Dispatch.zig b/lib/std/Io/Dispatch.zig index 328fb59162fd8bbdaa28401a3513e1a1c3172cdc..1a1d644276565f00ad65ca4e7183124351159a59 100644 --- a/lib/std/Io/Dispatch.zig +++ b/lib/std/Io/Dispatch.zig @@ -1331,7 +1331,7 @@ const Group = struct { evented: *Evented, group: Group, fiber: *Fiber, - start: *const fn (context: *const anyopaque) Io.Cancelable!void, + start: *const fn (context: *const anyopaque) void, fn fromFiber(fiber: *Fiber) *Group.AsyncClosure { return @ptrFromInt(Fiber.max_context_align.max(.of(Group.AsyncClosure)).backward( @@ -1370,11 +1370,7 @@ const Group = struct { const ev = closure.evented; const fiber = closure.fiber; message.handle(ev); - if (closure.start(closure.contextPointer())) { - assert(!fiber.cancel_protection.acknowledged); // group task acknowledged cancelation but did not return `error.Canceled` - } else |err| switch (err) { - error.Canceled => assert(fiber.cancel_protection.acknowledged), // group task returned `error.Canceled` but was never canceled - } + closure.start(closure.contextPointer()); if (closure.group.removeFiber(ev, fiber)) |awaiter| ev.queue.async(awaiter, &Fiber.@"resume"); ev.yield(.destroy); unreachable; // switched to dead fiber @@ -1387,28 +1383,11 @@ fn groupAsync( type_erased: *Io.Group, context: []const u8, context_alignment: Alignment, - start: *const fn (context: *const anyopaque) Io.Cancelable!void, + start: *const fn (context: *const anyopaque) void, ) void { const ev: *Evented = @ptrCast(@alignCast(userdata)); return groupConcurrent(ev, type_erased, context, context_alignment, start) catch { - const fiber = Thread.current().currentFiber(); - const pre_acknowledged = fiber.cancel_protection.acknowledged; - const result = start(context.ptr); - const post_acknowledged = fiber.cancel_protection.acknowledged; - if (result) { - if (pre_acknowledged) { - assert(post_acknowledged); // group task called `recancel` but was not canceled - } else { - assert(!post_acknowledged); // group task acknowledged cancelation but did not return `error.Canceled` - } - } else |err| switch (err) { - // Don't swallow the cancelation: make it visible to the `Group.async` caller. - error.Canceled => { - assert(!pre_acknowledged); // group task called `recancel` but was not canceled - assert(post_acknowledged); // group task returned `error.Canceled` but was never canceled - fiber.cancel_protection.recancel(); - }, - } + start(context.ptr); }; } @@ -1417,7 +1396,7 @@ fn groupConcurrent( type_erased: *Io.Group, context: []const u8, context_alignment: Alignment, - start: *const fn (context: *const anyopaque) Io.Cancelable!void, + start: *const fn (context: *const anyopaque) void, ) Io.ConcurrentError!void { assert(context_alignment.compare(.lte, Fiber.max_context_align)); // TODO assert(context.len <= Fiber.max_context_size); // TODO diff --git a/lib/std/Io/Kqueue.zig b/lib/std/Io/Kqueue.zig index e70a3b1274c25333564c8a9655d0d974c64b2007..3a4dadea2c6df3586f4982901516d0acae6bc7f1 100644 --- a/lib/std/Io/Kqueue.zig +++ b/lib/std/Io/Kqueue.zig @@ -766,7 +766,7 @@ fn groupAsync( type_erased: *Io.Group, context: []const u8, context_alignment: Alignment, - start: *const fn (context: *const anyopaque) Io.Cancelable!void, + start: *const fn (context: *const anyopaque) void, ) void { const k: *Kqueue = @ptrCast(@alignCast(userdata)); _ = k; @@ -782,7 +782,7 @@ fn groupConcurrent( type_erased: *Io.Group, context: []const u8, context_alignment: Alignment, - start: *const fn (context: *const anyopaque) Io.Cancelable!void, + start: *const fn (context: *const anyopaque) void, ) Io.ConcurrentError!void { const k: *Kqueue = @ptrCast(@alignCast(userdata)); _ = k; diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index a7e9668309eab37ccf91f34fc4bcdb57744b0b42..c0b6b2b47d8e20503e1c31bc65a9a5f81445712a 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -412,7 +412,7 @@ const Group = struct { const Task = struct { runnable: Runnable, group: *Io.Group, - func: *const fn (context: *const anyopaque) Io.Cancelable!void, + func: *const fn (context: *const anyopaque) void, context_alignment: Alignment, alloc_len: usize, @@ -422,7 +422,7 @@ const Group = struct { group: Group, context: []const u8, context_alignment: Alignment, - func: *const fn (context: *const anyopaque) Io.Cancelable!void, + func: *const fn (context: *const anyopaque) void, ) Allocator.Error!*Task { const max_context_misalignment = context_alignment.toByteUnits() -| @alignOf(Task); const worst_case_context_offset = context_alignment.forward(@sizeOf(Task) + max_context_misalignment); @@ -477,21 +477,7 @@ const Group = struct { }, .monotonic); } - const result = task.func(task.contextPointer()); - const cancel_acknowledged = switch (thread.status.load(.monotonic).cancelation) { - .none, .canceling => false, - .canceled => true, - .parked => unreachable, - .blocked => unreachable, - .blocked_alertable => unreachable, - .blocked_alertable_canceling => unreachable, - .blocked_canceling => unreachable, - }; - if (result) { - assert(!cancel_acknowledged); // group task acknowledged cancelation but did not return `error.Canceled` - } else |err| switch (err) { - error.Canceled => assert(cancel_acknowledged), // group task returned `error.Canceled` but was never canceled - } + task.func(task.contextPointer()); thread.status.store(.{ .cancelation = .none, .awaitable = .null }, .monotonic); const old_status = group.status().fetchSub(.{ @@ -2272,7 +2258,7 @@ fn groupAsync( type_erased: *Io.Group, context: []const u8, context_alignment: Alignment, - start: *const fn (context: *const anyopaque) Io.Cancelable!void, + start: *const fn (context: *const anyopaque) void, ) void { const t: *Threaded = @ptrCast(@alignCast(userdata)); const g: Group = .{ .ptr = type_erased }; @@ -2323,47 +2309,10 @@ fn groupAsync( condSignal(&t.cond); } fn groupAsyncEager( - start: *const fn (context: *const anyopaque) Io.Cancelable!void, + start: *const fn (context: *const anyopaque) void, context: *const anyopaque, ) void { - const pre_acknowledged = if (Thread.current) |thread| ack: { - break :ack switch (thread.status.load(.monotonic).cancelation) { - .none, .canceling => false, - .canceled => true, - .parked => unreachable, - .blocked => unreachable, - .blocked_alertable => unreachable, - .blocked_alertable_canceling => unreachable, - .blocked_canceling => unreachable, - }; - } else false; - const result = start(context); - const post_acknowledged = if (Thread.current) |thread| ack: { - break :ack switch (thread.status.load(.monotonic).cancelation) { - .none, .canceling => false, - .canceled => true, - .parked => unreachable, - .blocked => unreachable, - .blocked_alertable => unreachable, - .blocked_alertable_canceling => unreachable, - .blocked_canceling => unreachable, - }; - } else false; - - if (result) { - if (pre_acknowledged) { - assert(post_acknowledged); // group task called `recancel` but was not canceled - } else { - assert(!post_acknowledged); // group task acknowledged cancelation but did not return `error.Canceled` - } - } else |err| switch (err) { - // Don't swallow the cancelation: make it visible to the `Group.async` caller. - error.Canceled => { - assert(!pre_acknowledged); // group task called `recancel` but was not canceled - assert(post_acknowledged); // group task returned `error.Canceled` but was never canceled - recancelInner(); - }, - } + start(context); } fn groupConcurrent( @@ -2371,7 +2320,7 @@ fn groupConcurrent( type_erased: *Io.Group, context: []const u8, context_alignment: Alignment, - start: *const fn (context: *const anyopaque) Io.Cancelable!void, + start: *const fn (context: *const anyopaque) void, ) Io.ConcurrentError!void { if (builtin.single_threaded) return error.ConcurrencyUnavailable; diff --git a/lib/std/Io/Uring.zig b/lib/std/Io/Uring.zig index 12c715c205b4920a6dc6adb5a72a040dab1d11fc..0f4f64334f3da1bbb5eec2701c513bb3762e56d0 100644 --- a/lib/std/Io/Uring.zig +++ b/lib/std/Io/Uring.zig @@ -1738,7 +1738,7 @@ const Group = struct { evented: *Evented, group: Group, fiber: *Fiber, - start: *const fn (context: *const anyopaque) Io.Cancelable!void, + start: *const fn (context: *const anyopaque) void, fn fromFiber(fiber: *Fiber) *Group.AsyncClosure { return @ptrFromInt(Fiber.max_context_align.max(.of(Group.AsyncClosure)).backward( @@ -1784,11 +1784,7 @@ const Group = struct { const fiber = closure.fiber; message.handle(ev); assert(fiber.status.queue_next == null); - if (closure.start(closure.contextPointer())) { - assert(!fiber.cancel_protection.acknowledged); // group task acknowledged cancelation but did not return `error.Canceled` - } else |err| switch (err) { - error.Canceled => assert(fiber.cancel_protection.acknowledged), // group task returned `error.Canceled` but was never canceled - } + closure.start(closure.contextPointer()); ev.yield(closure.group.removeFiber(ev, fiber), .destroy); unreachable; // switched to dead fiber } @@ -1800,28 +1796,11 @@ fn groupAsync( type_erased: *Io.Group, context: []const u8, context_alignment: Alignment, - start: *const fn (context: *const anyopaque) Io.Cancelable!void, + start: *const fn (context: *const anyopaque) void, ) void { const ev: *Evented = @ptrCast(@alignCast(userdata)); return groupConcurrent(ev, type_erased, context, context_alignment, start) catch { - const fiber = Thread.current().currentFiber(); - const pre_acknowledged = fiber.cancel_protection.acknowledged; - const result = start(context.ptr); - const post_acknowledged = fiber.cancel_protection.acknowledged; - if (result) { - if (pre_acknowledged) { - assert(post_acknowledged); // group task called `recancel` but was not canceled - } else { - assert(!post_acknowledged); // group task acknowledged cancelation but did not return `error.Canceled` - } - } else |err| switch (err) { - // Don't swallow the cancelation: make it visible to the `Group.async` caller. - error.Canceled => { - assert(!pre_acknowledged); // group task called `recancel` but was not canceled - assert(post_acknowledged); // group task returned `error.Canceled` but was never canceled - fiber.cancel_protection.recancel(); - }, - } + start(context.ptr); }; } @@ -1830,7 +1809,7 @@ fn groupConcurrent( type_erased: *Io.Group, context: []const u8, context_alignment: Alignment, - start: *const fn (context: *const anyopaque) Io.Cancelable!void, + start: *const fn (context: *const anyopaque) void, ) Io.ConcurrentError!void { assert(context_alignment.compare(.lte, Fiber.max_context_align)); // TODO assert(context.len <= Fiber.max_context_size); // TODO diff --git a/lib/std/Io/test.zig b/lib/std/Io/test.zig index 587c23eb85defb5c01dc2435ab50140dfdac43cc..2de8d1022c4e8da958c2fedfdbd9c0b7249be80e 100644 --- a/lib/std/Io/test.zig +++ b/lib/std/Io/test.zig @@ -255,8 +255,6 @@ test "Group.cancel" { } test "Group.concurrent" { - if (builtin.os.tag == .linux and !builtin.link_libc) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30096 - const io = testing.io; var group: Io.Group = .init; @@ -265,14 +263,14 @@ test "Group.concurrent" { group.concurrent(io, count, .{ 1, 10, &results[0] }) catch |err| switch (err) { error.ConcurrencyUnavailable => { - try testing.expect(builtin.single_threaded); + try expect(builtin.single_threaded); return; }, }; group.concurrent(io, count, .{ 20, 30, &results[1] }) catch |err| switch (err) { error.ConcurrencyUnavailable => { - try testing.expect(builtin.single_threaded); + try expect(builtin.single_threaded); return; }, }; @@ -282,6 +280,57 @@ test "Group.concurrent" { try testing.expectEqualSlices(usize, &.{ 45, 245 }, &results); } +test "Group materializes error.Cancel" { + const S = struct { + fn task() Io.Cancelable!void { + return error.Canceled; + } + }; + + const io = testing.io; + + var group: Io.Group = .init; + + group.async(io, S.task, .{}); + group.concurrent(io, S.task, .{}) catch |err| switch (err) { + error.ConcurrencyUnavailable => { + try expect(builtin.single_threaded); + return; + }, + }; + + try group.await(io); +} + +test "Group task receives cancelation unknowingly" { + const S = struct { + io: Io, + err: ?Io.Cancelable!void, + + fn task(s: *@This()) void { + foo(s); + } + + fn foo(s: *@This()) void { + s.err = s.io.sleep(.fromSeconds(300), .awake); + } + }; + + const io = testing.io; + + var group: Io.Group = .init; + var result: S = .{ .io = io, .err = null }; + group.concurrent(io, S.task, .{&result}) catch |err| switch (err) { + error.ConcurrencyUnavailable => { + try expect(builtin.single_threaded); + return; + }, + }; + group.cancel(io); + + try expectError(error.Canceled, result.err.?); +} + fn testQueue(comptime len: usize) !void { const io = testing.io; var buf: [len]usize = undefined; @@ -541,7 +590,7 @@ test "random" { io.random(@ptrCast(&b)); io.random(@ptrCast(&c)); - try std.testing.expect(a ^ b ^ c != 0); + try expect(a ^ b ^ c != 0); } test "randomSecure" {