rust-lang / wg-async

Working group dedicated to improving the foundations of Async I/O in Rust
https://rust-lang.github.io/wg-async/
Apache License 2.0
379 stars 88 forks source link

Barbara considers the ecosystem challenges of writing a ?Send executor without providing an entire std-like interface #128

Open jbr opened 3 years ago

jbr commented 3 years ago

Brief summary

Background: Barbara considers writing a new futures-compatible executor to fill a niche in the executor landscape Aspect relevant to wg: Barbara hesitates / decides not to, for reasons that primarily are ecosystem and interop related

I'm writing this as an issue before writing the story because it's not clear to me if this is a distinct story as it is a mirror of a lot of the other stories that already have been well expressed. It may very well be a duplicate.

This is a real life story: I was reading #87 and realized that a fair-ish1 futures-compatible multi-threaded executor for ?Send futures would be perfect for web servers. However, there are ecosystem hurdles that make this substantially more challenging than writing the code:

Shiny opinionated future: Any character can start with a simple/standard executor and swap it out for another one without having to rewrite their entire application. Replacing libraries with ones specific to their new executor will often provide a performance benefit, but can be done over time as needed, not as a mandatory wholesale change when switching executors. Example: Alan starts with async-std's executor and a bunch of executor-independent libraries, writes a substantial application against those types and then decides he really wants the perf characteristics of tokio, so he switches just the executor and sees a benefit for his application. Later on, he finds some time to replace the networking stack with a tokio-tuned one, and sees further benefits. Everything else still is using the neutral executor-independent libraries that provide lowest-common-denominator performance. Similarly, if in a different codebase Alan gets a compiler warning about Send types but isn't sure that spawn_local has the right perf characteristics for his application, he can swap out the executor and everything else still works. Obviously either of these switches to applications-specific executors wouldn't be as seamless to reverse.

Is it worth writing this as one or more stories?

Optional details

1 fair-ish: creates new !Send futures on whichever runtime thread has the least of them, or some other similar strategy, acknowledging that this is more likely to become lopsided than a work-stealing multithreaded executor of Send futures would be, especially if the spawned tasks take an unpredictable wall-time duration. On that note, however, an optional duration_hint function on the Future trait might be useful to executors

nikomatsakis commented 3 years ago

@jbr I for one would very much like to read more stories about this. I'm not of exactly how they compare to the existing stories, but I still feel like we haven't really covered the "portability" space very fully, and this definitely touches on some of those aspects.

One clarification:

'futures-compatible'

After many reads of this sentence, I realized that you meant compatible with the futures crate, right?

jbr commented 3 years ago

After many reads of this sentence, I realized that you meant compatible with the futures crate, right?

exactly that. futures/futures-util/futures-lite/async-std/smol all share a bunch of types that I was broadly calling "futures-compatible"

uazu commented 3 years ago

I'm coming from the point of view of interfacing the Stakker actor runtime to the async/await ecosystem as a single-threaded executor. As I see it, I don't want to give up the benefits of Stakker which suits the work I do really well, but I would like to bring in third party code that uses async/await, where possible without running it on another thread with its own executor (which is still an option if all else fails).

There should be a number of tasks which require async/await but don't actually need to do I/O directly themselves, for example protocol handlers or whatever. (Whether people have actually structured their crates to work that way or not is a separate question). But in principle it should be possible for there to exist an ecosystem of executor-independent crates that do useful work and can share effort between the different executor-specific ecosystems.

From my point of view, I'm interested in what level of support executor-independent crates might need. Here are some possible things to consider:

If there was a clear API for that (and whatever other common foundation is decided on), then that would enable an ecosystem of executor-independent crates, and give someone with a runtime that they want to interface to the async/await ecosystem a clear target to implement.

I'm working on interfacing Stakker to the ecosystem using the existing traits and interfaces in the ecosystem, to see how I get on. But this is just in spare moments, so I'm not sure whether I'll be able to finish before the period of this review is over.

jbr commented 3 years ago

I'd add Async(Buf)?(Read|Write) traits to that list as absolutely essential for runtime-independent crates to be possible.

I'm not sure that timers are all that much more important than io (network and fs) and that although both would be amazing to standardize, there's probably a lot of complexity there

I think there's probably a lot of nuance hidden in that wishlist, though, since there are a bunch of flavors of spawn for each of the runtimes:

uazu commented 3 years ago

I think timers are vital for some protocols. Anything with a lossy link (UDP?) needs timeouts to retry. Things may have to time out and abort (or maybe attempt recovery) in case of remote failure. This is all independent of how exactly the stream or packet I/O is being performed. Also a timer interface's contract is relatively simple. Yes, I forgot AsyncRead/Write/etc.

I guess the question is what kind of code (or classes of code) can usefully be written as executor-independent. Then what interface do those need? This doesn't have to be a universal executor interface that covers every possible feature. Just an interface that's sufficient for the type of code that's useful to write portably.