I'm experimentally porting parts of an app to Rust. There is no global async runtime at the moment, and won't be for some time.
Some of the new Rust function implementations are to use other frameworks (e.g. AWS Rust SDK) which uses Tokio.
Some old code that invoked a C based S3 upload, now calls a Rust sync function which ultimately uses AWS S3 tokio based async functions.
This means that for now I will need to instantiate a short-lived tokio async runtime on demand to run the AWS stuff. But I also should detect if this sync function is already running in (and blocking) a runtime, in order to cope with nested runtimes and the natural inability to re-run block_on.
So this is my attempt:
/* Tips from: https://users.rust-lang.org/t/function-currying-that-ultimately-returns-a-future/62847/8
https://play.rust-lang.org/? version=stable&mode=debug&edition=2018&gist=1a4302fe790bb8f1f8a95c7aa37c67a1
But for possible improvement, see: https://users.rust-lang.org/t/simplest-possible-block-on/48364/9
*/
pub fn run_on_tokio<FA, R>(func: impl FnOnce() -> FA + std::marker::Send + 'static) -> R
where
FA: std::future::Future<Output = R>,
R: std::marker::Send + 'static,
{
// get or make a runtime
match tokio::runtime::Handle::try_current() {
Ok(_) => {
/* A runtime exists, so start a new thread for a new runtime */
thread::spawn(|| {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap().block_on(func())
})
.join()
.unwrap()
}
Err(_) => {
/* No runtime exists so we can run one in this thread */
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap().block_on(func())
}
}
}
It is invoked from a sync function with something like this ghastly invocation:
fn do_thing(a: &str, b: &str) -> i32 {
run_on_tokio(move || async move { call_async_do_thing(a, b).await })
}
I know that it is clearly lamentable to start a new thread when the sync function was indirectly blocking (via uncountable opaque library functions) an async runtime, but it is no worse that what was happening anyway, and will be solved when all the old code is finally ported.
Certainly if my sync function was called from an async function (indirectly) we hope that spawn_blocking was used somewhere at the beginning of the chain.
Setting that aside, is this a reasonable approach? have I lied fatally about lifetimes? Is the move stuff fine? Are there likely to be parameters for which this fails?
Closures are passed for 2 reasons, to cope with any number and type of arguments (my generics foo is weak) and also to defer the initial invocation of the wrapped function until the new runtime context.
Perhaps it should be based on some kind of async trait marker, so when invoked from an async functions it uses spawn_blocking instead, but that is just polish, and I'm not sure how to start that yet. Probably better to just avoid this like the plague in async functions.