😱 Status quo stories
🚧 Under construction! Help needed! 🚧
We are still in the process of drafting the vision document. The stories you see on this page are examples meant to give a feeling for how a status quo story looks; you can expect them to change. See the "How to vision" page for instructions and details.
What is this
The "status quo" stories document the experience of using Async Rust today. Each story narrates the challenges encountered by one of our characters as they try (and typically fail in dramatic fashion) to achieve their goals.
Writing the "status quo" stories helps us to compensate for the curse of knowledge: the folks working on Async Rust tend to be experts in Async Rust. We've gotten used to the workarounds required to be productive, and we know the little tips and tricks that can get you out of a jam. The stories help us gauge the cumulative impact all the paper cuts can have on someone still learning their way around. This gives us the data we need to prioritize.
Based on a true story
These stories may not be true, but they are not fiction. They are based on real-life experiences of actual people. Each story contains a "Frequently Asked Questions" section referencing sources used to create the story. In some cases, it may link to notes or summaries in the conversations section, though that is not required. The "Frequently Asked Questions" section also contains a summary of what the "morals" of the story are (i.e., what are the key takeaways), along with answers to questions that people have raised along the way.
The stories provide data we use to prioritize, not a prioritization itself
Just because a user story is represented here doesn't mean we're going to be able to fix it right now. Some of these user stories will indicate more severe problems than others. As we consider the stories, we'll select some subset to try and address; that choice is reflected in the roadmap.
Metanarrative
What follows is a kind of "metanarrative" of using async Rust that summarizes the challenges that are present today. At each point, we link to the various stories; you can read the full set in the table of contents on the left. We would like to extend this to also cover some of its glories, since reading the current stories is a litany of difficulties, but obviouly we see great promise in async Rust. Note that many stories here appear more than once.
Rust strives to be a language that brings together performance, productivity, and correctness. Rust programs are designed to surface bugs early and to make common patterns both ergonomic and efficient, leading to a sense that "if it compiles, it generally works, and works efficiently". Async Rust aims to extend that same feeling to an async setting, in which a single process interweaves numerous tasks that execute concurrently. Sometimes this works beautifully. However, other times, the reality falls short of that goal.
Making hard choices from a complex ecosystem from the start
The problems begin from the very first moment a user starts to try out async Rust. The async Rust support in Rust itself is very basic, consisting only of the core Future mechanism. Everything else -- including the basic async runtimes themselves -- lives in user space. This means that users must make a number of choices from the very beginning:
- what runtime to use
- what http libraries to use
- basic helpers and utility crates are hard to find, and there are many choices, often with subtle differences between them
- Furthermore, the async ecosystem is fractured. Choosing one library may entail choosing a specific runtime. Sometimes you may wind up with multiple runtimes running at once. But sometimes you want that!
- Of course, sometimes you want multiple runtimes running together
- There is a lack of common, standardized abstractions, which means that often there are multiple attempts to establish common traits and different libraries will employ a distinct subset.
- Some of the problems are due to the design of Rust itself. The coherence rules in particular.
Once your basic setup is done, the best design patterns are subtle and not always known.
Writing async programs turns out to have all kinds of subtle tradeoffs. Rust aims to be a language that gives its users control, but that also means that users wind up having to make a lot of choices, and we don't give them much guidance.
- If you need synchronization, you might want an async lock, but you might want a synchronous lock, it's hard to know.
- Mixing sync and async code is tricky and it's not always obvious how to do it -- something it's not even clear what is "sync" (how long does a loop have to run before you can consider it blocking?)
- Barbara bridges sync and async
- Barbara compares some C++ code
- Alan thinks he needs async locks -- "locks are ok if they don't take too long"
- There are often many options for doing things like writing futures or other core concepts; which libraries or patterns are best?
- Barbara needs async helpers
- Grace wants to integrate c api
- Barbara plays with async, where she tries a number of combinations before she lands on
Box::pin(async move { .. })
- If you would to have data or task parallel operations, it's not always obvious how to do that
- Sometimes it's hard to understand what will happen when the code runs
- Sometimes async may not even be the right solution
Even once you've chosen a pattern, gettings things to compile can be a challenge.
- Async fn doesn't work everywhere
- not in traits
- not in closures -- barbara plays with async
- barbara needs async helpers
- Recursion doesn't work
- Things have to be Send all the time, some things can't live across an await
- The tricks you know from Sync rust apply but don't quite work
- e.g., Box::pin, not Box::new -- barbara plays with async
- Sometimes you have to add
boxed
- Writing strings is hard
- When you stray from the happy path, the complexity cliff is very steep
- Working with Pin is really hard, but necessary in various scenarios
- It's easy to forget to invoke a waker
- Ownership and borrowing rules get really complicated when async is involved
- Sometimes you want
&mut
access that ends while the future is suspended - Writing executors is pretty non-trivial, things have to have references to one another in a way that is not very rusty
Once you get it to compile, things don't "just work" at runtime, or they may be unexpectedly slow.
- Libraries are tied to particular runtimes and those runtimes can panic when combined, or require special setup
- Cancellation can in principle occur at any point in time, which leads to subtle bugs
- Dropping is synchronous but sometimes wants to do asynchronous things and block for them to complete
- Nested awaits mean that outer awaits cannot make progress
- Async functions let you build up large futures that execute without allocation, which is great, but can be its own cost
- It's easy to have async functions that inadvertently spend too long in between awaits
When you have those problems, you can't readily debug them or get visibility into what is going on.
- The state of the executor can be very opaque: what tasks exist? why are they blocked?
- Stacktraces are full of gobbly gook and hard to read.
- Tooling doesn't work as well with async or just plain doesn't exist.
Rust has always aimed to interoperate well with other languages and to fit itself into every niche, but that's harder with async.
- Runtimes like tokio and async-std are not designed to "share ownership" of the event loop with foreign runtimes
- Embedded environments can have pretty stringent requirements; Future was designed to be minimal, but perhaps not minimal enough
- Evolving specs for C and C++ require careful thought to integrate with async Rust's polling model
- Advanced new techniques like Ghostcell may not fit into the traits as designed