Skip to main content

segment_buffer/
error.rs

1//! Error types for segment-buffer.
2//!
3//! Errors carry the context an operator needs to diagnose a failure at 3am:
4//! the path to the offending segment file, the phase that failed, and the
5//! underlying cause. Use [`Result`](crate::Result) as the alias.
6//!
7//! # Matching on a failure to recover the offending path
8//!
9//! Every non-I/O variant carries the segment file's [`PathBuf`](std::path::PathBuf),
10//! so an operator can match on the variant and act (move the bad file aside,
11//! alert, etc.) without parsing the rendered message:
12//!
13//! ```
14//! use segment_buffer::{SegmentBuffer, SegmentConfig, SegmentError, IoSite};
15//! use tempfile::tempdir;
16//!
17//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
18//! let dir = tempdir()?;
19//! let buf: SegmentBuffer<u64> =
20//!     SegmentBuffer::open(dir.path(), SegmentConfig::default())?;
21//!
22//! // Drop a corrupt segment so the next read surfaces a typed error.
23//! std::fs::write(
24//!     dir.path().join("seg_000000000000_000000000000.zst"),
25//!     b"this is not zstd+CBOR",
26//! )?;
27//!
28//! match buf.read_from(0, 10) {
29//!     Ok(_) => { /* happy path */ }
30//!     Err(SegmentError::Cbor { path, phase, .. }) => {
31//!         eprintln!(
32//!             "CBOR {phase} failed on {}; quarantining",
33//!             path.display()
34//!         );
35//!         let quarantined = format!("{}.quarantined", path.display());
36//!         let _ = std::fs::rename(&path, quarantined);
37//!     }
38//!     Err(SegmentError::Cipher { path, .. }) => {
39//!         eprintln!("cipher failure on {} — likely wrong key", path.display());
40//!     }
41//!     Err(SegmentError::Integrity { path, reason }) => {
42//!         eprintln!("integrity failure on {}: {reason}", path.display());
43//!     }
44//!     Err(SegmentError::Io { site, source }) => {
45//!         match site {
46//!             IoSite::Dir => {
47//!                 eprintln!("directory-level I/O failure: {source}");
48//!             }
49//!             IoSite::Segment(p) => {
50//!                 eprintln!("I/O failure on {}: {source}", p.display());
51//!             }
52//!             IoSite::Unknown => {
53//!                 eprintln!("unspecified I/O failure: {source}");
54//!             }
55//!             // Forward-compat: future sites (e.g. lock file) fall through.
56//!             _ => eprintln!("I/O failure: {source}"),
57//!         }
58//!     }
59//!     // `SegmentError` is `#[non_exhaustive]`, so a catch-all is required
60//!     // for forward compatibility with future variants.
61//!     Err(other) => {
62//!         eprintln!("unhandled segment-buffer error: {other}");
63//!     }
64//! }
65//! # Ok(())
66//! # }
67//! ```
68
69use std::path::PathBuf;
70
71/// Which filesystem site an [`SegmentError::Io`] failure happened on.
72///
73/// Replaces the pre-v0.5.0 `Option<PathBuf>` on the Io variant with an
74/// explicit enumeration: directory operations (create_dir_all, scan,
75/// clean_tmp, dir fsync), segment-file operations (read/write/rename), and
76/// the catch-all for `?`-propagated io::Errors that have not yet been
77/// tagged with context.
78///
79/// `Dir` carries no path: the directory is reachable via
80/// [`crate::SegmentBuffer::path`], so the variant just records the *kind*
81/// of operation that failed. `Segment` carries the offending segment's
82/// path so an operator can quarantine, alert, or move it aside without
83/// re-deriving it. `Unknown` is what `?` produces before any high-value
84/// call site upgrades the error with [`SegmentError::with_path`] or
85/// [`SegmentError::with_dir`].
86#[derive(Debug, Clone, PartialEq, Eq)]
87#[non_exhaustive]
88pub enum IoSite {
89    /// The failure happened on the segment directory itself (create_dir_all,
90    /// scan, clean_tmp, directory fsync). The directory path is reachable
91    /// via [`crate::SegmentBuffer::path`].
92    Dir,
93    /// The failure happened on a specific segment file. Carries the file's
94    /// path so an operator can act on it (move aside, quarantine, alert)
95    /// without re-deriving it.
96    Segment(PathBuf),
97    /// The failure has no specific site attached — typically an io::Error
98    /// propagated via `?` before a high-value call site has upgraded it
99    /// via [`SegmentError::with_path`] or [`SegmentError::with_dir`].
100    Unknown,
101}
102
103/// Errors produced by segment-buffer operations.
104///
105/// Every variant carries the [`site`](Self::Io) or
106/// [`path`](Self::Cbor) of the segment file involved (when one is in
107/// scope), so an operator can act on the failure without spelunking
108/// through logs.
109#[derive(Debug, thiserror::Error)]
110#[non_exhaustive]
111pub enum SegmentError {
112    /// Filesystem I/O failure (directory creation, segment read/write, rename, etc.).
113    ///
114    /// Carries the [`IoSite`] the failure happened on plus the underlying
115    /// [`std::io::Error`] as `source`. When `?` propagates an `io::Error`
116    /// without context, the site is [`IoSite::Unknown`]; use
117    /// [`with_path`](Self::with_path) (or
118    /// [`with_dir`](Self::with_dir)) to attach the site at high-value call
119    /// sites.
120    #[error("I/O error{site_clause}: {source}", site_clause = format_site_clause(site))]
121    Io {
122        /// Which site the I/O failed on. See [`IoSite`] for the variants.
123        site: IoSite,
124        /// The underlying io::Error, reachable via [`std::error::Error::source`].
125        #[source]
126        source: std::io::Error,
127    },
128
129    /// CBOR serialization or deserialization of a segment file failed.
130    #[error("CBOR {phase} failed for {path}: {message}")]
131    Cbor {
132        /// Which direction failed: `"serialize"` (writing) or `"deserialize"` (reading).
133        phase: &'static str,
134        /// Path to the offending segment file.
135        path: PathBuf,
136        /// Underlying CBOR error message.
137        message: String,
138    },
139
140    /// Cipher encrypt or decrypt of a segment file failed (key mismatch, AEAD
141    /// tag invalid, cipher misconfiguration).
142    #[error("cipher error for {path}: {message}")]
143    Cipher {
144        /// Path to the offending segment file.
145        path: PathBuf,
146        /// Underlying cipher error message.
147        message: String,
148    },
149
150    /// Segment file failed an integrity check: truncated, too small for the
151    /// AEAD nonce, or unrecognized envelope.
152    #[error("integrity failure for {path}: {reason}")]
153    Integrity {
154        /// Path to the offending segment file.
155        path: PathBuf,
156        /// What failed, in one short phrase.
157        reason: &'static str,
158    },
159
160    /// Another process holds the exclusive single-process lock on the buffer
161    /// directory. Returned by [`crate::SegmentBuffer::open`] when the
162    /// `flock` on `.segment-buffer.lock` cannot be acquired. See
163    /// `AGENTS.md` § "Single-process invariant".
164    #[error(
165        "buffer directory {path} is locked by another process \
166             (only one SegmentBuffer may open a directory at a time)"
167    )]
168    Locked {
169        /// Path of the lock file that was contended (typically
170        /// `<dir>/.segment-buffer.lock`).
171        path: PathBuf,
172    },
173}
174
175impl SegmentError {
176    /// Attach a segment-file path to an existing [`SegmentError::Io`] variant
177    /// whose site is [`IoSite::Unknown`]. Returns the error unchanged for
178    /// other variants or for Io errors already tagged (Dir or Segment) —
179    /// the first call site to attach context wins.
180    ///
181    /// Use [`SegmentError::with_dir`] for operations on the directory itself
182    /// (create_dir_all, scan, clean_tmp, dir fsync).
183    #[must_use = "the upgraded error is meaningless if discarded"]
184    pub fn with_path(self, path: impl Into<PathBuf>) -> Self {
185        match self {
186            SegmentError::Io {
187                site: IoSite::Unknown,
188                source,
189            } => SegmentError::Io {
190                site: IoSite::Segment(path.into()),
191                source,
192            },
193            other => other,
194        }
195    }
196
197    /// Tag an [`IoSite::Unknown`] Io error as a directory operation. Returns
198    /// the error unchanged for other variants or for Io errors already
199    /// tagged (Dir or Segment). Use this at directory-operation call sites
200    /// (create_dir_all, scan, clean_tmp, dir fsync) so operators can
201    /// distinguish "the directory itself failed" from "a specific segment
202    /// file failed."
203    #[must_use = "the upgraded error is meaningless if discarded"]
204    pub fn with_dir(self) -> Self {
205        match self {
206            SegmentError::Io {
207                site: IoSite::Unknown,
208                source,
209            } => SegmentError::Io {
210                site: IoSite::Dir,
211                source,
212            },
213            other => other,
214        }
215    }
216}
217
218impl From<std::io::Error> for SegmentError {
219    fn from(source: std::io::Error) -> Self {
220        SegmentError::Io {
221            site: IoSite::Unknown,
222            source,
223        }
224    }
225}
226
227/// Helper used by the `#[error]` attribute on [`SegmentError::Io`]. Produces
228/// ` for the segment directory` for [`IoSite::Dir`], ` for {path.display()}`
229/// for [`IoSite::Segment`], or the empty string for [`IoSite::Unknown`] — so
230/// the rendered message has no spurious clause when no site is attached.
231fn format_site_clause(site: &IoSite) -> String {
232    match site {
233        IoSite::Dir => " for the segment directory".to_string(),
234        IoSite::Segment(p) => format!(" for {}", p.display()),
235        IoSite::Unknown => String::new(),
236    }
237}
238
239/// Result alias used throughout the crate.
240pub type Result<T> = std::result::Result<T, SegmentError>;