Using tokio::select!

Thursday 16 March 2023, 12:51

tokio::select! is a very useful macro to await multiple Rust futures at the same time and react when one of them completes. However, the pattern matching behaviour is a bit known obvious and can lead to some incorrect behaviour that might be hard to spot.

Example

loop {
    tokio::select! {
        Some(msg) = channel1.recv() => { ... }
        Some(msg) = channel2.recv() => { ... }
        _ = interval.tick() => { ... }
    }
}

The branches take the form:

<pattern> = <async expression> (, if <precondition>)? => <handler>,

The pattern part allows you to specify that you want the result of the future in one of the branches to match a particular pattern, however what is non-obvious without reading the documentation is what happens when a the result of a branch does not match the pattern specified.

Example (broken)

loop {
    tokio::select! {
        Some(Message::Trade(trade)) = channel.recv() => { ... }
        _ = interval.tick() => { ... }
    }
}

What happens when the channel receives a message that is not a Message::Trade(_) variant of message. I assumed that the result to would be discarded and it would go round the loop again and have the opportunity to receive the next message and check if it matches the pattern. This is not what happens, what actually happens is that branch containing the channel.recv() is disabled for the remainder of the select. In this case it means that no further messages will be received from the channel until the interval ticks and we create a fresh select in the next iteration of the loop. This makes reading from the channel much slower than intended and is generally not what you want.

Example (fixed):

loop {
    tokio::select! {
        Some(msg) = channel.recv() => {
            if let Message::Trade(trade) = msg {
                ...
            }
        }
        _ = interval.tick() => { ... }
    }
}

The fix is to move the pattern to check for the desired Message variant to an if let statement inside the handler.

This means that it will only disable the branch if the result of the channel.recv() is None, which usually indicates that the channel has been closed and in this case disabling the branch is a sensible thing to do to avoid hot looping.