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>;