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:
| Strategy | Description | Driver | Example |
|---|---|---|---|
| Single | Standard 1:1 dispatch. One SQE results in one CQE. | Op | Sleep |
| Chain | Strict kernel-side ordering via IOSQE_IO_LINK. Defines dependent sequences without userspace latency. | OpList::new_chain | TcpListener::bind |
| Batch | Same as Chain, except ops execute concurrently and complete in any order. | OpList::new_batch | N/A |
| Multishot | Single SQE establishes a persistent request that generates a stream of CQEs (e.g. timers, accept). | Multishot | Tick |
§⚙️ 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:
-
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 theROOT_NODEof the task tree. -
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§
- Task
Metadata - A set of string tags used to identify a task.
- Task
Opts - Configuration options for a new task.
Functions§
- block_
on - We
block_onon a special future that we refer to as theroot_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
Runtimewithout 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
Runtimewithout requiring the user to useRuntimeorBuilderdirectly.