Uringy - Non-blocking Rust without the pain and complexity of async/await

A simple single-threaded concurrency runtime based on io_uring.

diagram

Writing concurrent code in Rust doesn't need to be painful. Uringy is a runtime that combines structured concurrency, a single-threaded design, and Linux's io_uring. Intended for server applications, from simple single-threaded to highly scalable thread-per-core designs.

The runtime runs on a single thread (inspired by NodeJS). Unlike NodeJS which uses epoll under the hood, there is no need for an auxiliary thread pool for file IO operations, since io_uring supports filesystem IO natively. For parallelism, you can spawn multiple threads with their own runtime.

io_uring

io_uring is a new asynchronous syscall interface for Linux, implemented using a pair of memory mapped ring buffers. It has several benefits:

  • Buffers aren't copied to and from the kernel, instead ownership of a buffer is lent out to the kernel
  • Syscall overhead is amortized due to batching several submission queue entries before issuing the system call that submits them all
  • Provides efficient IPC from one io_uring instance to another

Structured Concurrency

Inspired by trio and scoped threads, Uringy doesn't allow globally spawning tasks. Instead, tasks form a tree starting with start. Parent tasks wait for their children to complete. Parent tasks propagate cancellation to their children. Child tasks propagate panics to their parent after waiting for its own children to complete.

Here's an example of a TCP echo server. Note the lack of async and await.

#[uringy::start]
fn main() {
    let handle = uringy::fiber::spawn(|| tcp_echo_server(9000));

    uringy::signals().filter(Signal::is_terminal).next().unwrap();
    uringy::println!("gracefully shutting down");
    handle.cancel(); // Cancellation propagates throughout the entire fiber hierarchy

    // Automatically waits for all fibers to complete
}

fn tcp_echo_server(port: u16) {
    let listener = uringy::net::TcpListener::bind(("0.0.0.0", port)).unwrap();
    uringy::println!("listening for TCP connections on port {port}");
    let mut connections = listener.incoming();
    while let Ok((stream, _)) = connections.next() {
        uringy::fiber::spawn(move || handle_connection(stream));
    }
}

fn handle_connection(tcp: TcpStream) {
    let (mut r, mut w) = stream.split();
    let _ = std::io::copy(&mut r, &mut w);
}