aselect

Crates.ioaselect
lib.rsaselect
version0.4.0
created_at2025-12-30 14:40:24.364894+00
updated_at2026-01-01 16:56:22.027843+00
descriptionOpinionated replacement for tokio::select!, avoiding certain pitfalls.
homepagehttps://github.com/avl/aselect/
repositoryhttps://github.com/avl/aselect/
max_upload_size
id2012746
size115,341
Anders Musikka (avl)

documentation

https://docs.rs/aselect/

README

Build

ASelect

This is a rust crate intended to provide an opinionated solution to two potential pitfalls when the tokio select!-macro is used in loops. While said macro is useful, it has two error-prone characteristics, when used in loops:

  • Each invocation of select! will cancel all but one future.
  • Handlers with async blocks may starve all select arms (when one select! arm becomes ready, the others are not polled until its handler completes).

This crate solves this by:

  • Never cancelling futures (except when explicitly asked to)
  • Allowing futures to live for multiple iterations of select loops
  • Not allowing async code in handler blocks (only in arms).

Additionally, aselect:

  • Can be formatted by rustfmt.
  • Does not allocate memory
  • Can be used in a no_std context.
  • Allows sharing mutable state between select arms
  • Implements Stream.

Motivation

See motivating example for a detailed example showcasing the problems aselect is meant to solve.

Example

#[tokio::main]
async fn main() {
    let counter = 0u32;
    let mut stream = pin!(aselect!(
        {
            mutable(counter);
        },
        timer(
            {
                tokio::time::sleep(std::time::Duration::from_millis(1))
            },
            async |fut1| {
                fut1.await;
            },
            |result| {
                *counter += 1;
                None
            }
        ),
        output(
            {
                tokio::time::sleep(std::time::Duration::from_millis(3))
            },
            async |fut2| {
                fut2.await;
            },
            |result| {
                Some(*counter)
            }
        ),
    ));
    while let Some(item) = stream.next().await {
        println!("Value: {}", item);
    }
}

See docs and examples/ for more complex examples. Especially, the above simple example does not show more advanced state tracking (for example, constant and borrowed captures). This example also doesn't show canceling.

Motivation for this crate

Being able to easily cancel futures is a useful feature of Rust. As is the ability to have explicit control over the execution of async programs, using Rust's extendable and programmable async features.

However, there are a few patterns that have turned out to be error-prone:

  1. Having futures that exist, but are not polled
  2. Canceling futures

The async programming model allows applications to be written mostly as if they were normal sequential programs. However, this abstraction can be quite leaky. Point 1 above means that a program can deadlock, if a future that owns a lock is not polled. Point 2 means that async methods may not behave as expected.

A good introduction to the first problem can be found here: https://rfd.shared.oxide.computer/rfd/0609 (TLDR, if future that is not being polled owns a resource, that resource will never become available).

An illustration of the other problem is something like this:

    async fn receive_command(&mut self) -> (Command, UserInfo) {
        let cmd = self.command_link.receive().await;
        self.security_log.send(cmd).await;
        let user_info = self.db.lookup_user_info(cmd.user).await;
        (cmd, user_info)
    }

This method receives a command, logs it in a security log, then looks up some additional information and returns this to the caller. Consider what happens if this receive_command method is used as a future in regular tokio select!. Things may appear to work, but if another future becomes ready after a command has been received and logged, but before lookup_user_info completes, the command will be silently dropped. In this case, the command will still appear in the security_log, it will just not have any other effect.

Troubleshooting this kind of problem can be frustrating. Basically, any application that uses tokio select! needs to annotate all its methods with a # Cancel safety header in its documentation, and all code must be inspected to see if cancel safety invariants are honored or not. Once again, Oxide has an excellent guide on the subject: https://rfd.shared.oxide.computer/rfd/400 .

Note that this analysis must be done globally. It is typically hard to make all code Cancel safe. Thus, one must ensure that async code in one module isn't called from a sometimes-canceled future elsewhere in the application.

Surely there is a better way!

This crate suggests that futures should never be canceled, except potentially during shutdown or as a response to faults. aselect! is a macro that allows writing select loops that never cancel futures, and which always poll all live futures. This means that the pitfalls mentioned in the beginning of this README are avoided.

Commit count: 0

cargo fmt