Handle clients with std.Io.Batch

How can I implement something like this with std.Io.Batch? I’m confused about its usage. This is just a draft, but it would be helpful to understand how it works.

pub const Event = struct {
    from: std.posix.fd_t,
    type: Type = .none,

    const Type = union(enum) {
        none,

        accept,
        close,
        // contains the length of data
        // to read afterwards
        data: u64,
    };
};

pub fn wait(poll_fds: []std.posix.pollfd) error{ PollFailed, UnknownEvent }!Event {
    _ = std.posix.poll(poll_fds, -1) catch return error.PollFailed;

    const accept_fd = poll_fds[0].fd;

    for (poll_fds) |*poll_fd| {
        if (poll_fd.revents == 0) continue;
        const revents = poll_fd.revents;
        poll_fd.revents = 0;

        assert((revents & std.posix.POLL.NVAL) == 0);

        const event_type: Event.Type = if (poll_fd.fd == accept_fd)
            .accept
        else if ((revents & std.posix.POLL.IN) != 0) blk: {
            var buf: [8]u8 = undefined;
            const n = std.posix.read(poll_fd.fd, &buf) catch 0;
            break :blk if (n != 0) .{ .data = @bitCast(buf) } else .close;
        } else if ((revents & (std.posix.POLL.ERR | std.posix.POLL.HUP)) != 0)
            .close
        else
            return error.UnknownEvent;

        return .{
            .from = poll_fd.fd,
            .type = event_type,
        };
    }

    unreachable;
}

This code detects I/O readiness and converts it into a higher-level event.
On the other hand, std.Io.Batch is a low-level API for running multiple read/write operations concurrently.

Therefore, it is not a good fit to replace this code with std.Io.Batch.
Instead, I would prefer using something like std.Io.async().

Which is 1) incorrect (you want concurrency, not asynchrony), and, 2) wasteful (at least until we get stackless coroutines)

It should become more or less feasible once networking gets fully migrated into Operations, however, even then it comes with caveats: you have to pre-allocate an array for all operations upfront (Batch is not resizable), which you can try to workaround by either resizing it yourself (which would require to cancel all of the in-flight operations before doing so) or booting least-active connections out - which also has an issue - you cannot cancel individual operations, only all at once.

Batch uses index’s into the storage buffer, so it can be grown, you just need to initialise the new memory correctly and add it to the unused list.

Just make sure you don’t do that while awaiting.

Isn’t this a usecase for std.Io.Select ? Correct me if I’m wrong, this is still new stuff for me

This is what I meant by “resizing it yourself”. And even then, that invalidates the internal state of batch. Look no further than std.Io.Threaded’s implementation of batchAwaitConcurrent. Once the operations exceed a certain number, it’ll allocate a poll buffer with size of the Batch storage and store it as userdata. When you will be manually resizing batch storage, the implementation won’t adjust it. This means that you still cannot avoid canceling the entire batch.

1 Like

Select will give similar semantics, but Batch should be more optimal.

2 Likes

I don’t think this is really a matter of choosing between concurrency or asynchrony, nor is it about efficiency in this case.
The issue is more about abstraction mismatch.

The original code uses std.posix.poll() to wait for I/O readiness and then converts a ready FD into a single high-level event.
This pattern can be represented using a Future-based abstraction, since it essentially produces a single completion value per wait.

Regarding std.Io.Select, I see some overlap with poll, but they operate at slightly different abstraction levels.
Select is more about coordinating multiple asynchronous operations, whereas this code is closer to readiness-based event demultiplexing.

So I don’t think either is a perfect fit here, though Select could be useful if the goal is to unify interfaces at a higher level.

1 Like

It is. Waiting on multiple file descriptors using poll(2) is a way of achieving concurrency, NOT asynchrony.

You still mismatched readiness and completion in 2 successive sentences. But whatever, the intent still remains the same - multiplex over multiple file descriptors - you can do it with both readiness-based and completion-based mechanisms. The latter is more portable because it can be implemented in terms of former, but not the other way around, which is likely why std.Io took this approach (the original motivation for it in Io was reading from stdout and stderr of a child process at once without task overhead, which was implemented in terms of std.Io.Poller before)

Batch is basically what they’re looking for. It provides a way to “submit” many I/O operations and perform them concurrently. Select is a higher-level abstraction that is way less optimal than this, because it provides concurrency at a task level, which with current available implementations implies either an OS thread per task or a stackful coroutine per task. Either are wasteful if you just want to do multiple I/O operations at once.

3 Likes

I see your point. I was looking at it more from the perspective of preserving the original abstraction/API shape ( wait([]std.posix.pollfd) -> Event ) rather than optimizing the underlying multiplexing strategy itself.

In that sense, the Future/async comment was more about how the readiness-to-event conversion could be represented, not about implementing the underlying I/O mechanism with task-level concurrency.

But yeah, I think I understand better now why std.Io.Batch is considered a good fit for this kind of multiplexing workload internally.

Two important points that seem to be missing in this thread:

  1. std.Io.Batch is still completion based, main operation there is to read data, not wait for socket readiness, so it’s not a direct replacement for poll, it’s more like io_uring/IOCP.

  2. std.Io.Operation is currently missing most of the useful functionality. There is no TCP networking there, for example. You could hack it on POSIX by creating std.Io.File structs from sockets and using the streaming file writer, but I wouldn’t do that.

If you need truly a replacement for Zig 0.16, I’d look for some external library. There are at least two event loop libraries that I know have been migrated.

1 Like