Skip to main content

vfsi_uring/
lib.rs

1//! Linux io_uring execution of independent descriptor reads, writes and fsyncs.
2//!
3//! [`connect`] returns a client implementing [`vfsi_core::Vfsi`] and
4//! [`vfsi_core::VfsiExt`]. The local backend retains namespace resolution,
5//! descriptor ownership, bounded directory paging and ordered dependent I/O.
6//! Open/close, path writes, append, overlapping writes, and namespace operations
7//! use ordinary syscalls. This is synchronous batching, not an async runtime,
8//! direct I/O or zero-copy API. Completion order never changes result order.
9//! Opt-in cache-read and syscall-write paths avoid ring overhead for warm
10//! buffered I/O. Their counters are separate from actual SQEs and CQEs.
11//! Owned read cohorts fitting the configured byte window use their final result
12//! allocations directly. Borrowed destinations and larger cohorts retain
13//! bounded scratch storage; neither path is kernel zero-copy.
14//!
15//! Ring setup failures (including sandbox/seccomp restrictions) are returned;
16//! there is no silent fallback to a different backend.
17#![cfg(target_os = "linux")]
18
19mod engine;
20
21use std::{
22    num::{NonZeroU32, NonZeroUsize},
23    path::Path,
24};
25use vfsi_core::{VfError, VfResult};
26
27#[bitfields::bitfield(u8)]
28struct Flags {
29    cached_reads: bool,
30    syscall_writes: bool,
31    #[bits(6)]
32    _reserved: u8,
33}
34
35/// Bounds both in-flight SQEs and the reusable owned scratch arena. No SQPOLL
36/// thread is started. A large operation is split into bounded contiguous chunks.
37#[derive(Clone, Copy, Debug)]
38pub struct Options {
39    pub(crate) queue_depth: NonZeroU32,
40    pub(crate) max_batch_bytes: NonZeroUsize,
41    pub(crate) flags: Flags,
42}
43
44impl Default for Options {
45    fn default() -> Self {
46        Self {
47            queue_depth: NonZeroU32::new(256).unwrap(),
48            max_batch_bytes: NonZeroUsize::new(2 * 1024 * 1024).unwrap(),
49            flags: Flags::new(),
50        }
51    }
52}
53
54impl Options {
55    /// Execute buffered positional writes with ordered syscalls, avoiding
56    /// worker scheduling and the kernel-facing scratch copy. Disabled by
57    /// default; reads and fsyncs still use the ring unless separately configured.
58    pub fn syscall_writes(mut self, enabled: bool) -> Self {
59        self.flags.set_syscall_writes(enabled);
60        self
61    }
62    /// Require two kernel-page-cache probe hits per cohort of 16 small reads
63    /// (<=64 KiB) before selecting ordinary positional reads. A miss switches
64    /// the rest of the call to bounded ring batching. A mixed/evicted cohort may
65    /// block for at most 14 unprobed reads before checking again. Large reads
66    /// always use the ring; partial or zero probe replies never imply EOF.
67    /// Disabled by default; unsupported filesystems retain ordinary ring reads.
68    pub fn cached_reads(mut self, enabled: bool) -> Self {
69        self.flags.set_cached_reads(enabled);
70        self
71    }
72    /// Maximum submissions in a wave (1..=4096). Oversized values fail at
73    /// connection rather than allocating an arbitrarily large kernel ring.
74    pub fn queue_depth(mut self, depth: NonZeroU32) -> Self {
75        self.queue_depth = depth;
76        self
77    }
78    /// Maximum aggregate transfer bytes per wave, also bounding scratch storage,
79    /// independent of caller-facing
80    /// read allocation limits. Shared read limits still default to 16 MiB.
81    /// Defaults to 2 MiB to limit copy working sets while preserving many-file
82    /// concurrency; tune upward for storage that benefits from larger windows.
83    pub fn max_batch_bytes(mut self, bytes: NonZeroUsize) -> Self {
84        self.max_batch_bytes = bytes;
85        self
86    }
87}
88
89pub use engine::{Stats, Telemetry};
90
91/// Construct a rooted VFSI client. Roots follow the local backend's semantics
92/// (including creating a missing root); configure [`vfsi_core::api::ResourceLimits`]
93/// on the returned client to change application allocation/traversal budgets.
94pub fn connect(
95    root: impl AsRef<Path>,
96    options: Options,
97) -> VfResult<vfsi_sync::FsClient<vfsi_local::LocalBackend>> {
98    connect_with_telemetry(root, options).map(|(fs, _)| fs)
99}
100
101/// Like [`connect`], retaining low-cost counters for actual submissions,
102/// completions, waves, ring entries, arena growths and peak scratch bytes.
103/// Ring counters do not include syscalls. Opt-in cache probes and descriptor
104/// syscall requests have separate counters; namespace/dependency fallbacks
105/// remain excluded.
106pub fn connect_with_telemetry(
107    root: impl AsRef<Path>,
108    options: Options,
109) -> VfResult<(vfsi_sync::FsClient<vfsi_local::LocalBackend>, Telemetry)> {
110    let (engine, telemetry) = engine::Engine::new(options).map_err(|e| {
111        VfError::client(0, e.raw_os_error().unwrap_or(libc::EIO) as u32)
112            .with_context("io_uring_setup", root.as_ref())
113    })?;
114    let backend = vfsi_local::LocalBackend::new(root.as_ref().to_path_buf(), engine)?;
115    Ok((vfsi_sync::FsClient::new(backend), telemetry))
116}