Ideas on how to model structured concurrency in Rust, as well as some thoughts on tasks, errors, and panics... 1/29
-
-
Put succinctly, structured concurrency is all about robustly spawning/awaiting tasks and propagating errors/panics from child tasks into parent tasks, more or less. 3/29
Show this thread -
Tokio kicked off a research effort on structured concurrency. Some great ideas were proposed, but we're still pretty much at square one and don't have readily available solutions. 4/29https://github.com/tokio-rs/tokio/issues/1879 …
Show this thread -
Right now, all our "hello world" TCP echo examples are suffering from glaring problems. Take for example these from tokio and async-std. 5/29 https://github.com/tokio-rs/tokio/blob/57ba37c97854d32e691ea68006c8d69d58c79b23/examples/echo.rs … https://github.com/async-rs/async-std/blob/2dbebe54ede4d2c0a18380f51e785d5306022ac5/examples/tcp-echo.rs …
Show this thread -
Tokio's example panics on errors with expect(), which is a shame because it would be much nicer to use the ? operator. Panics inside tasks are then silently "swallowed" by tokio and ignored, i.e. they don't propagate anywhere. 6/29pic.twitter.com/g9r1la9nfR
Show this thread -
Async-std's example uses the ? operator inside tasks, but we have to remember to unwrap() the result after awaiting process(), or else errors get lost. Panics inside tasks will get async-std to crash the whole process. 7/29pic.twitter.com/WAxGp4SgFp
Show this thread -
It feels natural and tempting to just do task::spawn(process(stream)), but in that case errors will be totally ignored, and the compiler won't even give us a warning! Both async-std and tokio have this pitfall. 8/29
Show this thread -
Neither async-std nor tokio have mechanisms to handle errors robustly. The compiler does not give us errors nor warnings. Instead, the programmer needs to be careful and remember to add some extra code to handle errors and should at least think about panics. 9/29
Show this thread -
There is good news, though! Rust is a powerful language when it comes to encouraging robust code through warnings and compile-time errors, and we can take advantage of that. Here's how... 10/29
Show this thread -
Let's replace JoinHandle<T> with a new type simply called Task<T> that is very similar except it also acts like a guard that cancels the task when dropped. Task<T> is an awaitable future that resolves to a value of type T, which is the task's result. 11/29
Show this thread -
When a task is cancelled, it gets immediately woken and the next time the executor takes it out from the task queue, it will be simply dropped. Automatic cancellation is one of the core tenets of structured concurrency. 12/29
Show this thread -
Task<T> is also marked with #[must_use], so if you accidentally drop it without ever using it, the compiler warns you. That aligns closely with how futures typically work - dropping a future implies its cancellation. 13/29pic.twitter.com/dqAI9mWFUY
Show this thread -
If you want to keep the task running in the background with no strings attached, forget() its Task<T> handle. 14/29pic.twitter.com/USMwYCt1pL
Show this thread -
But here's the catch: you can only forget() tasks that resolve to (), which means you can't accidentally forget() a task that resolves to a Result or some other type. Now spawn(process(stream)) doesn't even compile - problem solved! 15/29
Show this thread -
The compiler now requires us to unwrap results in spawned tasks. But it'd be nice to have some API sugar here that is not as verbose as this... 16/29pic.twitter.com/YB7eLhDSgh
Show this thread -
What if there were unwrap() and expect() methods on Task<Result<T,E>> that transform it into Task<T> and panic on error? Those methods spawn a new task that simply unwraps the result and returns the success value. 17/29pic.twitter.com/7eQfRDqSN8
Show this thread -
Now we can do spawn(process(stream)).unwrap().forget(), which is pretty nice! This is what the entire TCP echo server looks like in my new runtime I'm working on. 18/29pic.twitter.com/H9Z2Nm9TOJ
Show this thread -
Finally, what about panics? Tokio silently ignores panics, meaning they might accidentally slip through. In particular, if an assertion fails in a task spawned from tokio within a unit test, the test will pass when it should fail! 19/29pic.twitter.com/3Mf9WskU4I
Show this thread -
Async-std crashes on panics. If an assertion fails in a task spawned from async-std within a unit test, the whole process crashes. No panics can slip through. 20/29pic.twitter.com/PTOTXiuOaE
Show this thread -
Crashing the entire process immediately is a bit annoying. If the whole process crashes, the test suite will not display the nicely formatted report of failed unit tests at the end. 21/29
Show this thread -
We need a panic handling strategy that is harsher than tokio's and gentler than async-std's. Why don't we propagate panics into the executor? 22/29
Show this thread -
In the case of async-std, it's not really obvious where the executor exactly is. Its thread pool is running in the background and we can only crash the process or perhaps let the user specify a custom panic handler. 23/29
Show this thread -
In tokio, the executor is the tokio Runtime instance, so instead of ignoring errors it'd make sense for Runtime::block_on() to propagate panics from tasks. 24/29pic.twitter.com/EF6m8hvgWK
Show this thread -
For now, suppose we had a very simple single-threaded runtime invoked by a function called run(), which propagates panics upwards. Don't think too much about it because I will tweet more about runtimes later... 25/29pic.twitter.com/qJaXb9eKbc
Show this thread -
In unit tests, panic propagation does the right thing by default, and it doesn't crash the whole test suite so we get a nice report at the end. 26/29pic.twitter.com/cQoG6uwvyo
Show this thread -
When run() propagates panics, it's up to the user to handle them however they wish. Panics can be ignored, logged, or simply left to continue unwinding. 27/29
Show this thread -
In summary: 28/29 1. Tasks are cancelled when dropped. 2. Tasks can't get accidentally dropped because we get compiler warnings. 3. Errors in tasks cannot get silently lost because we get compiler errors. 4. Unwrapping errors is easy. 5. Panics are propagated into the executor.
Show this thread -
That's all! This design isn't the "holy grail" of structured concurrency by any means, but it gets us very far with little effort and eliminates a lot of common pitfalls in async Rust. 29/29
Show this thread
End of conversation
New conversation -
Loading seems to be taking a while.
Twitter may be over capacity or experiencing a momentary hiccup. Try again or visit Twitter Status for more information.