Using tokio::select! with UdpSocket::recv_from and Sleep causes Sleep to never resolve

H!,

I'm relatively new to tokio (and also Rust) so it's possible that I'm missing something important here. Basically I have a control loop where I use select to poll some futures. My select will wait on a timeout (if there is one to wait on) and the arrival of a datagram on a UDP socket. It looks like this:

loop {
  let next_timeout: Option<Instant> = self.timeouts.get_next();

  tokio::select! {
     _ = async { sleep_until(next_timeout.unwrap()).await }, if next_timeout.is_some() => {
         println!("timeout expired:");
     }
     result = self.udpsocket.recv_from(&mut buf) => {
         let (size, addr) = result?;
         println!("received = {}", String::from_utf8(buf[..size].to_vec()).unwrap());
     }
  }
}

The problem is that the async block with the sleep_until never resolves, even if there is a timeout (next_timeout.is_some() == true) and it has expired. If I send a packet over the network, the recv_from branch gets executed immediately.

If I however disable the recv_from branch when there is a timeout to wait on:

result = self.udpsocket.recv_from(&mut buf), if next_timeout.is_none() =>

Then the sleep_until resolves as expected.

I also tried to put the async block behind a variable and pin it:

let f = async { sleep_until(next_timeout.unwrap()).await };
tokio::pin!(f);

but it didn't change anything.

I really don't know how to make sense of this behavior. It's the first time I'm really stuck while learning Rust/Tokio, so that's why I'm here :slight_smile: any help is very appreciated! Thanks.

rustc: 1.71.0
tokio: 1.28.2

Are you creating the udp socket with from_std?

2 Likes

It will resolve as long as no UDP data arrives before next_timeout expires.

If next_timeout exists, what happens is this:

  1. The async block containing sleep_until() will be evaluated and polled. However, until the timeout expires, there is no return value to work with, so tokio::select! will continue to poll both branches.
  2. If no UDP data arrives before the expiry of next_timeout, the first branch returns and you get your "timeout expired:" message as expected.
  3. But if you receive UDP data in the meantime, tokio::select! will return from the second branch instead. It will also cancel the sleep_until and enter the next iteration of your loop, which means you will never see the expiry message.

As per the tokio::select! docs:

Waits on multiple concurrent branches, returning when the first branch completes, cancelling the remaining branches.

I'm guessing what you want is for the sleep_until function to block the tokio::select! so that no UDP data is read until the timeout expires. If that is the case, you can cut and paste the sleep_until into the code block containing println!("timeout expired:"). Something like this:

tokio::select! {
    _ = async { }, if next_timeout.is_some() => {
        sleep_until(next_timeout.unwrap()).await;
        println!("timeout expired:");
    }

This will cause the first branch to return immediately when there is a timeout, which will cause sleep_until to execute, which in this case will block the current loop.

Yes! And looking at the docs of that function I now see that I need to set the nonblocking mode. I just did it and things work as expected. Thanks for pointing me to the right direction.

1 Like

You're welcome.