Move 17 library crates into crates/, CLI binary into bin/fxt, consolidate 10 test crates into testing/, split config crate from deployment config files. Root directory reduced from 38+ to ~17 directories. All Cargo.toml paths and build.rs proto refs updated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
333 lines
10 KiB
Rust
333 lines
10 KiB
Rust
//! Error types for storage operations
|
|
|
|
use common::error::{CommonError, ErrorCategory};
|
|
use thiserror::Error;
|
|
|
|
/// Result type for storage operations
|
|
pub type StorageResult<T> = Result<T, StorageError>;
|
|
|
|
/// Storage-related errors
|
|
#[derive(Error, Debug)]
|
|
pub enum StorageError {
|
|
/// IO error during file operations
|
|
#[error("IO error: {message}")]
|
|
IoError {
|
|
/// Error message describing the IO failure
|
|
message: String,
|
|
},
|
|
|
|
/// Network error during remote storage operations
|
|
#[error("Network error: {message}")]
|
|
NetworkError {
|
|
/// Error message describing the network failure
|
|
message: String,
|
|
},
|
|
|
|
/// Authentication/authorization error
|
|
#[error("Authentication error: {message}")]
|
|
AuthError {
|
|
/// Error message describing the authentication failure
|
|
message: String,
|
|
},
|
|
|
|
/// Data not found at the specified path
|
|
#[error("Data not found at path: {path}")]
|
|
NotFound {
|
|
/// The path where data was not found
|
|
path: String,
|
|
},
|
|
|
|
/// Permission denied for storage operation
|
|
#[error("Permission denied for operation on: {path}")]
|
|
PermissionDenied {
|
|
/// The path where permission was denied
|
|
path: String,
|
|
},
|
|
|
|
/// Storage quota exceeded
|
|
#[error("Storage quota exceeded (used: {used}, limit: {limit})")]
|
|
QuotaExceeded {
|
|
/// Current storage usage in bytes
|
|
used: u64,
|
|
/// Storage quota limit in bytes
|
|
limit: u64,
|
|
},
|
|
|
|
/// Data integrity check failed
|
|
#[error("Data integrity check failed for: {path} (expected: {expected}, actual: {actual})")]
|
|
IntegrityError {
|
|
/// The path where integrity check failed
|
|
path: String,
|
|
/// Expected checksum or hash value
|
|
expected: String,
|
|
/// Actual checksum or hash value
|
|
actual: String,
|
|
},
|
|
|
|
/// Serialization/deserialization error
|
|
#[error("Serialization error: {message}")]
|
|
SerializationError {
|
|
/// Error message describing the serialization failure
|
|
message: String,
|
|
},
|
|
|
|
/// Compression/decompression error
|
|
#[error("Compression error: {message}")]
|
|
CompressionError {
|
|
/// Error message describing the compression failure
|
|
message: String,
|
|
},
|
|
|
|
/// Configuration error
|
|
#[error("Configuration error: {message}")]
|
|
ConfigError {
|
|
/// Error message describing the configuration issue
|
|
message: String,
|
|
},
|
|
|
|
/// Operation failed with detailed context
|
|
#[error("Operation '{operation}' failed on path '{path}': {source}")]
|
|
OperationFailed {
|
|
/// The operation that failed
|
|
operation: String,
|
|
/// The path where the operation failed
|
|
path: String,
|
|
/// The underlying error that caused the failure
|
|
#[source]
|
|
source: std::sync::Arc<CommonError>,
|
|
},
|
|
/// Timeout during storage operation
|
|
#[error("Operation timed out after {timeout_ms}ms")]
|
|
Timeout {
|
|
/// Timeout duration in milliseconds
|
|
timeout_ms: u64,
|
|
},
|
|
|
|
/// Rate limit exceeded
|
|
#[error("Rate limit exceeded, retry after {retry_after_ms}ms")]
|
|
RateLimited {
|
|
/// Time to wait before retrying in milliseconds
|
|
retry_after_ms: u64,
|
|
},
|
|
|
|
/// Generic storage error
|
|
#[error("Storage error: {message}")]
|
|
Generic {
|
|
/// Error message describing the generic failure
|
|
message: String,
|
|
},
|
|
|
|
/// S3-specific error
|
|
#[cfg(feature = "s3")]
|
|
#[error("S3 error: {message}")]
|
|
S3Error {
|
|
/// Error message describing the S3 failure
|
|
message: String,
|
|
},
|
|
}
|
|
|
|
impl StorageError {
|
|
/// Check if the error is retryable
|
|
pub fn is_retryable(&self) -> bool {
|
|
match self {
|
|
StorageError::NetworkError { .. } => true,
|
|
StorageError::Timeout { .. } => true,
|
|
StorageError::RateLimited { .. } => true,
|
|
StorageError::Generic { .. } => true,
|
|
#[cfg(feature = "s3")]
|
|
StorageError::S3Error { .. } => true,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Get retry delay in milliseconds for retryable errors
|
|
pub fn retry_delay_ms(&self) -> Option<u64> {
|
|
match self {
|
|
StorageError::NetworkError { .. } => Some(100),
|
|
StorageError::Timeout { .. } => Some(200),
|
|
StorageError::RateLimited { retry_after_ms } => Some(*retry_after_ms),
|
|
StorageError::Generic { .. } => Some(100),
|
|
#[cfg(feature = "s3")]
|
|
StorageError::S3Error { .. } => Some(100),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Check if error indicates a transient issue
|
|
pub fn is_transient(&self) -> bool {
|
|
matches!(
|
|
self,
|
|
StorageError::NetworkError { .. }
|
|
| StorageError::Timeout { .. }
|
|
| StorageError::RateLimited { .. }
|
|
)
|
|
}
|
|
|
|
/// Get error category for metrics and monitoring
|
|
pub fn category(&self) -> &'static str {
|
|
match self {
|
|
StorageError::IoError { .. } => "io",
|
|
StorageError::NetworkError { .. } => "network",
|
|
StorageError::AuthError { .. } => "auth",
|
|
StorageError::NotFound { .. } => "not_found",
|
|
StorageError::PermissionDenied { .. } => "permission",
|
|
StorageError::QuotaExceeded { .. } => "quota",
|
|
StorageError::IntegrityError { .. } => "integrity",
|
|
StorageError::SerializationError { .. } => "serialization",
|
|
StorageError::CompressionError { .. } => "compression",
|
|
StorageError::ConfigError { .. } => "config",
|
|
StorageError::OperationFailed { .. } => "operation_failed",
|
|
StorageError::Timeout { .. } => "timeout",
|
|
StorageError::RateLimited { .. } => "rate_limit",
|
|
StorageError::Generic { .. } => "generic",
|
|
|
|
#[cfg(feature = "s3")]
|
|
StorageError::S3Error { .. } => "s3",
|
|
}
|
|
}
|
|
/// Create a sanitized error message safe for logging (removes sensitive info)
|
|
pub fn safe_message(&self) -> String {
|
|
match self {
|
|
StorageError::AuthError { .. } => {
|
|
"Authentication error (details masked for security)".to_owned()
|
|
},
|
|
|
|
_ => self.to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Conversion implementations for common error types
|
|
|
|
impl From<std::io::Error> for StorageError {
|
|
fn from(err: std::io::Error) -> Self {
|
|
use std::io::ErrorKind;
|
|
match err.kind() {
|
|
ErrorKind::NotFound => StorageError::NotFound {
|
|
path: "unknown".to_owned(),
|
|
},
|
|
ErrorKind::PermissionDenied => StorageError::PermissionDenied {
|
|
path: "unknown".to_owned(),
|
|
},
|
|
ErrorKind::TimedOut => StorageError::Timeout { timeout_ms: 5000 },
|
|
_ => StorageError::IoError {
|
|
message: err.to_string(),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<serde_json::Error> for StorageError {
|
|
fn from(err: serde_json::Error) -> Self {
|
|
StorageError::SerializationError {
|
|
message: err.to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<bincode::Error> for StorageError {
|
|
fn from(err: bincode::Error) -> Self {
|
|
StorageError::SerializationError {
|
|
message: err.to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Conversion to CommonError for integration with common error system
|
|
impl From<StorageError> for CommonError {
|
|
fn from(err: StorageError) -> Self {
|
|
let category = match &err {
|
|
StorageError::IoError { .. } => ErrorCategory::System,
|
|
StorageError::NetworkError { .. } => ErrorCategory::Network,
|
|
StorageError::AuthError { .. } => ErrorCategory::Security,
|
|
StorageError::NotFound { .. } => ErrorCategory::System,
|
|
StorageError::PermissionDenied { .. } => ErrorCategory::Security,
|
|
StorageError::QuotaExceeded { .. } => ErrorCategory::System,
|
|
StorageError::IntegrityError { .. } => ErrorCategory::System,
|
|
StorageError::SerializationError { .. } => ErrorCategory::System,
|
|
StorageError::CompressionError { .. } => ErrorCategory::System,
|
|
StorageError::ConfigError { .. } => ErrorCategory::Configuration,
|
|
StorageError::OperationFailed { .. } => ErrorCategory::System,
|
|
StorageError::Timeout { .. } => ErrorCategory::System,
|
|
StorageError::RateLimited { .. } => ErrorCategory::System,
|
|
StorageError::Generic { .. } => ErrorCategory::System,
|
|
StorageError::S3Error { .. } => ErrorCategory::Network,
|
|
};
|
|
|
|
CommonError::service(category, err.to_string())
|
|
}
|
|
}
|
|
|
|
// Note: AWS SDK error conversions removed - we use object_store now
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_error_retryability() {
|
|
assert!(StorageError::NetworkError {
|
|
message: "test".to_owned()
|
|
}
|
|
.is_retryable());
|
|
|
|
assert!(!StorageError::NotFound {
|
|
path: "test".to_owned()
|
|
}
|
|
.is_retryable());
|
|
|
|
assert!(StorageError::Timeout { timeout_ms: 1000 }.is_retryable());
|
|
}
|
|
|
|
#[test]
|
|
fn test_error_categories() {
|
|
assert_eq!(
|
|
StorageError::IoError {
|
|
message: "test".to_owned()
|
|
}
|
|
.category(),
|
|
"io"
|
|
);
|
|
|
|
assert_eq!(
|
|
StorageError::NetworkError {
|
|
message: "test".to_owned()
|
|
}
|
|
.category(),
|
|
"network"
|
|
);
|
|
|
|
assert_eq!(
|
|
StorageError::NotFound {
|
|
path: "test".to_owned()
|
|
}
|
|
.category(),
|
|
"not_found"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_error_transient() {
|
|
assert!(StorageError::NetworkError {
|
|
message: "test".to_owned()
|
|
}
|
|
.is_transient());
|
|
|
|
assert!(!StorageError::ConfigError {
|
|
message: "test".to_owned()
|
|
}
|
|
.is_transient());
|
|
}
|
|
|
|
#[test]
|
|
fn test_safe_message() {
|
|
let auth_error = StorageError::AuthError {
|
|
message: "sensitive auth details".to_owned(),
|
|
};
|
|
|
|
let safe_msg = auth_error.safe_message();
|
|
assert!(safe_msg.contains("masked"));
|
|
assert!(!safe_msg.contains("sensitive"));
|
|
}
|
|
}
|