mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-11 23:56:53 +00:00
ab35681928
* perf(ecstore): memoize bucket-incarnation fence validation under lifecycle read-lock coverage The PUT commit fence from #5648 validated the bucket incarnation with an uncached read (a distributed metadata-transaction read lock plus an EC quorum read of bucket metadata) on every PUT commit. Under 64-concurrency 4KiB PUT load this adds two quorum round-trips per PUT and the resulting lock-manager pressure produced ~1,000 client-visible 'Lock acquisition timeout' failures per 5-minute window (see rustfs/backlog#1776). Memoize the validation per node while lifecycle read-lock coverage is continuous: bucket deletion/recreation requires the lifecycle WRITE lock, so while at least one read guard on this node has been held continuously the incarnation cannot have changed. The first fenced PUT in a coverage window performs the exact authoritative disk validation as before; overlapping PUTs reuse its result. The memo clears when the node's last guard drops or any guard observes a lost lock, so the next PUT revalidates from disk. Fence semantics are unchanged; only the redundant re-validations under continuous coverage are elided. Also right-size the s3s footprint ratchet baselines: -1 s3_error! line from this change's error-path consolidation, and +1 s3s-importing file inherited from #5763 (crates/obs/src/telemetry/filter.rs) which landed on main without the baseline bump. * fix(ecstore): carry the bucket fence registry through the rebalance test store The rebalance entry test constructor landed on main after this branch was cut and needs the new field.
1063 lines
38 KiB
Rust
1063 lines
38 KiB
Rust
// 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.
|
|
|
|
#![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_invalid_upload_id, is_err_object_not_found, is_err_read_quorum,
|
|
is_err_strict_volume_not_found, 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<ObjectInfo>;
|
|
type ListObjectVersionsInfo = StorageListObjectVersionsInfo<ObjectInfo>;
|
|
type ObjectInfoOrErr = StorageObjectInfoOrErr<ObjectInfo, Error>;
|
|
type WalkOptions = StorageWalkOptions<fn(&FileInfo) -> 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.
|
|
pub(crate) async fn has_xlmeta_files(path: &std::path::Path) -> std::io::Result<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(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
|
|
Err(err) => return Err(err),
|
|
};
|
|
|
|
while let Some(entry) = entries.next_entry().await? {
|
|
let file_name = entry.file_name();
|
|
let file_name_str = file_name.to_string_lossy();
|
|
|
|
// Check if this is an xl.meta file
|
|
if file_name_str == STORAGE_FORMAT_FILE {
|
|
return Ok(true);
|
|
}
|
|
|
|
// If it's a directory, add to stack for further exploration
|
|
if entry.file_type().await?.is_dir() {
|
|
stack.push(entry.path());
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(false)
|
|
}
|
|
|
|
async fn enqueue_transition_after_write(result: Result<ObjectInfo>, src: LcEventSrc) -> Result<ObjectInfo> {
|
|
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 bucket_fence;
|
|
pub(crate) use bucket::await_bucket_namespace_operation;
|
|
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<DiskStore>,
|
|
pub disk_map: HashMap<usize, Vec<Option<DiskStore>>>,
|
|
pub pools: Vec<Arc<Sets>>,
|
|
pub peer_sys: S3PeerSys,
|
|
// pub local_disks: Vec<DiskStore>,
|
|
pub pool_meta: RwLock<PoolMeta>,
|
|
pub rebalance_meta: RwLock<Option<RebalanceMeta>>,
|
|
pub decommission_cancelers: RwLock<Vec<Option<CancellationToken>>>,
|
|
/// 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<InstanceContext>,
|
|
/// Memoizes bucket-incarnation validation under continuous lifecycle
|
|
/// read-lock coverage (see [`bucket_fence`]).
|
|
pub(crate) bucket_fence_registry: Arc<bucket_fence::BucketFenceRegistry>,
|
|
}
|
|
|
|
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<Config> {
|
|
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<crate::config::storageclass::Config> {
|
|
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<std::sync::Arc<crate::services::notification_sys::NotificationSys>> {
|
|
runtime_sources::notification_sys()
|
|
}
|
|
|
|
/// Get the bucket metadata system
|
|
pub fn bucket_metadata_sys(&self) -> Option<Arc<tokio::sync::RwLock<crate::bucket::metadata_sys::BucketMetadataSys>>> {
|
|
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<EndpointServerPools> {
|
|
self.ctx.endpoints()
|
|
}
|
|
|
|
/// Get the global region
|
|
pub fn region(&self) -> Option<s3s::region::Region> {
|
|
runtime_sources::region()
|
|
}
|
|
|
|
/// Get the tier config manager
|
|
pub fn tier_config_mgr(&self) -> Arc<tokio::sync::RwLock<crate::services::tier::tier::TierConfigMgr>> {
|
|
self.ctx.tier_config_mgr()
|
|
}
|
|
|
|
/// Get the server configuration
|
|
pub fn server_config(&self) -> Option<Config> {
|
|
runtime_sources::server_config()
|
|
}
|
|
|
|
/// Get the storage class configuration
|
|
pub fn storage_class(&self) -> Option<crate::config::storageclass::Config> {
|
|
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()
|
|
}
|
|
|
|
pub async fn scanner_data_movement_active(&self) -> bool {
|
|
let (decommission, rebalance) = tokio::join!(self.is_decommission_running(), self.is_rebalance_started());
|
|
decommission || rebalance
|
|
}
|
|
}
|
|
|
|
// 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<HTTPRangeSpec>,
|
|
h: HeaderMap,
|
|
opts: &ObjectOptions,
|
|
) -> Result<GetObjectReader> {
|
|
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<ObjectInfo> {
|
|
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<crate::disk::OldCurrentSize>)> {
|
|
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 ENABLED_OBJECT_LOCK_CONFIG: ObjectLockConfiguration = ObjectLockConfiguration {
|
|
object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)),
|
|
..Default::default()
|
|
};
|
|
static ref ENABLED_VERSIONING_CONFIG: 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<()> {
|
|
Box::pin(self.handle_make_bucket(bucket, opts)).await
|
|
}
|
|
|
|
#[instrument(skip(self))]
|
|
async fn get_bucket_info(&self, bucket: &str, opts: &BucketOptions) -> Result<BucketInfo> {
|
|
self.handle_get_bucket_info(bucket, opts).await
|
|
}
|
|
#[instrument(skip(self))]
|
|
async fn list_bucket(&self, opts: &BucketOptions) -> Result<Vec<BucketInfo>> {
|
|
self.handle_list_bucket(opts).await
|
|
}
|
|
#[instrument(skip(self))]
|
|
async fn delete_bucket(&self, bucket: &str, opts: &DeleteBucketOptions) -> Result<()> {
|
|
Box::pin(self.handle_delete_bucket(bucket, opts)).await
|
|
}
|
|
}
|
|
|
|
#[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<ObjectInfo> {
|
|
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<ObjectInfo> {
|
|
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<ObjectInfo> {
|
|
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, objects, opts))]
|
|
async fn delete_objects(
|
|
&self,
|
|
bucket: &str,
|
|
objects: Vec<ObjectToDelete>,
|
|
opts: ObjectOptions,
|
|
) -> (Vec<DeletedObject>, Vec<Option<Error>>) {
|
|
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<ObjectInfo> {
|
|
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<String> {
|
|
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<ObjectInfo> {
|
|
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<ObjectInfo> {
|
|
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<Self>, 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<ObjectInfoOrErr>;
|
|
|
|
// @continuation_token marker
|
|
// @start_after as marker when continuation_token empty
|
|
// @delimiter default="/", empty when recursive
|
|
// @max_keys limit
|
|
#[instrument(level = "trace", skip(self))]
|
|
async fn list_objects_v2(
|
|
self: Arc<Self>,
|
|
bucket: &str,
|
|
prefix: &str,
|
|
continuation_token: Option<String>,
|
|
delimiter: Option<String>,
|
|
max_keys: i32,
|
|
fetch_owner: bool,
|
|
start_after: Option<String>,
|
|
incl_deleted: bool,
|
|
) -> Result<ListObjectsV2Info> {
|
|
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<Self>,
|
|
bucket: &str,
|
|
prefix: &str,
|
|
marker: Option<String>,
|
|
version_marker: Option<String>,
|
|
delimiter: Option<String>,
|
|
max_keys: i32,
|
|
) -> Result<ListObjectVersionsInfo> {
|
|
self.handle_list_object_versions(bucket, prefix, marker, version_marker, delimiter, max_keys)
|
|
.await
|
|
}
|
|
|
|
async fn walk(
|
|
self: Arc<Self>,
|
|
rx: CancellationToken,
|
|
bucket: &str,
|
|
prefix: &str,
|
|
result: tokio::sync::mpsc::Sender<ObjectInfoOrErr>,
|
|
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<String>,
|
|
upload_id_marker: Option<String>,
|
|
delimiter: Option<String>,
|
|
max_uploads: usize,
|
|
) -> Result<ListMultipartsInfo> {
|
|
self.handle_list_multipart_uploads(
|
|
bucket,
|
|
multipart::MultipartUploadListRequest {
|
|
prefix: prefix.to_string(),
|
|
key_marker,
|
|
upload_id_marker,
|
|
delimiter,
|
|
max_uploads,
|
|
expected_incarnation_id: None,
|
|
},
|
|
)
|
|
.await
|
|
}
|
|
|
|
#[instrument(skip(self))]
|
|
async fn new_multipart_upload(&self, bucket: &str, object: &str, opts: &ObjectOptions) -> Result<MultipartUploadResult> {
|
|
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<PartInfo> {
|
|
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<MultipartInfo> {
|
|
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<usize>,
|
|
max_parts: usize,
|
|
opts: &ObjectOptions,
|
|
) -> Result<ListPartsInfo> {
|
|
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<Self>,
|
|
bucket: &str,
|
|
object: &str,
|
|
upload_id: &str,
|
|
uploaded_parts: Vec<CompletePart>,
|
|
opts: &ObjectOptions,
|
|
) -> Result<ObjectInfo> {
|
|
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<Error>)> {
|
|
self.handle_heal_format(dry_run).await
|
|
}
|
|
|
|
#[instrument(skip(self))]
|
|
async fn heal_bucket(&self, bucket: &str, opts: &HealOpts) -> Result<HealResultItem> {
|
|
self.handle_heal_bucket(bucket, opts).await
|
|
}
|
|
#[instrument(level = "trace", skip(self, opts), fields(bucket = %bucket, object = %object, version_id = %version_id))]
|
|
async fn heal_object(
|
|
&self,
|
|
bucket: &str,
|
|
object: &str,
|
|
version_id: &str,
|
|
opts: &HealOpts,
|
|
) -> Result<(HealResultItem, Option<Error>)> {
|
|
self.handle_heal_object(bucket, object, version_id, opts).await
|
|
}
|
|
|
|
#[instrument(skip(self))]
|
|
async fn get_pool_and_set(&self, id: &str) -> Result<(Option<usize>, Option<usize>, Option<usize>)> {
|
|
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<NamespaceLockWrapper> {
|
|
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<Vec<Option<Self::Disk>>> {
|
|
self.handle_get_disks(selector.pool_idx, selector.set_idx).await
|
|
}
|
|
|
|
#[instrument(skip(self))]
|
|
fn set_drive_counts(&self) -> Vec<usize> {
|
|
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<InstanceContext>) -> Arc<ECStore> {
|
|
let endpoint_pools = EndpointServerPools::default();
|
|
Arc::new(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,
|
|
bucket_fence_registry: Arc::default(),
|
|
})
|
|
}
|
|
|
|
// 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::<Vec<_>>();
|
|
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 (mut 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, &mut 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));
|
|
}
|
|
}
|