Search code examples
rustselectchannelrust-tokio

Something wrong with my usage of tokio select! macro. Process block on the first branch notwithstanding there is an else clause


I attempt to set up a channel to exit process gracefully by capturing signals like SIGTERM or SIGINT. In order to execute business logic by default, i code like this:

    let (t_stop, mut r_stop) = oneshot::channel::<Empty>();
    let handle1 = tokio::spawn(async move {
        for _ in sigs.forever() {
            t_stop.send(Empty {}).expect("stop failed");
            return;
        }
    });

...
    let handle2 = tokio::spawn(async move {
        loop {
            select! {
                Ok(_) = &mut r_stop =>{
                    return;
                }
                else => {
                    info!("default");
                    ...
                }
            };
        }
    });

but, which is frustrating, process seems blocked on the first branch.

For solving the problem, I try to read the document on tokio's home page. Unfortunately, it just narrates that else branch wouldn't be executed unless none of the other branch patterns match.I expect that Ok(_) = &mut r_stop fails in pattern match until signal has been sent, so else clause should be executed.By the way, after press ctrl+c, the process exit as I expect.


Solution

  • select! always waits for some future. See the documentation:

    The complete lifecycle of a select! expression is as follows:

    ...

    1. Concurrently await on the results for all remaining <async expression>s.
    2. Once an <async expression> returns a value, ...
    3. If all branches are disabled, evaluate the else expression. If no else branch is provided, panic.

    Until at least one of the provided futures becomes ready, the else isn't even considered. Therefore, it doesn't make sense to use select! with only one future; you can always use a regular match on the result of awaiting the future.

    What you can do instead is select! with two arms: the channel and your entire loop. That way the loop is cancelled as soon as the channel has a message.

    select! {
        Ok(_) = r_stop => {}
        () = async {
            loop {
                // do your actual work here
            }
        }
    }
    

    However, if the task doesn't need to do any cleanup, consider using Tokio's built-in abort functionality:

    let handle2 = tokio::spawn(async move {
        loop {
            // do stuff
        }
    });
    
    let abort = handle2.abort_handle();
    tokio::spawn(async move {
        for _ in sigs.forever() {
            abort.abort();
        }
    });
    

    Or, if you might have multiple things to cancel, see if CancellationToken might be more convenient than your oneshot usage.