New to Claude Skills? Learn how to install them →

tursodatabase on GitHub

Async I/O Model

Free

Master asynchronous patterns for TursoDB.

Get this skill

Free · Opens the source repo

What Async I/O Model does

The Async I/O Model skill provides a comprehensive guide to implementing asynchronous programming patterns in TursoDB. Unlike traditional Rust async/await, Turso employs cooperative yielding with explicit state machines. This approach allows developers to manage I/O operations effectively while avoiding common pitfalls associated with re-entrancy. The skill covers essential types such as IOResult and Completion, which are fundamental to handling asynchronous tasks in a structured manner.

Key components include the Completion struct, which tracks individual I/O operations, and CompletionGroup, which aggregates multiple completions into a single operation. This is particularly useful when you need to wait for several I/O tasks to finish before proceeding. The skill also introduces helper macros like return_if_io! and io_yield_one!, which simplify the process of yielding and handling I/O results, ensuring that your state management remains robust.

Additionally, the skill emphasizes the importance of state machine patterns for managing complex operations that may yield. By using state enums, developers can preserve progress across yields, minimizing the risk of bugs caused by state mutations before yield points. The documentation includes examples of both correct and incorrect implementations, helping to clarify best practices in asynchronous programming.

This skill is designed for developers working with TursoDB who need to implement asynchronous I/O operations efficiently. It is particularly beneficial for those looking to avoid common re-entrancy bugs and to write cleaner, more maintainable code. By following the patterns and practices outlined in this guide, developers can enhance their understanding of asynchronous programming in the context of TursoDB.

When to use it

Use this skill when developing applications with TursoDB that require efficient handling of asynchronous I/O operations.

When not to use it

This skill may not be suitable for projects that do not utilize TursoDB or require traditional async/await patterns in Rust.

What you can build with it

Implementing Asynchronous Database Operations

Use this skill to learn how to handle multiple asynchronous database operations in TursoDB without running into re-entrancy issues.

Managing Complex I/O Workflows

When building applications that require complex I/O workflows, this skill provides the necessary patterns to manage state and completions efficiently.

Avoiding Common Bugs in Asynchronous Code

Leverage the guidance in this skill to identify and avoid common re-entrancy bugs that can arise during asynchronous programming.

How to install Async I/O Model

View source

1. Install with the skills CLI

npx skills add tursodatabase/turso/async-io-model --agent claude-code

2. Or install it manually

Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.

Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs

Inside SKILL.md

Written by tursodatabase

Async I/O Model Guide

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

PatternProblem
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, CompletionGroup
  • core/util.rs - io_yield_one! macro
  • core/state_machine.rs - Generic StateMachine wrapper
  • core/storage/btree.rs - Many state machine examples
  • core/storage/pager.rs - CompletionGroup usage 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.md section on I/O

Frequently asked questions about Async I/O Model

Similar skills