Run tasks at regular time intervals

I am trying to run some tasks every ten minutes (at every 10 minute multiple). I'm trying the following approach.

use chrono::DateTime;
use chrono::{Duration, Timelike, Utc};
use tokio::time;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    loop {
        let now = Utc::now();
        let target_dt = get_next_10_min_dt(now);
        let diff = (target_dt - now).to_std().unwrap();
        time::sleep(diff).await;

        // Do something...
        let task1 = tokio::spawn(async move { });
        let task2 = tokio::spawn(async move { });
        task1.await.unwrap();
        task2.await.unwrap();
    }
}

fn get_next_10_min_dt(current_dt: DateTime<Utc>) -> DateTime<Utc> {
    // Add 10 min * 60 sec - 1 sec = 599 sec
    let target_dt = current_dt.checked_add_signed(Duration::seconds(599)).unwrap();
    // Round to 10 min multiple
    let target_minute = (target_dt.minute() / 10) * 10;
    
    target_dt.with_minute(target_minute).unwrap()
}

I realize an alternative is to use tokio::time::interval, but I didn't go with this approach due to discussion here [Q] How to schedule tasks at specific time of day using tokio.

I wanted to get your feedback about the above approach and please let me know if there's a better way to do this.

Don't calculate the next time point based on now, that will cause slow drift. Get the initial time using now once outside the loop, and then keep adding the exact interval to it.

3 Likes

This is take number 2 :slight_smile:
In this version I know exactly when the next DateTime is, but still have to subtract Utc::now() to know how long to sleep..

What do you think?

use chrono::DateTime;
use chrono::{Duration, Timelike, Utc};
use tokio::time;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut target_dt = get_prev_10_min_dt(Utc::now());
    loop {
        target_dt = target_dt.checked_add_signed(Duration::seconds(600)).unwrap();
        let diff = (target_dt - Utc::now()).to_std().unwrap();
        time::sleep(diff).await;

        // Do something...
        // ...
    }
}

fn get_prev_10_min_dt(current_dt: DateTime<Utc>) -> DateTime<Utc> {
    let target_dt = current_dt.checked_add_signed(Duration::seconds(1)).unwrap();
    // Round to 10 min multiple
    let target_minute = (target_dt.minute() / 10) * 10;
    
    let target_dt = target_dt.with_minute(target_minute).unwrap();
    target_dt.with_second(0).unwrap()
}

Of course – at some point you'll have to calculate a time difference. And that won't always be exact, either (the only reliable way to do timers is to implement them in hardware). But it's the best you can do, since at least the error won't be cumulative. The delay will still be noisy, but it will not be the case that the error adds up and the delay will consistently overestimate what the caller requested.

1 Like

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.