Async I/O Patterns in Turso Core: Understanding IOResult and State Machines
Turso implements a lightweight asynchronous I/O model using the IOResult<T> enum and Completion objects to interleave database computation with kernel I/O without blocking threads, enabling cooperative multitasking through poll-based state machines.
The Turso database engine (tursodatabase/turso) uses a novel async I/O architecture that avoids traditional async/await syntax while still achieving non-blocking disk operations. At the heart of this system lies the IOResult state machine, which allows the virtual database engine (VDBE) to suspend execution when hitting disk I/O and resume seamlessly once the kernel signals completion.
The IOResult Enum – Core of the State Machine
The IOResult<T> enum in core/types.rs provides the fundamental abstraction for distinguishing between synchronous completion and pending I/O operations. This enum acts as a discriminated union that propagates state through the entire call stack.
/// https://github.com/tursodatabase/turso/blob/main/core/types.rs#L3041-L3046
#[must_use]
#[derive(Debug, Clone)]
pub enum IOResult<T> {
/// The operation finished synchronously; the value is ready.
Done(T),
/// The operation needs to wait for an asynchronous I/O completion.
IO(IOCompletions),
}
Done(T)represents the synchronous path where the value is immediately available.IO(IOCompletions)indicates that the operation must yield control to the scheduler until the kernel completes the underlying I/O.
Helper methods such as is_io(), io(), and map() make the enum ergonomic to work with throughout the codebase, allowing the VM to test for pending I/O without manual pattern matching in hot paths.
Completion Objects and Kernel I/O Representation
IOCompletions wraps concrete kernel-level I/O requests and implements the Future trait, enabling integration with Rust's async ecosystem while maintaining Turso's poll-based approach. The Completion struct in core/io/completions.rs encapsulates the internal state of an operation.
/// https://github.com/tursodatabase/turso/blob/main/core/io/completions.rs#L24-L38
#[must_use]
#[derive(Debug, Clone)]
pub struct Completion {
/// Holds the internal state of the operation; `None` means a cooperative yield.
pub(super) inner: Option<Arc<CompletionInner>>,
}
The Future implementation drives the state machine by checking whether the kernel has signaled completion:
/// https://github.com/tursodatabase/turso/blob/main/core/io/completions.rs#L31-L44
impl Future for Completion {
type Output = Result<(), crate::LimboError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.set_waker(cx.waker());
if self.finished() {
self.wake();
let res = self.get_error()
.map_or(Ok(()), |err| Err(crate::LimboError::CompletionError(err)));
return Poll::Ready(res);
}
Poll::Pending
}
}
When a kernel operation completes, CompletionInner stores the result or error and invokes the waker. The IOResult::IO variant carries this Completion upward through the call stack, allowing the scheduler to register it for wakeup when the I/O finishes.
Propagating Pending I/O with return_if_io!
The return_if_io! macro in core/types.rs eliminates boilerplate when handling IOResult values throughout the VDBE. This macro extracts the inner result, returning early if the operation is pending I/O while preserving the synchronous coding style for the common case.
/// https://github.com/tursodatabase/turso/blob/main/core/types.rs#L71-L84
#[macro_export]
macro_rules! return_if_io {
($expr:expr) => {
match $expr {
Ok(IOResult::Done(v)) => v,
Ok(IOResult::IO(io)) => return Ok(IOResult::IO(io)),
Err(err) => {
branches::mark_unlikely();
return Err(err);
}
}
};
}
Typical usage inside the VDBE executor appears in core/vdbe/execute.rs:
// https://github.com/tursodatabase/turso/blob/main/core/vdbe/execute.rs#L1158-L1162
let value = return_if_io!(cursor.rowid());
// `value` is only reachable if the rowid was ready synchronously.
If cursor.rowid() needs to read a page from disk, it returns IOResult::IO(completion). The macro immediately bubbles this variant up to the caller, which yields control to the cooperative scheduler. Once the kernel finishes the read, the scheduler re-invokes the VM, where the macro now matches IOResult::Done and execution proceeds.
Platform-Specific I/O Backends
All I/O implementations—whether Linux io_uring, Unix syscalls, Windows IOCP, or generic fallbacks—implement the IO trait defined in core/io/mod.rs. This trait abstracts platform specifics while maintaining the Completion-based contract.
/// https://github.com/tursodatabase/turso/blob/main/core/io/mod.rs#L51-L57
pub trait IO: Send + Sync {
fn open_file(&self, path: &str, flags: OpenFlags, direct: bool) -> Result<Arc<dyn File>>;
fn pread(&self, pos: u64, c: Completion) -> Result<Completion>;
fn pwrite(&self, pos: u64, buffer: Arc<Buffer>, c: Completion) -> Result<Completion>;
// …
}
Concrete implementations like UnixIO in core/io/unix.rs translate these calls to the appropriate system APIs (pread, pwrite, or io_uring submit operations) and construct a Completion that understands how to complete the future when the kernel signals readiness.
End-to-End Flow in the VDBE
Understanding how these components interact requires tracing a complete I/O operation. When the VDBE executes an opcode requiring disk access, the following sequence occurs:
- The opcode calls a method that returns
IOResult<T>. - If the data is cached, the method returns
IOResult::Done(value). - If the data requires disk I/O, the backend creates a
Completion, submits it to the kernel, and returnsIOResult::IO(completion). - The
return_if_io!macro catches this variant and returns it up the call stack to the scheduler. - The scheduler parks the current task and registers the
Completionwaker. - When the kernel completes the I/O, it invokes the waker, marking the task as runnable.
- The scheduler re-enters the VM at the same opcode, which now receives
IOResult::Donewith the buffered data.
This pattern allows Turso to achieve full async I/O without viral async annotations throughout the codebase, keeping the core VM code straightforward and debuggable while maintaining high concurrency.
Summary
IOResult<T>provides a strict state machine distinguishing synchronous results (Done) from pending kernel operations (IO).Completionobjects encapsulate platform-specific I/O requests and implementFuturefor integration with the cooperative scheduler.return_if_io!macro eliminates boilerplate when propagating pending I/O up the call stack, preserving synchronous coding patterns.IOtrait abstracts over Linuxio_uring, Unix syscalls, Windows IOCP, and generic backends, ensuring portable async behavior.- Zero-cost abstraction allows the VDBE to interleave computation with I/O without thread blocking or
async/awaitsyntax overhead.
Frequently Asked Questions
What is the purpose of IOResult in Turso?
IOResult serves as the fundamental state machine for async I/O operations in Turso's core engine. It allows the database to distinguish between operations that complete immediately in memory (Done) versus those that require waiting for the kernel to finish disk I/O (IO). This enum propagates through the entire call stack, enabling the cooperative scheduler to suspend and resume execution without blocking OS threads.
How does Turso handle async I/O without async/await syntax?
Turso uses a poll-based model where Completion objects implement the Future trait but are driven by a custom scheduler rather than Rust's async runtime. The return_if_io! macro allows the VDBE to write straight-line code that automatically bubbles pending I/O up to the scheduler. When the kernel completes an operation, it wakes the corresponding Completion, allowing the scheduler to resume the specific opcode that was waiting.
What platforms does Turso's async I/O support?
The IO trait in core/io/mod.rs abstracts over multiple backends including Linux io_uring, traditional Unix syscalls (pread/pwrite), Windows IOCP, and generic fallbacks. Each implementation in core/io/unix.rs or platform-specific modules constructs Completion objects appropriate for that OS's async I/O mechanisms while exposing the same IOResult-based interface to the VDBE.
How does the return_if_io! macro improve performance?
The return_if_io! macro improves performance by eliminating branch prediction failures on the hot path. By marking error branches with branches::mark_unlikely(), it hints to the compiler that I/O suspension is the exceptional case, keeping the fast path optimized for cache hits. This macro also prevents the code bloat associated with manual pattern matching on IOResult throughout the hundreds of opcode implementations in the VDBE.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →