Schedule a blocking task every x minutes

Hi all,
I want to schedule a task every X minutes (say 5). I know that if I'm in a tokio task I can do something like this

    let mut interval = time::interval(Duration::from(10));
    let _maintainance = tokio::task::spawn(async {
        loop {
             interval.tick().await;
             do_something_now().await;
        }
    });

this will guarantee that I can call do_something_now() every 5 minutes independently from the function duration.

Now, since my function can take 50-60 secs to complete the execution, I am using spawn_blocking instead. I wanted to ask what's the idiomatic way to achieve the same?

Right now I'm measuring the time of the function and performing a sleep with a duration that is 5-last_run. But I believe there must be a better way.. any idea?

What's your new code? It's not clear why you'd need to switch to spawn_blocking, or what "different seconds" means.

sorry I've clarified in the post, the function I'm calling after the sleep can take up to 1 min to complete so I want to make sure that the next round is in 5 min minus the duration of the heavy function if that makes sense?

Is this ok

use tokio::{spawn, task::JoinError};
use tokio_schedule::{every, Job};

#[tokio::main]
async fn main() -> Result<(), JoinError> {
    let every_5_mins = every(5).minutes()
        .perform(|| async { println!("heavy task") });
    spawn(every_5_mins).await
}

for some reason rust playground says that there is no crate tokio_schedule but there is tokio_schedule - Rust

(Not every crate is available in the playground.)

Just use a 5 minute interval.

let mut interval = time::interval(Duration::from_secs(60 * 5));
let _maintainance = tokio::task::spawn(async {
    loop {
        interval.tick().await;
        do_something_now().await;
    }
});

thanks for your suggestions, just to be clear I want to achieve the same in a spawn_blocking as opposed to spawn..

let mut interval = time::interval(Duration::from_secs(60 * 5));
let _maintainance = tokio::task::spawn(async {
    loop {
        interval.tick().await;
        tokio::task::spawn_blocking(do_something_now).await;
    }
});

thanks for this suggestion! would the spaw_blocking inside a spawn have any counter effect? I want to isolate this task from the standard spawns where all my requests are flowing in the service

What kind of isolation are you looking for? As long as they're in the same process on the same computer, they can't be completely isolated.

from my understanding when you spawn_blocking the code is executed on a separate pool, I was just wondering if the blocking is spawned inside a normal task whether that creates some form of dependency?

This is correct.

This sounds more like how block_in_place works. spawn_blocking just sends a function to the blocking threadpool. If you don't abort _maintainance or the tokio runtime, then it'll keep looping forever.

Also, the task calling spawn_blocking does not have to wait for it to complete. spawn_blocking returns a JoinHandle that implements Future, and you choose whether to await that Future or not. So there is no dependency unless you choose to create one.

I see thanks folks.
I will do some tests but I'm correct to say that no matter whether or not I await the spawn_blocking, it will be called exactly every 5 minutes independently from the duration of the spawn_blocking task?

If you use Interval and not sleep then yes. Unless the spawn_blocking task itself takes more time than 5 minutes. In that case it depends on your missed tick behaviour.

2 Likes

If that's the only background task in your program just spawn a thread, loop, sleep.

If you have many such tasks then set the timer in tokio and submit the blocking work to a thread pool.

And unless you need the precise 5 minute interval it's usually good practice to schedule things with a delay between tasks, rather than on a schedule. That way you don't build up a growing backlog when something goes wrong and the task takes longer than the schedule.

I like java's ScheduledThreadPoolExecutor for that, which has scheduleWithFixedDelay​. Not sure if there's a crate like that.

1 Like

sounds good folks thanks for all your help! since we briefly talked about this I just want to make sure I'm doing the right thing with the spawn_blocking.
My spawn_blocking is executing few copies of in memory objects (pretty big objects around 10gb) and in total that takes around 40-50 sec. Since the same service is also serving async requests I noticed that moving that maintainance task in a spawn blocking doesn't create too much noise with the incoming API I'm receiving on the regular tokio tasks.
However, I've see some of you suggesting to schedule tasks into a dedicated threadpool. I just wanted to doublecheck whether this is effectively the same than invoke spawn_blocking or there is any other advantage?

@drewtato is there a way to miss the first tick round by any chance? I'd like that the first round is not ticket immediately but after 5 minutes

Sorry, misread your requirements.

1 Like

found the solution, I can do something like this

let _maintainance = tokio::task::spawn(async move {
        let start = Instant::now() + Duration::from_secs(ROTATION_TIME_SECS);
        let mut interval = time::interval_at(start, Duration::from_secs(ROTATION_TIME_SECS));
        loop {
            interval.tick().await;

            // Spawning the task, awaiting the results to ensure
            // there will never be more than 1 execution happening
            // at the same time.
            let _ = tokio::task::spawn_blocking(do_something_now).await;

        }
    });
2 Likes