Skip to content

Asynchronous storage: co_await on fibers with io_uring (Linux) - #1524

Draft
fnc12 wants to merge 1 commit into
devfrom
feature/async-storage
Draft

fnc12 wants to merge 1 commit into
devfrom
feature/async-storage

Conversation

@fnc12

@fnc12 fnc12 commented Sep 6, 2026 •

Copy link
Copy Markdown
Owner

Summary

async_storage: the ordinary storage driven with co_await, with real asynchronous file I/O underneath (io_uring). The synchronous ORM code is not modified; async_storage mirrors the public interface of storage_t.

io_context io;
auto storage = make_async_storage(io, "app.db",
    make_table("users",
        make_column("id", &User::id, primary_key().autoincrement()),
        make_column("name", &User::name),
        make_column("age", &User::age)));

io.spawn([&storage]() -> task<void> {
    co_await storage.sync_schema();
    co_await storage.insert(User{0, "ann", 30});
    auto adults = co_await storage.get_all<User>(where(c(&User::age) >= 18));
    co_await storage.transaction([](auto& storage) { storage.insert(User{0, "bob", 41}); return true; });
    auto statement = co_await storage.prepare(select(columns(&User::name), where(c(&User::id) == 1)));
    auto names = co_await storage.execute(statement);
}());
io.run();

How it works

  • Every operation runs on a fiber (stackful coroutine, dev/async/context_switch.h, fiber.h), so sqlite3_step() can suspend in the middle of a page read without blocking the thread.
  • A VFS wrapping unix (dev/async/async_vfs.h) routes xRead/xWrite/xSync/xSleep to a single-threaded scheduler over io_uring (scheduler.h, io_uring_engine.h, raw syscalls, no liburing). Locking, WAL shared memory and open/delete stay with the wrapped VFS. Every operation is awaited in order (writes are not batched: measured on one connection, batching cost 30% of commit throughput because every read had to wait for queued writes, while awaited writes match the synchronous path). Memory-mapped I/O is disabled for these connections.
  • The VFS is injected through connection_control (#1392 : Add support for SQLite VFS and open mode flags #1393); nothing global is registered.
  • C++20 front-end: task<T>, io_context, when_all, io_pool for M:N, one class per header.
  • async_storage<Storage> (async_storage.h): one connection, opened lazily and kept open. Like a network connection in an asynchronous client, requests do not block the thread but execute one after another; operations from several coroutines are queued in order, so prepared statements always run on the connection they were prepared on. run(function) is the primitive; CRUD, get_all, select, with, aggregates, prepare/execute, schema functions, transaction/savepoint, connection information and backups are mirrored, with arguments moved into the operation. Functions without I/O (filename, vfs_name, open_mode, dump, interrupt, is_opened) are plain calls.

Scope

Gated by SQLITE_ORM_ASYNC_SUPPORTED: Linux, C++20 coroutines, GCC 13 or newer or Clang, x86_64 or aarch64; part of the main amalgamation (dev/async/ in config.json) and inert elsewhere. GCC 11 and 12 are excluded because they miscompile temporaries that live across a suspension in a co_await expression (co_await storage.insert(User{...})), verified with a minimal reproduction against GCC 11.4, 12.5, 13.4, 14.3, 15.2 and Clang 14. If the kernel refuses io_uring at runtime the io_context constructor throws; io_uring_available() tells in advance and the tests skip in that case.

Measured on a 2.6 GB WAL database on NVMe (random point lookups, 4 KB pages): one connection matches the synchronous storage (cold cache: 13.3k vs 13.4k lookups/s; warm cache: 405k vs 467k, the fiber switch and the io_uring syscall cost ~0.3 µs per read; a writer committing with synchronous=FULL: 670 vs 669 commits/s), while the thread stays free during the wait. With 32 connections on one thread the same workload does ~200k lookups/s cold.

Testing

  • tests/async/async_storage_tests.cpp (Catch2): factory and VFS injection, lazy open, user connection_control, CRUD, aggregates, prepared statements, transactions and savepoints, schema and connection information, backups, queued operations from several coroutines, io_pool.
  • Run: ./build/tests/unit_tests -# "[#async_storage_tests]".
  • Verified on Ubuntu 22.04, kernel 6.8, SQLite 3.54 built from the amalgamation: the full suite with GCC 13.4 (async included) and with GCC 11.4 (async excluded).

Open points

  • Windows (IOCP / IoRing) is a possible future backend; macOS has no real asynchronous file I/O and is intentionally not supported.
  • Row-by-row iterate<T>() is not exposed as an asynchronous generator; iterate inside run.
  • xOpen, xFileSize and xTruncate remain synchronous.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Hk4PTDLH4bDkSxx7Y6A384

@fnc12
fnc12 requested a review from trueqbit September 6, 2026 14:14
@fnc12
fnc12 force-pushed the feature/async-storage branch 3 times, most recently from fb465ed to ae1292d Compare September 6, 2026 15:53
`async_storage`, available where SQLITE_ORM_ASYNC_SUPPORTED is defined
(Linux, C++20 coroutines, GCC 13+ or Clang, x86_64/aarch64), mirrors the
public interface of storage_t with `co_await`:

- every operation runs the unchanged synchronous storage code on a fiber
  (stackful coroutine, inline asm for x86_64/aarch64) so that sqlite3_step()
  can suspend inside file I/O without blocking the thread
- a single-threaded scheduler over io_uring on raw syscalls (no liburing)
- an SQLite VFS wrapping "unix" that routes xRead/xWrite/xSync/xSleep
  through the scheduler, every operation awaited in order
- C++20 front-end: task<T>, io_context, when_all, io_pool (M:N)
- one storage is one connection, opened lazily and kept open; operations
  from several coroutines are queued in order, prepared statements always
  run on the connection they were prepared on
- make_async_storage(io, filename, args...) mirrors make_storage; the VFS is
  injected through connection_control, nothing global changes

Part of the main amalgamation (dev/async/ via config.json), inert on other
platforms and on GCC 11/12, which miscompile temporaries living across a
suspension in a co_await expression. Tests in tests/async, docs in
docs/async.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hk4PTDLH4bDkSxx7Y6A384
@fnc12
fnc12 force-pushed the feature/async-storage branch from ae1292d to 23273f2 Compare September 6, 2026 17:00

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant