diff --git a/Cargo.lock b/Cargo.lock index 48277415d..c4c73ed53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9335,6 +9335,7 @@ dependencies = [ "rustfs-rio-v2", "rustfs-s3-types", "rustfs-signer", + "rustfs-storage-api", "rustfs-tls-runtime", "rustfs-utils", "rustix 1.1.4", diff --git a/crates/ecstore/Cargo.toml b/crates/ecstore/Cargo.toml index 4f5de209e..5d0425383 100644 --- a/crates/ecstore/Cargo.toml +++ b/crates/ecstore/Cargo.toml @@ -51,6 +51,7 @@ rustfs-kms.workspace = true rustfs-s3-types = { workspace = true } rustfs-data-usage.workspace = true rustfs-object-capacity.workspace = true +rustfs-storage-api.workspace = true async-trait.workspace = true bytes.workspace = true byteorder = { workspace = true } diff --git a/crates/ecstore/src/error.rs b/crates/ecstore/src/error.rs index 7bb84b8a0..a75ab8769 100644 --- a/crates/ecstore/src/error.rs +++ b/crates/ecstore/src/error.rs @@ -14,6 +14,7 @@ use crate::bucket::error::BucketMetadataError; use crate::disk::error::DiskError; +use rustfs_storage_api::StorageErrorCode; use rustfs_utils::path::decode_dir_object; use s3s::{S3Error, S3ErrorCode}; @@ -460,156 +461,181 @@ impl Clone for StorageError { } impl StorageError { - pub fn to_u32(&self) -> u32 { + fn code(&self) -> StorageErrorCode { match self { - StorageError::Io(_) => 0x01, - StorageError::FaultyDisk => 0x02, - StorageError::DiskFull => 0x03, - StorageError::VolumeNotFound => 0x04, - StorageError::VolumeExists => 0x05, - StorageError::FileNotFound => 0x06, - StorageError::FileVersionNotFound => 0x07, - StorageError::FileNameTooLong => 0x08, - StorageError::FileAccessDenied => 0x09, - StorageError::FileCorrupt => 0x0A, - StorageError::IsNotRegular => 0x0B, - StorageError::VolumeNotEmpty => 0x0C, - StorageError::VolumeAccessDenied => 0x0D, - StorageError::CorruptedFormat => 0x0E, - StorageError::CorruptedBackend => 0x0F, - StorageError::UnformattedDisk => 0x10, - StorageError::DiskNotFound => 0x11, - StorageError::DriveIsRoot => 0x12, - StorageError::FaultyRemoteDisk => 0x13, - StorageError::DiskAccessDenied => 0x14, - StorageError::Unexpected => 0x15, - StorageError::NotImplemented => 0x16, - StorageError::InvalidArgument(_, _, _) => 0x17, - StorageError::MethodNotAllowed => 0x18, - StorageError::BucketNotFound(_) => 0x19, - StorageError::BucketNotEmpty(_) => 0x1A, - StorageError::BucketNameInvalid(_) => 0x1B, - StorageError::ObjectNameInvalid(_, _) => 0x1C, - StorageError::BucketExists(_) => 0x1D, - StorageError::StorageFull => 0x1E, - StorageError::SlowDown => 0x1F, - StorageError::PrefixAccessDenied(_, _) => 0x20, - StorageError::InvalidUploadIDKeyCombination(_, _) => 0x21, - StorageError::MalformedUploadID(_) => 0x22, - StorageError::ObjectNameTooLong(_, _) => 0x23, - StorageError::ObjectNamePrefixAsSlash(_, _) => 0x24, - StorageError::ObjectNotFound(_, _) => 0x25, - StorageError::VersionNotFound(_, _, _) => 0x26, - StorageError::InvalidUploadID(_, _, _) => 0x27, - StorageError::InvalidVersionID(_, _, _) => 0x28, - StorageError::DataMovementOverwriteErr(_, _, _) => 0x29, - StorageError::ObjectExistsAsDirectory(_, _) => 0x2A, - // StorageError::InsufficientReadQuorum => 0x2B, - // StorageError::InsufficientWriteQuorum => 0x2C, - StorageError::DecommissionNotStarted => 0x2D, - StorageError::InvalidPart(_, _, _) => 0x2E, - StorageError::DoneForNow => 0x2F, - StorageError::DecommissionAlreadyRunning => 0x30, - StorageError::RebalanceAlreadyRunning => 0x40, - StorageError::OperationCanceled => 0x41, - StorageError::ErasureReadQuorum => 0x31, - StorageError::ErasureWriteQuorum => 0x32, - StorageError::NotFirstDisk => 0x33, - StorageError::FirstDiskWait => 0x34, - StorageError::ConfigNotFound => 0x35, - StorageError::TooManyOpenFiles => 0x36, - StorageError::NoHealRequired => 0x37, - StorageError::Lock(_) => 0x38, - StorageError::InsufficientReadQuorum(_, _) => 0x39, - StorageError::InsufficientWriteQuorum(_, _) => 0x3A, - StorageError::PreconditionFailed => 0x3B, - StorageError::EntityTooSmall(_, _, _) => 0x3C, - StorageError::InvalidRangeSpec(_) => 0x3D, - StorageError::NotModified => 0x3E, - StorageError::InvalidPartNumber(_) => 0x3F, - StorageError::NamespaceLockQuorumUnavailable { .. } => 0x42, + StorageError::Io(_) => StorageErrorCode::Io, + StorageError::FaultyDisk => StorageErrorCode::FaultyDisk, + StorageError::DiskFull => StorageErrorCode::DiskFull, + StorageError::VolumeNotFound => StorageErrorCode::VolumeNotFound, + StorageError::VolumeExists => StorageErrorCode::VolumeExists, + StorageError::FileNotFound => StorageErrorCode::FileNotFound, + StorageError::FileVersionNotFound => StorageErrorCode::FileVersionNotFound, + StorageError::FileNameTooLong => StorageErrorCode::FileNameTooLong, + StorageError::FileAccessDenied => StorageErrorCode::FileAccessDenied, + StorageError::FileCorrupt => StorageErrorCode::FileCorrupt, + StorageError::IsNotRegular => StorageErrorCode::IsNotRegular, + StorageError::VolumeNotEmpty => StorageErrorCode::VolumeNotEmpty, + StorageError::VolumeAccessDenied => StorageErrorCode::VolumeAccessDenied, + StorageError::CorruptedFormat => StorageErrorCode::CorruptedFormat, + StorageError::CorruptedBackend => StorageErrorCode::CorruptedBackend, + StorageError::UnformattedDisk => StorageErrorCode::UnformattedDisk, + StorageError::DiskNotFound => StorageErrorCode::DiskNotFound, + StorageError::DriveIsRoot => StorageErrorCode::DriveIsRoot, + StorageError::FaultyRemoteDisk => StorageErrorCode::FaultyRemoteDisk, + StorageError::DiskAccessDenied => StorageErrorCode::DiskAccessDenied, + StorageError::Unexpected => StorageErrorCode::Unexpected, + StorageError::NotImplemented => StorageErrorCode::NotImplemented, + StorageError::InvalidArgument(_, _, _) => StorageErrorCode::InvalidArgument, + StorageError::MethodNotAllowed => StorageErrorCode::MethodNotAllowed, + StorageError::BucketNotFound(_) => StorageErrorCode::BucketNotFound, + StorageError::BucketNotEmpty(_) => StorageErrorCode::BucketNotEmpty, + StorageError::BucketNameInvalid(_) => StorageErrorCode::BucketNameInvalid, + StorageError::ObjectNameInvalid(_, _) => StorageErrorCode::ObjectNameInvalid, + StorageError::BucketExists(_) => StorageErrorCode::BucketExists, + StorageError::StorageFull => StorageErrorCode::StorageFull, + StorageError::SlowDown => StorageErrorCode::SlowDown, + StorageError::PrefixAccessDenied(_, _) => StorageErrorCode::PrefixAccessDenied, + StorageError::InvalidUploadIDKeyCombination(_, _) => StorageErrorCode::InvalidUploadIDKeyCombination, + StorageError::MalformedUploadID(_) => StorageErrorCode::MalformedUploadID, + StorageError::ObjectNameTooLong(_, _) => StorageErrorCode::ObjectNameTooLong, + StorageError::ObjectNamePrefixAsSlash(_, _) => StorageErrorCode::ObjectNamePrefixAsSlash, + StorageError::ObjectNotFound(_, _) => StorageErrorCode::ObjectNotFound, + StorageError::VersionNotFound(_, _, _) => StorageErrorCode::VersionNotFound, + StorageError::InvalidUploadID(_, _, _) => StorageErrorCode::InvalidUploadID, + StorageError::InvalidVersionID(_, _, _) => StorageErrorCode::InvalidVersionID, + StorageError::DataMovementOverwriteErr(_, _, _) => StorageErrorCode::DataMovementOverwriteErr, + StorageError::ObjectExistsAsDirectory(_, _) => StorageErrorCode::ObjectExistsAsDirectory, + StorageError::DecommissionNotStarted => StorageErrorCode::DecommissionNotStarted, + StorageError::InvalidPart(_, _, _) => StorageErrorCode::InvalidPart, + StorageError::DoneForNow => StorageErrorCode::DoneForNow, + StorageError::DecommissionAlreadyRunning => StorageErrorCode::DecommissionAlreadyRunning, + StorageError::RebalanceAlreadyRunning => StorageErrorCode::RebalanceAlreadyRunning, + StorageError::OperationCanceled => StorageErrorCode::OperationCanceled, + StorageError::ErasureReadQuorum => StorageErrorCode::ErasureReadQuorum, + StorageError::ErasureWriteQuorum => StorageErrorCode::ErasureWriteQuorum, + StorageError::NotFirstDisk => StorageErrorCode::NotFirstDisk, + StorageError::FirstDiskWait => StorageErrorCode::FirstDiskWait, + StorageError::ConfigNotFound => StorageErrorCode::ConfigNotFound, + StorageError::TooManyOpenFiles => StorageErrorCode::TooManyOpenFiles, + StorageError::NoHealRequired => StorageErrorCode::NoHealRequired, + StorageError::Lock(_) => StorageErrorCode::Lock, + StorageError::InsufficientReadQuorum(_, _) => StorageErrorCode::InsufficientReadQuorum, + StorageError::InsufficientWriteQuorum(_, _) => StorageErrorCode::InsufficientWriteQuorum, + StorageError::PreconditionFailed => StorageErrorCode::PreconditionFailed, + StorageError::EntityTooSmall(_, _, _) => StorageErrorCode::EntityTooSmall, + StorageError::InvalidRangeSpec(_) => StorageErrorCode::InvalidRangeSpec, + StorageError::NotModified => StorageErrorCode::NotModified, + StorageError::InvalidPartNumber(_) => StorageErrorCode::InvalidPartNumber, + StorageError::NamespaceLockQuorumUnavailable { .. } => StorageErrorCode::NamespaceLockQuorumUnavailable, } } + pub fn to_u32(&self) -> u32 { + self.code().as_u32() + } + pub fn from_u32(error: u32) -> Option { - match error { - 0x01 => Some(StorageError::Io(std::io::Error::other("Io error"))), - 0x02 => Some(StorageError::FaultyDisk), - 0x03 => Some(StorageError::DiskFull), - 0x04 => Some(StorageError::VolumeNotFound), - 0x05 => Some(StorageError::VolumeExists), - 0x06 => Some(StorageError::FileNotFound), - 0x07 => Some(StorageError::FileVersionNotFound), - 0x08 => Some(StorageError::FileNameTooLong), - 0x09 => Some(StorageError::FileAccessDenied), - 0x0A => Some(StorageError::FileCorrupt), - 0x0B => Some(StorageError::IsNotRegular), - 0x0C => Some(StorageError::VolumeNotEmpty), - 0x0D => Some(StorageError::VolumeAccessDenied), - 0x0E => Some(StorageError::CorruptedFormat), - 0x0F => Some(StorageError::CorruptedBackend), - 0x10 => Some(StorageError::UnformattedDisk), - 0x11 => Some(StorageError::DiskNotFound), - 0x12 => Some(StorageError::DriveIsRoot), - 0x13 => Some(StorageError::FaultyRemoteDisk), - 0x14 => Some(StorageError::DiskAccessDenied), - 0x15 => Some(StorageError::Unexpected), - 0x16 => Some(StorageError::NotImplemented), - 0x17 => Some(StorageError::InvalidArgument(Default::default(), Default::default(), Default::default())), - 0x18 => Some(StorageError::MethodNotAllowed), - 0x19 => Some(StorageError::BucketNotFound(Default::default())), - 0x1A => Some(StorageError::BucketNotEmpty(Default::default())), - 0x1B => Some(StorageError::BucketNameInvalid(Default::default())), - 0x1C => Some(StorageError::ObjectNameInvalid(Default::default(), Default::default())), - 0x1D => Some(StorageError::BucketExists(Default::default())), - 0x1E => Some(StorageError::StorageFull), - 0x1F => Some(StorageError::SlowDown), - 0x20 => Some(StorageError::PrefixAccessDenied(Default::default(), Default::default())), - 0x21 => Some(StorageError::InvalidUploadIDKeyCombination(Default::default(), Default::default())), - 0x22 => Some(StorageError::MalformedUploadID(Default::default())), - 0x23 => Some(StorageError::ObjectNameTooLong(Default::default(), Default::default())), - 0x24 => Some(StorageError::ObjectNamePrefixAsSlash(Default::default(), Default::default())), - 0x25 => Some(StorageError::ObjectNotFound(Default::default(), Default::default())), - 0x26 => Some(StorageError::VersionNotFound(Default::default(), Default::default(), Default::default())), - 0x27 => Some(StorageError::InvalidUploadID(Default::default(), Default::default(), Default::default())), - 0x28 => Some(StorageError::InvalidVersionID(Default::default(), Default::default(), Default::default())), - 0x29 => Some(StorageError::DataMovementOverwriteErr( + match StorageErrorCode::from_u32(error)? { + StorageErrorCode::Io => Some(StorageError::Io(std::io::Error::other("Io error"))), + StorageErrorCode::FaultyDisk => Some(StorageError::FaultyDisk), + StorageErrorCode::DiskFull => Some(StorageError::DiskFull), + StorageErrorCode::VolumeNotFound => Some(StorageError::VolumeNotFound), + StorageErrorCode::VolumeExists => Some(StorageError::VolumeExists), + StorageErrorCode::FileNotFound => Some(StorageError::FileNotFound), + StorageErrorCode::FileVersionNotFound => Some(StorageError::FileVersionNotFound), + StorageErrorCode::FileNameTooLong => Some(StorageError::FileNameTooLong), + StorageErrorCode::FileAccessDenied => Some(StorageError::FileAccessDenied), + StorageErrorCode::FileCorrupt => Some(StorageError::FileCorrupt), + StorageErrorCode::IsNotRegular => Some(StorageError::IsNotRegular), + StorageErrorCode::VolumeNotEmpty => Some(StorageError::VolumeNotEmpty), + StorageErrorCode::VolumeAccessDenied => Some(StorageError::VolumeAccessDenied), + StorageErrorCode::CorruptedFormat => Some(StorageError::CorruptedFormat), + StorageErrorCode::CorruptedBackend => Some(StorageError::CorruptedBackend), + StorageErrorCode::UnformattedDisk => Some(StorageError::UnformattedDisk), + StorageErrorCode::DiskNotFound => Some(StorageError::DiskNotFound), + StorageErrorCode::DriveIsRoot => Some(StorageError::DriveIsRoot), + StorageErrorCode::FaultyRemoteDisk => Some(StorageError::FaultyRemoteDisk), + StorageErrorCode::DiskAccessDenied => Some(StorageError::DiskAccessDenied), + StorageErrorCode::Unexpected => Some(StorageError::Unexpected), + StorageErrorCode::NotImplemented => Some(StorageError::NotImplemented), + StorageErrorCode::InvalidArgument => { + Some(StorageError::InvalidArgument(Default::default(), Default::default(), Default::default())) + } + StorageErrorCode::MethodNotAllowed => Some(StorageError::MethodNotAllowed), + StorageErrorCode::BucketNotFound => Some(StorageError::BucketNotFound(Default::default())), + StorageErrorCode::BucketNotEmpty => Some(StorageError::BucketNotEmpty(Default::default())), + StorageErrorCode::BucketNameInvalid => Some(StorageError::BucketNameInvalid(Default::default())), + StorageErrorCode::ObjectNameInvalid => Some(StorageError::ObjectNameInvalid(Default::default(), Default::default())), + StorageErrorCode::BucketExists => Some(StorageError::BucketExists(Default::default())), + StorageErrorCode::StorageFull => Some(StorageError::StorageFull), + StorageErrorCode::SlowDown => Some(StorageError::SlowDown), + StorageErrorCode::PrefixAccessDenied => { + Some(StorageError::PrefixAccessDenied(Default::default(), Default::default())) + } + StorageErrorCode::InvalidUploadIDKeyCombination => { + Some(StorageError::InvalidUploadIDKeyCombination(Default::default(), Default::default())) + } + StorageErrorCode::MalformedUploadID => Some(StorageError::MalformedUploadID(Default::default())), + StorageErrorCode::ObjectNameTooLong => Some(StorageError::ObjectNameTooLong(Default::default(), Default::default())), + StorageErrorCode::ObjectNamePrefixAsSlash => { + Some(StorageError::ObjectNamePrefixAsSlash(Default::default(), Default::default())) + } + StorageErrorCode::ObjectNotFound => Some(StorageError::ObjectNotFound(Default::default(), Default::default())), + StorageErrorCode::VersionNotFound => { + Some(StorageError::VersionNotFound(Default::default(), Default::default(), Default::default())) + } + StorageErrorCode::InvalidUploadID => { + Some(StorageError::InvalidUploadID(Default::default(), Default::default(), Default::default())) + } + StorageErrorCode::InvalidVersionID => { + Some(StorageError::InvalidVersionID(Default::default(), Default::default(), Default::default())) + } + StorageErrorCode::DataMovementOverwriteErr => Some(StorageError::DataMovementOverwriteErr( Default::default(), Default::default(), Default::default(), )), - 0x2A => Some(StorageError::ObjectExistsAsDirectory(Default::default(), Default::default())), - // 0x2B => Some(StorageError::InsufficientReadQuorum), - // 0x2C => Some(StorageError::InsufficientWriteQuorum), - 0x2D => Some(StorageError::DecommissionNotStarted), - 0x2E => Some(StorageError::InvalidPart(Default::default(), Default::default(), Default::default())), - 0x2F => Some(StorageError::DoneForNow), - 0x30 => Some(StorageError::DecommissionAlreadyRunning), - 0x40 => Some(StorageError::RebalanceAlreadyRunning), - 0x41 => Some(StorageError::OperationCanceled), - 0x31 => Some(StorageError::ErasureReadQuorum), - 0x32 => Some(StorageError::ErasureWriteQuorum), - 0x33 => Some(StorageError::NotFirstDisk), - 0x34 => Some(StorageError::FirstDiskWait), - 0x35 => Some(StorageError::ConfigNotFound), - 0x36 => Some(StorageError::TooManyOpenFiles), - 0x37 => Some(StorageError::NoHealRequired), - 0x38 => Some(StorageError::Lock(rustfs_lock::LockError::internal("Generic lock error".to_string()))), - 0x39 => Some(StorageError::InsufficientReadQuorum(Default::default(), Default::default())), - 0x3A => Some(StorageError::InsufficientWriteQuorum(Default::default(), Default::default())), - 0x3B => Some(StorageError::PreconditionFailed), - 0x3C => Some(StorageError::EntityTooSmall(Default::default(), Default::default(), Default::default())), - 0x3D => Some(StorageError::InvalidRangeSpec(Default::default())), - 0x3E => Some(StorageError::NotModified), - 0x3F => Some(StorageError::InvalidPartNumber(Default::default())), - 0x42 => Some(StorageError::NamespaceLockQuorumUnavailable { + StorageErrorCode::ObjectExistsAsDirectory => { + Some(StorageError::ObjectExistsAsDirectory(Default::default(), Default::default())) + } + StorageErrorCode::DecommissionNotStarted => Some(StorageError::DecommissionNotStarted), + StorageErrorCode::InvalidPart => { + Some(StorageError::InvalidPart(Default::default(), Default::default(), Default::default())) + } + StorageErrorCode::DoneForNow => Some(StorageError::DoneForNow), + StorageErrorCode::DecommissionAlreadyRunning => Some(StorageError::DecommissionAlreadyRunning), + StorageErrorCode::RebalanceAlreadyRunning => Some(StorageError::RebalanceAlreadyRunning), + StorageErrorCode::OperationCanceled => Some(StorageError::OperationCanceled), + StorageErrorCode::ErasureReadQuorum => Some(StorageError::ErasureReadQuorum), + StorageErrorCode::ErasureWriteQuorum => Some(StorageError::ErasureWriteQuorum), + StorageErrorCode::NotFirstDisk => Some(StorageError::NotFirstDisk), + StorageErrorCode::FirstDiskWait => Some(StorageError::FirstDiskWait), + StorageErrorCode::ConfigNotFound => Some(StorageError::ConfigNotFound), + StorageErrorCode::TooManyOpenFiles => Some(StorageError::TooManyOpenFiles), + StorageErrorCode::NoHealRequired => Some(StorageError::NoHealRequired), + StorageErrorCode::Lock => { + Some(StorageError::Lock(rustfs_lock::LockError::internal("Generic lock error".to_string()))) + } + StorageErrorCode::InsufficientReadQuorum => { + Some(StorageError::InsufficientReadQuorum(Default::default(), Default::default())) + } + StorageErrorCode::InsufficientWriteQuorum => { + Some(StorageError::InsufficientWriteQuorum(Default::default(), Default::default())) + } + StorageErrorCode::PreconditionFailed => Some(StorageError::PreconditionFailed), + StorageErrorCode::EntityTooSmall => { + Some(StorageError::EntityTooSmall(Default::default(), Default::default(), Default::default())) + } + StorageErrorCode::InvalidRangeSpec => Some(StorageError::InvalidRangeSpec(Default::default())), + StorageErrorCode::NotModified => Some(StorageError::NotModified), + StorageErrorCode::InvalidPartNumber => Some(StorageError::InvalidPartNumber(Default::default())), + StorageErrorCode::NamespaceLockQuorumUnavailable => Some(StorageError::NamespaceLockQuorumUnavailable { mode: "write", bucket: Default::default(), object: Default::default(), required: Default::default(), achieved: Default::default(), }), - _ => None, } } } @@ -1048,6 +1074,25 @@ mod tests { assert!(StorageError::from_u32(0xFF).is_none()); } + #[test] + fn test_storage_error_code_contract_matches_storage_api() { + assert_eq!(StorageError::DiskFull.to_u32(), StorageErrorCode::DiskFull.as_u32()); + assert_eq!( + StorageError::ObjectNotFound("bucket".to_string(), "object".to_string()).to_u32(), + StorageErrorCode::ObjectNotFound.as_u32() + ); + assert!(matches!( + StorageError::from_u32(StorageErrorCode::ErasureReadQuorum.as_u32()), + Some(StorageError::ErasureReadQuorum) + )); + assert!(matches!( + StorageError::from_u32(StorageErrorCode::NamespaceLockQuorumUnavailable.as_u32()), + Some(StorageError::NamespaceLockQuorumUnavailable { .. }) + )); + assert!(StorageError::from_u32(0x2B).is_none()); + assert!(StorageError::from_u32(0x2C).is_none()); + } + #[test] fn test_storage_error_partial_eq() { // Test IO error comparison diff --git a/crates/storage-api/src/error.rs b/crates/storage-api/src/error.rs new file mode 100644 index 000000000..a647894c8 --- /dev/null +++ b/crates/storage-api/src/error.rs @@ -0,0 +1,323 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Stable storage error contracts shared across storage consumers. + +pub type StorageResult = core::result::Result; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum StorageErrorCode { + Io, + FaultyDisk, + DiskFull, + VolumeNotFound, + VolumeExists, + FileNotFound, + FileVersionNotFound, + FileNameTooLong, + FileAccessDenied, + FileCorrupt, + IsNotRegular, + VolumeNotEmpty, + VolumeAccessDenied, + CorruptedFormat, + CorruptedBackend, + UnformattedDisk, + DiskNotFound, + DriveIsRoot, + FaultyRemoteDisk, + DiskAccessDenied, + Unexpected, + NotImplemented, + InvalidArgument, + MethodNotAllowed, + BucketNotFound, + BucketNotEmpty, + BucketNameInvalid, + ObjectNameInvalid, + BucketExists, + StorageFull, + SlowDown, + PrefixAccessDenied, + InvalidUploadIDKeyCombination, + MalformedUploadID, + ObjectNameTooLong, + ObjectNamePrefixAsSlash, + ObjectNotFound, + VersionNotFound, + InvalidUploadID, + InvalidVersionID, + DataMovementOverwriteErr, + ObjectExistsAsDirectory, + DecommissionNotStarted, + InvalidPart, + DoneForNow, + DecommissionAlreadyRunning, + ErasureReadQuorum, + ErasureWriteQuorum, + NotFirstDisk, + FirstDiskWait, + ConfigNotFound, + TooManyOpenFiles, + NoHealRequired, + Lock, + InsufficientReadQuorum, + InsufficientWriteQuorum, + PreconditionFailed, + EntityTooSmall, + InvalidRangeSpec, + NotModified, + InvalidPartNumber, + RebalanceAlreadyRunning, + OperationCanceled, + NamespaceLockQuorumUnavailable, +} + +impl StorageErrorCode { + pub const fn as_u32(self) -> u32 { + match self { + Self::Io => 0x01, + Self::FaultyDisk => 0x02, + Self::DiskFull => 0x03, + Self::VolumeNotFound => 0x04, + Self::VolumeExists => 0x05, + Self::FileNotFound => 0x06, + Self::FileVersionNotFound => 0x07, + Self::FileNameTooLong => 0x08, + Self::FileAccessDenied => 0x09, + Self::FileCorrupt => 0x0A, + Self::IsNotRegular => 0x0B, + Self::VolumeNotEmpty => 0x0C, + Self::VolumeAccessDenied => 0x0D, + Self::CorruptedFormat => 0x0E, + Self::CorruptedBackend => 0x0F, + Self::UnformattedDisk => 0x10, + Self::DiskNotFound => 0x11, + Self::DriveIsRoot => 0x12, + Self::FaultyRemoteDisk => 0x13, + Self::DiskAccessDenied => 0x14, + Self::Unexpected => 0x15, + Self::NotImplemented => 0x16, + Self::InvalidArgument => 0x17, + Self::MethodNotAllowed => 0x18, + Self::BucketNotFound => 0x19, + Self::BucketNotEmpty => 0x1A, + Self::BucketNameInvalid => 0x1B, + Self::ObjectNameInvalid => 0x1C, + Self::BucketExists => 0x1D, + Self::StorageFull => 0x1E, + Self::SlowDown => 0x1F, + Self::PrefixAccessDenied => 0x20, + Self::InvalidUploadIDKeyCombination => 0x21, + Self::MalformedUploadID => 0x22, + Self::ObjectNameTooLong => 0x23, + Self::ObjectNamePrefixAsSlash => 0x24, + Self::ObjectNotFound => 0x25, + Self::VersionNotFound => 0x26, + Self::InvalidUploadID => 0x27, + Self::InvalidVersionID => 0x28, + Self::DataMovementOverwriteErr => 0x29, + Self::ObjectExistsAsDirectory => 0x2A, + Self::DecommissionNotStarted => 0x2D, + Self::InvalidPart => 0x2E, + Self::DoneForNow => 0x2F, + Self::DecommissionAlreadyRunning => 0x30, + Self::ErasureReadQuorum => 0x31, + Self::ErasureWriteQuorum => 0x32, + Self::NotFirstDisk => 0x33, + Self::FirstDiskWait => 0x34, + Self::ConfigNotFound => 0x35, + Self::TooManyOpenFiles => 0x36, + Self::NoHealRequired => 0x37, + Self::Lock => 0x38, + Self::InsufficientReadQuorum => 0x39, + Self::InsufficientWriteQuorum => 0x3A, + Self::PreconditionFailed => 0x3B, + Self::EntityTooSmall => 0x3C, + Self::InvalidRangeSpec => 0x3D, + Self::NotModified => 0x3E, + Self::InvalidPartNumber => 0x3F, + Self::RebalanceAlreadyRunning => 0x40, + Self::OperationCanceled => 0x41, + Self::NamespaceLockQuorumUnavailable => 0x42, + } + } + + pub const fn from_u32(code: u32) -> Option { + match code { + 0x01 => Some(Self::Io), + 0x02 => Some(Self::FaultyDisk), + 0x03 => Some(Self::DiskFull), + 0x04 => Some(Self::VolumeNotFound), + 0x05 => Some(Self::VolumeExists), + 0x06 => Some(Self::FileNotFound), + 0x07 => Some(Self::FileVersionNotFound), + 0x08 => Some(Self::FileNameTooLong), + 0x09 => Some(Self::FileAccessDenied), + 0x0A => Some(Self::FileCorrupt), + 0x0B => Some(Self::IsNotRegular), + 0x0C => Some(Self::VolumeNotEmpty), + 0x0D => Some(Self::VolumeAccessDenied), + 0x0E => Some(Self::CorruptedFormat), + 0x0F => Some(Self::CorruptedBackend), + 0x10 => Some(Self::UnformattedDisk), + 0x11 => Some(Self::DiskNotFound), + 0x12 => Some(Self::DriveIsRoot), + 0x13 => Some(Self::FaultyRemoteDisk), + 0x14 => Some(Self::DiskAccessDenied), + 0x15 => Some(Self::Unexpected), + 0x16 => Some(Self::NotImplemented), + 0x17 => Some(Self::InvalidArgument), + 0x18 => Some(Self::MethodNotAllowed), + 0x19 => Some(Self::BucketNotFound), + 0x1A => Some(Self::BucketNotEmpty), + 0x1B => Some(Self::BucketNameInvalid), + 0x1C => Some(Self::ObjectNameInvalid), + 0x1D => Some(Self::BucketExists), + 0x1E => Some(Self::StorageFull), + 0x1F => Some(Self::SlowDown), + 0x20 => Some(Self::PrefixAccessDenied), + 0x21 => Some(Self::InvalidUploadIDKeyCombination), + 0x22 => Some(Self::MalformedUploadID), + 0x23 => Some(Self::ObjectNameTooLong), + 0x24 => Some(Self::ObjectNamePrefixAsSlash), + 0x25 => Some(Self::ObjectNotFound), + 0x26 => Some(Self::VersionNotFound), + 0x27 => Some(Self::InvalidUploadID), + 0x28 => Some(Self::InvalidVersionID), + 0x29 => Some(Self::DataMovementOverwriteErr), + 0x2A => Some(Self::ObjectExistsAsDirectory), + 0x2D => Some(Self::DecommissionNotStarted), + 0x2E => Some(Self::InvalidPart), + 0x2F => Some(Self::DoneForNow), + 0x30 => Some(Self::DecommissionAlreadyRunning), + 0x31 => Some(Self::ErasureReadQuorum), + 0x32 => Some(Self::ErasureWriteQuorum), + 0x33 => Some(Self::NotFirstDisk), + 0x34 => Some(Self::FirstDiskWait), + 0x35 => Some(Self::ConfigNotFound), + 0x36 => Some(Self::TooManyOpenFiles), + 0x37 => Some(Self::NoHealRequired), + 0x38 => Some(Self::Lock), + 0x39 => Some(Self::InsufficientReadQuorum), + 0x3A => Some(Self::InsufficientWriteQuorum), + 0x3B => Some(Self::PreconditionFailed), + 0x3C => Some(Self::EntityTooSmall), + 0x3D => Some(Self::InvalidRangeSpec), + 0x3E => Some(Self::NotModified), + 0x3F => Some(Self::InvalidPartNumber), + 0x40 => Some(Self::RebalanceAlreadyRunning), + 0x41 => Some(Self::OperationCanceled), + 0x42 => Some(Self::NamespaceLockQuorumUnavailable), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const ERROR_CODES: &[(StorageErrorCode, u32)] = &[ + (StorageErrorCode::Io, 0x01), + (StorageErrorCode::FaultyDisk, 0x02), + (StorageErrorCode::DiskFull, 0x03), + (StorageErrorCode::VolumeNotFound, 0x04), + (StorageErrorCode::VolumeExists, 0x05), + (StorageErrorCode::FileNotFound, 0x06), + (StorageErrorCode::FileVersionNotFound, 0x07), + (StorageErrorCode::FileNameTooLong, 0x08), + (StorageErrorCode::FileAccessDenied, 0x09), + (StorageErrorCode::FileCorrupt, 0x0A), + (StorageErrorCode::IsNotRegular, 0x0B), + (StorageErrorCode::VolumeNotEmpty, 0x0C), + (StorageErrorCode::VolumeAccessDenied, 0x0D), + (StorageErrorCode::CorruptedFormat, 0x0E), + (StorageErrorCode::CorruptedBackend, 0x0F), + (StorageErrorCode::UnformattedDisk, 0x10), + (StorageErrorCode::DiskNotFound, 0x11), + (StorageErrorCode::DriveIsRoot, 0x12), + (StorageErrorCode::FaultyRemoteDisk, 0x13), + (StorageErrorCode::DiskAccessDenied, 0x14), + (StorageErrorCode::Unexpected, 0x15), + (StorageErrorCode::NotImplemented, 0x16), + (StorageErrorCode::InvalidArgument, 0x17), + (StorageErrorCode::MethodNotAllowed, 0x18), + (StorageErrorCode::BucketNotFound, 0x19), + (StorageErrorCode::BucketNotEmpty, 0x1A), + (StorageErrorCode::BucketNameInvalid, 0x1B), + (StorageErrorCode::ObjectNameInvalid, 0x1C), + (StorageErrorCode::BucketExists, 0x1D), + (StorageErrorCode::StorageFull, 0x1E), + (StorageErrorCode::SlowDown, 0x1F), + (StorageErrorCode::PrefixAccessDenied, 0x20), + (StorageErrorCode::InvalidUploadIDKeyCombination, 0x21), + (StorageErrorCode::MalformedUploadID, 0x22), + (StorageErrorCode::ObjectNameTooLong, 0x23), + (StorageErrorCode::ObjectNamePrefixAsSlash, 0x24), + (StorageErrorCode::ObjectNotFound, 0x25), + (StorageErrorCode::VersionNotFound, 0x26), + (StorageErrorCode::InvalidUploadID, 0x27), + (StorageErrorCode::InvalidVersionID, 0x28), + (StorageErrorCode::DataMovementOverwriteErr, 0x29), + (StorageErrorCode::ObjectExistsAsDirectory, 0x2A), + (StorageErrorCode::DecommissionNotStarted, 0x2D), + (StorageErrorCode::InvalidPart, 0x2E), + (StorageErrorCode::DoneForNow, 0x2F), + (StorageErrorCode::DecommissionAlreadyRunning, 0x30), + (StorageErrorCode::ErasureReadQuorum, 0x31), + (StorageErrorCode::ErasureWriteQuorum, 0x32), + (StorageErrorCode::NotFirstDisk, 0x33), + (StorageErrorCode::FirstDiskWait, 0x34), + (StorageErrorCode::ConfigNotFound, 0x35), + (StorageErrorCode::TooManyOpenFiles, 0x36), + (StorageErrorCode::NoHealRequired, 0x37), + (StorageErrorCode::Lock, 0x38), + (StorageErrorCode::InsufficientReadQuorum, 0x39), + (StorageErrorCode::InsufficientWriteQuorum, 0x3A), + (StorageErrorCode::PreconditionFailed, 0x3B), + (StorageErrorCode::EntityTooSmall, 0x3C), + (StorageErrorCode::InvalidRangeSpec, 0x3D), + (StorageErrorCode::NotModified, 0x3E), + (StorageErrorCode::InvalidPartNumber, 0x3F), + (StorageErrorCode::RebalanceAlreadyRunning, 0x40), + (StorageErrorCode::OperationCanceled, 0x41), + (StorageErrorCode::NamespaceLockQuorumUnavailable, 0x42), + ]; + + #[test] + fn storage_error_codes_roundtrip() { + for (error_code, raw_code) in ERROR_CODES { + assert_eq!(error_code.as_u32(), *raw_code); + assert_eq!(StorageErrorCode::from_u32(*raw_code), Some(*error_code)); + } + } + + #[test] + fn storage_error_code_rejects_unknown_values() { + assert_eq!(StorageErrorCode::from_u32(0x00), None); + assert_eq!(StorageErrorCode::from_u32(0x2B), None); + assert_eq!(StorageErrorCode::from_u32(0x2C), None); + assert_eq!(StorageErrorCode::from_u32(0xFF), None); + } + + #[test] + fn storage_result_defaults_to_error_code() { + let success: StorageResult = Ok(7); + let failure: StorageResult = Err(StorageErrorCode::DiskFull); + + assert_eq!(success, Ok(7)); + assert_eq!(failure, Err(StorageErrorCode::DiskFull)); + } +} diff --git a/crates/storage-api/src/lib.rs b/crates/storage-api/src/lib.rs index f1a336a15..4e3dd4a02 100644 --- a/crates/storage-api/src/lib.rs +++ b/crates/storage-api/src/lib.rs @@ -13,3 +13,7 @@ // limitations under the License. //! Storage API contracts for RustFS. + +pub mod error; + +pub use error::{StorageErrorCode, StorageResult}; diff --git a/docs/architecture/migration-progress.md b/docs/architecture/migration-progress.md index cc7be8ba5..ca06024dc 100644 --- a/docs/architecture/migration-progress.md +++ b/docs/architecture/migration-progress.md @@ -5,14 +5,14 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block ## Current Context - Issue: [`rustfs/backlog#660`](https://github.com/rustfs/backlog/issues/660) -- Branch: `overtrue/arch-background-controller-contract` -- Baseline: `upstream/main` at `f9a5e6d7e67322ac6f626b6f437a5e722fbe22e2` -- PR type for this branch: `docs-only` +- Branch: `overtrue/arch-storage-api-error-contracts` +- Baseline: `upstream/main` at `5fef10548477d9d25b0d391874f8280bf259d10e` +- PR type for this branch: `contract` - Runtime behavior changes: none. -- Rust code changes: none. +- Rust code changes: add the `rustfs-storage-api` error-code/result contract + and route ECStore storage error numeric conversion through that contract. - CI/script changes: none -- Docs changes: add BGC-002 background controller contract vocabulary and index - it from the background service inventory and architecture overview. +- Docs changes: record the API-002 first-slice contract boundary. ## Phase 0 Tasks @@ -114,6 +114,27 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block - Verification: focused KMS redaction/status tests, full KMS tests, migration guards, Rust quality scan, clippy, and `make pre-commit` passed. +## Phase 2 Storage API Tasks + +- [x] `API-001` Add `crates/storage-api`. + - Acceptance: `rustfs-storage-api` is a workspace member and remains a + dependency-free contract crate. + - Verification: `cargo check -p rustfs-storage-api`. +- [~] `API-002` Move public storage error/result contracts. + - Current slice: add public `StorageErrorCode` and `StorageResult` contracts + in `rustfs-storage-api`, then make ECStore `StorageError::to_u32/from_u32` + consume the shared code table. + - Deferred: keep the full ECStore `StorageError` enum and ECStore-specific + conversions in `rustfs-ecstore` until the `DiskError`, filemeta, lock, and + `std::io::Error` downcast boundary is proven safe. + - Acceptance: storage-api contract tests pass, ECStore compatibility tests + prove numeric codes match the new contract, and + `cargo check -p rustfs-storage-api -p rustfs-ecstore` passes. + - Must preserve: storage error display, conversions, object error mapping, + quorum classification, and reserved code gaps `0x2B/0x2C`. + - Risk defense: no storage hot-path enum move in this PR; only numeric code + mapping uses the new contract. + ## Phase 8 Background Controller Tasks - [x] `BGC-001` Inventory background services. @@ -137,43 +158,53 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block ## Next PRs -1. `test-only`: add focused preservation tests before moving scanner, heal, - replication, lifecycle, or disk health workers. -2. `contract`: add a side-effect-free BGC-003 status snapshot for a low-risk - service such as memory observability. -3. `behavior-change`: migrate one low-risk controller behind idempotence and - shutdown preservation tests. +1. Continue `API-002` only after reviewing whether `DiskError` and + `std::io::Error` conversion ownership can move without orphan-rule or + downcast behavior loss. +2. `contract`: move DTOs that are contract-only in `API-003`; keep ECStore + implementation, KMS/SSE readers, erasure logic, and remote disk internals out + of rustfs-storage-api. +3. `test-only`: add focused compatibility checks before moving store traits or + consumer imports. ## Pre-Push Review Log | Expert | Status | Notes | |---|---|---| -| Quality/architecture | pass | Single `docs-only` BGC-002 contract; it defines vocabulary and boundaries without adding a Rust trait, scheduler, or service registry. | -| Migration preservation | pass | No Rust source, Cargo manifest, workflow, script, runtime config, worker start/stop path, readiness path, or storage hot path diff. | -| Testing/verification | pass | Architecture migration rules, layer dependency guard, metrics reference guard, docs diff hygiene, and no-code-diff check passed. | +| Quality/architecture | pass | rustfs-storage-api only adds `StorageErrorCode` and `StorageResult`; ECStore keeps `StorageError`, `DiskError`, filemeta, lock, and `std::io::Error` conversion ownership. | +| Migration preservation | pass | ECStore numeric conversion now consumes the shared code table while preserving old variant defaults, reserved gaps `0x2B/0x2C`, and existing display/conversion logic. | +| Testing/verification | pass | Focused storage-api and ECStore tests, cargo check, dependency guard, migration guards, Rust quality scan, `make pre-commit`, nextest, and doctests passed. | ## Verification Notes Passed: +- `cargo fmt --all` +- `cargo fmt --all --check` +- `cargo test -p rustfs-storage-api` +- `cargo test -p rustfs-ecstore error -- --nocapture` +- `cargo check -p rustfs-storage-api -p rustfs-ecstore` +- `cargo tree -p rustfs-storage-api --edges normal` - `./scripts/check_architecture_migration_rules.sh` - `./scripts/check_layer_dependencies.sh` - `./scripts/check_metrics_migration_refs.sh` - `git diff --check` -- `git diff --name-only -- '*.rs' 'Cargo.toml' 'Cargo.lock' '.github/**' 'Makefile' 'Justfile'` +- Rust quality scan on changed Rust files. +- `make pre-commit` + - Full nextest: 5704 passed, 111 skipped. + - Workspace doctests passed. Notes: -- This branch changes architecture documentation only. -- No Rust source, Cargo manifest, workflow, script, or runtime configuration is - changed. -- `make pre-commit` is intentionally not required for this docs-only PR. +- This branch changes Rust storage contract code and ECStore contract mapping. +- Rust quality scan reported only existing patterns in `crates/ecstore/src/error.rs`: + a test-only `unwrap()` and the pre-existing `StorageError::other` boxed error + boundary. This PR does not add either pattern. ## Handoff Notes -- Keep this BGC-002 branch as a focused `docs-only` PR. -- Do not add controller traits, status structs, service registry code, - scheduling, shutdown wiring, worker tests, or runtime behavior changes in this - PR. -- Follow-up BGC-003 should add a side-effect-free status snapshot for one - low-risk service first, preferably memory observability. +- Keep this branch as the API-002 first slice, not the full error enum move. +- Do not move `StorageError`, `DiskError`, filemeta conversion, lock conversion, + or `std::io::Error` downcast behavior out of ECStore in this PR. +- Do not add ECStore implementation, KMS/SSE reader logic, erasure logic, or + remote disk internals to `rustfs-storage-api`. - Do not add temporary compatibility code without a matching `RUSTFS_COMPAT_TODO()` marker and cleanup-register entry.