|
| 1 | +# 😱 Status quo stories: Barbara bridges sync and async in `perf.rust-lang.org` |
| 2 | + |
| 3 | +[How To Vision: Status Quo]: ../how_to_vision/status_quo.md |
| 4 | +[the raw source from this template]: https://raw.githubusercontent.com/rust-lang/wg-async-foundations/master/src/vision/status_quo/template.md |
| 5 | +[`status_quo`]: https://github.com/rust-lang/wg-async-foundations/tree/master/src/vision/status_quo |
| 6 | +[`SUMMARY.md`]: https://github.com/rust-lang/wg-async-foundations/blob/master/src/SUMMARY.md |
| 7 | +[open issues]: https://github.com/rust-lang/wg-async-foundations/issues?q=is%3Aopen+is%3Aissue+label%3Astatus-quo-story-ideas |
| 8 | +[open an issue of your own]: https://github.com/rust-lang/wg-async-foundations/issues/new?assignees=&labels=good+first+issue%2C+help+wanted%2C+status-quo-story-ideas&template=-status-quo--story-issue.md&title= |
| 9 | + |
| 10 | +## 🚧 Warning: Draft status 🚧 |
| 11 | + |
| 12 | +This is a draft "status quo" story submitted as part of the brainstorming period. It is derived from real-life experiences of actual Rust users and is meant to reflect some of the challenges that Async Rust programmers face today. |
| 13 | + |
| 14 | +If you would like to expand on this story, or adjust the answers to the FAQ, feel free to open a PR making edits (but keep in mind that, as they reflect peoples' experiences, status quo stories [cannot be wrong], only inaccurate). Alternatively, you may wish to [add your own status quo story][htvsq]! |
| 15 | + |
| 16 | +## The story |
| 17 | + |
| 18 | +### Introducing `block_on` |
| 19 | + |
| 20 | +Barbara is working on the code for [perf.rust-lang.org](https://perf.rust-lang.org/) and she wants to do a web request to load various intermediate results. She has heard that the `reqwest` crate is quite nice, so she decides to give it a try. She writes up an async function that does her web request: |
| 21 | + |
| 22 | +```rust |
| 23 | +async fn do_web_request(url: &Url) -> Data { |
| 24 | + ... |
| 25 | +} |
| 26 | +``` |
| 27 | + |
| 28 | +She needs to apply this async function to a number of urls. She wants to use the iterator map function, like so: |
| 29 | + |
| 30 | +```rust |
| 31 | +async fn do_web_request(url: &Url) -> Data {...} |
| 32 | + |
| 33 | +fn aggregate(urls: &[Url]) -> Vec<Data> { |
| 34 | + urls |
| 35 | + .iter() |
| 36 | + .map(|url| do_web_request(url)) |
| 37 | + .collect() |
| 38 | +} |
| 39 | + |
| 40 | +fn main() { |
| 41 | + /* do stuff */ |
| 42 | + let data = aggregate(); |
| 43 | + /* do more stuff */ |
| 44 | +} |
| 45 | +``` |
| 46 | + |
| 47 | +Of course, since `do_web_request` is an async fn, she gets a type error from the compiler: |
| 48 | + |
| 49 | +``` |
| 50 | +error[E0277]: a value of type `Vec<Data>` cannot be built from an iterator over elements of type `impl Future` |
| 51 | + --> src/main.rs:11:14 |
| 52 | + | |
| 53 | +11 | .collect(); |
| 54 | + | ^^^^^^^ value of type `Vec<Data>` cannot be built from `std::iter::Iterator<Item=impl Future>` |
| 55 | + | |
| 56 | + = help: the trait `FromIterator<impl Future>` is not implemented for `Vec<Data>` |
| 57 | +``` |
| 58 | + |
| 59 | +"Of course," she thinks, "I can't call an async function from a closure." She decides that since she is not overly concerned about performance, so she decides she'll just use a call to [`block_on` from the `futures` crate](https://docs.rs/futures/0.3.14/futures/executor/fn.block_on.html) and execute the function synchronously: |
| 60 | + |
| 61 | +```rust |
| 62 | +async fn do_web_request(url: &Url) -> Data {...} |
| 63 | + |
| 64 | +fn aggregate(urls: &[Url]) -> Vec<Data> { |
| 65 | + urls |
| 66 | + .iter() |
| 67 | + .map(|url| futures::executor::block_on(do_web_request(url))) |
| 68 | + .collect() |
| 69 | +} |
| 70 | + |
| 71 | +fn main() { |
| 72 | + /* do stuff */ |
| 73 | + let data = aggregate(); |
| 74 | + /* do more stuff */ |
| 75 | +} |
| 76 | +``` |
| 77 | + |
| 78 | +The code compiles, and it seems to work. |
| 79 | + |
| 80 | +### Introducing `block_on` |
| 81 | + |
| 82 | +As Barbara works on play, she realizes that she needs to do more and more async operations. She decides to convert her synchronous `main` function into an `async main`. She's using tokio, so she is able to do this very conveniently with the `#[tokio::main]` decorator: |
| 83 | + |
| 84 | +```rust |
| 85 | +#[tokio::main] |
| 86 | +async fn main() { |
| 87 | + /* do stuff */ |
| 88 | + let data = aggregate(); |
| 89 | + /* do more stuff */ |
| 90 | +} |
| 91 | +``` |
| 92 | + |
| 93 | +Everything seems to work ok on her laptop, but when she pushes the code to production, it deadlocks immediately. "What's this?" she says. Confused, she runs the code on her laptop a few more times, but it seems to work fine. |
| 94 | + |
| 95 | +She decides to try debugging. She fires up a debugger but finds it is isn't really giving her useful information about what is stuck (she has [basically the same problems that Alan has](https://rust-lang.github.io/wg-async-foundations/vision/status_quo/alan_tries_to_debug_a_hang.html)). [She wishes she could get insight into tokio's state.](https://rust-lang.github.io/wg-async-foundations/vision/status_quo/barbara_wants_async_insights.html) |
| 96 | + |
| 97 | +Frustrated, she starts reading the tokio docs more closely and she realizes that `tokio` runtimes offer their own `block_on` method. "What the hey," she thinks, "let's see if that works any better." She changes the `aggregate` function to use tokio's `block_on`: |
| 98 | + |
| 99 | +```rust= |
| 100 | +fn block_on<O>(f: impl Future<Output = O>) -> O { |
| 101 | + let rt = tokio::runtime::Runtime::new().unwrap(); |
| 102 | + rt.block_on(f) |
| 103 | +} |
| 104 | +
|
| 105 | +fn aggregate(urls: &[Url]) -> Vec<Data> { |
| 106 | + urls |
| 107 | + .iter() |
| 108 | + .map(|url| block_on(do_web_request(url))) |
| 109 | + .collect() |
| 110 | +} |
| 111 | +``` |
| 112 | + |
| 113 | +The good news is that the deadlock is gone. The bad news is that now she is getting a panic: |
| 114 | + |
| 115 | +> thread 'main' panicked at 'Cannot start a runtime from within a runtime. This happens because a function (like `block_on`) attempted to block the current thread while the thread is being used to drive asynchronous tasks.' |
| 116 | +
|
| 117 | +"Well," she thinks, "I could use that `Handle` API to get the current runtime, maybe that will work?" |
| 118 | + |
| 119 | +```rust |
| 120 | +fn aggregate(urls: &[&str]) -> Vec<String> { |
| 121 | + let handle = Handle::current(); |
| 122 | + urls.iter() |
| 123 | + .map(|url| handle.block_on(do_web_request(url))) |
| 124 | + .collect() |
| 125 | +} |
| 126 | +``` |
| 127 | + |
| 128 | +But it seems to give her the same panic. |
| 129 | + |
| 130 | +### Trying out `spawn_blocking` |
| 131 | + |
| 132 | +Reading more into this problem, she realizes she is supposed to be using `spawn_blocking`. She tries replacing `block_on` with `tokio::task::spawn_blocking`: |
| 133 | + |
| 134 | +```rust= |
| 135 | +fn aggregate(urls: &[Url]) { |
| 136 | + let data: Vec<Data> = |
| 137 | + urls |
| 138 | + .iter() |
| 139 | + .map(|url| tokio::task::spawn_blocking(move || do_web_request(url))) |
| 140 | + .collect(); |
| 141 | +} |
| 142 | +``` |
| 143 | + |
| 144 | + |
| 145 | +but now she gets a type error again: |
| 146 | + |
| 147 | +``` |
| 148 | +error[E0277]: a value of type `Vec<Data>` cannot be built from an iterator over elements of type `tokio::task::JoinHandle<impl futures::Future>` |
| 149 | + --> src/main.rs:22:14 |
| 150 | + | |
| 151 | +22 | .collect(); |
| 152 | + | ^^^^^^^ value of type `Vec<Data>` cannot be built from `std::iter::Iterator<Item=tokio::task::JoinHandle<impl futures::Future>>` |
| 153 | + | |
| 154 | + = help: the trait `FromIterator<tokio::task::JoinHandle<impl futures::Future>>` is not implemented for `Vec<Data>` |
| 155 | +``` |
| 156 | + |
| 157 | +Of course! `spawn_blocking`, like `map`, just takes a regular closure, not an async closure. She's getting a bit frustrated now. "Well," she thinks, "I can use `spawn` to get into an async context!" So she adds a call to `spawn` inside the `spawn_blocking` closure: |
| 158 | + |
| 159 | +```rust |
| 160 | +fn aggregate(urls: &[Url]) { |
| 161 | + let data: Vec<Data> = |
| 162 | + urls |
| 163 | + .iter() |
| 164 | + .map(|url| tokio::task::spawn_blocking(move || { |
| 165 | + tokio::task::spawn(async move { |
| 166 | + do_web_request(url).await |
| 167 | + }) |
| 168 | + })) |
| 169 | + .collect(); |
| 170 | +} |
| 171 | +``` |
| 172 | + |
| 173 | +But this isn't really helping, as `spawn` still yields a future. She's getting the same errors. |
| 174 | + |
| 175 | +### Async all the way |
| 176 | + |
| 177 | +She remembers now that this whole drama started because she was converting her `main` function to be `async`. Maybe she doesn't have to bridge between sync and async? She starts digging around in the docs and finds `futures::join_all`. Using that, she can change `aggregate` to be an async function too: |
| 178 | + |
| 179 | +```rust |
| 180 | +async fn aggregate(urls: &[Url]) { |
| 181 | + let data: Vec<Data> = futures::join_all( |
| 182 | + urls |
| 183 | + .iter() |
| 184 | + .map(|url| do_web_request(url)) |
| 185 | + ).await; |
| 186 | +} |
| 187 | +``` |
| 188 | + |
| 189 | +Things are working again now, so she is happy. |
| 190 | + |
| 191 | +### Filtering |
| 192 | + |
| 193 | +Later on, she would like to apply a filter to the aggregation operation. She realizes that if she wants to use the fetched data when doing the filtering, she has to filter the vector after the join has completed; `join_all` doesn't have a way to put a filter into the iterator chain like she wants. She is annoyed, but performance isn't critical, so it's ok. |
| 194 | + |
| 195 | +### And the cycle begins again |
| 196 | + |
| 197 | +Later on, she wants to call `aggregate` from another binary. This one doesn't have an `async main`. This context is deep inside of an iterator chain and was previously entirely synchronous. She realizes it would be a lot of work to change all the intervening stack frames to be `async fn`, rewrite the iterators into streams, etc. She decides to just call `block_on` again, even though it make her nervous. |
| 198 | + |
| 199 | +## 🤔 Frequently Asked Questions |
| 200 | + |
| 201 | +### What are the morals of the story? |
| 202 | + |
| 203 | +* Some projects don't care about max performance and just want things to work. |
| 204 | + * They would probably be happy with sync but as the most popular libraries for web requests, databases, etc, offer async interfaces, they may still be using async code. |
| 205 | +* There are contexts where you can't easily add an `await`. |
| 206 | + * For example, inside of an iterator chain. |
| 207 | + * Big block of existing code. |
| 208 | +* Mixing sync and async code (`block_on`) can cause deadlocks that are really painful to diagnose. |
| 209 | + |
| 210 | + |
| 211 | +### Why did you choose Barbara to tell this story? |
| 212 | + |
| 213 | +* Because Mark (who experienced most of it) is a very experienced Rust developer. |
| 214 | +* Because you could experience this story regardless of language background or being new to Rust. |
| 215 | + |
| 216 | +### How would this story have played out differently for the other characters? |
| 217 | + |
| 218 | +I would expect it would work out fairly similarly, except that the type errors and things might well have been more challenging for people to figure out, assuming they aren't already familiar with Rust. |
| 219 | + |
| 220 | +### What are other ways people could experience similar problems mixing sync and async? |
| 221 | + |
| 222 | +* Using `std::Mutex` in async code. |
| 223 | +* Calling the blocking version of an asynchronous API. |
| 224 | + * For example, `reqwest::blocking`, the synchronous `[zbus`](https://gitlab.freedesktop.org/dbus/zbus/-/blob/main/zbus/src/proxy.rs#L121) and [`rumqtt`](https://github.com/bytebeamio/rumqtt/blob/8de24cbc0484f459246251873aa6c80be8b6e85f/rumqttc/src/client.rs#L224) APIs. |
| 225 | + * These are commonly implemented by using some variant of `block_on` internally. |
| 226 | + * Therefore they can lead to panics or deadlocks depending on what async runtime they are built from and used with. |
| 227 | + |
| 228 | +### How many variants of `block_on` are there? |
| 229 | + |
| 230 | +* the `futures` crate offers a runtime-independent block-on (which can lead to deadlocks, as in this story) |
| 231 | +* the `tokio` crate offers a `block_on` method (which will panic if used inside of another tokio runtime, as in this story) |
| 232 | +* the [`pollster`](https://crates.io/crates/pollster) crate exists just to offer `block_on` |
| 233 | +* the [`futures-lite`](https://docs.rs/futures-lite/1.11.3/futures_lite/future/fn.block_on.html) crate offers a `block_on` |
| 234 | +* the [`aysnc-std`](https://docs.rs/async-std/1.9.0/async_std/task/fn.block_on.html) crate offers `block_on` |
| 235 | +* the [`async-io`](https://docs.rs/async-std/1.9.0/async_std/task/fn.block_on.html) crate offers `block_on` |
| 236 | +* ...there are probably more, but I think you get the point. |
| 237 | + |
| 238 | +[character]: ../characters.md |
| 239 | +[status quo stories]: ./status_quo.md |
| 240 | +[Alan]: ../characters/alan.md |
| 241 | +[Grace]: ../characters/grace.md |
| 242 | +[Niklaus]: ../characters/niklaus.md |
| 243 | +[Barbara]: ../characters/barbara.md |
| 244 | +[htvsq]: ../how_to_vision/status_quo.md |
| 245 | +[cannot be wrong]: ../how_to_vision/comment.md#comment-to-understand-or-improve-not-to-negate-or-dissuade |
0 commit comments