yeah, that is what you get from io.cancel() out of the box, in the the official docs (I recommend giving it a read) they state:
Future, Group, and Batch APIs all support requesting cancelation. When cancelation is requested, the request may or may not be acknowledged. Acknowledged cancelation requests cause I/O operations to return
error.Canceled.
and
Only the logic that made the cancelation request can soundly ignore an
error.Canceled. Otherwise, there are three ways to handleerror.Canceled. In order of most common:
- Propagate it.
- After receiving it,
io.recancel()and then don’t propagate it. This rearms the cancelation request, so that the next check will have a chance to detect and acknowledge the request.- Make it unreachable with
io.swapCancelProtection().
so if for the case where you want to close the task eventually (since freeing resources usually means that there is no reason to keep the task running anyways) calling io.checkCancel() might be the thing you want. docs
you can do something like
pub fn main(init: std.process.Init) !void {
var stdout = std.Io.File.stdout().writerStreaming(init.io, &.{});
try stdout.interface.writeAll("بسم الله الرحمن الرحيم\n");
var future1 = try init.io.concurrent(writeEveryBeat, .{ init.io, &stdout.interface, 0, .fromSeconds(3) });
defer future1.cancel(init.io) catch {};
var future2 = try init.io.concurrent(writeEveryBeat, .{ init.io, &stdout.interface, 1, .fromSeconds(1) });
defer future2.cancel(init.io) catch {};
try init.io.sleep(.fromSeconds(10), .real);
}
pub fn writeEveryBeat(io: std.Io, w: *std.Io.Writer, id: usize, delay: std.Io.Duration) !void {
var count: usize = 0;
while (true) : (count += 1) {
// if cancelation was requested we won't access resources
// since it returns `error.Canceled` in that case
io.checkCancel() catch |err| {
std.log.info("sooooo, we are terminating. and I felt like logging it :)", .{});
return err;
};
// our arbitrary code here
try w.print("[{d}] foo {d}\n", .{ id, count });
// this calls `io.checkCancel()` for us
// and btw, the code above probably calls
// `io.checkCancel()` i.e is a cancelation
// point
try io.sleep(delay, .real);
}
}
which results in
[1] foo 0
[0] foo 0
[1] foo 1
[1] foo 2
[0] foo 1
[1] foo 3
[1] foo 4
[1] foo 5
[0] foo 2
[1] foo 6
[1] foo 7
[1] foo 8
[0] foo 3
[1] foo 9
notice how the log function was not called since it returned while calling sleep (io.checkCancel just adds another cancelation point)
to cancel them all at once you can use std.Io.Group, and if you want to keep the results you might add a queue to store the results like this
pub fn main(init: std.process.Init) !void {
var stdout = std.Io.File.stdout().writerStreaming(init.io, &.{});
var group: std.Io.Group = .init;
defer group.cancel(init.io);
var queue_buffer: [5]ResultType = undefined;
var queue: std.Io.Queue(ResultType) = .init(&queue_buffer);
// should finish before cancelation is requested
try group.concurrent(init.io, generateResultAfterIterations, .{ init.io, 5, .fromSeconds(1), &queue }); // should add a value after 5s
try group.concurrent(init.io, generateResultAfterIterations, .{ init.io, 4, .fromSeconds(2), &queue }); // should add a value after 8s
try group.concurrent(init.io, generateResultAfterIterations, .{ init.io, 1, .fromSeconds(5), &queue }); // should add a value after 5s
// should not finish before cancelation is requested
try group.concurrent(init.io, generateResultAfterIterations, .{ init.io, 10, .fromSeconds(8), &queue }); // should add a value after 80s
try group.concurrent(init.io, generateResultAfterIterations, .{ init.io, 100, .fromSeconds(4), &queue }); // should add a value after 400s
try group.concurrent(init.io, generateResultAfterIterations, .{ init.io, 50, .fromSeconds(100), &queue }); // should add a value after 5_000s
try init.io.sleep(.fromSeconds(10), .real);
group.cancel(init.io);
queue.close(init.io);
std.log.info("this line should only be shown after canceled tasks log which iteration they were in", .{});
try useResults(init.io, &stdout.interface, &queue);
}
pub const ResultType = struct {
id: u8,
name: [:0]const u8, // we are not allocating any memory since we will use "these"
/// creation timestamp
timestamp: std.Io.Timestamp,
};
pub fn generateResultAfterIterations(io: std.Io, iterations: u8, delay: std.Io.Duration, queue: *std.Io.Queue(ResultType)) std.Io.Cancelable!void {
var i: usize = 0;
var id: u8 = 0;
errdefer std.log.err("requested cancelation while on iteration {d}/{d}", .{ i, iterations });
while (i < iterations) : (i += 1) {
io.random(std.mem.asBytes(&id));
try io.sleep(delay, .real);
}
const name: [:0]const u8 = switch (id) {
0...100 => "under-100",
101...200 => "under-200",
else => "above-200",
};
// putOne might return error.Closed so you should consider handling it
queue.putOne(io, .{ .id = id, .name = name, .timestamp = .now(io, .real) }) catch return error.Canceled;
}
pub fn useResults(io: std.Io, w: *std.Io.Writer, queue: *std.Io.Queue(ResultType)) !void {
var results_buffer: [3]ResultType = undefined;
_ = try queue.get(io, &results_buffer, 3);
for (results_buffer) |result| {
try w.print("result: {{id: {d}, name: \"{s}\", timestamp: {f}}}\n", .{ result.id, result.name, result.timestamp.untilNow(io, .real) });
}
}
const std = @import("std");
which results in
error: requested cancelation while on iteration 0/50
error: requested cancelation while on iteration 2/100
error: requested cancelation while on iteration 1/10
info: this line should only be shown after canceled tasks log which iteration they were in
result: {id: 58, name: "under-100", timestamp: 5.001s}
result: {id: 97, name: "under-100", timestamp: 5.001s}
result: {id: 121, name: "under-200", timestamp: 2s}
I did not add io.checkCancel here since io.sleep() is a cancelation point, and resources should only [or just safer to] be freed after calling future.cancel
practically, that means the io implementation has a bug since in the std docs it says about Future.cancel():
Equivalent to
awaitbut places a cancelation request.
and that’s about it