Context

I have been working on a project where I am writing the off-chain processes that make RPC calls to on-chain contracts, which we are calling “keepers.” Their job is to just tick along on a cadence or react to events on-chain and “keep things correct” .. or as I like to joke, “keep’er running.”

For this project, I decided this would be a good time to learn some Rust and dove in head-first. This was also a time I’d figured, “let’s learn about tokio” because all my other Rust mini-projects have been fairly simple.

Problem

Some of the tasks just needed to wait in their loop instead of constantly rapid-firing (let’s not DDOS a server), so I would need that task/thread to sleep…

tokio::spawn(async || {
    loop {
        info!("Ticking task 1");
        // do some work...
        thread::sleep(Duration::from_secs(1));
    }
});

tokio::spawn(async || {
    loop {
        info!("Ticking task 2");
        // do some work...
        thread::sleep(Duration::from_secs(1));
    }
});

My code had some Arc<Mutex<T>>’s being passed around too, but generally it seems straightforward enough, right? Just two tasks doing their jobs.. forever.

When running this program however, I’d often only see one of the tasks actually running its ticks on the right cadence– or one thread completely freeze and never actually fire.

But why?

Tokio is a runtime that runs within your application to be the receiving end of rust’s async traits you want to use. The thing to remember is that it’s abstracting away all the actual parallelism/concurrency you’re asking for by leveraging Futures (aka “promises” in other languages) and checking back on them later by polling their status.

By using thread::sleep you’ve actually paused the tokio runtime in the middle of trying to execute the task with a blocking sleep– it might be doing absolutely nothing else except waiting on your task code, based on how tokio is deciding to manage the execution.

Solution

Well, it would appear I hadn’t read the documentation well enough, but thread::sleep is the wrong choice– it’s a synchronous sleep call on the current thread, which will halt the entire tokio runtime that’s keeping your program running.

The correct way to approach sleeping is to use the tokio::time::sleep async function, which you will then .await in your code:

tokio::spawn(async || {
    info!("Ticking task 1");
    // do some work...
    tokio::time::sleep(Duration::from_secs(1)).await;
});

tokio::spawn(async || {
    info!("Ticking task 2");
    // do some work...
    tokio::time::sleep(Duration::from_secs(1)).await;
});

There, now it works as I’d expected!