Tokio TaskTracker not awaiting for spawned futures to complete

As an effort to get rid of the indeterminism introduced by spawning tasks with tokio::spawn, I decided to give tokio's TaskTracker a try.

The code looks roughly like this:

pub struct MyService {
  pub my_other_service: Arc<MyOtherService>,
  pub task_tracker: TaskTracker
}

impl MyService {
  pub async fn do_async_work(&self) -> Result<(), MyError> {
    let my_other_service_clone = self.my_other_service.clone();

    self.task_tracker.spawn(async move {
      my_other_service_clone.do_more_async_work().await;
    });

    Ok(())
  }
}

Then, in my integration test I close the task tracker and wait for the futures to complete:

async fn do_async_work_does_async_work() {
  my_service.do_async_work().await.unwrap();
  task_tracker.close();
  task_tracker.wait().await;
  // assert that MyOtherService::do_more_async_work did some work
}  

The problem is that waiting for the task tracker is useless; by the time I'm doing the test assertions, MyOtherService::do_more_async_work hasn't finished doing work, so it's like my efforts on replacing tokio::spawn were futile, since the code is behaving exactly the same.

I have tried with all the flavors offered by TaskTracker (spawn, spawn_local, track_future) without any progress whatsoever.

Is my understanding of what TaskTracker is meant for, wrong?

Is MyOtherService itself spawning tasks — or sending messages on channels, or anything else that has effects after do_more_async_work() returns?

If so, then your problem is intrinsic to the API of MyOtherService — if you want to be able to wait for it then it has to give you the information to be able to wait. It needs to either participate in the TaskTracker or return some future that only resolves when the work is done.

If not, then it seems like your code should work as expected. Can you create a self-contained example, so we can see all the code involved, and experiment with it?

Nope, it's just a wrapper for the database client (MongoDB). I'll try to recreate the code and get back once I have it :slight_smile: .

By the way, I recently learned of a concept that might be useful for thinking about this problem: structured concurrency. Structured concurrency is the idea that concurrency should be tree-structured just like the set of function call frames in a single-threaded program is: even when an operation has many concurrent parts, they should all be required to complete before the operation as a whole is considered finished.

(I'm currently skeptical of the idea of applying exclusively structured concurrency, but it seems like a useful name for a property of parts of programs, regardless.)

awaiting an async fn is structured; so are the various join-multiple-tasks operators provided by tokio and futures; but tokio's spawn doesn't obey the structured concurrency paradigm, and that's what you're trying to recover from by adding the TaskTracker. So, the problem you have now can be framed as finding the other un-structured part that you haven't yet accounted for.

1 Like

Yeah, I did know about the concept and the implications, but not under that name. Thanks for the link!

I'm spawning tasks for convenience since I don't want to resort to more complex solutions at the moment, and had delayed this techdebt for quite a while.

By the way, when trying to replicate the problem I realised of a tiny detail that my system has that I totally skipped: In tests, I create an instance of my services for the HTTP server alone, and this instance had its own instance of TaskTracker, which made the whole thing fall apart. Now I've fixed this so that I can move on :sweat_smile:. Thanks for your time anyway!