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}