feat(storage-api): add error code contract (#3313)

This commit is contained in:
安正超
2026-06-10 09:03:29 +08:00
committed by GitHub
parent 5fef105484
commit 3795d44b86
6 changed files with 566 additions and 161 deletions
Generated
+1
View File
@@ -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",
+1
View File
@@ -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 }
+180 -135
View File
@@ -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<Self> {
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
+323
View File
@@ -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<T, E = StorageErrorCode> = core::result::Result<T, E>;
#[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<Self> {
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<u8> = Ok(7);
let failure: StorageResult<u8> = Err(StorageErrorCode::DiskFull);
assert_eq!(success, Ok(7));
assert_eq!(failure, Err(StorageErrorCode::DiskFull));
}
}
+4
View File
@@ -13,3 +13,7 @@
// limitations under the License.
//! Storage API contracts for RustFS.
pub mod error;
pub use error::{StorageErrorCode, StorageResult};
+57 -26
View File
@@ -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(<task-id>)` marker and cleanup-register entry.