// 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. // #730: store startup/listing helpers are staged while global state is moved to owners. #![allow(dead_code)] #![allow(clippy::map_entry)] use crate::bucket::lifecycle::bucket_lifecycle_audit::LcEventSrc; use crate::bucket::lifecycle::bucket_lifecycle_ops::{ enqueue_immediate_expiry, enqueue_transition_immediate, init_background_expiry, }; use crate::bucket::metadata_sys; use crate::bucket::utils::check_abort_multipart_args; use crate::bucket::utils::check_complete_multipart_args; use crate::bucket::utils::check_copy_obj_args; use crate::bucket::utils::check_del_obj_args; use crate::bucket::utils::check_get_obj_args; use crate::bucket::utils::check_list_multipart_args; use crate::bucket::utils::check_list_parts_args; use crate::bucket::utils::check_new_multipart_args; use crate::bucket::utils::check_object_args; use crate::bucket::utils::check_put_object_args; use crate::bucket::utils::check_put_object_part_args; use crate::bucket::utils::{check_valid_bucket_name, check_valid_bucket_name_strict, is_meta_bucketname}; use crate::cluster::rpc::{RemoteClient, S3PeerSys}; use crate::config::storageclass; use crate::core::pools::PoolMeta; use crate::disk::endpoint::{Endpoint, EndpointType}; use crate::disk::{DiskAPI, DiskInfo, DiskInfoOptions}; use crate::error::{Error, Result}; use crate::error::{ StorageError, is_err_bucket_exists, is_err_bucket_not_found, is_err_invalid_upload_id, is_err_object_not_found, is_err_read_quorum, is_err_version_not_found, to_object_err, }; use crate::runtime::global::DISK_RESERVE_FRACTION; use crate::runtime::instance::InstanceContext; use crate::runtime::sources as runtime_sources; use crate::services::rebalance::RebalanceMeta; use crate::storage_api_contracts::{ bucket::{BucketInfo, BucketOperations, BucketOptions, DeleteBucketOptions, MakeBucketOptions}, list::{StorageListObjectVersionsInfo, StorageListObjectsV2Info, StorageObjectInfoOrErr, StorageWalkOptions}, multipart::{CompletePart, ListMultipartsInfo, ListPartsInfo, MultipartInfo, MultipartUploadResult, PartInfo}, object::{DeletedObject, ObjectToDelete}, range::HTTPRangeSpec, }; use crate::store::init_format::{check_disk_fatal_errs, ec_drives_no_config}; use crate::{ bucket::{lifecycle::bucket_lifecycle_ops::TransitionState, metadata::BucketMetadata}, core::sets::Sets, disk::{BUCKET_META_PREFIX, DiskOption, DiskStore, RUSTFS_META_BUCKET}, layout::endpoints::EndpointServerPools, object_api::{GetObjectReader, ObjectInfo, ObjectOptions, PutObjReader}, }; use futures::future::join_all; use http::HeaderMap; use lazy_static::lazy_static; use rand::RngExt as _; use rustfs_common::heal_channel::{HealItemType, HealOpts}; use rustfs_config::server_config::Config; use rustfs_filemeta::FileInfo; use rustfs_lock::{LocalClient, LockClient, NamespaceLockWrapper}; use rustfs_madmin::heal_commands::HealResultItem; use rustfs_utils::path::{decode_dir_object, encode_dir_object, path_join_buf}; use s3s::dto::{BucketVersioningStatus, ObjectLockConfiguration, ObjectLockEnabled, VersioningConfiguration}; use std::net::SocketAddr; use std::process::exit; use std::{collections::HashMap, sync::Arc, time::Duration}; use time::OffsetDateTime; use tokio::select; use tokio::sync::{Mutex, RwLock}; use tokio::time::sleep; use tokio_util::sync::CancellationToken; use tracing::{debug, error, info, instrument, warn}; use uuid::Uuid; type ListObjectsV2Info = StorageListObjectsV2Info; type ListObjectVersionsInfo = StorageListObjectVersionsInfo; type ObjectInfoOrErr = StorageObjectInfoOrErr; type WalkOptions = StorageWalkOptions bool>; /// Check if a directory contains any xl.meta files (indicating actual S3 objects) /// This is used to determine if a bucket is empty for deletion purposes. async fn has_xlmeta_files(path: &std::path::Path) -> bool { use crate::disk::STORAGE_FORMAT_FILE; use tokio::fs; let mut stack = vec![path.to_path_buf()]; while let Some(current_path) = stack.pop() { let mut entries = match fs::read_dir(¤t_path).await { Ok(entries) => entries, Err(_) => continue, }; while let Ok(Some(entry)) = entries.next_entry().await { let file_name = entry.file_name(); let file_name_str = file_name.to_string_lossy(); // Skip hidden files/directories (like .rustfs.sys) if file_name_str.starts_with('.') { continue; } // Check if this is an xl.meta file if file_name_str == STORAGE_FORMAT_FILE { return true; } // If it's a directory, add to stack for further exploration if let Ok(file_type) = entry.file_type().await && file_type.is_dir() { stack.push(entry.path()); } } } false } async fn enqueue_transition_after_write(result: Result, src: LcEventSrc) -> Result { match result { Ok(oi) => { if should_enqueue_transition_immediately(&oi) { enqueue_transition_immediate(&oi, src.clone()).await; enqueue_immediate_expiry(&oi, src).await; } Ok(oi) } Err(err) => Err(err), } } fn should_enqueue_transition_immediately(oi: &ObjectInfo) -> bool { !is_meta_bucketname(&oi.bucket) } const MAX_UPLOADS_LIST: usize = 10000; mod bucket; mod heal; mod heal_walk; pub use heal_walk::HealWalkVersion; mod init; pub(crate) mod init_format; mod list; pub(crate) mod list_objects; mod multipart; mod object; pub use object::PreparedGetObjectReader; mod peer; mod rebalance; pub(crate) mod utils; use peer::init_local_peer; pub use peer::{ all_local_disk, all_local_disk_path, find_local_disk_by_ref, get_disk_infos, init_local_disks, init_local_disks_with_instance_ctx, init_lock_clients, prewarm_local_disk_id_map, prewarm_local_disk_id_map_with_instance_ctx, }; pub struct ECStore { pub id: Uuid, // pub disks: Vec, pub disk_map: HashMap>>, pub pools: Vec>, pub peer_sys: S3PeerSys, // pub local_disks: Vec, pub pool_meta: RwLock, pub rebalance_meta: RwLock>, pub decommission_cancelers: RwLock>>, /// Serializes rebalance/decommission start transitions. /// /// Lock order: acquire `start_gate` before `pool_meta`, `rebalance_meta`, /// or `decommission_cancelers`. The guarded sections may perform bounded /// async metadata work so check/init/start cannot race across operations. pub(crate) start_gate: Mutex<()>, /// Serializes full-document pool metadata saves. /// /// Lock order: acquire `pool_meta_save_gate` without holding `pool_meta`. /// The saver then clones the latest `pool_meta` under a short read lock and /// releases it before awaiting disk writes. pub(crate) pool_meta_save_gate: Mutex<()>, /// Per-instance runtime state (Phase 5, backlog#939). /// /// Carries this instance's identity/runtime out of the process globals so /// multiple instances can coexist without cross-contamination. `new` /// adopts the process bootstrap context (never mints a fresh one) so that /// startup writes and post-construction reads share one cell — single /// instance behavior is unchanged. pub(crate) ctx: Arc, } impl std::fmt::Debug for ECStore { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ECStore") .field("id", &self.id) .field("disk_map", &self.disk_map) .field("pools", &self.pools) .field("pool_meta", &self.pool_meta) .finish_non_exhaustive() } } /// Phase 2: Accessor methods for config globals /// These delegate to the process-global statics. No local state — the globals /// remain the single source of truth until the migration is complete. impl ECStore { /// Get server configuration (delegates to global) pub fn get_server_config(&self) -> Option { runtime_sources::server_config() } /// Set server configuration (delegates to global) pub fn set_server_config(&self, cfg: Config) { runtime_sources::set_server_config(cfg); } /// Get storage class configuration (delegates to global) pub fn get_storage_class(&self) -> Option { runtime_sources::storage_class_config() } /// Set storage class configuration (delegates to global) pub fn set_storage_class(&self, cfg: crate::config::storageclass::Config) { runtime_sources::set_storage_class_config(cfg); } } /// Phase 3: Accessor methods for service globals /// These provide a unified API through ECStore for accessing cross-cutting /// service singletons. The globals remain the source of truth. impl ECStore { /// Get the notification system pub fn notification_system(&self) -> Option> { runtime_sources::notification_sys() } /// Get the bucket metadata system pub fn bucket_metadata_sys(&self) -> Option>> { runtime_sources::bucket_metadata_sys() } /// Get the global endpoints pub fn endpoints(&self) -> EndpointServerPools { runtime_sources::endpoint_pools().unwrap_or_else(|| Vec::new().into()) } /// Get this store instance's endpoint topology without consulting process globals. pub fn instance_endpoints(&self) -> Option { self.ctx.endpoints() } /// Get the global region pub fn region(&self) -> Option { runtime_sources::region() } /// Get the tier config manager pub fn tier_config_mgr(&self) -> Arc> { self.ctx.tier_config_mgr() } /// Get the server configuration pub fn server_config(&self) -> Option { runtime_sources::server_config() } /// Get the storage class configuration pub fn storage_class(&self) -> Option { runtime_sources::storage_class_config() } } /// Phase 4: Server address accessors /// These provide a unified API through ECStore for accessing server-level /// configuration globals. The globals remain the source of truth. impl ECStore { /// Get the server port pub fn port(&self) -> u16 { runtime_sources::rustfs_port() } /// Get the server host pub async fn host(&self) -> String { runtime_sources::rustfs_host().await } /// Get the server address (host:port) pub async fn addr(&self) -> String { runtime_sources::rustfs_addr().await } } /// Phase 5: Per-instance erasure setup accessors (backlog#939) /// /// These read this instance's own [`InstanceContext`] rather than a process /// global, so two instances carrying different contexts stay isolated. The /// legacy free-function facade (`runtime::global::is_erasure` etc.) forwards to /// the current instance's context, preserving single-instance behavior. impl ECStore { /// Whether this instance uses erasure coding (single-node or distributed). pub async fn setup_is_erasure(&self) -> bool { self.ctx.is_erasure().await } /// Whether this instance uses distributed erasure coding. pub async fn setup_is_dist_erasure(&self) -> bool { self.ctx.is_dist_erasure().await } /// Whether this instance uses single-drive erasure coding. pub async fn setup_is_erasure_sd(&self) -> bool { self.ctx.is_erasure_sd().await } pub fn scanner_namespace_mutation_generation(&self) -> u64 { list_objects::scanner_namespace_mutation_generation() } } // impl Clone for ECStore { // fn clone(&self) -> Self { // let pool_meta = match self.pool_meta.read() { // Ok(pool_meta) => pool_meta.clone(), // Err(_) => PoolMeta::default(), // }; // Self { // id: self.id.clone(), // disk_map: self.disk_map.clone(), // pools: self.pools.clone(), // peer_sys: self.peer_sys.clone(), // pool_meta: std_RwLock::new(pool_meta), // decommission_cancelers: self.decommission_cancelers.clone(), // } // } // } // #[derive(Debug, Default, Clone)] // pub struct ListPathOptions { // pub id: String, // // Bucket of the listing. // pub bucket: String, // // Directory inside the bucket. // // When unset listPath will set this based on Prefix // pub base_dir: String, // // Scan/return only content with prefix. // pub prefix: String, // // FilterPrefix will return only results with this prefix when scanning. // // Should never contain a slash. // // Prefix should still be set. // pub filter_prefix: String, // // Marker to resume listing. // // The response will be the first entry >= this object name. // pub marker: String, // // Limit the number of results. // pub limit: i32, // } #[async_trait::async_trait] impl crate::storage_api_contracts::object::ObjectIO for ECStore { type Error = Error; type RangeSpec = HTTPRangeSpec; type HeaderMap = HeaderMap; type ObjectOptions = ObjectOptions; type ObjectInfo = ObjectInfo; type GetObjectReader = GetObjectReader; type PutObjectReader = PutObjReader; #[instrument(level = "debug", skip(self))] async fn get_object_reader( &self, bucket: &str, object: &str, range: Option, h: HeaderMap, opts: &ObjectOptions, ) -> Result { self.handle_get_object_reader(bucket, object, range, h, opts).await } #[instrument(level = "debug", skip(self, data))] async fn put_object(&self, bucket: &str, object: &str, data: &mut PutObjReader, opts: &ObjectOptions) -> Result { self.put_object_with_old_current_size(bucket, object, data, opts) .await .map(|(object_info, _)| object_info) } } impl ECStore { /// `put_object` plus the rename_data old-size backfill /// (rustfs/backlog#1009); see `SetDisks::put_object_with_old_current_size`. /// Post-write hooks (immediate ILM transition enqueue, list-cache /// invalidation) match the plain `put_object` path exactly. #[instrument(level = "debug", skip(self, data))] pub async fn put_object_with_old_current_size( &self, bucket: &str, object: &str, data: &mut PutObjReader, opts: &ObjectOptions, ) -> Result<(ObjectInfo, Option)> { let result = match self.handle_put_object(bucket, object, data, opts).await { Ok((object_info, old_current_size)) => enqueue_transition_after_write(Ok(object_info), LcEventSrc::S3PutObject) .await .map(|object_info| (object_info, old_current_size)), Err(err) => Err(err), }; if result.is_ok() { list_objects::observe_list_objects_mutation(self, bucket).await; } result } } lazy_static! { static ref enableObjcetLockConfig: ObjectLockConfiguration = ObjectLockConfiguration { object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)), ..Default::default() }; static ref enableVersioningConfig: VersioningConfiguration = VersioningConfiguration { status: Some(BucketVersioningStatus::from_static(BucketVersioningStatus::ENABLED)), ..Default::default() }; } #[async_trait::async_trait] impl BucketOperations for ECStore { type Error = Error; #[instrument(skip(self))] async fn make_bucket(&self, bucket: &str, opts: &MakeBucketOptions) -> Result<()> { let result = self.handle_make_bucket(bucket, opts).await; if result.is_ok() { list_objects::observe_scanner_namespace_mutations(bucket, 1); } result } #[instrument(skip(self))] async fn get_bucket_info(&self, bucket: &str, opts: &BucketOptions) -> Result { self.handle_get_bucket_info(bucket, opts).await } #[instrument(skip(self))] async fn list_bucket(&self, opts: &BucketOptions) -> Result> { self.handle_list_bucket(opts).await } #[instrument(skip(self))] async fn delete_bucket(&self, bucket: &str, opts: &DeleteBucketOptions) -> Result<()> { let result = self.handle_delete_bucket(bucket, opts).await; if result.is_ok() { list_objects::observe_scanner_namespace_mutations(bucket, 1); } result } } #[async_trait::async_trait] impl crate::storage_api_contracts::object::ObjectOperations for ECStore { type Error = Error; type ObjectInfo = ObjectInfo; type ObjectOptions = ObjectOptions; type FileInfo = FileInfo; type ObjectToDelete = ObjectToDelete; type DeletedObject = DeletedObject; async fn get_object_info(&self, bucket: &str, object: &str, opts: &ObjectOptions) -> Result { self.handle_get_object_info(bucket, object, opts).await } async fn verify_object_integrity(&self, bucket: &str, object: &str, opts: &ObjectOptions) -> Result<()> { self.handle_verify_object_integrity(bucket, object, opts).await } #[instrument(skip(self))] async fn copy_object( &self, src_bucket: &str, src_object: &str, dst_bucket: &str, dst_object: &str, src_info: &mut ObjectInfo, src_opts: &ObjectOptions, dst_opts: &ObjectOptions, ) -> Result { let result = enqueue_transition_after_write( self.handle_copy_object(src_bucket, src_object, dst_bucket, dst_object, src_info, src_opts, dst_opts) .await, LcEventSrc::S3CopyObject, ) .await; if result.is_ok() { list_objects::observe_list_objects_mutation(self, dst_bucket).await; } result } #[instrument(skip(self))] async fn delete_object_version(&self, bucket: &str, object: &str, fi: &FileInfo, force_del_marker: bool) -> Result<()> { let result = self.handle_delete_object_version(bucket, object, fi, force_del_marker).await; if result.is_ok() { list_objects::observe_list_objects_mutation(self, bucket).await; } result } #[instrument(skip(self))] async fn delete_object(&self, bucket: &str, object: &str, opts: ObjectOptions) -> Result { let result = self.handle_delete_object(bucket, object, opts).await; if result.is_ok() { list_objects::observe_list_objects_mutation(self, bucket).await; } result } #[instrument(skip(self))] async fn delete_objects( &self, bucket: &str, objects: Vec, opts: ObjectOptions, ) -> (Vec, Vec>) { let result = self.handle_delete_objects(bucket, objects, opts).await; let success_count = result.1.iter().filter(|err| err.is_none()).count(); if success_count > 0 { list_objects::observe_list_objects_mutations(self, bucket, success_count).await; } result } #[instrument(skip(self))] async fn put_object_metadata(&self, bucket: &str, object: &str, opts: &ObjectOptions) -> Result { self.handle_put_object_metadata(bucket, object, opts).await } #[instrument(skip(self))] async fn get_object_tags(&self, bucket: &str, object: &str, opts: &ObjectOptions) -> Result { self.handle_get_object_tags(bucket, object, opts).await } #[instrument(level = "debug", skip(self))] async fn put_object_tags(&self, bucket: &str, object: &str, tags: &str, opts: &ObjectOptions) -> Result { self.handle_put_object_tags(bucket, object, tags, opts).await } #[instrument(skip(self))] async fn delete_object_tags(&self, bucket: &str, object: &str, opts: &ObjectOptions) -> Result { self.handle_delete_object_tags(bucket, object, opts).await } #[instrument(skip(self))] async fn add_partial(&self, bucket: &str, object: &str, version_id: &str) -> Result<()> { self.handle_add_partial(bucket, object, version_id).await } #[instrument(skip(self))] async fn transition_object(&self, bucket: &str, object: &str, opts: &ObjectOptions) -> Result<()> { self.handle_transition_object(bucket, object, opts).await } #[instrument(skip(self))] async fn restore_transitioned_object(self: Arc, bucket: &str, object: &str, opts: &ObjectOptions) -> Result<()> { self.handle_restore_transitioned_object(bucket, object, opts).await } } #[async_trait::async_trait] impl crate::storage_api_contracts::list::ListOperations for ECStore { type Error = Error; type ListObjectsV2Info = ListObjectsV2Info; type ListObjectVersionsInfo = ListObjectVersionsInfo; type ObjectInfoOrErr = ObjectInfoOrErr; type WalkOptions = WalkOptions; type WalkCancellation = CancellationToken; type WalkResultSender = tokio::sync::mpsc::Sender; // @continuation_token marker // @start_after as marker when continuation_token empty // @delimiter default="/", empty when recursive // @max_keys limit #[instrument(skip(self))] async fn list_objects_v2( self: Arc, bucket: &str, prefix: &str, continuation_token: Option, delimiter: Option, max_keys: i32, fetch_owner: bool, start_after: Option, incl_deleted: bool, ) -> Result { self.handle_list_objects_v2( bucket, prefix, continuation_token, delimiter, max_keys, fetch_owner, start_after, incl_deleted, ) .await } #[instrument(skip(self))] async fn list_object_versions( self: Arc, bucket: &str, prefix: &str, marker: Option, version_marker: Option, delimiter: Option, max_keys: i32, ) -> Result { self.handle_list_object_versions(bucket, prefix, marker, version_marker, delimiter, max_keys) .await } async fn walk( self: Arc, rx: CancellationToken, bucket: &str, prefix: &str, result: tokio::sync::mpsc::Sender, opts: WalkOptions, ) -> Result<()> { self.handle_walk(rx, bucket, prefix, result, opts).await } } #[async_trait::async_trait] impl crate::storage_api_contracts::multipart::MultipartOperations for ECStore { type Error = Error; type ObjectInfo = ObjectInfo; type ObjectOptions = ObjectOptions; type PutObjectReader = PutObjReader; type CompletePart = CompletePart; type ListMultipartsInfo = ListMultipartsInfo; type MultipartUploadResult = MultipartUploadResult; type PartInfo = PartInfo; type MultipartInfo = MultipartInfo; type ListPartsInfo = ListPartsInfo; #[instrument(skip(self))] async fn list_multipart_uploads( &self, bucket: &str, prefix: &str, key_marker: Option, upload_id_marker: Option, delimiter: Option, max_uploads: usize, ) -> Result { self.handle_list_multipart_uploads(bucket, prefix, key_marker, upload_id_marker, delimiter, max_uploads) .await } #[instrument(skip(self))] async fn new_multipart_upload(&self, bucket: &str, object: &str, opts: &ObjectOptions) -> Result { self.handle_new_multipart_upload(bucket, object, opts).await } #[instrument(skip(self))] async fn copy_object_part( &self, src_bucket: &str, src_object: &str, _dst_bucket: &str, _dst_object: &str, _upload_id: &str, _part_id: usize, _start_offset: i64, _length: i64, _src_info: &ObjectInfo, _src_opts: &ObjectOptions, _dst_opts: &ObjectOptions, ) -> Result<()> { self.handle_copy_object_part( src_bucket, src_object, _dst_bucket, _dst_object, _upload_id, _part_id, _start_offset, _length, _src_info, _src_opts, _dst_opts, ) .await } #[instrument(skip(self, data))] async fn put_object_part( &self, bucket: &str, object: &str, upload_id: &str, part_id: usize, data: &mut PutObjReader, opts: &ObjectOptions, ) -> Result { self.handle_put_object_part(bucket, object, upload_id, part_id, data, opts) .await } #[instrument(skip(self))] async fn get_multipart_info( &self, bucket: &str, object: &str, upload_id: &str, opts: &ObjectOptions, ) -> Result { self.handle_get_multipart_info(bucket, object, upload_id, opts).await } #[instrument(skip(self))] async fn list_object_parts( &self, bucket: &str, object: &str, upload_id: &str, part_number_marker: Option, max_parts: usize, opts: &ObjectOptions, ) -> Result { self.handle_list_object_parts(bucket, object, upload_id, part_number_marker, max_parts, opts) .await } #[instrument(skip(self))] async fn abort_multipart_upload(&self, bucket: &str, object: &str, upload_id: &str, opts: &ObjectOptions) -> Result<()> { self.handle_abort_multipart_upload(bucket, object, upload_id, opts).await } #[instrument(skip(self))] async fn complete_multipart_upload( self: Arc, bucket: &str, object: &str, upload_id: &str, uploaded_parts: Vec, opts: &ObjectOptions, ) -> Result { let result = enqueue_transition_after_write( self.clone() .handle_complete_multipart_upload(bucket, object, upload_id, uploaded_parts, opts) .await, LcEventSrc::S3CompleteMultipartUpload, ) .await; if result.is_ok() { list_objects::observe_list_objects_mutation(self.as_ref(), bucket).await; } result } } #[async_trait::async_trait] impl crate::storage_api_contracts::heal::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 } #[instrument(skip(self))] async fn heal_bucket(&self, bucket: &str, opts: &HealOpts) -> Result { self.handle_heal_bucket(bucket, opts).await } #[instrument(skip(self))] async fn heal_object( &self, bucket: &str, object: &str, version_id: &str, opts: &HealOpts, ) -> Result<(HealResultItem, Option)> { self.handle_heal_object(bucket, object, version_id, opts).await } #[instrument(skip(self))] async fn get_pool_and_set(&self, id: &str) -> Result<(Option, Option, Option)> { self.handle_get_pool_and_set(id).await } #[instrument(skip(self))] async fn check_abandoned_parts(&self, bucket: &str, object: &str, opts: &HealOpts) -> Result<()> { self.handle_check_abandoned_parts(bucket, object, opts).await } } #[async_trait::async_trait] impl crate::storage_api_contracts::namespace::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 } } #[async_trait::async_trait] impl crate::storage_api_contracts::admin::StorageAdminApi for ECStore { type BackendInfo = rustfs_madmin::BackendInfo; type StorageInfo = rustfs_madmin::StorageInfo; type Disk = DiskStore; type Error = Error; #[instrument(skip(self))] async fn backend_info(&self) -> Self::BackendInfo { self.handle_backend_info().await } #[instrument(skip(self))] async fn storage_info(&self) -> Self::StorageInfo { self.handle_storage_info().await } #[instrument(skip(self))] async fn local_storage_info(&self) -> Self::StorageInfo { self.handle_local_storage_info().await } #[instrument(skip(self))] async fn disk_set_inventory( &self, selector: crate::storage_api_contracts::admin::DiskSetSelector, ) -> Result>> { self.handle_get_disks(selector.pool_idx, selector.set_idx).await } #[instrument(skip(self))] fn set_drive_counts(&self) -> Vec { self.handle_set_drive_counts() } } #[cfg(test)] mod tests { use super::*; use crate::layout::endpoints::{Endpoints, PoolEndpoints, SetupType}; use crate::runtime::global::reset_local_disk_test_state; use crate::runtime::sources::{clear_local_disk_id_map_for_test, local_disk_path_by_id}; use crate::store::init_format::{connect_load_init_formats, init_disks}; use serial_test::serial; use tempfile::TempDir; #[tokio::test] async fn test_get_disk_infos() { let disks = vec![None, None]; // Empty disks for testing let infos = get_disk_infos(&disks).await; assert_eq!(infos.len(), disks.len()); // All should be None since we passed None disks assert!(infos.iter().all(|info| info.is_none())); } // Build a minimal ECStore carrying an explicit instance context. Empty // pools/disks are sufficient: the Phase 5 accessors read only `self.ctx`. fn build_store_with_ctx(ctx: Arc) -> ECStore { let endpoint_pools = EndpointServerPools::default(); ECStore { id: uuid::Uuid::new_v4(), disk_map: std::collections::HashMap::new(), pools: Vec::new(), peer_sys: crate::cluster::rpc::S3PeerSys::new_with_instance_ctx(&endpoint_pools, ctx.clone()), pool_meta: RwLock::new(PoolMeta::default()), rebalance_meta: RwLock::new(None), decommission_cancelers: RwLock::new(Vec::new()), start_gate: Mutex::new(()), pool_meta_save_gate: Mutex::new(()), ctx, } } // The object graph is the isolation carrier: two ECStore instances holding // distinct contexts report independent erasure state through their real // `&self` accessors — no cross-contamination. #[tokio::test] async fn instance_context_carrier_isolates_two_stores() { let ctx_a = Arc::new(InstanceContext::new()); let ctx_b = Arc::new(InstanceContext::new()); ctx_a.update_erasure_type(SetupType::DistErasure).await; ctx_b.update_erasure_type(SetupType::ErasureSD).await; ctx_a.set_endpoints(EndpointServerPools::from(vec![PoolEndpoints { legacy: false, set_count: 1, drives_per_set: 1, endpoints: Endpoints::default(), cmd_line: "instance-a".to_string(), platform: String::new(), }])); ctx_b.set_endpoints(EndpointServerPools::from(vec![PoolEndpoints { legacy: true, set_count: 2, drives_per_set: 2, endpoints: Endpoints::default(), cmd_line: "instance-b".to_string(), platform: String::new(), }])); let store_a = build_store_with_ctx(ctx_a); let store_b = build_store_with_ctx(ctx_b); // store_a: distributed erasure (implies is_erasure), not single-drive. assert!(store_a.setup_is_erasure().await); assert!(store_a.setup_is_dist_erasure().await); assert!(!store_a.setup_is_erasure_sd().await); // store_b: single-drive erasure only. assert!(store_b.setup_is_erasure_sd().await); assert!(!store_b.setup_is_erasure().await); assert!(!store_b.setup_is_dist_erasure().await); let endpoints_a = store_a.instance_endpoints().expect("instance A endpoints"); let endpoints_b = store_b.instance_endpoints().expect("instance B endpoints"); assert_eq!(endpoints_a.as_ref()[0].set_count, 1); assert_eq!(endpoints_b.as_ref()[0].set_count, 2); assert!(!endpoints_a.as_ref()[0].legacy); assert!(endpoints_b.as_ref()[0].legacy); } // The production/test constructors ADOPT the process bootstrap context // (same Arc), so a startup write recorded before the store existed is // visible through the store afterward — single-instance behavior preserved. #[tokio::test] async fn store_adopts_bootstrap_context() { let store = build_store_with_ctx(crate::runtime::instance::bootstrap_ctx()); assert!( Arc::ptr_eq(&store.ctx, &crate::runtime::instance::bootstrap_ctx()), "store built via adoption must share the bootstrap context Arc" ); } #[tokio::test] async fn test_has_space_for() { let disk_infos = vec![None, None]; // No actual disk info let result = crate::layout::pool_space::has_space_for(&disk_infos, 1024).await; // Should fail due to no valid disk info assert!(result.is_err()); } #[tokio::test] async fn test_find_local_disk() { let result = peer::find_local_disk("/nonexistent/path").await; assert!(result.is_none(), "Should return None for nonexistent path"); } #[tokio::test] #[serial] async fn test_find_local_disk_by_ref_backfills_uuid_map() { reset_local_disk_test_state().await; let temp_dir = TempDir::new().expect("create temp dir for local disk ref test"); let disk_paths = (0..4) .map(|idx| temp_dir.path().join(format!("disk{}", idx + 1))) .collect::>(); for disk_path in &disk_paths { std::fs::create_dir_all(disk_path).expect("create disk path"); } let mut endpoints = Vec::new(); for (idx, disk_path) in disk_paths.iter().enumerate() { let mut endpoint = Endpoint::try_from(disk_path.to_str().expect("disk path to str")).expect("endpoint"); endpoint.set_pool_index(0); endpoint.set_set_index(0); endpoint.set_disk_index(idx); endpoints.push(endpoint); } let endpoint_pools = EndpointServerPools(vec![PoolEndpoints { legacy: false, set_count: 1, drives_per_set: 4, endpoints: Endpoints::from(endpoints), cmd_line: "find-local-disk-by-ref-test".to_string(), platform: "test".to_string(), }]); init_local_disks(endpoint_pools.clone()).await.expect("init local disks"); let (disks, errs) = init_disks( &endpoint_pools.as_ref().first().expect("pool endpoints").endpoints, &DiskOption { cleanup: true, health_check: false, }, ) .await; assert!(errs.iter().all(|err| err.is_none()), "disk init should succeed: {errs:?}"); connect_load_init_formats(true, &disks, 1, 4, None) .await .expect("initialize format metadata"); clear_local_disk_id_map_for_test().await; let local_disks = all_local_disk().await; let first_disk = local_disks.first().expect("local disk exists"); let disk_id = first_disk .get_disk_id() .await .expect("get disk id should succeed") .expect("disk id should exist"); let found = find_local_disk_by_ref(&disk_id.to_string()).await; assert!(found.is_some(), "disk lookup by id should backfill cache"); assert_eq!(local_disk_path_by_id(&disk_id).await, Some(first_disk.endpoint().to_string())); reset_local_disk_test_state().await; } #[tokio::test] async fn test_all_local_disk_path() { let paths = all_local_disk_path().await; // Should return empty or some paths depending on global state assert!(paths.is_empty() || !paths.is_empty()); } #[tokio::test] async fn test_all_local_disk() { let disks = all_local_disk().await; // Should return empty or some disks depending on global state assert!(disks.is_empty() || !disks.is_empty()); } #[test] fn test_should_not_enqueue_transition_for_internal_metadata_bucket() { let oi = ObjectInfo { bucket: RUSTFS_META_BUCKET.to_string(), name: format!("{BUCKET_META_PREFIX}/bucket/.metadata.bin"), ..Default::default() }; assert!(!should_enqueue_transition_immediately(&oi)); } }