1use std::any::Any;
8use std::borrow::Cow;
9use std::error::Error as StdError;
10use std::fmt;
11
12use objectstore_log::Level;
13
14use crate::change_stream::ChangeStreamError;
15
16#[derive(Debug)]
18pub struct Panic {
19 message: Cow<'static, str>,
20}
21
22impl Panic {
23 pub fn new(payload: Box<dyn Any + Send>) -> Self {
25 let message = if let Some(s) = payload.downcast_ref::<&str>() {
26 Cow::Borrowed(*s)
27 } else if let Some(s) = payload.downcast_ref::<String>() {
28 Cow::Owned(s.clone())
29 } else {
30 Cow::Borrowed("unknown panic")
31 };
32 Self { message }
33 }
34}
35
36impl fmt::Display for Panic {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 f.write_str(&self.message)
39 }
40}
41
42impl StdError for Panic {}
43
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
46pub enum ErrorKind {
47 InvalidMetadata,
49 ClientStream,
51 RangeNotSatisfiable {
53 total: u64,
55 },
56 UploadOffsetMismatch {
58 offset: u64,
60 },
61 UploadSessionGone,
63 InvalidUploadSessionToken,
65 UnknownUploadSession,
67 ChunkExceedsUploadLength {
69 offset: u64,
71 content_length: u64,
73 upload_length: u64,
75 },
76 ChunkTooSmall {
78 chunk_length: u64,
80 upload_granularity: u64,
82 },
83 AtCapacity,
85 Unsupported,
87 BackendFailure,
89 BackendRateLimited,
91 BackendTimeout,
93 BackendUnavailable,
95 Panic,
97 UnexpectedTombstone,
99 CorruptData,
101 Internal,
103}
104
105impl fmt::Display for ErrorKind {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 match self {
108 Self::InvalidMetadata => f.write_str("invalid object metadata"),
109 Self::ClientStream => f.write_str("invalid client stream"),
110 Self::RangeNotSatisfiable { total } => {
111 write!(f, "range not satisfiable (object size: {total} bytes)")
112 }
113 Self::UploadOffsetMismatch { offset } => {
114 write!(f, "upload offset mismatch (server holds {offset} bytes)")
115 }
116 Self::UploadSessionGone => f.write_str("upload session gone"),
117 Self::InvalidUploadSessionToken => {
118 f.write_str("invalid upload session token: could not decrypt or authenticate it")
119 }
120 Self::UnknownUploadSession => f.write_str("unknown upload session"),
121 Self::ChunkExceedsUploadLength {
122 offset,
123 content_length,
124 upload_length,
125 } => write!(
126 f,
127 "chunk at offset {offset} with length {content_length} exceeds upload length {upload_length}"
128 ),
129 Self::ChunkTooSmall {
130 chunk_length,
131 upload_granularity,
132 } => write!(
133 f,
134 "non-final chunk length {chunk_length} is smaller than upload granularity {upload_granularity}"
135 ),
136 Self::AtCapacity => f.write_str("service at capacity"),
137 Self::Unsupported => f.write_str("unsupported operation"),
138 Self::BackendFailure => f.write_str("backend operation failed"),
139 Self::BackendRateLimited => f.write_str("backend rate limited"),
140 Self::BackendTimeout => f.write_str("backend timed out"),
141 Self::BackendUnavailable => f.write_str("backend unavailable"),
142 Self::CorruptData => f.write_str("corrupt stored data"),
143 Self::Panic => f.write_str("service task panicked"),
144 Self::UnexpectedTombstone => f.write_str("unexpected tombstone"),
145 Self::Internal => f.write_str("internal service error"),
146 }
147 }
148}
149
150pub struct Error {
155 kind: ErrorKind,
156 context: Option<Cow<'static, str>>,
157 source: Option<Box<dyn StdError + Send + Sync>>,
158}
159
160impl Error {
161 pub fn kind(&self) -> ErrorKind {
163 self.kind
164 }
165
166 pub fn new(kind: ErrorKind, context: impl Into<Cow<'static, str>>) -> Self {
168 Self::build(kind, Some(context.into()), None)
169 }
170
171 pub fn with_source<E>(kind: ErrorKind, source: E) -> Self
173 where
174 E: StdError + Send + Sync + 'static,
175 {
176 Self::build(kind, None, Some(Box::new(source)))
177 }
178
179 pub(crate) fn with_context<E>(
180 kind: ErrorKind,
181 context: impl Into<Cow<'static, str>>,
182 source: E,
183 ) -> Self
184 where
185 E: StdError + Send + Sync + 'static,
186 {
187 Self::build(kind, Some(context.into()), Some(Box::new(source)))
188 }
189
190 fn build(
191 kind: ErrorKind,
192 context: Option<Cow<'static, str>>,
193 source: Option<Box<dyn StdError + Send + Sync>>,
194 ) -> Self {
195 Self {
196 kind,
197 context,
198 source,
199 }
200 }
201
202 pub fn level(&self) -> Level {
204 match self.kind {
205 ErrorKind::InvalidMetadata => Level::DEBUG,
207 ErrorKind::ClientStream => Level::DEBUG,
208 ErrorKind::RangeNotSatisfiable { .. } => Level::DEBUG,
209 ErrorKind::UploadOffsetMismatch { .. } => Level::DEBUG,
210 ErrorKind::UploadSessionGone => Level::DEBUG,
211 ErrorKind::InvalidUploadSessionToken => Level::DEBUG,
212 ErrorKind::UnknownUploadSession => Level::DEBUG,
213 ErrorKind::ChunkExceedsUploadLength { .. } => Level::DEBUG,
214 ErrorKind::ChunkTooSmall { .. } => Level::DEBUG,
215 ErrorKind::Unsupported => Level::INFO,
219 ErrorKind::AtCapacity => Level::WARN,
221 ErrorKind::BackendRateLimited => Level::WARN,
222 ErrorKind::BackendTimeout => Level::WARN,
223 ErrorKind::BackendUnavailable => Level::WARN,
224 ErrorKind::BackendFailure => Level::ERROR,
226 ErrorKind::Panic => Level::ERROR,
227 ErrorKind::UnexpectedTombstone => Level::ERROR,
228 ErrorKind::CorruptData => Level::ERROR,
229 ErrorKind::Internal => Level::ERROR,
230 }
231 }
232}
233
234impl fmt::Display for Error {
235 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236 self.kind.fmt(f)?;
237 if let Some(context) = &self.context {
238 write!(f, ": {context}")?;
239 }
240 Ok(())
241 }
242}
243
244impl fmt::Debug for Error {
245 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246 f.debug_struct("Error")
247 .field("kind", &self.kind)
248 .field("context", &self.context)
249 .field("source", &self.source)
250 .finish()
251 }
252}
253
254impl StdError for Error {
255 fn source(&self) -> Option<&(dyn StdError + 'static)> {
256 self.source.as_deref().map(|source| source as _)
257 }
258}
259
260impl From<ErrorKind> for Error {
261 fn from(kind: ErrorKind) -> Self {
262 Self::build(kind, None, None)
263 }
264}
265
266impl From<Panic> for Error {
267 fn from(source: Panic) -> Self {
268 Self::with_source(ErrorKind::Panic, source)
269 }
270}
271
272pub trait ResultExt<T> {
274 fn context(self, kind: ErrorKind, context: impl Into<Cow<'static, str>>) -> Result<T>;
291
292 fn kind(self, kind: ErrorKind) -> Result<T>;
304}
305
306impl<T, E> ResultExt<T> for std::result::Result<T, E>
307where
308 E: StdError + Send + Sync + 'static,
309{
310 fn context(self, kind: ErrorKind, context: impl Into<Cow<'static, str>>) -> Result<T> {
311 self.map_err(|source| Error::with_context(kind, context, source))
312 }
313
314 fn kind(self, kind: ErrorKind) -> Result<T> {
315 self.map_err(|source| Error::with_source(kind, source))
316 }
317}
318
319impl From<ChangeStreamError> for Error {
320 fn from(source: ChangeStreamError) -> Self {
321 let kind = match source {
322 ChangeStreamError::Unavailable(_) => ErrorKind::BackendUnavailable,
323 ChangeStreamError::Timeout(_) => ErrorKind::BackendTimeout,
324 ChangeStreamError::Failure(_) => ErrorKind::BackendFailure,
325 };
326 Self::with_context(kind, "writing to the change stream", source)
327 }
328}
329
330impl From<std::io::Error> for Error {
331 fn from(source: std::io::Error) -> Self {
332 Self::with_source(ErrorKind::BackendFailure, source)
333 }
334}
335
336impl From<crate::stream::ClientError> for Error {
337 fn from(source: crate::stream::ClientError) -> Self {
338 Self::with_source(ErrorKind::ClientStream, source)
339 }
340}
341
342pub type Result<T, E = Error> = std::result::Result<T, E>;
344
345#[cfg(test)]
346mod tests {
347 use std::error::Error as _;
348 use std::io;
349
350 use super::{Error, ErrorKind, Panic};
351
352 #[test]
353 fn opaque_error_preserves_source() {
354 let error = Error::with_source(ErrorKind::BackendFailure, io::Error::other("backend down"));
355 let standard_error: &dyn std::error::Error = &error;
356
357 assert_eq!(error.kind(), ErrorKind::BackendFailure);
358 assert_eq!(standard_error.source().unwrap().to_string(), "backend down");
359 }
360
361 #[test]
362 fn context_renders_after_kind() {
363 let error = Error::with_context(
364 ErrorKind::BackendFailure,
365 "reading local object",
366 io::Error::other("backend down"),
367 );
368
369 assert_eq!(
370 error.to_string(),
371 "backend operation failed: reading local object"
372 );
373 }
374
375 #[test]
376 fn error_kind_default_message_includes_range_size() {
377 let error: Error = ErrorKind::RangeNotSatisfiable { total: 42 }.into();
378
379 assert_eq!(
380 error.to_string(),
381 "range not satisfiable (object size: 42 bytes)"
382 );
383 }
384
385 #[test]
386 fn panic_uses_the_payload_message() {
387 let panic = Panic::new(Box::new("task panicked"));
388 let error: Error = panic.into();
389
390 assert_eq!(error.kind(), ErrorKind::Panic);
391 assert_eq!(error.to_string(), "service task panicked");
392 assert_eq!(error.source().unwrap().to_string(), "task panicked");
393 }
394}