I will preface this by saying that i have no experience with async in any language. I mostly understand how its supposed to be implemented and I do have some experience with callback async in c++ with asio.
I’m playing around with the new Io in the master branch and trying to build a simple echo server. What I’m stuck on is how I’m supposed to register multiple in-flight tasks from the accept loop. I mean in here:
With callback it’s easy, just register a callback in the loop and handle everything in the callback. But in the code above, it would need to await anything that might be inside handleConnection before starting to accept the next connection. Also I might want to do something more complex inside it, like echoing each line, not just the first. For this I would either need a loop or to re-register it.
The real issue that I’m running into is that io.async() returns a future that I need to take care off, and that I need to await in order to have it execute its task. Either I still await it inside the accept loop or keep an array of all connection futures and only await at the end (this is essentially a memory leak). I’ve also looked into std.Io.Group but that also seems to be a await-at-the-end kind of deal.
Is Zig missing some sort of yield or am I just missing an obvious solution?
You can’t detach futures, you must handle them eventually, but it doesn’t have to be right away either.
Your more immediate problem is, you should asynchandleConnection instead of accept.
Well, you can do both, but the code you provided doesn’t take advantage of that, so it’s just adding unnecessary overhead to a call.
std.Io.Group is exactly what you want to await the futures later, think of it as an arena for futures. wait or cancel the group outside the loop, basically before your sever exits. you can do so with a defer.
I assumed that std.Io.Group would eventually fill up with futures after a lot of connection, but thinking more about it, it’d make more sense for it to start executing them as soon as possible. Even so, it doesn’t seem to integrate with normal error handling. If this is the idiomatic way, it seems very cumbersome. What if I want to bubble an error up to the group’s scope? Would I need to use a std.Io.Queue(MyError!void)?
For anyone trying to achieve a similar thing, I did manage to scrape something together:
It works fine, but I don’t love all the catch |err|s. Also I’m not sure that it’s impossible to leak a stream from defer _ = accept.cancel(io) catch {}; or that operations on the reader and writer integrate implicitly into the event loop, though I would assume so.
Those details are up to the Io implementation, Threaded uses a linked list with heap allocated nodes.
You can propagate the error through an out parameter, but it is just better practice to deal with them in handleConnection.
it is, cancel does return the result of the task if it finished before it could be cancelled, you have to deal with that defer if(accept.cance(io)) |stream| stream.close() else |_| {};
I’m surprised that discarding hides the type mismatch of the success case being a stream and the error case being void (your catch {}). Though I can see how It’d make sense to do that, I dont think it should as it hides resource managment errors like this.
the interfaces have no knowledge of Io, and they shouldn’t, otherwise they wouldn’t be arbitrary streams of data.
The implementation however should go through Io, which you can see as you have to give it io when you create them.