Async I/O Model Guide
SkillAI & modelsThis skill teaches your AI the asynchronous I/O patterns and state machine conventions used in the Turso database codebase. Once added, it can write and review I/O code in core that follows the project's established patterns instead of making up its own approach.
Available today. Use it from your connected AI after setup.
No other account needed.
Add the skill, then give your AI I/O tasks in core. It will follow the Turso patterns as it writes and reviews code.
Then ask your AI: use the Async I/O Model Guide skill
What your AI can do with it
- Apply the async I/O patterns used in tursodb when working on core code
- Handle I/O results with IOResult the way the codebase expects
- Structure async code as state machines matching project conventions
- Avoid re-entrancy pitfalls in async code
- Follow the codebase's CompletionGroup pattern
What this skill tells your AI
The instructions your AI receives, as published by glommer/pgmicro in .claude/skills/async-io-model/SKILL.md and read by ahel’s review.
Turso uses cooperative yielding with explicit state machines instead of Rust async/await.
Core Types
pub enum IOCompletions {
Single(Completion),
}
#[must_use]
pub enum IOResult<T> {
Done(T), // Operation complete, here's the result
IO(IOCompletions), // Need I/O, call me again after completions finish
}
Functions returning IOResult must be called repeatedly until Done.
Completion and CompletionGroup
A Completion tracks a single I/O operation:
pub struct Completion { /* ... */ }
impl Completion {
pub fn finished(&self) -> bool;
pub fn succeeded(&self) -> bool;
pub fn get_error(&self) -> Option<CompletionError>;
}
To wait for multiple I/O operations, use CompletionGroup:
let mut group = CompletionGroup::new(|_| {});
// Add individual completions
group.add(&completion1);
group.add(&completion2);
// Build into single completion that finishes when all complete
let combined = group.build();
io_yield_one!(combined);
CompletionGroup features:
- Aggregates multiple completions into one
- Calls callback when all complete (or any errors)
- Can nest groups (add a group's completion to another group)
- Cancellable via
group.cancel()
Helper Macros
return_if_io!
Unwraps IOResult, propagates IO variant up the call stack:
let result = return_if_io!(some_io_operation());
// Only reaches here if operation returned Done
io_yield_one!
Yields a single completion:
io_yield_one!(completion); // Returns Ok(IOResult::IO(Single(completion)))
State Machine Pattern
Operations that may yield use explicit state enums:
enum MyOperationState {
Start,
WaitingForRead { page: PageRef },
Processing { data: Vec<u8> },
Done,
}
The function loops, matching on state and transitioning:
fn my_operation(&mut self) -> Result<IOResult<Output>> {
loop {
match &mut self.state {
MyOperationState::Start => {
let (page, completion) = start_read();
self.state = MyOperationState::WaitingForRead { page };
io_yield_one!(completion);
}
MyOperationState::WaitingForRead { page } => {
let data = page.get_contents();
self.state = MyOperationState::Processing { data: data.to_vec() };
// No yield, continue loop
}
MyOperationState::Processing { data } => {
let result = process(data);
self.state = MyOperationState::Done;
return Ok(IOResult::Done(result));
}
MyOperationState::Done => unreachable!(),
}
}
}
Re-Entrancy: The Critical Pitfall
State mutations before yield points cause bugs on re-entry.
Wrong
fn bad_example(&mut self) -> Result<IOResult<()>> {
self.counter += 1; // Mutates state
return_if_io!(something_that_might_yield()); // If yields, re-entry will increment again!
Ok(IOResult::Done(()))
}
If something_that_might_yield() returns IO, caller waits for completion, then calls bad_example() again. counter gets incremented twice (or more).
Correct: Mutate After Yield
fn good_example(&mut self) -> Result<IOResult<()>> {
return_if_io!(something_that_might_yield());
self.counter += 1; // Only reached once, after IO completes
Ok(IOResult::Done(()))
}
Correct: Use State Machine
enum State { Start, AfterIO }
fn good_example(&mut self) -> Result<IOResult<()>> {
loop {
match self.state {
State::Start => {
// Don't mutate shared state here
self.state = State::AfterIO;
return_if_io!(something_that_might_yield());
}
State::AfterIO => {
self.counter += 1; // Safe: only entered once
return Ok(IOResult::Done(()));
}
}
}
}
Common Re-Entrancy Bugs
| Pattern | Problem |
|---|---|
vec.push(x); return_if_io!(...) | Vec grows on each re-entry |
idx += 1; return_if_io!(...) | Index advances multiple times |
map.insert(k,v); return_if_io!(...) | Duplicate inserts or overwrites |
flag = true; return_if_io!(...) | Usually ok, but check logic |
State Enum Design
Encode progress in state variants:
// Good: index is part of state, preserved across yields
enum ProcessState {
Start,
ProcessingItem { idx: usize, items: Vec<Item> },
Done,
}
// Loop advances idx only when transitioning states
ProcessingItem { idx, items } => {
return_if_io!(process_item(&items[idx]));
if idx + 1 < items.len() {
self.state = ProcessingItem { idx: idx + 1, items };
} else {
self.state = Done;
}
}
Turso Implementation
Key files:
core/types.rs-IOResult,IOCompletions,return_if_io!,return_and_restore_if_io!core/io/completions.rs-Completion,CompletionGroupcore/util.rs-io_yield_one!macrocore/state_machine.rs- GenericStateMachinewrappercore/storage/btree.rs- Many state machine examplescore/storage/pager.rs-CompletionGroupusage examples
Testing Async Code
Re-entrancy bugs often only manifest under specific IO timing. Use:
- Deterministic simulation (
testing/simulator/) - Whopper concurrent DST (
testing/concurrent-simulator/) - Fault injection to force yields at different points
References
docs/manual.mdsection on I/O
Signals
- GitHub stars
- 1k
- Forks
- 27
- Last commit
- Aug 2026
Others that do the same job
Advanced
- Catalog kind
- skill
- Gateway key
async-io-model- Source
- github.com/glommer/pgmicro