Thanks for bringing this up, this is an important topic and it’s worth understanding.
Let’s start at a very high level with the top-line answer to “What does this mean for us?” The answer from the NIO core team as of the 31st of October 2020 is we don’t know yet. This proposal is very young, there is little running code, we haven’t been able to prototype and investigate.
More importantly, this is not something for the NIO team to decide by fiat. We want to land in a world where users are able to write high performance, flexible code with minimal overhead when that is necessary, but also to have an easy time writing scalable concurrent code. How these things trade off is not necessarily straightforward to reason about.
With that said, let’s dive into some more detailed thinking.
The Swift core team has signalled that they are considering a breaking language change as part of stage 2 of the concurrency work. With that signaled, I would highly prioritise adopting stage 1 incrementally, without a semver major in NIO. Having a language-break accompanied by a NIO break is a nice natural fit.
This would mean that in the short term we’d be looking at incremental changes. The goal will be to enable users to adopt actors in NIO-based programs without needing to fundamentally rewrite much of what they have already written. Existing programs should continue to work just as they do now.
So where will these abstractions come into being?
I think there are three places to investigate, in roughly the following priority order:
- Allowing async calls to the Channel API, or something similar
- Allowing EventLoops to be Executors
- Allowing actor ChannelHandlers
All of these are built on a foundational point, which is “work out how to plumb Futures through the concurrency code sensibly”.
I think the first point is the most important. The vast majority of users don’t write ChannelHandlers, they work on top of Channels. It would be extremely valuable if users could treat a Channel like an actor. In my mind, this will also want to include having an async function you can call to read data from as well, something the Channel API doesn’t provide today.
Normally this would mean turning a Channel into an actor, but because we don’t want to have breaking changes, this will likely instead mean having a way to perform the transformation. This implies an
actor class that wraps a Channel and forwards the calls as needed.
This kind of abstraction frees higher level libraries (such as @mordil’s RediStack) up to reinvent themselves as actor-based libraries. They would continue to provide regular, synchronous ChannelHandlers to do the low-level protocol work, but the high-level connection wrangling would become actor-based, at least optionally.
The second point seems to go hand-in-hand. NIO has a core executor concept: it’s an event loop. We need to do the work to enable users to have an actor that lives on an event loop. However, as @ktoso has noted, the executor concept as it exists in the current pitch isn’t very useful to us. This is because being able to enqueue on an event loop isn’t itself any more useful than being able to enqueue onto a dispatch queue. What matters is that you can take advantage of that knowledge to build a big shared mutex. While we should do this irregardless, for this to be actually useful in server side programs we need to press the core team to consider allowing us to say that actors with the same executor do not need to dispatch across each other.
Finally, ChannelHandlers. The ChannelHandler API is low-level and, for the most part, entirely synchronous. The only wrinkle is that sometimes ChannelHandlers want to do things in response to writes completing, say, or closures happening. It would be nice to be able to provide users the opportunity to write their ChannelHandler as an actor and use the source code transformation that actors provide. For the NIO 2 timeframe, we’d probably do that in a similar way to how we do
ByteToMessageDecoder: users would implement a different protocol and we’d wrap it in a
ChannelHandler to manage the bridging.
I think doing the ChannelHandler thing is contingent on having a way to make confident assertions about what executor is being used. I don’t know how easy it’ll be to do this and I don’t know if it’ll be worth it.
Longer term, when breaking changes come along, they may warrant a more substantial rethink of the NIO API. In particular, they will challenge us to decide whether NIO’s API needs reinvention in terms of these new abstractions, at least in part. Some obvious options include requiring ChannelHandlers to be
actorlocal types, which would solidify their current thread-safety requirements, redefining Channels as actors entirely, and so-on.
How much of that we do, and how quickly, will depend on the performance story. Moving all of NIO over to an actor model requires that we do not give up too much performance. If we have to, then we’ll always want to make the actor interface an opt-in higher level abstraction that can be used in less performance-sensitive contexts.