Skip to main content

amalgam/
error.rs

1//! Error types.
2//!
3//! Following the project guideline that libraries expose typed errors via
4//! `thiserror`, every fallible boundary returns [`Error`]. Business outcomes
5//! that are *not* failures (a cache miss, a factory choosing to reuse a stale
6//! value) are modelled in the return *type*, never as errors.
7
8use std::time::Duration;
9
10/// The crate-wide result alias.
11pub type Result<T> = std::result::Result<T, Error>;
12
13/// An error surfaced from a cache operation.
14///
15/// Note what is deliberately *absent*: a "cache miss" is not an error (it is a
16/// `None`/`MaybeValue::none`), and a factory that fails while fail-safe rescues
17/// a stale value never produces an `Error` at all.
18#[derive(Debug, thiserror::Error)]
19#[non_exhaustive]
20pub enum Error {
21    /// The factory failed and no stale value or fail-safe default was available
22    /// to fall back to. Carries the message reported by the factory.
23    #[error("factory failed: {message}")]
24    Factory {
25        /// The failure message reported by the factory.
26        message: String,
27    },
28
29    /// The factory exceeded its hard timeout and no fallback value existed.
30    #[error("factory timed out after {elapsed:?}")]
31    FactoryTimeout {
32        /// How long the factory was allowed to run before timing out.
33        elapsed: Duration,
34    },
35
36    /// Acquiring the per-key single-flight lock exceeded its timeout and no
37    /// fallback value existed.
38    #[error("lock acquisition timed out after {elapsed:?}")]
39    LockTimeout {
40        /// How long the caller waited for the lock.
41        elapsed: Duration,
42    },
43
44    /// A value could not be serialized for the distributed (L2) cache.
45    #[error("serialization failed: {0}")]
46    Serialization(String),
47
48    /// A value could not be deserialized from the distributed (L2) cache.
49    #[error("deserialization failed: {0}")]
50    Deserialization(String),
51
52    /// The distributed (L2) cache backend returned an error.
53    #[error("distributed cache error: {0}")]
54    Distributed(String),
55
56    /// The backplane backend returned an error.
57    #[error("backplane error: {0}")]
58    Backplane(String),
59}
60
61/// The error a user-supplied factory returns to signal failure.
62///
63/// Returning this (or calling [`FactoryContext::fail`](crate::FactoryContext::fail))
64/// triggers the fail-safe path: a stale value or the `fail_safe_default` is
65/// served if available, otherwise the failure is surfaced as [`Error::Factory`].
66///
67/// It can wrap an arbitrary source error so the original cause is preserved in
68/// the error chain.
69#[derive(Debug, thiserror::Error)]
70#[error("{message}")]
71pub struct FactoryError {
72    message: String,
73    #[source]
74    source: Option<Box<dyn std::error::Error + Send + Sync>>,
75}
76
77impl FactoryError {
78    /// Creates a factory error with a human-readable message.
79    #[must_use]
80    pub fn new(message: impl Into<String>) -> Self {
81        Self {
82            message: into_nonblank(message.into()),
83            source: None,
84        }
85    }
86
87    /// Creates a factory error from an arbitrary source error, preserving it in
88    /// the error chain.
89    #[must_use]
90    pub fn from_source<E>(source: E) -> Self
91    where
92        E: std::error::Error + Send + Sync + 'static,
93    {
94        Self {
95            message: source.to_string(),
96            source: Some(Box::new(source)),
97        }
98    }
99
100    /// The failure message.
101    #[must_use]
102    pub fn message(&self) -> &str {
103        &self.message
104    }
105}
106
107fn into_nonblank(message: String) -> String {
108    if message.trim().is_empty() {
109        // Mirrors FusionCache's default factory-failure message.
110        "an error occurred while running the factory".to_owned()
111    } else {
112        message
113    }
114}
115
116impl From<FactoryError> for Error {
117    fn from(err: FactoryError) -> Self {
118        Error::Factory {
119            message: err.message,
120        }
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn blank_factory_message_is_defaulted() {
130        assert_eq!(
131            FactoryError::new("   ").message(),
132            "an error occurred while running the factory"
133        );
134    }
135
136    #[test]
137    fn factory_error_preserves_source() {
138        let io = std::io::Error::other("boom");
139        let err = FactoryError::from_source(io);
140        assert_eq!(err.message(), "boom");
141        assert!(std::error::Error::source(&err).is_some());
142    }
143}