168
@rackmount_rina ·

Async trait method compiles until I spawn it and then the future is not Send Help

I have a trait with an async method, implemented for a struct that holds an Rc<Config>. It compiles fine and the tests pass when I await it directly. The moment I call it inside tokio::spawn I get a wall of text ending in 'future is not Send', pointing at a line that doesn't obviously hold the Rc across an await. What's the systematic way to find the offending hold rather than staring at it?

8 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @borrowck_ben · 4mo ago · 3 replies

    You've already found it: Rc<Config> is the problem, and it doesn't need to be held across the await you're looking at, it only needs to be alive in the generated future's state at any await point. Swap it for Arc<Config> and the error usually vanishes on the spot. If you want the systematic version, the error text has a 'required because it appears within' chain, and reading it bottom to top names the exact field and the exact await, but the shortcut is that a non-Send type in a spawned future is almost always Rc, RefCell, or a MutexGuard from a non-async mutex.

    152
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @grepwitch · 4mo ago

      Reading it bottom to top is genuinely the trick with these. The last few lines name your type and your await, everything above is the machinery explaining why it cares.

      56
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @rackmount_rina · 4mo ago

      Arc fixed it. The chain does say it, I just wasn't reading the middle 40 lines because the top and bottom looked like noise.

      38
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @flux_and_solder · 4mo ago

    Adding a fn assert_send<T: Send>(_: &T) {} and calling it on your future in a test gives you the failure at the point you care about rather than at the spawn site. Handy when the spawn is three layers up.

    58
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @attic_server · 4mo ago · 2 replies

    The other one that catches people is a std::sync::MutexGuard held across an await. Even with Arc everywhere, let g = m.lock().unwrap(); something().await; makes the future non-Send and can also deadlock. Either drop the guard explicitly before the await or use an async-aware mutex.

    108
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @rackmount_rina · 4mo ago

      Noted, I have two of those in another module that currently only run on the local set. That's a bug waiting to happen.

      27
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @yaml_yusuf · 4mo ago

    If you genuinely need Rc, tokio::task::spawn_local on a LocalSet works and keeps everything on one thread. It's a real option for per-connection actors, just be aware you've now decided that task never migrates, which is fine until you want it to be.

    71
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @scripting_slowly · 4mo ago

    For the general debugging technique: comment out the body of the async method down to the smallest thing that still spawns, then add statements back until it breaks. Ugly, takes four minutes, and it beats reading the error when the error is 300 lines long.

    64
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report