Skip to main content

timeseries_table_format/storage/
error.rs

1use std::io;
2
3use snafu::{Backtrace, prelude::*};
4
5/// Errors returned by a concrete storage backend.
6#[derive(Debug, Snafu)]
7#[non_exhaustive]
8pub enum StorageBackendError {
9    /// Local filesystem operation failed.
10    #[snafu(display("Filesystem error: {source}"))]
11    Filesystem {
12        /// Underlying filesystem error.
13        source: io::Error,
14    },
15}
16
17impl From<io::Error> for StorageBackendError {
18    fn from(source: io::Error) -> Self {
19        Self::Filesystem { source }
20    }
21}
22
23/// Errors that can occur during storage operations.
24#[derive(Debug, Snafu)]
25#[snafu(visibility(pub(crate)))]
26#[non_exhaustive]
27pub enum StorageError {
28    /// A table-relative storage path is invalid or non-canonical.
29    #[snafu(display("Invalid table-relative storage path {path:?}: {reason}"))]
30    InvalidRelativePath {
31        /// Rejected table-relative path.
32        path: String,
33        /// Reason the path cannot be used as its canonical storage key.
34        reason: String,
35        /// Backtrace captured at the path-validation boundary.
36        backtrace: Box<Backtrace>,
37    },
38
39    /// The specified path was not found.
40    #[snafu(display("Path not found: {path}"))]
41    NotFound {
42        /// The path that was not found.
43        path: String,
44        /// Underlying storage backend error that caused the failure.
45        source: StorageBackendError,
46        /// The backtrace at the time the error occurred.
47        backtrace: Backtrace,
48    },
49
50    /// The specified path already exists when creation was requested with
51    /// create-new semantics.
52    #[snafu(display("Path already exists: {path}"))]
53    AlreadyExists {
54        /// The path that was found to already exist.
55        path: String,
56        /// Underlying storage backend error that indicates the existing resource.
57        source: StorageBackendError,
58        /// The backtrace captured when the error occurred.
59        backtrace: Backtrace,
60    },
61
62    /// An I/O error occurred in the storage backend.
63    #[snafu(display("Storage I/O error at {path}: {source}"))]
64    OtherIo {
65        /// The path where the I/O error occurred.
66        path: String,
67        /// Underlying storage backend error with platform-specific details.
68        source: StorageBackendError,
69        /// The backtrace at the time the error occurred.
70        backtrace: Backtrace,
71    },
72
73    /// A newly-created object could not be removed after its write failed.
74    #[snafu(display(
75        "Storage operation failed at {path}: {operation_error}; cleanup also failed: {cleanup_error}"
76    ))]
77    CleanupFailed {
78        /// Path of the object that may remain after the failed cleanup.
79        path: String,
80        /// Original write or sync failure.
81        #[snafu(source, backtrace)]
82        operation_error: Box<StorageError>,
83        /// Failure encountered while removing the newly-created object.
84        cleanup_error: Box<StorageError>,
85    },
86}
87
88#[cfg(test)]
89mod tests {
90    use std::error::Error as _;
91
92    use snafu::ErrorCompat;
93
94    use super::*;
95
96    fn filesystem_source(error: &StorageError) -> &io::Error {
97        error
98            .source()
99            .and_then(|source| source.downcast_ref::<StorageBackendError>())
100            .and_then(|source| source.source())
101            .and_then(|source| source.downcast_ref::<io::Error>())
102            .expect("filesystem source")
103    }
104
105    #[test]
106    fn storage_errors_preserve_filesystem_sources() {
107        let cases = [
108            (
109                StorageError::NotFound {
110                    path: "missing.parquet".to_string(),
111                    source: io::Error::from(io::ErrorKind::NotFound).into(),
112                    backtrace: Backtrace::capture(),
113                },
114                io::ErrorKind::NotFound,
115            ),
116            (
117                StorageError::AlreadyExists {
118                    path: "existing.parquet".to_string(),
119                    source: io::Error::from(io::ErrorKind::AlreadyExists).into(),
120                    backtrace: Backtrace::capture(),
121                },
122                io::ErrorKind::AlreadyExists,
123            ),
124            (
125                StorageError::OtherIo {
126                    path: "denied.parquet".to_string(),
127                    source: io::Error::from(io::ErrorKind::PermissionDenied).into(),
128                    backtrace: Backtrace::capture(),
129                },
130                io::ErrorKind::PermissionDenied,
131            ),
132        ];
133
134        for (error, expected_kind) in cases {
135            assert_eq!(filesystem_source(&error).kind(), expected_kind);
136            assert!(ErrorCompat::backtrace(&error).is_some());
137        }
138    }
139
140    #[test]
141    fn cleanup_failure_delegates_to_the_operation_backtrace() {
142        let operation = StorageError::OtherIo {
143            path: "data/segment.parquet".to_string(),
144            source: io::Error::other("write failed").into(),
145            backtrace: Backtrace::capture(),
146        };
147        let cleanup = StorageError::OtherIo {
148            path: "data/segment.parquet".to_string(),
149            source: io::Error::other("cleanup failed").into(),
150            backtrace: Backtrace::capture(),
151        };
152        let error = StorageError::CleanupFailed {
153            path: "data/segment.parquet".to_string(),
154            operation_error: Box::new(operation),
155            cleanup_error: Box::new(cleanup),
156        };
157
158        let wrapper_backtrace = ErrorCompat::backtrace(&error).expect("wrapper backtrace");
159        let (operation, cleanup) = match &error {
160            StorageError::CleanupFailed {
161                operation_error,
162                cleanup_error,
163                ..
164            } => (operation_error.as_ref(), cleanup_error.as_ref()),
165            _ => unreachable!(),
166        };
167        let operation_backtrace = ErrorCompat::backtrace(operation).expect("operation backtrace");
168        assert!(std::ptr::eq(wrapper_backtrace, operation_backtrace));
169        assert_eq!(filesystem_source(operation).to_string(), "write failed");
170        assert_eq!(filesystem_source(cleanup).to_string(), "cleanup failed");
171    }
172}