Aio readstream
Skill matejformanek/postgres-claude/.claude/skills/aio-readstream
Turn Claude Code into a long-term collaborator on PostgreSQL internals — cited knowledge corpus, agent skills, slash commands, and task-shaped scenarios for backend hacking.
npx -y skills add matejformanek/postgres-claude --skill aio-readstreamAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
PostgreSQL's async I/O subsystem + the read-stream API — `src/backend/storage/aio/` (introduced PG 17, matured PG 18). Loads when the user asks about the read_stream API, AIO methods (sync / worker / io_uring), how sequential + bitmap scans issue prefetch, migrating a code path from `StartReadBuffer`+`WaitReadBuffer` to `read_stream_*`, adding a new read-stream consumer, tuning `io_max_concurrency` / `io_workers` / `io_method`, or debugging AIO-related buildfarm failures. Also for the read-stream callbacks (per-block-lookup / per-buffer-release), completion callbacks (`aio_callback.c`), and the io_uring linkage. Skip when the ask is about client-side async (libpq) or about the WAL writer / walreceiver (those have their own I/O paths).
SKILL.md
10.0 KB, as published. Nobody here has run it
aio-readstream — the async I/O and read-stream API
PostgreSQL 17 introduced the read_stream API — a unified way for scan-shaped code (sequential scans, bitmap heap scans, VACUUM, ANALYZE, some indexes) to issue reads. PG 18 promoted the underlying AIO layer to a first-class subsystem in src/backend/storage/aio/, adding pluggable methods (sync / worker / io_uring). Together they let PG overlap CPU work with I/O without per-caller posix_fadvise bookkeeping.
The file map
| File | Lines | Role |
|---|---|---|
aio.c | 36.6K | Framework core — PgAioHandle pool, submit/wait state machine, method dispatch. |
aio_init.c | 6.6K | Shmem init + GUCs (io_method, io_max_concurrency, io_workers, io_uring_slots). |
aio_callback.c | 10.2K | Completion-callback registration + dispatch — modules extend AIO by registering a callback. |
aio_target.c | 3.2K | The set of "things that can be read" — currently smgr (files by relfilenode) + WAL (in progress). |
aio_io.c | 5.5K | The low-level read/write submission (per-target-per-method). |
aio_funcs.c | 6.2K | SQL-callable helpers backing pg_aios view + statistics. |
method_sync.c | 1.2K | Synchronous fallback — no actual async, just pread. Default on platforms without io_uring or when disabled. |
method_worker.c | 27.6K | Background-worker method — a small pool of dedicated I/O workers do the reads. Portable across Unix/Windows. |
method_io_uring.c | 22.1K | io_uring method (Linux 5.1+) — kernel-level async I/O ring. Highest throughput. |
read_stream.c | 49.3K | The read_stream API — consumer-facing layer on top of AIO. What most PG code uses. |
The two layers
AIO layer — low-level. PgAioHandles from a shmem pool. Direct users are read_stream and (soon) WAL. Applications rarely touch AIO directly.
read_stream layer — high-level. What scan code uses. You give it a callback that returns "the next block number to read" (or a stream of blocks); it issues reads asynchronously in advance, and read_stream_next_buffer returns a ready-to-use pinned buffer.
The read_stream API surface
Public headers: src/include/storage/read_stream.h.
ReadStream *read_stream_begin_relation(int flags,
BufferAccessStrategy strategy,
Relation rel,
ForkNumber forknum,
ReadStreamBlockNumberCB callback,
void *callback_private_data,
size_t per_buffer_data_size);
Buffer read_stream_next_buffer(ReadStream *stream, void **per_buffer_data);
void read_stream_end(ReadStream *stream);
The callback returns the next BlockNumber to read (or InvalidBlockNumber to signal end-of-stream). The API buffers reads ahead so next_buffer rarely stalls.
Flags:
READ_STREAM_SEQUENTIAL— hint that access is sequential (larger prefetch).READ_STREAM_MAINTENANCE— for VACUUM / CLUSTER (usesmaintenance_io_concurrencyinstead ofeffective_io_concurrency).READ_STREAM_USE_BATCHING— batches reads for thesmgrlayer.
Where read_stream is used
As of PG 18, roughly:
access/heap/heapam.c—heap_beginscanbuilds a read-stream for sequential scans (both parallel and non-parallel).access/heap/heapam_handler.c—bitmapheap_stream_read_nextfor bitmap heap scans.commands/vacuumlazy.c— VACUUM pass-1 (heap scan) and pass-2 (index cleanup with read-stream).commands/analyze.c— sampled block reads.access/nbtree/nbtsort.c— parallel index build.- Buffer-manager utility functions (
bufmgr.c—pg_prewarm-like paths).
Grep for read_stream_begin_relation in source/src/backend/ for the full list — as of PG 18 it's around 15 sites.
Callbacks vs streams — the dual
- read_stream_*_CB — the per-block-lookup callback (which block to read next).
- PgAioHandleCallbacks (in
aio_callback.c) — completion callbacks that run when an AIO handle completes. Modules register a family viapgaio_register_callbacks. Used by smgr to update the buffer-manager state on completion.
Extending AIO with a new "kind of read" = registering a callback family + adding a target in aio_target.c.
The three methods
Configured at postmaster start via io_method:
| Method | Where | When to use |
|---|---|---|
sync | method_sync.c | Fallback / testing / disabling AIO. Just pread, no async. |
worker | method_worker.c | Portable. Small pool of I/O worker processes handle reads. io_workers GUC. |
io_uring | method_io_uring.c | Linux only. Kernel async. Highest throughput; requires Linux 5.1+. |
Switching methods requires postmaster restart (they allocate different shmem structures).
Note: PG 18's assumption is that ordinary backends dispatch AIO work; workers or the kernel completes it. Backends still block in read_stream_next_buffer if a read they need isn't yet complete, but this is transparent.
Common patch shapes
Migrate an existing scan to read_stream
- Identify the block-source loop (usually
foreach block: StartReadBuffer + WaitReadBuffer + ReleaseBuffer). - Refactor into a callback that returns the next
BlockNumber. - Replace the loop with
read_stream_begin_relation+ awhile ((buf = read_stream_next_buffer(stream, NULL)) != InvalidBuffer) { ... ReleaseBuffer(buf); }. read_stream_end(stream)in the cleanup path.- Test with all 3 methods (
io_method=sync/worker/io_uring) locally.
Add per-buffer callback data
If your callback needs to remember per-block metadata (e.g. TID range for bitmap scans), size it via per_buffer_data_size in read_stream_begin_relation; retrieve via the per_buffer_data out-param of read_stream_next_buffer. Example: bitmapheap_stream_read_next stores the block-level TID list per buffer.
Extend AIO with a new completion callback
- Define a new
PgAioTargetInfoinaio_target.c(if the object being read is neither smgr nor WAL). - Register
PgAioHandleCallbacksviapgaio_register_callbacksat postmaster startup (or when the extension loads for extensions withshared_preload_libraries). - Handle
PGAIO_HCB_INVALIDfor callback lookups on stale handles.
Pitfalls
- Callback runs in the CALLER's context — the block-lookup callback executes in the process that called
read_stream_next_buffer, NOT in a worker. If the callback allocates memory, it goes in the caller's CurrentMemoryContext. Long-running callbacks stall the stream. - read_stream_reset is for restarting — if you need to seek back / restart,
read_stream_resetdiscards any in-flight reads. It's cheap; don't fear using it. InvalidBufferfromnext_buffermeans EOS, not error — errors get raised viaereport(ERROR)inside the API. If you check for InvalidBuffer, that's the "stream is done" signal.- BufferAccessStrategy is honored — a stream inherits the strategy passed in. VACUUM's ring buffer,
pg_prewarm, and default all work as expected. io_method=syncdisables AIO but not read_stream — you can still call the read_stream API; it just resolves to synchronous reads under the hood.method_workerscaling —io_workersdefaults low (3). If you're benchmarking a heavy AIO workload, bump it; otherwise workers become the bottleneck.- io_uring limits on non-root — some Linux distros restrict io_uring to root or CAP_SYS_ADMIN. Postmaster startup will fall back to
workerand log; verify viaSHOW io_method. - Read-ahead does not fetch across relation boundaries — the callback deals in
(rel, forknum, blocknum)tuples all pinned to the same relation. If you need cross-relation prefetch, wrap multiple streams.
Related corpus
- Idiom:
read-stream-prefetch(this file's home idiom). - File docs:
knowledge/files/src/backend/storage/aio/*.md— 12 files, one per source file. - Subsystems:
storage-buffer(buffer-manager interface — every read_stream buffer is a shared-buffer),access-heap(main consumer viaheap_beginscan). - Data structures:
bufferdesc-state(the BufferDesc AIO adds a completion-in-progress state to). - Sessions:
2026-06-08-aio-rmgrdesc-sweep.md— corpus sweep of the AIO subsystem when it was added.
Corpus-chain shortcut
python3 scripts/corpus-chain.py --idiom read-stream-prefetch
python3 scripts/corpus-chain.py --file src/backend/storage/aio/read_stream.c
Both surface the ~15 callers + the AIO handle machinery.
Boundary
Use this skill for src/backend/storage/aio/ + read_stream.c + AIO-consuming scan code.
Don't use for:
- libpq async — client-side, entirely separate; use libpq skill.
- WAL writing (
xlog.c,walwriter.c) — separate I/O path; AIO integration is in-progress but not yet the primary path. walreceiver— replication-side network I/O; also unrelated.smgrlayer directly — that'sstorage/smgr/; AIO uses it but consumers don't reach through AIO to smgr.