I have a Rust-based app that, as in my previous Rust work, avoids async. (I don't want to start anything "political"; I've just preferred not to use it in the past, and have achieved very high performance using threads and channels.)
I said it "avoids async" but it could not completely, because it is a gRPC server and Tonic uses async.
That was fine, because I was able to easily confine async to the top-level of the app: The gRPC interface impl methods are async, but call no async code.
This all changed radically when an underlying service, deep in the implementation, was changed from HTTP to gRPC - my app is a client to that new, unrelated (my my server) gRPC interface.
This is what happened:
-
I tried to encapsulate async deep in that service layer by constructing my own Tokio runtime there, and wrapping the gRPC calls in block_on().
-
I thought this was a great idea, as everything compiled and looked neat.
-
WRONG - runtime error - "cannot start a runtime within a runtime..."
-
I tried using tokio::task::spawn_blocking as an alternative but that requires calling await - of course not possible in non-async functions - (or propagating Futures)
-
I bit the bullet - as an experiment - and changed the three layers of the application (the gRPC server entrypoints, the services layer, and dependent services) to use async fn instead of fn
-
I then ran into errors I did not expect, of this sort:
**error****: future cannot be sent between threads safely**
**-->** server/src/main.rs:72:57
**|**
**72** **|** ) -> Result<Response<CreateOrdersResponse>, Status> {
**|** **_________________________________________________________^**
**73** **|** **|** let span = span!(Level::INFO, "OrderService.create");
**74** **|** **|** let _guard = span.enter();
**75** **|** **|** info!("Received request: {:?}", request);
**...** **|**
**89** **|** **|** // }
**90** **|** **|** }
**|** **|_____^** **future created by async block is not `Send`**
**|**
**=** **help**: within `{async block@server/src/main.rs:72:57: 90:6}`, the trait `Send` is not implemented for `impl Future<Output = ()>`
**note**: `<O as OrderService>::create_order` is an `async fn` in trait, which does not automatically imply that its future is `Send`
**|**
**9** **|** async fn create_order(&self, order: Order) -> ();
**|** **^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^**
**note**: future is not `Send` as it awaits another future which is not `Send`
**-->** server/src/main.rs:78:9
**|**
**78** **|** self.orders.create_order(order).await;
**|** **^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^** **await occurs here on type `impl Future<Output = ()>`, which is not `Send`**
**=** **note**: required for the cast from `Pin<Box<{async block@server/src/main.rs:72:57: 90:6}>>` to `Pin<Box<dyn Future<Output = Result<tonic::Response<CreateOrdersResponse>, Status>> + Send>>`
Yikes! WTF? Why are these Futures not Send - when the Content are? (Order is a simple struct built from primitives.)
I then discovered that using async functions in traits is "not recommended" and is likely the cause of this problem. Are you possibly kidding me? Traits are Rust's main mechanism for implementation abstraction, and I use them throughout to put services behind an interface I can mock for unit testing. I can't stop using traits!
I then took the compiler's advice and started redoing my trait function signatures:
37 - async fn create_order(&self, order: Order) -> Result<Order, String>;
37 + fn create_order(&self, order: Order) -> impl std::future::Future<Output = Result<Order, String>> + Send;
At this point I was rather alarmed, as now I am realizing that swapping out an HTTP service for a gRPC one at the bottom of a dependency graph has indeed "gone viral," as they said, mutating the fn signatures across the board.
That still wasn't it, though. Because there seems to be no way to map the result or the error "channels" of a future in Rust, none of that code worked anymore. My code was translating from gRPC artifacts to domain objects and from proprietary errors to uniform ones at the various levels, etc.
This community has been great to me since I started Rust programming a year or more ago. I hope you can help now, as I am literally sick to my stomach. I am not going to make dev deadline, and I am really at loss in how to proceed here.
Specific questions:
-
Is there ANY way with Tokio, that I haven't thought of, to encapsulate async at a "bottom" level - even if I'm in the context of a Runtime? (If so this solves all the problem.)
-
Is there any way to make async fns in traits that will actually work - that won't result in the "future created by async block is not
Send
" error?
I am not blaming async per se for this - mess. Async mechanisms in any language are usually viral - it's just the way it goes. Rust's low-level nature - this strange matter with traits - exacerbates the problem.
I am hoping very much I can actually encapsulate async at this low level - exchange an HTTP service for an async (gRPC) one - even in the context of an async runtime (which I'm in, again, because the top level is async, because that's Tonic does it.