Search code examples
rustrust-tokio

why not panic when task timeout?


I set the timeout to 1s, but the task executes to 3s, but no panic occurs.

#code

    #[should_panic]
    fn test_timeout() {
        let rt = create_runtime();
        let timeout_duration = StdDuration::from_secs(1);
        let sleep_duration = StdDuration::from_secs(3);

        let _guard = rt.enter();

        let timeout = time::timeout(timeout_duration, async {
            log("timeout running");
            thread::sleep(sleep_duration);
            log("timeout finsihed");
            "Ding!".to_string()
        });

        rt.block_on(timeout).unwrap();
    }


Solution

  • Using thread::sleep in asynchronous code is almost always wrong.

    Conceptually, the timeout works like this:

    • tokio spawns a timer which would wake up after the specified duration.
    • tokio spawns your future. If it returns Poll::Ready, timer is thrown away and the future succeeds. If it returns Poll::Pending, tokio waits for the next event, i.e. for wakeup of either your future or the timer.
    • If the future wakes up, tokio polls it again. If it returns Poll::Ready - again, timer is thrown away, future succeeds.
    • If the timer wakes up, tokio polls the future one last time; if it's still Poll::Pending, it times out and is not polled anymore, and timeout returns an error.

    In your case, however, future do not return Poll::Pending - it blocks inside the thread::sleep. So, even though the timer could fire after one second has passed, tokio has no way to react - it waits for the future to return, future returns only after the thread is unblocked, and, since there's no await inside the block, it returns Poll::Ready - so the timer isn't even checked.

    To fix this, you're expected to use tokio::time::sleep for any pauses inside async code. With it, the future times out properly. To illustrate this claim, let's see the self-contained example equivalent to your original code:

    use core::time::Duration;
    use tokio::time::timeout;
    
    #[tokio::main]
    async fn main() {
        let timeout_duration = Duration::from_secs(1);
        let sleep_duration = Duration::from_secs(3);
    
        timeout(timeout_duration, async {
            println!("timeout running");
            std::thread::sleep(sleep_duration);
            println!("timeout finsihed");
            "Ding!".to_string()
        })
        .await
        .unwrap_err();
    }
    

    Playground

    As you've already noticed, this fails - unwrap_err panics when called on Ok, and timeout returns Ok since the future didn't time out properly.

    But when replacing std::thread::sleep(...) with tokio::time::sleep(...).await...

    use core::time::Duration;
    use tokio::time::timeout;
    
    #[tokio::main]
    async fn main() {
        let timeout_duration = Duration::from_secs(1);
        let sleep_duration = Duration::from_secs(3);
    
        timeout(timeout_duration, async {
            println!("timeout running");
            tokio::time::sleep(sleep_duration).await;
            println!("timeout finsihed");
            "Ding!".to_string()
        })
        .await
        .unwrap_err();
    }
    

    ...we get the expected behavior - playground.