Skip to main content

dialcache/
error.rs

1//! Public error types.
2
3use std::fmt;
4use std::sync::Arc;
5
6/// Boxed error returned by sources, codecs, providers and adapters.
7pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
8/// Shared error handed to every caller that joined one execution.
9pub type SharedError = Arc<dyn std::error::Error + Send + Sync + 'static>;
10
11/// Every failure a cached call or maintenance operation can report.
12///
13/// Cache plumbing fails open, so a call fails only when its source fails,
14/// its source deadline elapses, a callback panics, or static configuration
15/// is rejected before execution. Maintenance operations surface their own
16/// remote failures.
17#[derive(Debug, Clone)]
18pub enum Error {
19    /// The source returned an error. Every coalesced caller receives the same
20    /// shared error, so identity comparisons through [`Arc::ptr_eq`] work.
21    Source(SharedError),
22    /// The source did not settle before its deadline. Followers of one
23    /// execution share the leader's error instance.
24    FallbackTimeout(Arc<FallbackTimeout>),
25    /// A source, codec, provider or comparator panicked.
26    Panic(Arc<str>),
27    /// Static operation or instance configuration was rejected before execution.
28    Config(ConfigError),
29    /// Invalidation was requested without a configured remote adapter.
30    MissingRemote,
31    /// An explicit maintenance mutation failed at the remote adapter.
32    Remote(SharedError),
33}
34
35impl Error {
36    /// The source error, when the source itself failed.
37    pub fn source_error(&self) -> Option<&(dyn std::error::Error + Send + Sync + 'static)> {
38        match self {
39            Error::Source(error) => Some(error.as_ref()),
40            _ => None,
41        }
42    }
43
44    /// Whether this is a source deadline error: the library's own, or one a
45    /// source propagated from a nested cached call, either as the bare
46    /// [`FallbackTimeout`] or boxed inside a nested [`Error`].
47    pub fn is_fallback_timeout(&self) -> bool {
48        match self {
49            Error::FallbackTimeout(_) => true,
50            Error::Source(error) => {
51                error.downcast_ref::<FallbackTimeout>().is_some()
52                    || error
53                        .downcast_ref::<Error>()
54                        .is_some_and(Error::is_fallback_timeout)
55            }
56            _ => false,
57        }
58    }
59}
60
61impl fmt::Display for Error {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        match self {
64            Error::Source(error) => write!(f, "DialCache source failed: {error}"),
65            Error::FallbackTimeout(timeout) => timeout.fmt(f),
66            Error::Panic(message) => write!(f, "DialCache callback panicked: {message}"),
67            Error::Config(error) => error.fmt(f),
68            Error::MissingRemote => {
69                f.write_str("DialCache invalidation requires a configured remote adapter")
70            }
71            Error::Remote(error) => write!(f, "DialCache remote maintenance failed: {error}"),
72        }
73    }
74}
75
76impl std::error::Error for Error {
77    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
78        match self {
79            Error::Source(error) | Error::Remote(error) => Some(error.as_ref()),
80            Error::FallbackTimeout(timeout) => Some(timeout.as_ref()),
81            Error::Config(error) => Some(error),
82            _ => None,
83        }
84    }
85}
86
87impl From<ConfigError> for Error {
88    fn from(error: ConfigError) -> Self {
89        Error::Config(error)
90    }
91}
92
93/// The source deadline elapsed before the source settled.
94#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
95#[error("DialCache fallback for use case {use_case:?} timed out after {timeout_ms} ms")]
96pub struct FallbackTimeout {
97    /// The use case whose source timed out.
98    pub use_case: String,
99    /// The source budget that elapsed, in milliseconds.
100    pub timeout_ms: u64,
101}
102
103/// The remote read deadline elapsed before the adapter answered.
104#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
105#[error("DialCache remote read for use case {use_case:?} timed out after {timeout_ms} ms")]
106pub struct RemoteReadTimeout {
107    /// The use case whose remote read timed out.
108    pub use_case: String,
109    /// The read budget that elapsed, in milliseconds.
110    pub timeout_ms: u64,
111}
112
113/// Static configuration rejected at construction or registration.
114#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
115pub enum ConfigError {
116    /// A value is outside its domain; the message names it.
117    #[error("{0}")]
118    Invalid(String),
119    /// The use case name `watermark` is owned by invalidation.
120    #[error("DialCache use case name is reserved: {0}")]
121    ReservedUseCase(String),
122    /// A use case with this name is already registered on the instance.
123    #[error("DialCache use case already registered: {0}")]
124    UseCaseAlreadyRegistered(String),
125}
126
127impl ConfigError {
128    pub(crate) fn invalid(message: impl Into<String>) -> Self {
129        ConfigError::Invalid(message.into())
130    }
131}