diff --git a/crates/ecstore/src/bucket/metadata_sys.rs b/crates/ecstore/src/bucket/metadata_sys.rs index d4975ba8e..3a25ae382 100644 --- a/crates/ecstore/src/bucket/metadata_sys.rs +++ b/crates/ecstore/src/bucket/metadata_sys.rs @@ -21,11 +21,11 @@ use crate::bucket::utils::{deserialize, is_meta_bucketname}; use crate::error::{Error, Result, is_err_bucket_not_found}; use crate::global::{GLOBAL_Endpoints, is_dist_erasure, is_erasure, resolve_object_store_handle}; use crate::store::ECStore; -use crate::store_api::HealOperations as _; use futures::future::join_all; use lazy_static::lazy_static; use rustfs_common::heal_channel::HealOpts; use rustfs_policy::policy::BucketPolicy; +use rustfs_storage_api::HealOperations as _; use s3s::dto::ReplicationConfiguration; use s3s::dto::{ AccelerateConfiguration, BucketLifecycleConfiguration, BucketLoggingStatus, CORSConfiguration, NotificationConfiguration, diff --git a/crates/ecstore/src/config/com.rs b/crates/ecstore/src/config/com.rs index dadaa2de9..0c5467019 100644 --- a/crates/ecstore/src/config/com.rs +++ b/crates/ecstore/src/config/com.rs @@ -1217,7 +1217,7 @@ mod tests { use crate::error::{Error, Result}; use crate::global::{is_dist_erasure, is_erasure, is_erasure_sd, update_erasure_type}; use crate::set_disk::SetDisks; - use crate::store_api::{GetObjectReader, NamespaceLocking, ObjectInfo, ObjectOptions, PutObjReader}; + use crate::store_api::{GetObjectReader, ObjectInfo, ObjectOptions, PutObjReader}; use http::HeaderMap; use rustfs_config::audit::{AUDIT_AMQP_SUB_SYS, AUDIT_KAFKA_SUB_SYS, AUDIT_MQTT_SUB_SYS, AUDIT_WEBHOOK_SUB_SYS}; use rustfs_config::notify::{ @@ -1231,6 +1231,7 @@ mod tests { use rustfs_lock::client::LockClient; use rustfs_lock::client::local::LocalClient; use rustfs_lock::{LockError, LockInfo, LockResponse, LockStats}; + use rustfs_storage_api::NamespaceLocking as _; use rustfs_storage_api::{HTTPRangeSpec, StorageAdminApi}; use serde_json::Value; use serial_test::serial; @@ -1459,7 +1460,10 @@ mod tests { } #[async_trait::async_trait] - impl NamespaceLocking for LockingConfigStorage { + impl rustfs_storage_api::NamespaceLocking for LockingConfigStorage { + type Error = crate::error::Error; + type NamespaceLock = rustfs_lock::NamespaceLockWrapper; + async fn new_ns_lock(&self, bucket: &str, object: &str) -> Result { self.set_disks.new_ns_lock(bucket, object).await } diff --git a/crates/ecstore/src/pools.rs b/crates/ecstore/src/pools.rs index 32ac2190f..fa652c119 100644 --- a/crates/ecstore/src/pools.rs +++ b/crates/ecstore/src/pools.rs @@ -38,7 +38,7 @@ use crate::error::{ use crate::notification_sys::get_global_notification_sys; use crate::resolve_object_store_handle; use crate::set_disk::SetDisks; -use crate::store_api::{GetObjectReader, HealOperations, ObjectOptions}; +use crate::store_api::{GetObjectReader, ObjectOptions}; use crate::{global::GLOBAL_LifecycleSys, sets::Sets, store::ECStore}; use byteorder::{ByteOrder, LittleEndian, WriteBytesExt}; use futures::{StreamExt, future::BoxFuture, stream::FuturesUnordered}; @@ -51,7 +51,8 @@ use rustfs_common::heal_channel::HealOpts; use rustfs_concurrency::workers::Workers; use rustfs_filemeta::{FileInfoVersions, MetaCacheEntries, MetaCacheEntry, MetadataResolutionParams}; use rustfs_storage_api::{ - BucketOperations, BucketOptions, MakeBucketOptions, ObjectIO as _, ObjectOperations as _, StorageAdminApi, + BucketOperations, BucketOptions, HealOperations as _, MakeBucketOptions, ObjectIO as _, ObjectOperations as _, + StorageAdminApi, }; use rustfs_utils::path::{encode_dir_object, path_join, path_to_bucket_object, path_to_bucket_object_with_base_path}; use s3s::dto::{BucketLifecycleConfiguration, DefaultRetention, ReplicationConfiguration}; diff --git a/crates/ecstore/src/set_disk.rs b/crates/ecstore/src/set_disk.rs index 8fc2654bc..de06b3d82 100644 --- a/crates/ecstore/src/set_disk.rs +++ b/crates/ecstore/src/set_disk.rs @@ -52,8 +52,8 @@ use crate::{ event_notification::{EventArgs, send_event}, global::{GLOBAL_LOCAL_DISK_MAP, GLOBAL_LOCAL_DISK_SET_DRIVES, get_global_deployment_id, is_dist_erasure}, store_api::{ - DeletedObject, GetObjectReader, HealOperations, ListObjectsV2Info, MultipartOperations, NamespaceLocking, ObjectIO, - ObjectInfo, ObjectOperations, PutObjReader, + DeletedObject, GetObjectReader, ListObjectsV2Info, MultipartOperations, ObjectIO, ObjectInfo, ObjectOperations, + PutObjReader, }, store_init::load_format_erasure, }; @@ -90,7 +90,7 @@ use rustfs_storage_api::{ BucketInfo, BucketOperations, BucketOptions, CompletePart, DeleteBucketOptions, ListMultipartsInfo, ListPartsInfo, MakeBucketOptions, MultipartInfo, MultipartUploadResult, PartInfo, }; -use rustfs_storage_api::{MultipartOperations as _, ObjectIO as _, ObjectOperations as _}; +use rustfs_storage_api::{MultipartOperations as _, NamespaceLocking as _, ObjectIO as _, ObjectOperations as _}; use rustfs_utils::http::headers::AMZ_OBJECT_TAGGING; use rustfs_utils::http::headers::AMZ_STORAGE_CLASS; use rustfs_utils::http::headers::{ @@ -1729,7 +1729,10 @@ impl SetDisks { } #[async_trait::async_trait] -impl NamespaceLocking for SetDisks { +impl rustfs_storage_api::NamespaceLocking for SetDisks { + type Error = Error; + type NamespaceLock = NamespaceLockWrapper; + #[tracing::instrument(skip(self))] async fn new_ns_lock(&self, bucket: &str, object: &str) -> Result { let set_lock = if is_dist_erasure().await { @@ -4144,7 +4147,11 @@ impl rustfs_storage_api::MultipartOperations for SetDisks { } #[async_trait::async_trait] -impl HealOperations for SetDisks { +impl rustfs_storage_api::HealOperations for SetDisks { + type Error = Error; + type HealResultItem = HealResultItem; + type HealOptions = HealOpts; + #[tracing::instrument(skip(self))] async fn heal_format(&self, _dry_run: bool) -> Result<(HealResultItem, Option)> { unimplemented!() @@ -5019,7 +5026,7 @@ mod tests { use rustfs_filemeta::ReplicationState; use rustfs_lock::client::local::LocalClient; use rustfs_lock::{LockError, LockInfo, LockResponse, LockStats}; - use rustfs_storage_api::{CompletePart, ObjectOperations as _}; + use rustfs_storage_api::{CompletePart, NamespaceLocking as _, ObjectOperations as _}; use serial_test::serial; use std::collections::HashMap; use tempfile::TempDir; diff --git a/crates/ecstore/src/set_disk/heal.rs b/crates/ecstore/src/set_disk/heal.rs index 28f594660..9efdad716 100644 --- a/crates/ecstore/src/set_disk/heal.rs +++ b/crates/ecstore/src/set_disk/heal.rs @@ -14,6 +14,7 @@ use super::*; use rustfs_config::{DEFAULT_OBJECT_ZERO_COPY_ENABLE, ENV_OBJECT_ZERO_COPY_ENABLE}; +use rustfs_storage_api::NamespaceLocking as _; impl SetDisks { #[tracing::instrument(skip(self, opts), fields(bucket = %bucket, object = %object, version_id = %version_id))] diff --git a/crates/ecstore/src/sets.rs b/crates/ecstore/src/sets.rs index 545a4a7a9..ed79f76eb 100644 --- a/crates/ecstore/src/sets.rs +++ b/crates/ecstore/src/sets.rs @@ -28,8 +28,8 @@ use crate::{ global::{GLOBAL_LOCAL_DISK_SET_DRIVES, get_global_lock_clients, is_dist_erasure}, set_disk::SetDisks, store_api::{ - DeletedObject, GetObjectReader, HealOperations, ListObjectVersionsInfo, ListObjectsV2Info, NamespaceLocking, ObjectInfo, - ObjectOptions, ObjectToDelete, PutObjReader, + DeletedObject, GetObjectReader, ListObjectVersionsInfo, ListObjectsV2Info, ObjectInfo, ObjectOptions, ObjectToDelete, + PutObjReader, }, store_init::{check_format_erasure_values, get_format_erasure_in_quorum, load_format_erasure_all, save_format_file}, }; @@ -826,7 +826,11 @@ impl rustfs_storage_api::MultipartOperations for Sets { } #[async_trait::async_trait] -impl HealOperations for Sets { +impl rustfs_storage_api::HealOperations for Sets { + type Error = Error; + type HealResultItem = HealResultItem; + type HealOptions = HealOpts; + #[tracing::instrument(skip(self))] async fn heal_format(&self, dry_run: bool) -> Result<(HealResultItem, Option)> { let (disks, _) = init_storage_disks_with_errors( @@ -938,7 +942,10 @@ impl HealOperations for Sets { } #[async_trait::async_trait] -impl NamespaceLocking for Sets { +impl rustfs_storage_api::NamespaceLocking for Sets { + type Error = Error; + type NamespaceLock = NamespaceLockWrapper; + async fn new_ns_lock(&self, bucket: &str, object: &str) -> Result { self.disk_set[0].new_ns_lock(bucket, object).await } diff --git a/crates/ecstore/src/store.rs b/crates/ecstore/src/store.rs index 0355ef753..c1960030e 100644 --- a/crates/ecstore/src/store.rs +++ b/crates/ecstore/src/store.rs @@ -62,10 +62,7 @@ use crate::{ endpoints::EndpointServerPools, rpc::S3PeerSys, sets::Sets, - store_api::{ - DeletedObject, GetObjectReader, HealOperations, ListObjectsV2Info, NamespaceLocking, ObjectInfo, ObjectOptions, - ObjectToDelete, PutObjReader, - }, + store_api::{DeletedObject, GetObjectReader, ListObjectsV2Info, ObjectInfo, ObjectOptions, ObjectToDelete, PutObjReader}, store_init, }; use futures::future::join_all; @@ -708,7 +705,11 @@ impl rustfs_storage_api::MultipartOperations for ECStore { } #[async_trait::async_trait] -impl HealOperations for ECStore { +impl rustfs_storage_api::HealOperations for ECStore { + type Error = Error; + type HealResultItem = HealResultItem; + type HealOptions = HealOpts; + #[instrument(skip(self))] async fn heal_format(&self, dry_run: bool) -> Result<(HealResultItem, Option)> { self.handle_heal_format(dry_run).await @@ -741,7 +742,10 @@ impl HealOperations for ECStore { } #[async_trait::async_trait] -impl NamespaceLocking for ECStore { +impl rustfs_storage_api::NamespaceLocking for ECStore { + type Error = Error; + type NamespaceLock = NamespaceLockWrapper; + async fn new_ns_lock(&self, bucket: &str, object: &str) -> Result { self.handle_new_ns_lock(bucket, object).await } diff --git a/crates/ecstore/src/store/bucket.rs b/crates/ecstore/src/store/bucket.rs index 8b330575c..8dea47bef 100644 --- a/crates/ecstore/src/store/bucket.rs +++ b/crates/ecstore/src/store/bucket.rs @@ -19,6 +19,7 @@ use crate::bucket::{ }; use crate::global::get_global_bucket_monitor; use crate::set_disk::get_lock_acquire_timeout; +use rustfs_storage_api::NamespaceLocking as _; fn should_override_created_from_metadata(created: OffsetDateTime) -> bool { created != OffsetDateTime::UNIX_EPOCH diff --git a/crates/ecstore/src/store/heal.rs b/crates/ecstore/src/store/heal.rs index ccef0de02..2e698c514 100644 --- a/crates/ecstore/src/store/heal.rs +++ b/crates/ecstore/src/store/heal.rs @@ -13,6 +13,7 @@ // limitations under the License. use super::*; +use rustfs_storage_api::HealOperations as _; const LOG_COMPONENT_ECSTORE: &str = "ecstore"; const LOG_SUBSYSTEM_HEAL: &str = "heal"; diff --git a/crates/ecstore/src/store/rebalance.rs b/crates/ecstore/src/store/rebalance.rs index c16bc75a4..475739663 100644 --- a/crates/ecstore/src/store/rebalance.rs +++ b/crates/ecstore/src/store/rebalance.rs @@ -14,7 +14,7 @@ use super::*; use crate::config::get_global_storage_class; -use rustfs_storage_api::{ObjectOperations as _, StorageAdminApi}; +use rustfs_storage_api::{NamespaceLocking as _, ObjectOperations as _, StorageAdminApi}; struct LatestObjectInfoCandidate { info: Option, diff --git a/crates/ecstore/src/store_api/traits.rs b/crates/ecstore/src/store_api/traits.rs index 854ce79bc..5fd75a167 100644 --- a/crates/ecstore/src/store_api/traits.rs +++ b/crates/ecstore/src/store_api/traits.rs @@ -131,24 +131,27 @@ impl MultipartOperations for T where } /// Healing and repair operations. -#[async_trait::async_trait] -pub trait HealOperations: Send + Sync + Debug { - async fn heal_format(&self, dry_run: bool) -> Result<(HealResultItem, Option)>; - async fn heal_bucket(&self, bucket: &str, opts: &HealOpts) -> Result; - async fn heal_object( - &self, - bucket: &str, - object: &str, - version_id: &str, - opts: &HealOpts, - ) -> Result<(HealResultItem, Option)>; - async fn get_pool_and_set(&self, id: &str) -> Result<(Option, Option, Option)>; - async fn check_abandoned_parts(&self, bucket: &str, object: &str, opts: &HealOpts) -> Result<()>; +pub trait HealOperations: + rustfs_storage_api::HealOperations + Send + Sync + Debug +{ +} + +impl HealOperations for T where + T: rustfs_storage_api::HealOperations + + Send + + Sync + + Debug +{ } /// Namespace lock operations needed by consumers that only coordinate object /// mutations but do not require the full storage API surface. -#[async_trait::async_trait] -pub trait NamespaceLocking: Send + Sync + Debug + 'static { - async fn new_ns_lock(&self, bucket: &str, object: &str) -> Result; +pub trait NamespaceLocking: + rustfs_storage_api::NamespaceLocking + Send + Sync + Debug + 'static +{ +} + +impl NamespaceLocking for T where + T: rustfs_storage_api::NamespaceLocking + Send + Sync + Debug + 'static +{ } diff --git a/crates/ecstore/tests/ecstore_contract_compat_test.rs b/crates/ecstore/tests/ecstore_contract_compat_test.rs index f311f2ce2..fe5674e65 100644 --- a/crates/ecstore/tests/ecstore_contract_compat_test.rs +++ b/crates/ecstore/tests/ecstore_contract_compat_test.rs @@ -12,8 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -use rustfs_ecstore::{disk::DiskStore, error::Error, store::ECStore, store_api::NamespaceLocking}; -use rustfs_storage_api::StorageAdminApi; +use rustfs_common::heal_channel::HealOpts; +use rustfs_ecstore::{ + disk::DiskStore, + error::Error, + store::ECStore, + store_api::{HealOperations, NamespaceLocking}, +}; +use rustfs_lock::NamespaceLockWrapper; +use rustfs_madmin::heal_commands::HealResultItem; +use rustfs_storage_api::{HealOperations as StorageHealOperations, NamespaceLocking as StorageNamespaceLocking, StorageAdminApi}; fn storage_admin_api_type_name() -> &'static str where @@ -34,6 +42,27 @@ where std::any::type_name::() } +fn heal_operations_type_name() -> &'static str +where + T: HealOperations, +{ + std::any::type_name::() +} + +fn storage_namespace_locking_type_name() -> &'static str +where + T: StorageNamespaceLocking, +{ + std::any::type_name::() +} + +fn storage_heal_operations_type_name() -> &'static str +where + T: StorageHealOperations, +{ + std::any::type_name::() +} + #[test] fn ecstore_implements_storage_admin_api_contract() { assert!(storage_admin_api_type_name::().ends_with("::ECStore")); @@ -43,3 +72,18 @@ fn ecstore_implements_storage_admin_api_contract() { fn ecstore_implements_namespace_locking_contract() { assert!(namespace_locking_type_name::().ends_with("::ECStore")); } + +#[test] +fn ecstore_implements_heal_operations_contract() { + assert!(heal_operations_type_name::().ends_with("::ECStore")); +} + +#[test] +fn ecstore_implements_storage_namespace_locking_contract() { + assert!(storage_namespace_locking_type_name::().ends_with("::ECStore")); +} + +#[test] +fn ecstore_implements_storage_heal_operations_contract() { + assert!(storage_heal_operations_type_name::().ends_with("::ECStore")); +} diff --git a/crates/heal/src/heal/storage.rs b/crates/heal/src/heal/storage.rs index 60913f799..b005770cd 100644 --- a/crates/heal/src/heal/storage.rs +++ b/crates/heal/src/heal/storage.rs @@ -19,11 +19,12 @@ use rustfs_ecstore::{ disk::{DiskStore, endpoint::Endpoint}, error::StorageError, store::ECStore, - store_api::{HealOperations, ObjectOptions}, + store_api::ObjectOptions, }; use rustfs_madmin::heal_commands::HealResultItem; use rustfs_storage_api::{ - BucketInfo, BucketOperations, DiskSetSelector, ListOperations as _, ObjectIO as _, ObjectOperations as _, StorageAdminApi, + BucketInfo, BucketOperations, DiskSetSelector, HealOperations as _, ListOperations as _, ObjectIO as _, + ObjectOperations as _, StorageAdminApi, }; use std::sync::Arc; use tracing::{debug, error, warn}; diff --git a/crates/scanner/src/scanner.rs b/crates/scanner/src/scanner.rs index 266262340..a58b21662 100644 --- a/crates/scanner/src/scanner.rs +++ b/crates/scanner/src/scanner.rs @@ -45,8 +45,8 @@ use rustfs_ecstore::disk::RUSTFS_META_BUCKET; use rustfs_ecstore::error::Error as EcstoreError; use rustfs_ecstore::global::is_erasure_sd; use rustfs_ecstore::store::ECStore; -use rustfs_ecstore::store_api::{NamespaceLocking as _, ObjectIO}; -use rustfs_storage_api::{BucketOperations, BucketOptions}; +use rustfs_ecstore::store_api::ObjectIO; +use rustfs_storage_api::{BucketOperations, BucketOptions, NamespaceLocking as _}; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; use tokio::time::{Duration, Instant}; diff --git a/crates/storage-api/src/lib.rs b/crates/storage-api/src/lib.rs index 1a54d71ab..7207ba3cc 100644 --- a/crates/storage-api/src/lib.rs +++ b/crates/storage-api/src/lib.rs @@ -25,7 +25,7 @@ pub use bucket::{BucketInfo, BucketOperations, BucketOptions, DeleteBucketOption pub use error::{StorageErrorCode, StorageResult}; pub use multipart::{CompletePart, ListMultipartsInfo, ListPartsInfo, MultipartInfo, MultipartUploadResult, PartInfo}; pub use object::{HTTPPreconditions, HTTPRangeError, HTTPRangeSpec, ObjectLockRetentionOptions}; +pub use object::{HealOperations, MultipartOperations, NamespaceLocking, ObjectIO, ObjectOperations}; pub use object::{ListObjectVersionsInfo, ListObjectsInfo, ListObjectsV2Info, ListOperations, ObjectInfoOrErr}; -pub use object::{MultipartOperations, ObjectIO, ObjectOperations}; pub use object::{ObjectPreconditionError, ObjectPreconditionPart, ObjectPreconditionState}; pub use object::{VersionMarker, WalkOptions, WalkVersionsSortOrder}; diff --git a/crates/storage-api/src/object.rs b/crates/storage-api/src/object.rs index acec1802e..ecae2be95 100644 --- a/crates/storage-api/src/object.rs +++ b/crates/storage-api/src/object.rs @@ -408,6 +408,33 @@ pub trait MultipartOperations: Send + Sync + fmt::Debug { ) -> Result; } +#[async_trait::async_trait] +pub trait HealOperations: Send + Sync + fmt::Debug { + type Error: std::error::Error + Send + Sync + 'static; + type HealResultItem: Send + 'static; + type HealOptions: Send + Sync + 'static; + + async fn heal_format(&self, dry_run: bool) -> Result<(Self::HealResultItem, Option), Self::Error>; + async fn heal_bucket(&self, bucket: &str, opts: &Self::HealOptions) -> Result; + async fn heal_object( + &self, + bucket: &str, + object: &str, + version_id: &str, + opts: &Self::HealOptions, + ) -> Result<(Self::HealResultItem, Option), Self::Error>; + async fn get_pool_and_set(&self, id: &str) -> Result<(Option, Option, Option), Self::Error>; + async fn check_abandoned_parts(&self, bucket: &str, object: &str, opts: &Self::HealOptions) -> Result<(), Self::Error>; +} + +#[async_trait::async_trait] +pub trait NamespaceLocking: Send + Sync + fmt::Debug + 'static { + type Error: std::error::Error + Send + Sync + 'static; + type NamespaceLock: Send + 'static; + + async fn new_ns_lock(&self, bucket: &str, object: &str) -> Result; +} + #[derive(Debug, Default)] pub struct ListObjectsInfo { pub is_truncated: bool, diff --git a/docs/architecture/crate-boundaries.md b/docs/architecture/crate-boundaries.md index 5a3271aee..b08738711 100644 --- a/docs/architecture/crate-boundaries.md +++ b/docs/architecture/crate-boundaries.md @@ -95,12 +95,13 @@ Required `rustfs-storage-api` public re-exports: - `pub use error::{StorageErrorCode, StorageResult};` - `pub use multipart::{CompletePart, ListMultipartsInfo, ListPartsInfo, MultipartInfo, MultipartUploadResult, PartInfo};` - `pub use object::{HTTPPreconditions, HTTPRangeError, HTTPRangeSpec, ObjectLockRetentionOptions};` +- `pub use object::{HealOperations, MultipartOperations, NamespaceLocking, ObjectIO, ObjectOperations};` - `pub use object::{ListObjectVersionsInfo, ListObjectsInfo, ListObjectsV2Info, ListOperations, ObjectInfoOrErr};` - `pub use object::{ObjectPreconditionError, ObjectPreconditionPart, ObjectPreconditionState};` - `pub use object::{VersionMarker, WalkOptions, WalkVersionsSortOrder};` -ECStore must keep compile-time coverage for both `StorageAdminApi` and the -separate `NamespaceLocking` operation group. +ECStore must keep compile-time coverage for `StorageAdminApi`, `HealOperations`, +and the separate `NamespaceLocking` operation group. The old `StorageAPI` aggregate facade must not reappear in production `crates/ecstore/src` or `rustfs/src` code after the storage operation groups diff --git a/docs/architecture/migration-progress.md b/docs/architecture/migration-progress.md index a4cc6b139..df163e0d4 100644 --- a/docs/architecture/migration-progress.md +++ b/docs/architecture/migration-progress.md @@ -5,19 +5,19 @@ 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-object-operation-contracts` -- Baseline: `main` at `3fb4cb3d65a2e037fc2e5ede32bf81c1f15c9fb7` - after the list operations contract merge. +- Branch: `overtrue/arch-storage-operation-consumer-cleanup` +- Baseline: `main` at `e5cad7ed207265f5312c58d8dac6042e31ba95a3` + after the object and multipart operation contract merge. - PR type for this branch: `api-extraction` - Runtime behavior changes: no external behavior change expected. -- Rust code changes: move `ObjectIO`, `ObjectOperations`, and - `MultipartOperations` into `rustfs-storage-api` as generic operation - contracts with associated ECStore-bound types, then keep ECStore's existing - public names as fixed associated-type compatibility subtraits. -- CI/script changes: extend migration guards for the object/multipart operation - public re-exports and ECStore local-method regressions. -- Docs changes: record the object and multipart operation contract extraction - slice. +- Rust code changes: move `HealOperations` and `NamespaceLocking` into + `rustfs-storage-api` as generic operation contracts with associated + ECStore-bound types, then keep ECStore's existing public names as fixed + associated-type compatibility subtraits. +- CI/script changes: extend migration guards for the heal/namespace-lock + operation public re-exports and ECStore local-method regressions. +- Docs changes: record the heal and namespace-lock operation contract + extraction slice. ## Phase 0 Tasks @@ -709,6 +709,28 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block - Verification: focused storage-api tests, ECStore/RustFS/downstream compile checks, migration/layer guards, formatting, diff hygiene, Rust risk scan, full pre-commit, and required three-expert review passed. +- [x] `API-023` Move heal and namespace-lock operation contracts. + - Completed slice: move `HealOperations` and `NamespaceLocking` from ECStore + `store_api/traits.rs` into `rustfs-storage-api` as generic public + operation contracts over ECStore heal result/options, namespace-lock + wrapper, and error associated types; keep ECStore's old public trait names + as fixed associated-type compatibility subtraits. + - Acceptance: `rustfs-storage-api` exports the heal and namespace-lock + operation contracts, ECStore no longer defines local heal/namespace-lock + method signatures, focused consumers use the shared trait for method + resolution, and migration guards reject dropping the public storage-api + re-export or reintroducing local ECStore method definitions. + - Must preserve: heal format/bucket/object behavior, abandoned-part checks, + pool/set lookup behavior, namespace-lock acquisition behavior, ECStore + public compatibility bounds, and all runtime lock/heal implementation + bodies. + - Risk defense: only the trait contracts cross into `rustfs-storage-api`; + ECStore keeps concrete associated type bindings, `HealOpts`, + `HealResultItem`, `NamespaceLockWrapper`, lock implementation, peer heal + behavior, set/pool dispatch, and storage error mapping. + - Verification: focused storage-api/ECStore/RustFS/heal/scanner compile + checks, migration/layer guards, formatting, diff hygiene, Rust risk scan, + full pre-commit, and required three-expert review passed. ## Phase 8 Background Controller Tasks @@ -986,17 +1008,17 @@ Status values: `[ ]` not started, `[~]` in progress, `[x]` complete, `[!]` block | Expert | Status | Notes | |---|---|---| -| Quality/architecture | passed | Generic object I/O, object operation, and multipart operation traits now live in `rustfs-storage-api`; ECStore still owns concrete type bindings and implementations. | -| Migration preservation | passed | Object read/write, metadata/tag/delete, multipart, and existing ECStore generic bound import paths are preserved through compatibility subtraits. | -| Testing/verification | passed | Focused storage-api tests, downstream compile checks, migration/layer guards, formatting, diff hygiene, Rust risk scan, and full `make pre-commit` passed. | +| Quality/architecture | passed | Generic heal and namespace-lock traits now live in `rustfs-storage-api`; ECStore still owns concrete type bindings and implementations. | +| Migration preservation | passed | Existing ECStore public trait names remain as fixed associated-type compatibility subtraits while method resolution moves to shared traits where needed. | +| Testing/verification | passed | Focused storage-api tests, ECStore compat tests, downstream compile checks, migration/layer guards, formatting, diff hygiene, Rust risk scan, and full `make pre-commit` passed. | ## Verification Notes Passed before push: - `cargo test -p rustfs-storage-api`: passed. -- `cargo check --tests -p rustfs-storage-api -p rustfs-ecstore -p rustfs -p rustfs-scanner -p rustfs-protocols`: passed. -- `cargo check --tests -p rustfs-heal -p rustfs-protocols`: passed. +- `cargo test -p rustfs-ecstore --test ecstore_contract_compat_test`: passed. +- `cargo check --tests -p rustfs-storage-api -p rustfs-ecstore -p rustfs -p rustfs-heal -p rustfs-scanner`: passed. - `./scripts/check_architecture_migration_rules.sh`: passed. - `./scripts/check_layer_dependencies.sh`: passed. - `cargo fmt --all --check`: passed. @@ -1004,28 +1026,26 @@ Passed before push: - Rust risk scan: no new `unwrap`/`expect`, panic/todo markers, `unsafe`, process-spawning calls, lossy casts, println/eprintln, or relaxed ordering in added Rust lines. -- `make pre-commit`: passed; nextest reported 6204 tests passed and 111 +- `make pre-commit`: passed; nextest reported 6207 tests passed and 111 skipped, and doctests passed. Notes: -- This slice follows the list operations contract branch and keeps the old +- This slice follows the object and multipart operation contract branch and keeps the old aggregate facade, bucket DTO, multipart DTO, bucket operation contract, object helper, range helper, list helper, object precondition, list response, and - walk/list operation contract guards active. -- The shared object and multipart operation contracts are now owned by + walk/list/object/multipart operation contract guards active. +- The shared heal and namespace-lock operation contracts are now owned by `rustfs-storage-api`; ECStore keeps the concrete associated type bindings, - readers, `ObjectInfo`, `ObjectOptions`, `PutObjReader`, filemeta adaptation, - storage error mapping, lifecycle/replication, rio, compression/encryption, - and implementation behavior. -- The slice does not alter object, list, walk, multipart, bucket, delete, - namespace-lock, reader, or tag/metadata runtime behavior. + `HealOpts`, `HealResultItem`, `NamespaceLockWrapper`, lock implementation, + peer heal behavior, set/pool dispatch, and storage error mapping. +- The slice does not alter heal, namespace-lock, object, list, walk, + multipart, bucket, delete, reader, or tag/metadata runtime behavior. ## Handoff Notes -- Object and multipart operation contract cleanup is stacked on the list - operations contract - branch. -- After this lands, remaining storage work can continue by extracting larger - low-coupling object metadata/option/reader slices or by narrowing remaining - operation-group consumers. +- Heal and namespace-lock operation contract cleanup is based on the merged + object and multipart operation contract branch. +- After this lands, remaining storage work can continue by removing low-value + ECStore compatibility imports or extracting larger low-coupling object + metadata/option/reader slices. diff --git a/rustfs/src/admin/handlers/heal.rs b/rustfs/src/admin/handlers/heal.rs index 7b48d2fd5..97ed0a8ae 100644 --- a/rustfs/src/admin/handlers/heal.rs +++ b/rustfs/src/admin/handlers/heal.rs @@ -25,10 +25,10 @@ use matchit::Params; use rustfs_common::heal_channel::{HealChannelPriority, HealChannelRequest, HealOpts, HealRequestSource}; use rustfs_config::MAX_HEAL_REQUEST_SIZE; use rustfs_ecstore::bucket::utils::is_valid_object_prefix; -use rustfs_ecstore::store_api::HealOperations; use rustfs_ecstore::store_utils::is_reserved_or_invalid_bucket; use rustfs_policy::policy::action::{Action, AdminAction}; use rustfs_scanner::scanner::{BackgroundHealInfo, read_background_heal_info}; +use rustfs_storage_api::HealOperations as _; use rustfs_utils::path::path_join; use s3s::header::{CONTENT_LENGTH, CONTENT_TYPE}; use s3s::{Body, S3Request, S3Response, S3Result, s3_error}; diff --git a/rustfs/src/app/capacity_dirty_scope_test.rs b/rustfs/src/app/capacity_dirty_scope_test.rs index 1ae627255..de948d612 100644 --- a/rustfs/src/app/capacity_dirty_scope_test.rs +++ b/rustfs/src/app/capacity_dirty_scope_test.rs @@ -18,10 +18,10 @@ use rustfs_ecstore::{ disk::endpoint::Endpoint, endpoints::{EndpointServerPools, Endpoints, PoolEndpoints}, store::ECStore, - store_api::{HealOperations, ObjectOptions, PutObjReader}, + store_api::{ObjectOptions, PutObjReader}, }; use rustfs_object_capacity::capacity_manager::{HybridStrategyConfig, create_isolated_manager}; -use rustfs_storage_api::{BucketOperations, BucketOptions, MakeBucketOptions, ObjectIO as _}; +use rustfs_storage_api::{BucketOperations, BucketOptions, HealOperations as _, MakeBucketOptions, ObjectIO as _}; use serial_test::serial; use std::{ collections::HashSet, diff --git a/scripts/check_architecture_migration_rules.sh b/scripts/check_architecture_migration_rules.sh index 9b1fb93ae..a22ad0973 100755 --- a/scripts/check_architecture_migration_rules.sh +++ b/scripts/check_architecture_migration_rules.sh @@ -204,7 +204,7 @@ require_source_line \ "storage-api public list response contract re-export" require_source_line \ "crates/storage-api/src/lib.rs" \ - "pub use object::{MultipartOperations, ObjectIO, ObjectOperations};" \ + "pub use object::{HealOperations, MultipartOperations, NamespaceLocking, ObjectIO, ObjectOperations};" \ "storage-api public object operation contract re-export" require_source_line \ "crates/storage-api/src/lib.rs" \ @@ -300,12 +300,12 @@ fi ( cd "$ROOT_DIR" - rg -n --no-heading 'async fn (get_object_reader|put_object|get_object_info|verify_object_integrity|copy_object|delete_object_version|delete_object|delete_objects|put_object_metadata|get_object_tags|put_object_tags|delete_object_tags|add_partial|transition_object|restore_transitioned_object|list_multipart_uploads|new_multipart_upload|copy_object_part|put_object_part|get_multipart_info|list_object_parts|abort_multipart_upload|complete_multipart_upload)\b' \ + rg -n --no-heading 'async fn (get_object_reader|put_object|get_object_info|verify_object_integrity|copy_object|delete_object_version|delete_object|delete_objects|put_object_metadata|get_object_tags|put_object_tags|delete_object_tags|add_partial|transition_object|restore_transitioned_object|list_multipart_uploads|new_multipart_upload|copy_object_part|put_object_part|get_multipart_info|list_object_parts|abort_multipart_upload|complete_multipart_upload|heal_format|heal_bucket|heal_object|get_pool_and_set|check_abandoned_parts|new_ns_lock)\b' \ crates/ecstore/src/store_api/traits.rs || true ) >"$STORE_API_OBJECT_OPERATION_LOCAL_METHOD_HITS_FILE" if [[ -s "$STORE_API_OBJECT_OPERATION_LOCAL_METHOD_HITS_FILE" ]]; then - report_failure "old ecstore object/multipart operation method signatures reintroduced: $(paste -sd '; ' "$STORE_API_OBJECT_OPERATION_LOCAL_METHOD_HITS_FILE")" + report_failure "old ecstore operation method signatures reintroduced: $(paste -sd '; ' "$STORE_API_OBJECT_OPERATION_LOCAL_METHOD_HITS_FILE")" fi require_source_contains \ @@ -322,8 +322,12 @@ require_source_contains \ "ECStore MultipartOperations compatibility binding" require_source_contains \ "crates/ecstore/src/store_api/traits.rs" \ - "pub trait NamespaceLocking: Send + Sync + Debug + 'static" \ - "separate namespace-locking operation-group trait" + "rustfs_storage_api::HealOperations<" \ + "ECStore HealOperations compatibility binding" +require_source_contains \ + "crates/ecstore/src/store_api/traits.rs" \ + "rustfs_storage_api::NamespaceLocking<" \ + "ECStore NamespaceLocking compatibility binding" require_source_contains \ "crates/ecstore/tests/ecstore_contract_compat_test.rs" \ "fn ecstore_implements_storage_admin_api_contract()" \ @@ -332,6 +336,18 @@ require_source_contains \ "crates/ecstore/tests/ecstore_contract_compat_test.rs" \ "fn ecstore_implements_namespace_locking_contract()" \ "ECStore NamespaceLocking compile-time coverage test" +require_source_contains \ + "crates/ecstore/tests/ecstore_contract_compat_test.rs" \ + "fn ecstore_implements_heal_operations_contract()" \ + "ECStore HealOperations compile-time coverage test" +require_source_contains \ + "crates/ecstore/tests/ecstore_contract_compat_test.rs" \ + "fn ecstore_implements_storage_namespace_locking_contract()" \ + "ECStore storage-api NamespaceLocking compile-time coverage test" +require_source_contains \ + "crates/ecstore/tests/ecstore_contract_compat_test.rs" \ + "fn ecstore_implements_storage_heal_operations_contract()" \ + "ECStore storage-api HealOperations compile-time coverage test" if (( FAILURES > 0 )); then exit 1