ADR 0011 — io_uring File Sink (Phase 20: scaffold)
- Status: Accepted
- Date: 2026-07-05
- Phase: 20 (per
docs/IMPLEMENTATION-PLAN-v0.1.0.md) - Deciders: repository maintainers
- Related: ADR 0010 (OTLP pluggable transport) — same scaffold-then-fill pattern used for the gRPC transport.
Context
Linux 5.1+ ships io_uring, a submission-queue/completion-queue
async I/O interface that eliminates the per-syscall context-switch
overhead of write(2) for high-throughput writers. rlg’s file
sink writes formatted log payloads through
std::fs::File::write_all, which is one write(2) syscall per
payload — fine at 10 k events/sec, throughput-capped at 500 k+.
Enterprise adopters targeting Linux want the io_uring path. The
plan called for a new PlatformSink::UringFile variant behind a
uring feature.
Decision
Land the scaffold in Phase 20 and fill in the submission-queue integration in Phase 20.1 (planned):
Phase 20 (this commit) — scaffold
- New Cargo feature
uringonrlg. io-uring 0.7dep pinned under[target.'cfg(target_os = "linux")'.dependencies]. Only pulls on Linux; other targets get a resolved-but-inert feature.- New
PlatformSink::UringFile(std::fs::File)variant, gated by#[cfg(all(target_os = "linux", feature = "uring"))]. Compiles only on Linux, only with the feature. PlatformSink::emithandles the variant. The current implementation delegates to the syncwrite_allpath for correctness — no io_uring SQE loop yet. The variant exists so consumers can select it today and the type signature is fixed; the wire path is the follow-up.- No public API change to the sink constructors — the variant is only produced when a consumer explicitly selects it.
Phase 20.1 (follow-up) — full submission-queue integration
- Introduce a per-flusher-thread
io_uring::IoUringinstance with an SQ depth of 128 (batch-size × 2 headroom). - Batch outstanding writes into a single
submit_and_waitcall per flusher wake, matching the existing 64-event drain batch. - Handle short writes (partial-completion CQE) with a retry loop bounded by the same 3-retry policy the queue already uses.
- Benchmark methodology matches Phase 18’s sharded queue: run
the file-sink benches with and without
--features uringand publish deltas torustlogs.com/bench/.
Model
The scaffold’s model is deliberately conservative:
- Variant compiles only on Linux. Non-Linux targets never see
a
UringFilein amatcharm; the sink’s cross-platform usability is unaffected. - Enum uses
std::fs::Fileas the backing type, matching theFilevariant. Phase 20.1 replaces this with anio_uring-owned file descriptor plus a per-thread submission queue. emitwrites synchronously. The write path callsFile::write_all— the io_uring SQE submission is deferred. Consumers who selectUringFiletoday get the same throughput asFile; they select it to future-proof, not for a Phase 20 performance win.
This is the same scaffold-then-fill pattern used for the gRPC transport in Phase 19c (ADR 0010): the type layout, feature flag, and dep tree land now; the wire path fills in when the follow-up phase brings the ~200 LOC needed to do it well.
What Phase 20 is NOT
- Not a performance win. The scaffold doesn’t move any bytes
through io_uring. Users who want the win today wire the
submission queue themselves against the underlying
File— documented in the variant’s rustdoc. - Not a cross-platform sink. The variant is Linux-only, both
because io_uring is Linux-only and because
[target.'cfg(target_os = "linux")']gates the dep. - Not benchmarked yet. Phase 20.1 lands the benches.
Consequences
- Zero regression by default. The feature is off. The variant
doesn’t compile. Users who never set
--features uringsee identical behaviour to pre-Phase-20 rlg. - API future-proofing. Consumers targeting Linux today can
select
PlatformSink::UringFileand know the enum variant name is stable; Phase 20.1 changes the internals only. - Cold-build time. ~50 LOC of new dependency graph on Linux (io-uring 0.7). Negligible.
- CI cost. No new CI job — the existing Linux matrix leg
already tests both feature combinations under
cargo test --workspace --all-features.
Alternatives considered
tokio-uring. Original plan text. Rejected in favour of the rawio-uringcrate because tokio-uring requires a tokio-uring-managed runtime, which is incompatible with rlg’sstd::threadflusher model.io-uring 0.7is the low-level submission-queue API that works from any thread.- Full Phase 20 in one commit. Rejected on CI-risk grounds — the SQE integration needs benches, per-thread runtime state management, and error-recovery machinery. Landing it alongside Phases 19b/19c would collide with the concurrency work and balloon merge conflicts.
glommio. A userspace runtime built on io_uring with first-class file I/O primitives. Rejected — pulls a runtime dependency; conflicts with the “runtime-agnostic” positioning the workspace maintains.- Skip io_uring entirely. Rejected — the enterprise linux segment is a first-class rlg deployment target and io_uring is the industry standard for high-throughput file writes there from 2024 onwards.
References
io-uringcrate docs- Linux kernel io_uring documentation
- ADR 0010 — OTLP pluggable transport (same scaffold-then-fill pattern for the gRPC transport).