Non-blocking Api (Axum, Tokio)

Hello Rusties!

I am working on building a non-blocking api. I have an endpoint, lets say '/trigger', I would like this endpoint to trigger a long running task, but returns a response immediately with the job's id. Then the user should check on the status of the task by hitting a different endpoint '/task/:id/status'

My api endpoint is built on Axum and the task is a tokio::task.

The code for the '/trigger' endpoint looks something like:

pub fn trigger() -> Result<u32, StatusCode> { 
    tokio::task::spawn(some_job);

   return id; //Somehow we should have an id 
}

Now my problem here, is that I am not sure how to monitor the tokio task. Should I use a shared state?
Or is there a better approach to achieve the above than using tokio::task ?

And please if you have any example code, drop it in the comment. I am not able to find any resources online about non-blocking apis in rust

You could make a global hash map for running jobs? The task can put the result into the hash map when it is done, or if you need to query its state in more detail, make your task an actor and put an actor handle into the map.

My task is already updating the status in the db, and I am going to pull from there. However, what if a fatal error happens? I wont be able to update the db (or the hash map (if I use it)) how can I let the user know?

How about this?

  1. Create a struct with a destructor.
  2. If the destructor runs before your task succeeded, you tokio::spawn a new task.
  3. The purpose of this new task is to write the failure to the database.

You cannot write the failure directly from the destructor because Rust doesn't support async drop, but using tokio::spawn should let you sidestep that issue.

Now, you will also need some sort of timeout to detect things like the entire process getting unexpectedly killed. Perhaps you could have your task update the database every minute its running, and then consider it dead if it hasn't updated it for two minutes?

2 Likes

Thank you this sounds like a good solution! Do you by any chance have any example code I can look into where something like this is implemented? Or any resources relating to non-blocking apis. Would really appreciate it!

Sure, you create the struct like this:

pub struct ReportFailureOnDrop {
    should_report: bool,
    task_id: u64,
}

impl ReportFailureOnDrop {
    pub fn new(task_id: u64) -> Self {
        Self {
            should_report: true,
            task_id,
        }
    }
    
    pub fn success(&mut self) {
        self.should_report = false;
    }
}

impl Drop for ReportFailureOnDrop {
    fn drop(&mut self) {
        if self.should_report {
            tokio::spawn(report_failure(self.task_id));
        }
    }
}

async fn report_failure(task_id: u64) {
    todo!()
}

Then, you just create the struct right at the beginning of your background task, then you call success() on it when it succeeds.

If you task panics, is cancelled, or returns with an error without calling success, then the destructor will spawn a report_failure task.

Thank you so much! I am barely new to rust so understanding some concepts without looking at an example is hard for me.

Btw, I just realized you're Alice Rhyl, I was watching your video on Actors when you replied to my post haha. I love your blog, its been a great help!

1 Like

Thanks! I'm glad you liked my talk!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.