0

Hey I have a async process which is resource exhaustive. And I have to expose it to an API how to process jobs in background and in queue one after one. I used tokio::spawn but all spawned tasks are ending up running simultaneously.

I will attach a simple reproducible code below for reference.

use axum::{extract::Path, routing::get, Router};

use tokio::time::Duration;

extern crate diesel;
extern crate tracing;

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();

    let app = Router::new().route("/sleep/:id", get(sleep_and_print));
    let addr = std::net::SocketAddr::from(([0, 0, 0, 0], 3000));
    tracing::info!("Listening on {}", addr);

    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn sleep_and_print(Path(timer): Path<i32>) -> String {
    tokio::spawn(async move {
        start_timer_send_json(timer).await;
    });
    format!("{{\"timer\": {}}}", timer)
}

async fn start_timer_send_json(timer: i32) {
    println!("Start timer {}.", timer);

    tokio::time::sleep(Duration::from_secs(300)).await;

    println!("Timer {} done.", timer);
}

  • Use one `tokio::spawn()` and serial processing. – Chayim Friedman Aug 03 '23 at 12:32
  • You are using `axum`, so you can use [tower](https://docs.rs/tower/0.4.13/tower/) middleware. You could for example [limit](https://docs.rs/tower/0.4.13/tower/limit/concurrency/index.html) max number of requests that are allowed to be executed concurently. You can also combine `Routers`, so you could apply this middleware to only some specific handlers. – Aleksander Krauze Aug 03 '23 at 12:36
  • Hey @ChayimFriedman How to do serial processing in tokio::spawn() ? – 0xRoronoa Zoro Aug 03 '23 at 13:00
  • @AleksanderKrauze I need to accept all requests and process them sequentially. – 0xRoronoa Zoro Aug 03 '23 at 13:01

1 Answers1

0

Just spawn a single task to do the processing and use channels to communicate with it:

use axum::{extract::Path, routing::get, Router};

use tokio::time::Duration;
use tokio::sync::{mpsc, oneshot}

extern crate diesel;
extern crate tracing;

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();

    // Create a channel to send requests to the processing task
    let (tx, rx) = tokio::sync::channel();
    // Spawn a task to do all the processing. Since this is a single
    // task, all processing will be done sequentially.
    tokio::spawn (async move { process (rx).await; });

    // Pass the tx channel along to the GET handler so that it can
    // send requests to the processing task
    let app = Router::new().route("/sleep/:id", get(move |path| {
        sleep_and_print (path, &tx);
    }));
    let addr = std::net::SocketAddr::from(([0, 0, 0, 0], 3000));
    tracing::info!("Listening on {}", addr);

    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn process (rx: mpsc::Receiver<(i32, oneshot::Sender<String>)>) 
{
    // Receive the next queued request
    while let Ok ((timer, tx)) = rx.recv().await {
        // Process the request
        start_timer_send_json(timer).await;
        // Send back the result
        if let Err (e) = tx.send (format!("{{\"timer\": {}}}", timer)) {
            println!("{:?}", e);
        }
    }
}

async fn sleep_and_print(
    Path(timer): Path<i32>, 
    tx: &mpsc::Sender<(i32, oneshot::Sender<String>)>) -> String 
{
    // Create a channel to get the result
    let (otx, orx) = oneshot::new();
    // Send our request to the processing task
    tx.send ((timer, otx)).unwrap();
    // Wait for the processing result
    orx.await.unwrap()
}

async fn start_timer_send_json(timer: i32) {
    println!("Start timer {}.", timer);

    tokio::time::sleep(Duration::from_secs(300)).await;

    println!("Timer {} done.", timer);
}
Jmb
  • 18,893
  • 2
  • 28
  • 55