Busy loop timeout in new std.Io

I’m a bit confused on how to implement a busy loop with the new std.Io

pub const SendDatagramError = error{
    RecvTimeout,
    LinkError,
} || std.Io.Clock.Error;
pub fn sendRecvDatagram(
    self: *Port,
    io: std.Io,
    command: telegram.Command,
    address: u32,
    data: []u8,
    timeout: std.Io.Duration,
) SendDatagramError!u16 {
    assert(data.len <= telegram.Datagram.max_data_length);
    const deadline = (try std.Io.Clock.Timestamp.now(.boot)).addDuration(.{ .raw = timeout, .clock = .boot });

    const datagram: telegram.Datagram = .init(command, address, false, data);
    var transaction: Transaction = .{ .data = .init(datagram, null, null) };

    try self.sendTransactions((&transaction)[0..1]);
    defer self.releaseTransactions((&transaction)[0..1]);

    while ((try std.Io.Clock.Timestamp.now(io, .boot)).compare(.lt, deadline)) {
        if (try self.continueTransactions((&transaction)[0..1])) {
            break;
        }
    } else {
        return error.RecvTimeout;
    }
    return transaction.data.recv_datagram.wkc;
}
  1. should I accept a timeout: std.Io.Duration, or timeout: std.Io.Timeout?

std.Io.Timeout will be troublesome at the callsite. it is rather verbose:

try port.ping(io, .{ .duration = .{ .clock = .boot, .raw = .fromNanoseconds(args.recv_timeout_us * 1000) } });
  1. You can also see in my deadline calculation I have to mention .boot clock twice.

Not an answer, but I wish std.Io would adopt something like this:

var timeout: zio.Timeout = .init;
defer timeout.clear(rt);

timeout.set(rt, timeout_ms * std.time.ns_per_ms);

// anything further will be auto-canceled if the timeout triggers
// you can catch the error and remap it, if needed

I found this to be the only sensible way of dealing with timeout in async context.

that seems convenient but limiting, what if i dont want them all to have the same timeout, what if i want no timeout for some. what if i want to add a timeout to an existing task.

It depends on you use it. You can use this for in front of individual operations, or you can use it in front of a loop to have a more global effect.

In the original example, continueTransactionshas no timeout configured, so it can theoretically block forever. You pretty much never want that.

I found that the most common timeout handling in my code was something like this:

deadline = now + timeout
while (true) {
  remaining = deadline - now
  if (remaining < 0) {
      return error.Timeout
  }
  do_something(timeout=remaining)
}

My zio.Timeout is akin to the Go context with deadline, but instead of select waiting on the done channel, this is done via cancellation. The most similar concept is Python asyncio.timeout.

I figured, as such, I’m assuming there is also a way to disable it for future tasks.

But it doesn’t address my last point, though that is a very niche and can be achieved manually.

Anyway, this is an off-topic discussion. That isn’t helpful to op.

For the current std.Io this seems to be the best option:

Accepting Io.Timeout and doing checks like this.

Timeout is more flexible, it can be a duration, deadline or none.

I imagine there will be helper functions in the future, for now you can make your own to make it less verbose.

Not sure there is anything to do about that, it supports operations between different clocks, so you have to specify.
if its common enough to use the same clock, then there might be a wrapper for that in the future.
as always you can make one yourself.

take a look at

I don’t understand why a user would have an infinite (none) timeout. In this instance, each call to sendTransactions corresponds to a single ethernet frame and each call to continueTransactions is a single call to a non-blocking recv syscall. The frame may be corrupted and destroyed by the underlying ethernet interface and could never return.

Maybe I am missing some other interface I should be using like Poller or something.