Crate ringolo

Crate ringolo 

Source
Expand description

Ringolo is a completion-driven asynchronous runtime built on top of io_uring. Its task system is derived from Tokio’s battle-tested task module, which implements a state machine for managing a future’s lifecycle.

§⚡ Quick Start

#[ringolo::main]
async fn main() {
    println!("Hello from the runtime!");

    let join_handle = ringolo::spawn(async {
        // ... do some async work ...
        "Hello from a spawned task!"
    });

    let result = join_handle.await.unwrap();
    println!("{}", result);
}

§✨ Key features

§🚀 Submission Strategies

The runtime supports four distinct submission backends to optimize throughput and latency:

StrategyDescriptionDriverExample
SingleStandard 1:1 dispatch. One SQE results in one CQE.OpSleep
ChainStrict kernel-side ordering via IOSQE_IO_LINK. Defines dependent sequences without userspace latency.OpList::new_chainTcpListener::bind
BatchSame as Chain, except ops execute concurrently and complete in any order.OpList::new_batchN/A
MultishotSingle SQE establishes a persistent request that generates a stream of CQEs (e.g. timers, accept).MultishotTick

§⚙️ I/O Model: Readiness vs. Completion

A key difference from Tokio is the underlying I/O model. Tokio is readiness-based, typically using epoll. This epoll context is global and accessible by all threads. Consequently, if a task is stolen by another thread, readiness events remain valid.

Ringolo is completion-based, using io_uring. This model is fundamentally thread-local, as each worker thread manages its own ring. Many resources, such as registered direct descriptors or provided buffers, are bound to that specific ring and are invalid on any other thread.

§🧵 Work-Stealing and Thread-Local Resources

This thread-local design presents a core challenge for work-stealing. When an I/O operation is submitted on a thread’s ring, its corresponding completion event must be processed by the same thread.

If a task were migrated to another thread after submission but before completion, the resulting I/O event would be delivered to the original thread’s ring, but the task would no longer be there to process it. This would lead to lost I/O and undefined behavior.

Ringolo’s work-stealing scheduler is designed around this constraint. It performs resource and pending I/O accounting to determine when a task is “stealable”. View the detailed implementation within the task module.

§🌳 Structured Concurrency

Another key difference from Tokio is Ringolo’s adoption of Structured Concurrency. While this can be an overloaded term, in Ringolo it provides a simple guarantee: tasks are not allowed to outlive their parent.

To enforce this, the runtime maintains a global task tree to track the task hierarchy. When a parent task exits, all of its child tasks are automatically cancelled.

This behavior is controlled by the orphan policy. The default policy is OrphanPolicy::Enforced, which is the recommended setting for most programs. This behavior can be relaxed in two ways:

  1. Per-Task: To create a single “detached” task, you can explicitly use TaskOpts::BACKGROUND_TASK. This option bypasses the current task’s hierarchy by attaching the new task directly to the ROOT_NODE of the task tree.

  2. Globally: You can configure the entire runtime with OrphanPolicy::Permissive. This setting effectively disables structured concurrency guarantees for all tasks, but it is not the intended model for typical use.

§🛑 Motivation: Safer Cancellation APIs

The primary motivation for this design is to provide powerful and safe cancellation APIs.

Experience with other asynchronous frameworks, including Tokio and folly/coro, shows that relying on cancellation tokens has significant drawbacks. Manually passing tokens is error-prone, introduces code bloat, and becomes exceptionally difficult to manage correctly in a large codebase.

Ringolo’s design provides a user-friendly way to perform cancellation without requiring tokens to be passed throughout the call stack. The global task tree enables this robust, built-in cancellation model.

For a detailed guide, please see the cancellation APIs.

§🛡️ Kernel Interface & Pointer Stability

Interfacing with io_uring requires passing raw memory addresses to the kernel. To prevent undefined behavior, the runtime guarantees strict pointer stability based on the data’s role:

  • Read-only inputs: Pointers to input data (such as file paths) are guaranteed to remain stable until the request is submitted.
  • Writable outputs: Pointers to mutable buffers (such as read destinations) are guaranteed to remain stable until the operation completes.

Ringolo handles these complex lifetime requirements transparently using self-referential structs and pinning. By taking ownership of resources and pinning them in memory, the runtime ensures the kernel never encounters a dangling pointer or a use-after-free error.

For implementation details on these safe primitives, see the future::lib module.

§🧹 Async cleanup via RAII

The thread-local design of io_uring also dictates the model for resource cleanup. Certain “leaky” operations, like multishot timers, and thread-local resources, like direct descriptors, must be explicitly unregistered or closed.

This cleanup must happen on the same thread’s ring that created them. To solve this without blocking on drop, Ringolo uses a maintenance task on each worker thread to handle this transparently. When a future is dropped, it enqueues an async cleanup operation with its local maintenance task. This task then batches and submits these operations, ensuring all resources are freed on the correct thread.

The runtime’s behavior on a failed cleanup operation is controlled by the OnCleanupError policy.

Modules§

future
User-facing set of libraries exposing native futures for the runtime.
runtime
Builder API to configure and create a runtime.
spawn
Utilities to spawn new tasks. Provides functions and types for spawning new tasks onto the runtime.
task
Internal implementation of the task state machine that manages a future’s lifecycle.
time
Asynchronous Time Utilities

Structs§

TaskMetadata
A set of string tags used to identify a task.
TaskOpts
Configuration options for a new task.

Functions§

block_on
We block_on on a special future that we refer to as the root_future. It is guaranteed to be polled on the current thread, and is central in deciding how and when the runtime returns. This is why it has looser bounds (!Send and !Sync).
recursive_cancel_all
Recursively cancels all child tasks of the current task.
recursive_cancel_all_leaves
Recursively cancels all leaf tasks within the current task’s hierarchy.
recursive_cancel_all_metadata
Recursively cancels tasks in the hierarchy that match all metadata tags.
recursive_cancel_all_orphans
Cancels all tasks that have been “orphaned.”
recursive_cancel_any_metadata
Recursively cancels tasks in the hierarchy that match any metadata tag.
spawn
Spawns a new asynchronous task with default options.
spawn_builder
Creates a new SpawnBuilder for configuring and spawning a task.

Attribute Macros§

main
Marks async function to be executed by the selected runtime. This macro helps set up a Runtime without requiring the user to use Runtime or Builder.
test
Marks async function to be executed by runtime, suitable to test environment. This macro helps set up a Runtime without requiring the user to use Runtime or Builder directly.