fsys/lib.rs
1//! # fsys
2//!
3//! Foundation-tier filesystem IO for Rust storage engines: journal
4//! substrate, io_uring, NVMe passthrough, atomic writes, cross-platform
5//! durability.
6//!
7//! `fsys` sits one layer below your data structures and one layer above
8//! [`std::fs`]. It pairs an explicit durability model (you choose a
9//! [`Method`], you get the platform's best matching primitive, you
10//! observe any fallback via [`Handle::active_durability_primitive`])
11//! with a journal substrate built for write-ahead-log workloads.
12//!
13//! ## Quickstart
14//!
15//! Append-only journal — the canonical WAL pattern:
16//!
17//! ```no_run
18//! # fn example() -> fsys::Result<()> {
19//! use std::sync::Arc;
20//!
21//! let fs = Arc::new(fsys::builder().build()?);
22//! let log = fs.journal("/var/lib/myapp/log.wal")?;
23//!
24//! let _ = log.append(b"txn 1: insert")?;
25//! let _ = log.append(b"txn 2: update")?;
26//! let lsn = log.append(b"txn 3: commit")?;
27//!
28//! // One fsync covers every prior append — group-commit.
29//! log.sync_through(lsn)?;
30//! # Ok(())
31//! # }
32//! ```
33//!
34//! One-shot durable file write, no handle required:
35//!
36//! ```no_run
37//! # fn example() -> fsys::Result<()> {
38//! fsys::quick::write("/etc/myapp/config.toml", b"value = 42")?;
39//! let data = fsys::quick::read("/etc/myapp/config.toml")?;
40//! # Ok(())
41//! # }
42//! ```
43//!
44//! ## Three tiers of API
45//!
46//! ### Tier 1 — one-shot helpers
47//!
48//! For programs that issue one IO op and don't need a long-lived handle.
49//! Backed by a lazily-initialised default [`Handle`] with
50//! [`Method::Auto`].
51//!
52//! ```no_run
53//! # fn example() -> fsys::Result<()> {
54//! fsys::quick::write("/tmp/greeting.txt", b"hello")?;
55//! let data = fsys::quick::read("/tmp/greeting.txt")?;
56//! assert_eq!(data, b"hello");
57//! # Ok(())
58//! # }
59//! ```
60//!
61//! ### Tier 2 — handle-based
62//!
63//! The primary API for everything beyond one-shot use. Build a [`Handle`]
64//! with [`new()`] (default [`Method::Auto`]) or [`with(method)`](with);
65//! share across threads via [`Arc`](std::sync::Arc).
66//!
67//! ```no_run
68//! # fn example() -> fsys::Result<()> {
69//! let fs = fsys::new()?; // Method::Auto
70//! let fs = fsys::with(fsys::Method::Data)?; // explicit method
71//! fs.write("/tmp/world.txt", b"world")?;
72//! let read = fs.read("/tmp/world.txt")?;
73//! # Ok(())
74//! # }
75//! ```
76//!
77//! ### Tier 3 — full builder
78//!
79//! For advanced configuration: custom root, dev/prod mode, per-handle
80//! batch knobs, io_uring queue depth, buffer pool size, observer hook,
81//! workload presets.
82//!
83//! ```no_run
84//! # fn example() -> fsys::Result<()> {
85//! let fs = fsys::builder()
86//! .method(fsys::Method::Direct)
87//! .root("/var/lib/myapp")
88//! .mode(fsys::Mode::Prod)
89//! .tune_for(fsys::Workload::Database)
90//! .build()?;
91//! # Ok(())
92//! # }
93//! ```
94//!
95//! ## Choosing a method
96//!
97//! | If you... | Pick |
98//! |---|---|
99//! | Don't know what you need | [`Method::Auto`] |
100//! | Need universal correctness floor | [`Method::Sync`] |
101//! | Want Linux's `fdatasync` speedup | [`Method::Data`] |
102//! | Have read-heavy random-access workloads | [`Method::Mmap`] |
103//! | Need < 100 µs single-write latency on NVMe | [`Method::Direct`] |
104//!
105//! See [`docs/METHODS.md`](https://github.com/jamesgober/fsys-rs/blob/main/docs/METHODS.md)
106//! for the full per-platform matrix and the [`Method::Auto`] decision ladder.
107//!
108//! ## Crash safety
109//!
110//! Every write API (`write`, `write_copy`, `write_batch`,
111//! `Batch::commit`) uses an atomic temp-file + rename pattern. The target
112//! file is either entirely the old payload (kill before rename) or
113//! entirely the new payload (kill after rename) — never torn.
114//! The [`journal`] substrate adds explicit durability via
115//! [`JournalHandle::sync_through`]; group-commit amortises the fsync
116//! cost across many appends. See
117//! [`docs/CRASH-SAFETY.md`](https://github.com/jamesgober/fsys-rs/blob/main/docs/CRASH-SAFETY.md)
118//! for the full per-method contract.
119//!
120//! ## Async (feature `async`)
121//!
122//! ```no_run
123//! # async fn example() -> fsys::Result<()> {
124//! # #[cfg(feature = "async")] {
125//! let fs = std::sync::Arc::new(fsys::builder().build()?);
126//! fs.clone().write_async("/tmp/async.dat", b"payload".to_vec()).await?;
127//! let data = fs.clone().read_async("/tmp/async.dat").await?;
128//! # }
129//! # Ok(())
130//! # }
131//! ```
132//!
133//! On Linux + [`Method::Direct`], async ops submit directly to the
134//! per-handle io_uring ring — the *native substrate*, observable via
135//! [`Handle::async_substrate`] returning [`AsyncSubstrate::NativeIoUring`].
136//! Everywhere else, async ops route through `tokio::task::spawn_blocking`
137//! ([`AsyncSubstrate::SpawnBlocking`]).
138//!
139//! Calling sync `fs.write()` from inside a tokio runtime is supported
140//! (it just blocks the calling thread). Calling async `fs.write_async()`
141//! outside a tokio runtime returns [`Error::AsyncRuntimeRequired`]
142//! rather than panicking.
143//!
144//! ## Cargo features
145//!
146//! | Feature | Default | Purpose |
147//! |---|---|---|
148//! | `async` | off | `_async` siblings for every sync method; pulls in `tokio` |
149//! | `tracing` | off | Structured spans + events on the write / read / journal hot paths |
150//! | `stress` | off | Run `tests/stress.rs` for the full 1-hour soak (CI-nightly only) |
151//! | `fuzz` | off | Compile-only flag for fuzz-target wiring; targets live in `fuzz/` |
152//!
153//! ## Concept reference
154//!
155//! | Concept | Type / module | Reach for it when... |
156//! |---|---|---|
157//! | Handle to filesystem | [`Handle`] | Any non-one-shot IO. The primary type. |
158//! | Configure handle | [`Builder`] | Custom root, method, tuning, observer. |
159//! | Durability strategy | [`Method`] | Five variants: `Sync`, `Data`, `Mmap`, `Direct`, `Auto`. |
160//! | Append-only WAL | [`JournalHandle`] | High-throughput durable writes (WAL / ledger / queue). |
161//! | Multi-op transaction | [`Batch`] | Group N writes / deletes / copies under one durability barrier. |
162//! | One-shot helpers | [`mod@quick`] | Single-call file IO without holding a handle. |
163//! | Async layer | `fsys::async_io` (feature `async`) | Tokio integration; `_async` siblings for every sync method. |
164//! | Telemetry hook | [`observer::FsysObserver`] | Per-op events (append / sync / write / read). |
165//! | Hardware introspection | [`mod@hardware`] | Probe PLP status, atomic-write unit, sector size. |
166//! | Errors | [`Error`] / [`Result`] | 21 variants with stable `FS-XXXXX` codes. |
167//!
168//! ## Version history
169//!
170//! Per-version deltas live in
171//! [`CHANGELOG.md`](https://github.com/jamesgober/fsys-rs/blob/main/CHANGELOG.md).
172//! `1.0.0` is the first stable release; the `1.x` line is API-stable
173//! and on-disk-format-stable per the contract in
174//! [`docs/STABILITY-1.0.md`](https://github.com/jamesgober/fsys-rs/blob/main/docs/STABILITY-1.0.md).
175
176// 0.9.6 audit H-12 — `html_root_url` was previously pinned to a
177// specific version string that drifted every release. docs.rs
178// handles per-version routing automatically; pinning it here
179// just creates a fix-at-version-bump chore that we'd inevitably
180// forget. Removed entirely. If a future release needs to
181// override docs.rs's default routing (rare), the attribute can
182// be re-added at that point with a clear reason.
183#![deny(missing_docs)]
184#![deny(unsafe_op_in_unsafe_fn)]
185#![deny(unused_must_use)]
186#![deny(unused_results)]
187#![deny(clippy::unwrap_used)]
188#![deny(clippy::expect_used)]
189#![deny(clippy::todo)]
190#![deny(clippy::unimplemented)]
191#![deny(clippy::print_stdout)]
192#![deny(clippy::print_stderr)]
193#![deny(clippy::dbg_macro)]
194#![deny(clippy::unreachable)]
195#![deny(clippy::undocumented_unsafe_blocks)]
196#![deny(clippy::missing_safety_doc)]
197#![cfg_attr(test, allow(clippy::expect_used, clippy::unwrap_used))]
198#![warn(rust_2018_idioms)]
199#![warn(clippy::all)]
200
201pub mod advice;
202pub mod batch;
203pub(crate) mod buffer;
204pub mod builder;
205pub mod capability;
206pub mod crud;
207pub mod error;
208pub mod handle;
209pub mod hardware;
210pub mod journal;
211pub mod meta;
212pub mod method;
213pub mod observer;
214pub mod os;
215pub mod path;
216pub(crate) mod pipeline;
217pub(crate) mod platform;
218pub mod primitive;
219pub mod quick;
220pub mod substrate;
221
222#[cfg(feature = "async")]
223pub mod async_io;
224
225/// 0.9.7 H-7 — internal OOM-injection allocator hooks.
226///
227/// **NEVER enable the `oom_inject` feature in production.** Every
228/// allocation pays a thread-local lookup + comparison. The
229/// feature exists solely to make `tests/oom_injection.rs`
230/// compileable; the integration test is the regression guard for
231/// `AlignedBuf::new` / `read_all_direct` / other fallible-alloc
232/// paths.
233///
234/// When `oom_inject` is enabled, the global allocator is replaced
235/// with `OomInjectingAllocator` from `test_support`. The
236/// `OOM_THRESHOLD` thread-local controls injection: allocations
237/// of `>= threshold` bytes return `null` (OOM). Tests use the
238/// `OomThreshold` RAII guard to set + restore the threshold
239/// safely across scope exit (including panics).
240#[cfg(feature = "oom_inject")]
241#[doc(hidden)]
242pub mod test_support;
243
244#[cfg(feature = "oom_inject")]
245#[global_allocator]
246static FSYS_OOM_INJECTING_ALLOCATOR: test_support::OomInjectingAllocator =
247 test_support::OomInjectingAllocator;
248
249/// Internal fuzz-test surface. Wraps `pub(crate)` helpers under
250/// `cfg(feature = "fuzz")` so the cargo-fuzz workspace can reach
251/// them without making them part of the public 1.0 API surface.
252/// Subject to change without semver guarantees; do not use from
253/// non-fuzz code.
254#[cfg(feature = "fuzz")]
255#[doc(hidden)]
256pub mod __fuzz {
257 use crate::journal::format as fmt;
258
259 /// Public mirror of `crate::journal::format::FrameDecode` for
260 /// fuzz harness consumption.
261 #[derive(Debug)]
262 pub enum FrameDecode {
263 Ok {
264 consumed: usize,
265 payload_start: usize,
266 payload_end: usize,
267 },
268 Truncated,
269 BadMagic,
270 LengthOverflow,
271 ChecksumMismatch,
272 }
273
274 impl From<fmt::FrameDecode> for FrameDecode {
275 fn from(d: fmt::FrameDecode) -> Self {
276 match d {
277 fmt::FrameDecode::Ok {
278 consumed,
279 payload_start,
280 payload_end,
281 } => FrameDecode::Ok {
282 consumed,
283 payload_start,
284 payload_end,
285 },
286 fmt::FrameDecode::Truncated => FrameDecode::Truncated,
287 fmt::FrameDecode::BadMagic => FrameDecode::BadMagic,
288 fmt::FrameDecode::LengthOverflow => FrameDecode::LengthOverflow,
289 fmt::FrameDecode::ChecksumMismatch => FrameDecode::ChecksumMismatch,
290 }
291 }
292 }
293
294 /// Encode a payload as a journal frame; returns the encoded
295 /// `Vec<u8>`.
296 pub fn encode_frame_owned(payload: &[u8]) -> crate::Result<Vec<u8>> {
297 fmt::encode_frame_owned(payload)
298 }
299
300 /// Decode a journal frame from `bytes`.
301 pub fn decode_frame(bytes: &[u8]) -> FrameDecode {
302 fmt::decode_frame(bytes).into()
303 }
304
305 /// Compute the CRC-32C checksum of `bytes`.
306 pub fn crc32c(bytes: &[u8]) -> u32 {
307 fmt::crc32c(bytes)
308 }
309}
310
311pub use crate::advice::Advice;
312pub use crate::batch::Batch;
313pub use crate::builder::{Builder, SpdkConfig, Workload};
314pub use crate::capability::{
315 Capabilities, HardwareSummary, IoUringFeature, PciAddress, SpdkEligibility, SpdkSkipReason,
316};
317pub use crate::error::{BatchError, Error, Result};
318pub use crate::handle::Handle;
319pub use crate::journal::{
320 JournalBackend, JournalBackendHealth, JournalBackendInfo, JournalBackendKind, JournalHandle,
321 JournalOptions, JournalReader, JournalRecord, JournalTailState, Lsn, SyncMode,
322 WriteLifetimeHint,
323};
324pub use crate::meta::{DirEntry, FileMeta, Permissions};
325pub use crate::method::Method;
326pub use crate::path::Mode;
327pub use crate::substrate::AsyncSubstrate;
328
329/// Creates a default [`Handle`] using [`Method::Auto`] and no root scope.
330///
331/// Equivalent to `Builder::new().build()`.
332///
333/// # Errors
334///
335/// Returns an error if method validation fails (this will not happen for
336/// the default [`Method::Auto`] setting).
337pub fn new() -> Result<Handle> {
338 Builder::new().build()
339}
340
341/// Creates a [`Handle`] using the specified [`Method`].
342///
343/// # Errors
344///
345/// Returns [`Error::UnsupportedMethod`] if a reserved method variant is
346/// supplied.
347pub fn with(method: Method) -> Result<Handle> {
348 Builder::new().method(method).build()
349}
350
351/// Returns a new [`Builder`] with default settings.
352///
353/// # Example
354///
355/// ```
356/// # fn example() -> fsys::Result<()> {
357/// let handle = fsys::builder().method(fsys::Method::Sync).build()?;
358/// # Ok(())
359/// # }
360/// ```
361#[must_use]
362pub fn builder() -> Builder {
363 Builder::new()
364}
365
366/// Library version, matching the crate version declared in `Cargo.toml`.
367pub const VERSION: &str = env!("CARGO_PKG_VERSION");
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
374 fn version_is_set() {
375 assert!(!VERSION.is_empty());
376 }
377
378 #[test]
379 fn version_matches_cargo() {
380 assert_eq!(VERSION, env!("CARGO_PKG_VERSION"));
381 }
382}