0

I'm trying to use the #[async_recursion] macro on a constructor that takes an impl trait as an argument. The impl trait is just a shim around reqwest so I can insert a mock for testing:

#[async_trait]
pub trait NetFuncs {
    async fn get(&self, url: &str) -> Result<String, Error>;
}

It was working fine until I made my constructor recursive:

#[derive(Debug)]
pub struct Foo {
    map: serde_yaml::mapping::Mapping,
    filename: String,
    parent: Option<Box<Foo>>,
    receipt: Option<Receipt>,
}

impl Foo {
    #[async_recursion]
    pub async fn from_str(s: &str, filename: &str, net: &impl NetFuncs) -> Result<Foo, Error> {

throws the error:


error: future cannot be sent between threads safely
   --> src/main.rs:97:5
    |
97  |     #[async_recursion]
    |     ^^^^^^^^^^^^^^^^^^ future created by async block is not `Send`
    |
note: captured value is not `Send` because `&` references cannot be sent unless their referent is `Sync`
   --> src/main.rs:125:17
    |
125 |                 net,
    |                 ^^^ has type `&impl NetFuncs` which is not `Send`, because `impl NetFuncs` is not `Sync`
    = note: required for the cast to the object type `dyn Future<Output = Result<Foo, Error>> + Send`
    = note: this error originates in the attribute macro `async_recursion` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider further restricting this bound
    |
98  |     pub async fn from_str(s: &str, filename: &str, net: &impl NetFuncs + std::marker::Sync) -> Result<Foo, Error> {
    |                                                                        +++++++++++++++++++

There are other ways to mock a network for testing then the way I did it, but I liked my solution, at least until I hit this error. How do I fix this error without removing the net: &impl NetFuncs argument?

MRE

[package]
name = "mre2"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
async-recursion = "1.0"
async-trait = "0.1"
use async_trait::async_trait;
use async_recursion::async_recursion;

#[derive(Debug)]
pub struct Foo {
    s: String,
    filename: String,
    foo: String,
    parent: Option<Box<Foo>>,
}

#[async_trait]
pub trait NetFuncs {
    async fn get(&self, url: &str) -> String;
}

#[derive(Debug)]
pub struct FakeNet {}

#[async_trait]
impl NetFuncs for FakeNet {
    async fn get(&self, url: &str) -> String {
        "".to_string()
    }
}

impl Foo {
    #[async_recursion]
    pub async fn from_str(s: &str, filename: &str, net: &impl NetFuncs) -> Foo {
        Foo { s: s.to_string(), filename: filename.to_string(), parent: None, foo: net.get("").await.to_string() }
    }
}

1 Answer 1

1

The problem is, like explained by the compiler, that &impl NetFuncs may not necessarily impl Send but the async_recursion macro by default requires it, so, you have two options:

  • Require impl NetFuncs to be Sync, so that &impl NetFuncs is Send. This can be done either with &(impl NetFuncs + Sync) or by requiring every implementor to implement Send: trait NetFuncs: Sync.
  • Not requiring the resulting future to be Send. As documented in the async_recursion documentation, this can be done by changing #[async_recursion] to #[async_recursion(?Send)].

Without the macro it works since the compiler make the resulting future Send depend on whether all types kept across .await points are Send: if they are, the future is also Send. If they are not, it is not too. The macro changes the async fn to fn ...() -> Pin<Box<dyn Future>>, and unfortunately, it is not possible to have the same behavior as with async fn - this is something only the compiler can implement. Thus, the macro allows you to choose whether you want the resulting future to be Send - meaning all types should be too, or not.

Sign up to request clarification or add additional context in comments.

6 Comments

Your first solution was suggested by the error message, but it gives me "references cannot be sent unless their referent is Sync" which also stumped me. Your second solution compiles. If I understand correctly, if I choose your second solution, I should use #[tokio::main(basic_scheduler)] to ensure that my program is safe without Send.
@BryanLarsen The first solution probably didn't compile because the types that implemented NetFuncs weren't thread-safe. As for the second solution, you don't necessarily have to use a single-thread runtime - you can use something like LocalSet to bridge.
The FakeNet impl for NetFuncs in my MRE certainly looks thread safe. I'm glad to hear that I don't necessarily need a single-threaded runtime, so I'm cool just going with the second solution.
@BryanLarsen My bad, it should be Sync and not Send.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.