| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Cancel-safe async io_uring read backend for RustFS storage.
When a caller drops the future of an in-flight read (an erasure-code quorum was reached, a timeout, a disconnect), the kernel may still write into the read buffer until the CQE — so freeing it at future-drop is a use-after-free. This crate owns each buffer and file handle in the driver's pending table from submission until the CQE, reclaims only at the CQE, drains in-flight ops to zero on shutdown (with a bounded leak-over-UAF escape hatch for a hung disk), and aborts rather than free in-flight buffers on a driver-thread panic. The per-invariant rationale lives inline in src/driver.rs and on docs.rs.
Status: read path only, Linux only (an empty stub on other targets). Wired into rustfs/rustfs behind a runtime probe, off by default (RUSTFS_IO_URING_READ_ENABLE). See CHANGELOG.md.
[target.'cfg(target_os = "linux")'.dependencies]
rustfs-uring = "0.3.0"use std::fs::File;
use std::sync::Arc;
use rustfs_uring::UringDriver;
# async fn demo() -> std::io::Result<()> {
// Probe a real IORING_OP_READ before accepting work. On a restricted host,
// `ProbeFailure::is_expected_restriction()` identifies the expected fallback.
let driver = match UringDriver::probe_and_start(64) {
Ok(driver) => driver,
Err(err) if err.is_expected_restriction() => return Ok(()), // use std backend
Err(err) => return Err(std::io::Error::other(err)),
};
let file = Arc::new(File::open("/data/object")?);
// Positioned read (whole-range: short reads are resubmitted). Dropping the
// returned future before it completes is safe — the driver owns the buffer.
let bytes = driver.read_at(Arc::clone(&file), 0, 65536).await?;
let snapshot = driver.shutdown();
assert_eq!(snapshot.delivered + snapshot.orphan_reclaimed, snapshot.submitted);
# Ok(())
# }Round-robin binds each read to the next shard, even if that shard is busy or closed. Capacity-aware selection starts at the same cursor and tries each shard's count permit at most once, skipping closed count semaphores. It uses actual permit acquisition, not a free-capacity snapshot. If all healthy shards are busy, the handle waits on the first healthy candidate using Tokio's fair semaphore. The wait is local to that shard; it does not rebalance when another shard frees.
A shared byte-budget shortage waits on the first shard whose count permit was available, returning that temporary count reservation before constructing the waiter. A closed shared byte budget rejects admission globally. Once a read is accepted or deferred, its read, wakeup, retry, and cancel retain the same owning shard. read_current always keeps the original round-robin behavior; concurrent stream reads still require caller serialization when ordering matters.
Enable the policy explicitly on the constructed driver before sharing it:
let driver = UringDriver::probe_and_start_sharded(128, 4)?
.with_shard_policy(rustfs_uring::ShardPolicy::CapacityAware);This policy has additional admission work under contention. Throughput, CPU cost, and tail-latency acceptance remain pending target-hardware measurements; it is not enabled by default.
With max_in_flight_bytes: Some(budget), all shards share one byte budget. Buffered reads reserve len bytes; direct reads reserve the block-aligned superset length plus align - 1 bytes of allocation padding, including for zero-length direct reads. A request whose allocation exceeds the entire budget, or whose logical length exceeds max_read_len, returns InvalidInput before allocation. A byte budget of zero or above tokio::sync::Semaphore::MAX_PERMITS is rejected at construction. max_read_len: Some(0) allows only zero-length reads.
Admission acquires the shard's count permit before its byte permits. Saturated handles wait asynchronously, holding no read buffer; a byte waiter may hold a count permit, and Tokio's fair byte semaphore can put small reads behind a large waiter. Dropping a waiting handle returns all partial reservations. After enqueue, both permits travel with the read until its terminal CQE, even if its caller is canceled. Short-read retries retain the same reservation. A leaked read retains its charge. Shutdown or any shard-thread exit closes both the shared byte semaphore and every shard's count semaphore when byte limits are enabled. This rejects further admission and wakes waiters at either acquisition stage, even when another shard has a hung read or takes a bounded-drain escape. Registration and terminal closure are synchronized at startup/shutdown; ordinary read admission does not take a registry lock.
This limits reserved driver read-buffer allocation bytes, not process RSS. It excludes queued handle/FD metadata, allocator overhead, result copies and completed Vec results retained in channels or by callers. The caller must bound its task fan-out and result queue separately. Completion releases admission even when the returned result remains alive.
The public API is intentionally small and read-only:
StatsSnapshot is a point-in-time diagnostic view. The conservation identity submitted == delivered + orphan_reclaimed holds after all completions have been reaped; in_flight == 0 indicates a clean shutdown.
The optional diagnostics feature exposes sampled stage histograms through UringDriver::diagnostics() and shard_diagnostics(). It is off by default. See the measurement guide for sampling, stage overlap, cancellation, and instrumentation-overhead boundaries.
Benchmark configuration, CSV schema, timing boundaries, and performance gates are documented in the benchmarking guide. See implementation and acceptance status for completed correctness work and the still-open performance/integration gates. Application wiring has separate RustFS integration prerequisites. The ordered-prefetch example is a bounded consumer contract experiment, not a production streaming API or performance result.
Linux only; on other hosts cargo check builds the empty stub.
# On a Linux host with io_uring available:
cargo test -- --nocapture --test-threads=1
# Two legs in Docker (also on macOS via Docker Desktop / OrbStack):
# leg 1 — io_uring blocked by an explicit seccomp profile → ring-dependent
# tests gracefully skip; kernel-independent unit tests still run;
# leg 2 — seccomp=unconfined → real io_uring, and NO test may skip.
./run-docker.shThe harness fails on a non-degrading leg 1 or a vacuous-pass leg 2, so a skipped suite can never masquerade as coverage. The cancel-safety contract is pinned by the acceptance tests in tests/cancel.rs; the fault-injection feature (test-only) drives the panic-abort, bounded-drain-leak, and probe-failure escape hatches in tests/fault_injection.rs.
For bounded buffered read groups, see explicit batch reads. read_at_batch shares eager notifications per owning shard while keeping each read's admission, result and cancellation independent.
Apache-2.0. See LICENSE.
| Back | FazBrowse Home | New Git URL |