mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-20 11:32:19 +00:00
fix(tier): sweep transitioned copies from delete handlers (#2287)
This commit is contained in:
@@ -45,7 +45,7 @@ use lazy_static::lazy_static;
|
|||||||
use rustfs_common::data_usage::TierStats;
|
use rustfs_common::data_usage::TierStats;
|
||||||
use rustfs_common::heal_channel::rep_has_active_rules;
|
use rustfs_common::heal_channel::rep_has_active_rules;
|
||||||
use rustfs_common::metrics::{IlmAction, Metrics};
|
use rustfs_common::metrics::{IlmAction, Metrics};
|
||||||
use rustfs_filemeta::{NULL_VERSION_ID, RestoreStatusOps, is_restored_object_on_disk};
|
use rustfs_filemeta::{FileInfo, NULL_VERSION_ID, RestoreStatusOps, is_restored_object_on_disk};
|
||||||
use rustfs_s3_common::EventName;
|
use rustfs_s3_common::EventName;
|
||||||
use rustfs_utils::{get_env_i64, get_env_usize, path::encode_dir_object, string::strings_has_prefix_fold};
|
use rustfs_utils::{get_env_i64, get_env_usize, path::encode_dir_object, string::strings_has_prefix_fold};
|
||||||
use s3s::Body;
|
use s3s::Body;
|
||||||
@@ -393,12 +393,81 @@ impl ExpiryState {
|
|||||||
//delete_object_versions(api, &v.bucket, &v.versions, v.event).await;
|
//delete_object_versions(api, &v.bucket, &v.versions, v.event).await;
|
||||||
}
|
}
|
||||||
else if v.as_any().is::<Jentry>() {
|
else if v.as_any().is::<Jentry>() {
|
||||||
//transitionLogIf(es.ctx, deleteObjectFromRemoteTier(es.ctx, v.ObjName, v.VersionID, v.TierName))
|
let v = v.as_any().downcast_ref::<Jentry>().expect("err!");
|
||||||
|
if let Err(err) = delete_object_from_remote_tier(&v.obj_name, &v.version_id, &v.tier_name).await {
|
||||||
|
warn!(
|
||||||
|
object = %v.obj_name,
|
||||||
|
version_id = %v.version_id,
|
||||||
|
tier = %v.tier_name,
|
||||||
|
error = ?err,
|
||||||
|
"failed to delete transitioned object from remote tier"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if v.as_any().is::<FreeVersionTask>() {
|
else if v.as_any().is::<FreeVersionTask>() {
|
||||||
let v = v.as_any().downcast_ref::<FreeVersionTask>().expect("err!");
|
let v = v.as_any().downcast_ref::<FreeVersionTask>().expect("err!");
|
||||||
let _oi = v.0.clone();
|
let oi = v.0.clone();
|
||||||
|
if let Err(err) = delete_object_from_remote_tier(
|
||||||
|
&oi.transitioned_object.name,
|
||||||
|
&oi.transitioned_object.version_id,
|
||||||
|
&oi.transitioned_object.tier,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
warn!(
|
||||||
|
bucket = %oi.bucket,
|
||||||
|
object = %oi.name,
|
||||||
|
remote_object = %oi.transitioned_object.name,
|
||||||
|
remote_version_id = %oi.transitioned_object.version_id,
|
||||||
|
tier = %oi.transitioned_object.tier,
|
||||||
|
error = ?err,
|
||||||
|
"failed to sweep transitioned free version from remote tier"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut fi = FileInfo {
|
||||||
|
name: oi.name.clone(),
|
||||||
|
version_id: oi.version_id,
|
||||||
|
deleted: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
fi.set_tier_free_version();
|
||||||
|
|
||||||
|
let mut deleted_locally = false;
|
||||||
|
for pool in api.pools.iter() {
|
||||||
|
let set = pool.get_disks_by_key(&oi.name);
|
||||||
|
match set.delete_object_version(&oi.bucket, &oi.name, &fi, false).await {
|
||||||
|
Ok(()) => {
|
||||||
|
deleted_locally = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(err) if is_err_version_not_found(&err) || is_err_object_not_found(&err) => continue,
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
bucket = %oi.bucket,
|
||||||
|
object = %oi.name,
|
||||||
|
remote_object = %oi.transitioned_object.name,
|
||||||
|
remote_version_id = %oi.transitioned_object.version_id,
|
||||||
|
tier = %oi.transitioned_object.tier,
|
||||||
|
error = ?err,
|
||||||
|
"failed to delete transitioned free version after remote tier sweep"
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !deleted_locally {
|
||||||
|
warn!(
|
||||||
|
bucket = %oi.bucket,
|
||||||
|
object = %oi.name,
|
||||||
|
remote_object = %oi.transitioned_object.name,
|
||||||
|
remote_version_id = %oi.transitioned_object.version_id,
|
||||||
|
tier = %oi.transitioned_object.tier,
|
||||||
|
"transitioned free version was not found during local cleanup"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
//info!("Invalid work type - {:?}", v);
|
//info!("Invalid work type - {:?}", v);
|
||||||
|
|||||||
@@ -120,9 +120,9 @@ impl ObjSweeper {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
#[allow(unused_assignments)]
|
#[allow(unused_assignments)]
|
||||||
pub struct Jentry {
|
pub struct Jentry {
|
||||||
obj_name: String,
|
pub(crate) obj_name: String,
|
||||||
version_id: String,
|
pub(crate) version_id: String,
|
||||||
tier_name: String,
|
pub(crate) tier_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExpiryOp for Jentry {
|
impl ExpiryOp for Jentry {
|
||||||
@@ -147,5 +147,37 @@ pub async fn delete_object_from_remote_tier(obj_name: &str, rv_id: &str, tier_na
|
|||||||
w.remove(obj_name, rv_id).await
|
w.remove(obj_name, rv_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn transitioned_delete_journal_entry(
|
||||||
|
version_id: Option<Uuid>,
|
||||||
|
versioned: bool,
|
||||||
|
suspended: bool,
|
||||||
|
transitioned: &TransitionedObject,
|
||||||
|
) -> Option<Jentry> {
|
||||||
|
let sweeper = ObjSweeper {
|
||||||
|
version_id,
|
||||||
|
versioned,
|
||||||
|
suspended,
|
||||||
|
transition_status: transitioned.status.clone(),
|
||||||
|
transition_tier: transitioned.tier.clone(),
|
||||||
|
transition_version_id: transitioned.version_id.clone(),
|
||||||
|
remote_object: transitioned.name.clone(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
sweeper.should_remove_remote_object()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transitioned_force_delete_journal_entry(transitioned: &TransitionedObject) -> Option<Jentry> {
|
||||||
|
if transitioned.status != lifecycle::TRANSITION_COMPLETE {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Jentry {
|
||||||
|
obj_name: transitioned.name.clone(),
|
||||||
|
version_id: transitioned.version_id.clone(),
|
||||||
|
tier_name: transitioned.tier.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {}
|
mod test {}
|
||||||
|
|||||||
@@ -1077,7 +1077,7 @@ impl ObjectOperations for SetDisks {
|
|||||||
|
|
||||||
let (mut metas, errs) = {
|
let (mut metas, errs) = {
|
||||||
if let Some(vid) = &src_opts.version_id {
|
if let Some(vid) = &src_opts.version_id {
|
||||||
Self::read_all_fileinfo(&disks, "", src_bucket, src_object, vid, true, false).await?
|
Self::read_all_fileinfo(&disks, "", src_bucket, src_object, vid, true, false, false).await?
|
||||||
} else {
|
} else {
|
||||||
Self::read_all_xl(&disks, src_bucket, src_object, true, false).await
|
Self::read_all_xl(&disks, src_bucket, src_object, true, false).await
|
||||||
}
|
}
|
||||||
@@ -1565,8 +1565,6 @@ impl ObjectOperations for SetDisks {
|
|||||||
return Ok(oi);
|
return Ok(oi);
|
||||||
}
|
}
|
||||||
|
|
||||||
let version_id = opts.version_id.as_ref().and_then(|v| Uuid::parse_str(v).ok());
|
|
||||||
|
|
||||||
// Create a single object deletion request
|
// Create a single object deletion request
|
||||||
let mut dfi = FileInfo {
|
let mut dfi = FileInfo {
|
||||||
name: object.to_string(),
|
name: object.to_string(),
|
||||||
@@ -1673,7 +1671,7 @@ impl ObjectOperations for SetDisks {
|
|||||||
|
|
||||||
let (metas, errs) = {
|
let (metas, errs) = {
|
||||||
if let Some(version_id) = &opts.version_id {
|
if let Some(version_id) = &opts.version_id {
|
||||||
Self::read_all_fileinfo(&disks, "", bucket, object, version_id.to_string().as_str(), false, false).await?
|
Self::read_all_fileinfo(&disks, "", bucket, object, version_id.to_string().as_str(), false, false, false).await?
|
||||||
} else {
|
} else {
|
||||||
Self::read_all_xl(&disks, bucket, object, false, false).await
|
Self::read_all_xl(&disks, bucket, object, false, false).await
|
||||||
}
|
}
|
||||||
@@ -3279,7 +3277,7 @@ impl HealOperations for SetDisks {
|
|||||||
let disks = self.disks.read().await;
|
let disks = self.disks.read().await;
|
||||||
|
|
||||||
let disks = disks.clone();
|
let disks = disks.clone();
|
||||||
let (_, errs) = Self::read_all_fileinfo(&disks, "", bucket, object, version_id, false, false).await?;
|
let (_, errs) = Self::read_all_fileinfo(&disks, "", bucket, object, version_id, false, false, false).await?;
|
||||||
if DiskError::is_all_not_found(&errs) {
|
if DiskError::is_all_not_found(&errs) {
|
||||||
warn!(
|
warn!(
|
||||||
"heal_object failed, all obj part not found, bucket: {}, obj: {}, version_id: {}",
|
"heal_object failed, all obj part not found, bucket: {}, obj: {}, version_id: {}",
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ impl SetDisks {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let (mut parts_metadata, errs) = Self::read_all_fileinfo(&disks, "", bucket, object, version_id, true, true).await?;
|
let (mut parts_metadata, errs) =
|
||||||
|
Self::read_all_fileinfo(&disks, "", bucket, object, version_id, true, true, false).await?;
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
parts_count = parts_metadata.len(),
|
parts_count = parts_metadata.len(),
|
||||||
|
|||||||
@@ -105,7 +105,8 @@ impl SetDisks {
|
|||||||
let disks = disks.clone();
|
let disks = disks.clone();
|
||||||
|
|
||||||
let (parts_metadata, errs) =
|
let (parts_metadata, errs) =
|
||||||
Self::read_all_fileinfo(&disks, bucket, RUSTFS_META_MULTIPART_BUCKET, &upload_id_path, "", false, false).await?;
|
Self::read_all_fileinfo(&disks, bucket, RUSTFS_META_MULTIPART_BUCKET, &upload_id_path, "", false, false, false)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let map_err_notfound = |err: DiskError| {
|
let map_err_notfound = |err: DiskError| {
|
||||||
if err == DiskError::FileNotFound {
|
if err == DiskError::FileNotFound {
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ impl SetDisks {
|
|||||||
Ok(ret)
|
Ok(ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
#[tracing::instrument(level = "debug", skip(disks))]
|
#[tracing::instrument(level = "debug", skip(disks))]
|
||||||
pub(super) async fn read_all_fileinfo(
|
pub(super) async fn read_all_fileinfo(
|
||||||
disks: &[Option<DiskStore>],
|
disks: &[Option<DiskStore>],
|
||||||
@@ -140,13 +141,14 @@ impl SetDisks {
|
|||||||
version_id: &str,
|
version_id: &str,
|
||||||
read_data: bool,
|
read_data: bool,
|
||||||
healing: bool,
|
healing: bool,
|
||||||
|
incl_free_versions: bool,
|
||||||
) -> disk::error::Result<(Vec<FileInfo>, Vec<Option<DiskError>>)> {
|
) -> disk::error::Result<(Vec<FileInfo>, Vec<Option<DiskError>>)> {
|
||||||
let mut ress = Vec::with_capacity(disks.len());
|
let mut ress = Vec::with_capacity(disks.len());
|
||||||
let mut errors = Vec::with_capacity(disks.len());
|
let mut errors = Vec::with_capacity(disks.len());
|
||||||
let opts = Arc::new(ReadOptions {
|
let opts = Arc::new(ReadOptions {
|
||||||
|
incl_free_versions,
|
||||||
read_data,
|
read_data,
|
||||||
healing,
|
healing,
|
||||||
..Default::default()
|
|
||||||
});
|
});
|
||||||
let org_bucket = Arc::new(org_bucket.to_string());
|
let org_bucket = Arc::new(org_bucket.to_string());
|
||||||
let bucket = Arc::new(bucket.to_string());
|
let bucket = Arc::new(bucket.to_string());
|
||||||
@@ -474,7 +476,8 @@ impl SetDisks {
|
|||||||
let vid = opts.version_id.clone().unwrap_or_default();
|
let vid = opts.version_id.clone().unwrap_or_default();
|
||||||
|
|
||||||
// TODO: optimize concurrency and break once enough slots are available
|
// TODO: optimize concurrency and break once enough slots are available
|
||||||
let (parts_metadata, errs) = Self::read_all_fileinfo(&disks, "", bucket, object, vid.as_str(), read_data, false).await?;
|
let (parts_metadata, errs) =
|
||||||
|
Self::read_all_fileinfo(&disks, "", bucket, object, vid.as_str(), read_data, false, opts.incl_free_versions).await?;
|
||||||
// warn!("get_object_fileinfo parts_metadata {:?}", &parts_metadata);
|
// warn!("get_object_fileinfo parts_metadata {:?}", &parts_metadata);
|
||||||
// warn!("get_object_fileinfo {}/{} errs {:?}", bucket, object, &errs);
|
// warn!("get_object_fileinfo {}/{} errs {:?}", bucket, object, &errs);
|
||||||
|
|
||||||
@@ -541,6 +544,9 @@ impl SetDisks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if fi.deleted {
|
if fi.deleted {
|
||||||
|
if opts.incl_free_versions && fi.tier_free_version() && opts.version_id.is_some() {
|
||||||
|
return (oi, write_quorum, None);
|
||||||
|
}
|
||||||
return if opts.version_id.is_none() || opts.delete_marker {
|
return if opts.version_id.is_none() || opts.delete_marker {
|
||||||
(oi, write_quorum, Some(to_object_err(StorageError::FileNotFound, vec![bucket, object])))
|
(oi, write_quorum, Some(to_object_err(StorageError::FileNotFound, vec![bucket, object])))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -286,7 +286,9 @@ impl ECStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !errs.is_empty() && !opts.versioned && !opts.version_suspended {
|
if !errs.is_empty() && !opts.versioned && !opts.version_suspended {
|
||||||
return self.delete_object_from_all_pools(bucket, object, &opts, errs).await;
|
let mut obj = self.delete_object_from_all_pools(bucket, object, &opts, errs).await?;
|
||||||
|
obj.name = decode_dir_object(object);
|
||||||
|
return Ok(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
for pool in self.pools.iter() {
|
for pool in self.pools.iter() {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ pub struct ObjectOptions {
|
|||||||
|
|
||||||
pub versioned: bool,
|
pub versioned: bool,
|
||||||
pub version_suspended: bool,
|
pub version_suspended: bool,
|
||||||
|
pub incl_free_versions: bool,
|
||||||
|
|
||||||
pub skip_decommissioned: bool,
|
pub skip_decommissioned: bool,
|
||||||
pub skip_rebalancing: bool,
|
pub skip_rebalancing: bool,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ use rand::seq::SliceRandom as _;
|
|||||||
use rustfs_common::heal_channel::HealScanMode;
|
use rustfs_common::heal_channel::HealScanMode;
|
||||||
use rustfs_common::metrics::{Metric, Metrics, emit_scan_bucket_drive_complete};
|
use rustfs_common::metrics::{Metric, Metrics, emit_scan_bucket_drive_complete};
|
||||||
use rustfs_ecstore::bucket::bucket_target_sys::BucketTargetSys;
|
use rustfs_ecstore::bucket::bucket_target_sys::BucketTargetSys;
|
||||||
|
use rustfs_ecstore::bucket::lifecycle::bucket_lifecycle_ops::GLOBAL_ExpiryState;
|
||||||
use rustfs_ecstore::bucket::lifecycle::lifecycle::Lifecycle;
|
use rustfs_ecstore::bucket::lifecycle::lifecycle::Lifecycle;
|
||||||
use rustfs_ecstore::bucket::metadata_sys::{get_lifecycle_config, get_object_lock_config, get_replication_config};
|
use rustfs_ecstore::bucket::metadata_sys::{get_lifecycle_config, get_object_lock_config, get_replication_config};
|
||||||
use rustfs_ecstore::bucket::replication::{ReplicationConfig, ReplicationConfigurationExt};
|
use rustfs_ecstore::bucket::replication::{ReplicationConfig, ReplicationConfigurationExt};
|
||||||
@@ -530,6 +531,11 @@ impl ScannerIODisk for Disk {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|v| ObjectInfo::from_file_info(v, item.bucket.as_str(), item.object_path().as_str(), versioned))
|
.map(|v| ObjectInfo::from_file_info(v, item.bucket.as_str(), item.object_path().as_str(), versioned))
|
||||||
.collect::<Vec<ObjectInfo>>();
|
.collect::<Vec<ObjectInfo>>();
|
||||||
|
let free_version_infos = fivs
|
||||||
|
.free_versions
|
||||||
|
.iter()
|
||||||
|
.map(|v| ObjectInfo::from_file_info(v, item.bucket.as_str(), item.object_path().as_str(), versioned))
|
||||||
|
.collect::<Vec<ObjectInfo>>();
|
||||||
|
|
||||||
let mut size_summary = SizeSummary::default();
|
let mut size_summary = SizeSummary::default();
|
||||||
|
|
||||||
@@ -563,9 +569,14 @@ impl ScannerIODisk for Disk {
|
|||||||
item.apply_actions(ecstore, object_infos, lock_config, &mut size_summary)
|
item.apply_actions(ecstore, object_infos, lock_config, &mut size_summary)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
done_object();
|
if !free_version_infos.is_empty() {
|
||||||
|
let mut expiry_state = GLOBAL_ExpiryState.write().await;
|
||||||
|
for oi in free_version_infos {
|
||||||
|
expiry_state.enqueue_free_version(oi).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: enqueueFreeVersion
|
done_object();
|
||||||
|
|
||||||
Ok(size_summary)
|
Ok(size_summary)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ use rustfs_ecstore::{
|
|||||||
bucket::{lifecycle::bucket_lifecycle_ops::enqueue_transition_for_existing_objects, metadata_sys},
|
bucket::{lifecycle::bucket_lifecycle_ops::enqueue_transition_for_existing_objects, metadata_sys},
|
||||||
client::transition_api::{ReadCloser, ReaderImpl},
|
client::transition_api::{ReadCloser, ReaderImpl},
|
||||||
disk::endpoint::Endpoint,
|
disk::endpoint::Endpoint,
|
||||||
|
disk::{DiskAPI, DiskOption, STORAGE_FORMAT_FILE, new_disk},
|
||||||
endpoints::{EndpointServerPools, Endpoints, PoolEndpoints},
|
endpoints::{EndpointServerPools, Endpoints, PoolEndpoints},
|
||||||
global::GLOBAL_TierConfigMgr,
|
global::GLOBAL_TierConfigMgr,
|
||||||
|
pools::path2_bucket_object_with_base_path,
|
||||||
store::ECStore,
|
store::ECStore,
|
||||||
store_api::{
|
store_api::{
|
||||||
BucketOperations, MakeBucketOptions, MultipartOperations, ObjectIO, ObjectOperations, ObjectOptions, PutObjReader,
|
BucketOperations, MakeBucketOptions, MultipartOperations, ObjectIO, ObjectOperations, ObjectOptions, PutObjReader,
|
||||||
@@ -29,13 +31,17 @@ use rustfs_ecstore::{
|
|||||||
warm_backend::{WarmBackend, WarmBackendGetOpts, build_transition_put_options},
|
warm_backend::{WarmBackend, WarmBackendGetOpts, build_transition_put_options},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use rustfs_filemeta::FileMeta;
|
||||||
use rustfs_scanner::scanner::init_data_scanner;
|
use rustfs_scanner::scanner::init_data_scanner;
|
||||||
|
use rustfs_scanner::scanner_folder::ScannerItem;
|
||||||
|
use rustfs_scanner::scanner_io::ScannerIODisk;
|
||||||
|
use rustfs_utils::path::path_join_buf;
|
||||||
use s3s::dto::RestoreRequest;
|
use s3s::dto::RestoreRequest;
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
io::Cursor,
|
io::Cursor,
|
||||||
path::PathBuf,
|
path::{Path, PathBuf},
|
||||||
sync::{Arc, Once, OnceLock},
|
sync::{Arc, Once, OnceLock},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
@@ -137,6 +143,70 @@ async fn setup_test_env() -> (Vec<PathBuf>, Arc<ECStore>) {
|
|||||||
(disk_paths, ecstore)
|
(disk_paths, ecstore)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn setup_isolated_test_env(init_expiry: bool) -> (Vec<PathBuf>, Arc<ECStore>) {
|
||||||
|
init_tracing();
|
||||||
|
|
||||||
|
let test_base_dir = format!("/tmp/rustfs_scanner_lifecycle_test_{}", uuid::Uuid::new_v4());
|
||||||
|
let temp_dir = std::path::PathBuf::from(&test_base_dir);
|
||||||
|
if temp_dir.exists() {
|
||||||
|
fs::remove_dir_all(&temp_dir).await.ok();
|
||||||
|
}
|
||||||
|
fs::create_dir_all(&temp_dir).await.unwrap();
|
||||||
|
|
||||||
|
let disk_paths = vec![
|
||||||
|
temp_dir.join("disk1"),
|
||||||
|
temp_dir.join("disk2"),
|
||||||
|
temp_dir.join("disk3"),
|
||||||
|
temp_dir.join("disk4"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for disk_path in &disk_paths {
|
||||||
|
fs::create_dir_all(disk_path).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut endpoints = Vec::new();
|
||||||
|
for (i, disk_path) in disk_paths.iter().enumerate() {
|
||||||
|
let mut endpoint = Endpoint::try_from(disk_path.to_str().unwrap()).unwrap();
|
||||||
|
endpoint.set_pool_index(0);
|
||||||
|
endpoint.set_set_index(0);
|
||||||
|
endpoint.set_disk_index(i);
|
||||||
|
endpoints.push(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
let pool_endpoints = PoolEndpoints {
|
||||||
|
legacy: false,
|
||||||
|
set_count: 1,
|
||||||
|
drives_per_set: 4,
|
||||||
|
endpoints: Endpoints::from(endpoints),
|
||||||
|
cmd_line: "test".to_string(),
|
||||||
|
platform: format!("OS: {} | Arch: {}", std::env::consts::OS, std::env::consts::ARCH),
|
||||||
|
};
|
||||||
|
|
||||||
|
let endpoint_pools = EndpointServerPools(vec![pool_endpoints]);
|
||||||
|
rustfs_ecstore::store::init_local_disks(endpoint_pools.clone()).await.unwrap();
|
||||||
|
|
||||||
|
let server_addr: std::net::SocketAddr = "127.0.0.1:0".parse().unwrap();
|
||||||
|
let ecstore = ECStore::new(server_addr, endpoint_pools, CancellationToken::new())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let buckets_list = ecstore
|
||||||
|
.list_bucket(&rustfs_ecstore::store_api::BucketOptions {
|
||||||
|
no_metadata: true,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let buckets = buckets_list.into_iter().map(|v| v.name).collect();
|
||||||
|
rustfs_ecstore::bucket::metadata_sys::init_bucket_metadata_sys(ecstore.clone(), buckets).await;
|
||||||
|
|
||||||
|
if init_expiry {
|
||||||
|
rustfs_ecstore::bucket::lifecycle::bucket_lifecycle_ops::init_background_expiry(ecstore.clone()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
(disk_paths, ecstore)
|
||||||
|
}
|
||||||
|
|
||||||
/// Test helper: Create a test bucket
|
/// Test helper: Create a test bucket
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
async fn create_test_bucket(ecstore: &Arc<ECStore>, bucket_name: &str) {
|
async fn create_test_bucket(ecstore: &Arc<ECStore>, bucket_name: &str) {
|
||||||
@@ -374,6 +444,85 @@ async fn wait_for_object_absence(ecstore: &Arc<ECStore>, bucket: &str, object: &
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn wait_for_remote_absence(backend: &MockWarmBackend, object: &str, timeout: Duration) -> bool {
|
||||||
|
let deadline = tokio::time::Instant::now() + timeout;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if !backend.objects.lock().await.contains_key(object) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokio::time::Instant::now() >= deadline {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn free_version_count(disk_path: &Path, bucket: &str, object: &str) -> usize {
|
||||||
|
let mut endpoint = Endpoint::try_from(disk_path.to_str().unwrap()).unwrap();
|
||||||
|
endpoint.set_pool_index(0);
|
||||||
|
endpoint.set_set_index(0);
|
||||||
|
endpoint.set_disk_index(0);
|
||||||
|
let disk = new_disk(
|
||||||
|
&endpoint,
|
||||||
|
&DiskOption {
|
||||||
|
cleanup: false,
|
||||||
|
health_check: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("failed to open local disk");
|
||||||
|
let data = disk
|
||||||
|
.read_metadata(bucket, &path_join_buf(&[object, STORAGE_FORMAT_FILE]))
|
||||||
|
.await;
|
||||||
|
let Ok(data) = data else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
let meta = FileMeta::load(&data).expect("failed to load file metadata");
|
||||||
|
meta.get_file_info_versions(bucket, object, false)
|
||||||
|
.expect("failed to decode file info versions")
|
||||||
|
.free_versions
|
||||||
|
.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn scan_object_metadata(disk_path: &Path, bucket: &str, object: &str) {
|
||||||
|
let mut endpoint = Endpoint::try_from(disk_path.to_str().unwrap()).unwrap();
|
||||||
|
endpoint.set_pool_index(0);
|
||||||
|
endpoint.set_set_index(0);
|
||||||
|
endpoint.set_disk_index(0);
|
||||||
|
let disk = new_disk(
|
||||||
|
&endpoint,
|
||||||
|
&DiskOption {
|
||||||
|
cleanup: false,
|
||||||
|
health_check: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("failed to open local disk");
|
||||||
|
let metadata_path = disk_path.join(bucket).join(object).join(STORAGE_FORMAT_FILE);
|
||||||
|
let relative_path = metadata_path.to_string_lossy().to_string();
|
||||||
|
let (_, scanner_path) = path2_bucket_object_with_base_path(disk_path.to_string_lossy().as_ref(), relative_path.as_str());
|
||||||
|
let file_type = fs::metadata(&metadata_path)
|
||||||
|
.await
|
||||||
|
.expect("failed to stat object metadata")
|
||||||
|
.file_type();
|
||||||
|
let item = ScannerItem {
|
||||||
|
path: scanner_path.clone(),
|
||||||
|
bucket: bucket.to_string(),
|
||||||
|
prefix: object.to_string(),
|
||||||
|
object_name: STORAGE_FORMAT_FILE.to_string(),
|
||||||
|
file_type,
|
||||||
|
lifecycle: None,
|
||||||
|
replication: None,
|
||||||
|
heal_enabled: false,
|
||||||
|
heal_bitrot: false,
|
||||||
|
debug: false,
|
||||||
|
};
|
||||||
|
disk.get_size(item).await.expect("scanner get_size should succeed");
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
struct MockStoredObject {
|
struct MockStoredObject {
|
||||||
bytes: Vec<u8>,
|
bytes: Vec<u8>,
|
||||||
@@ -480,6 +629,15 @@ async fn register_mock_tier(tier_name: &str) -> MockWarmBackend {
|
|||||||
version: "v1".to_string(),
|
version: "v1".to_string(),
|
||||||
tier_type: TierType::MinIO,
|
tier_type: TierType::MinIO,
|
||||||
name: tier_name.to_string(),
|
name: tier_name.to_string(),
|
||||||
|
minio: Some(TierMinIO {
|
||||||
|
access_key: "minioadmin".to_string(),
|
||||||
|
secret_key: "minioadmin".to_string(),
|
||||||
|
bucket: "mock-tier".to_string(),
|
||||||
|
endpoint: "http://127.0.0.1:0".to_string(),
|
||||||
|
prefix: format!("mock/{}/", Uuid::new_v4()),
|
||||||
|
region: String::new(),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -913,4 +1071,64 @@ mod serial_tests {
|
|||||||
.expect("Failed to consume restored object stream");
|
.expect("Failed to consume restored object stream");
|
||||||
assert_eq!(data, expected);
|
assert_eq!(data, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
#[serial]
|
||||||
|
#[ignore = "requires isolated global object layer state"]
|
||||||
|
async fn test_scanner_enqueues_free_version_cleanup_for_stale_transitioned_object() {
|
||||||
|
let (disk_paths, ecstore) = setup_isolated_test_env(false).await;
|
||||||
|
|
||||||
|
let tier_name = format!("COLDTIER{}", &Uuid::new_v4().simple().to_string()[..8]).to_uppercase();
|
||||||
|
let backend = register_mock_tier(&tier_name).await;
|
||||||
|
|
||||||
|
let bucket_name = format!("test-scanner-free-version-{}", &Uuid::new_v4().simple().to_string()[..8]);
|
||||||
|
let object_name = "test/object.txt";
|
||||||
|
let initial_payload = b"scanner should clean stale transitioned null version";
|
||||||
|
create_test_bucket(&ecstore, bucket_name.as_str()).await;
|
||||||
|
set_bucket_lifecycle_transition_with_tier(bucket_name.as_str(), &tier_name)
|
||||||
|
.await
|
||||||
|
.expect("Failed to set lifecycle configuration");
|
||||||
|
|
||||||
|
upload_test_object(&ecstore, bucket_name.as_str(), object_name, initial_payload).await;
|
||||||
|
enqueue_transition_for_existing_objects(ecstore.clone(), bucket_name.as_str())
|
||||||
|
.await
|
||||||
|
.expect("Failed to enqueue transitioned object");
|
||||||
|
|
||||||
|
let transitioned = wait_for_transition(&ecstore, bucket_name.as_str(), object_name, TRANSITION_WAIT_TIMEOUT)
|
||||||
|
.await
|
||||||
|
.expect("object should transition before overwrite");
|
||||||
|
let stale_remote_object = transitioned.transitioned_object.name.clone();
|
||||||
|
assert!(backend.objects.lock().await.contains_key(&stale_remote_object));
|
||||||
|
|
||||||
|
ecstore
|
||||||
|
.delete_object(bucket_name.as_str(), object_name, ObjectOptions::default())
|
||||||
|
.await
|
||||||
|
.expect("Failed to delete transitioned object without expiry workers");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
free_version_count(&disk_paths[0], bucket_name.as_str(), object_name).await > 0,
|
||||||
|
"deleting a transitioned null version should leave a free version for async cleanup"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
backend.objects.lock().await.contains_key(&stale_remote_object),
|
||||||
|
"stale transitioned remote object should still exist before scanner fallback runs"
|
||||||
|
);
|
||||||
|
|
||||||
|
rustfs_ecstore::bucket::lifecycle::bucket_lifecycle_ops::init_background_expiry(ecstore.clone()).await;
|
||||||
|
scan_object_metadata(&disk_paths[0], bucket_name.as_str(), object_name).await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
wait_for_remote_absence(&backend, &stale_remote_object, TRANSITION_WAIT_TIMEOUT).await,
|
||||||
|
"scanner should enqueue stale free-version cleanup for the transitioned remote object"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
free_version_count(&disk_paths[0], bucket_name.as_str(), object_name).await,
|
||||||
|
0,
|
||||||
|
"free-version metadata should be removed after scanner-triggered cleanup"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
wait_for_object_absence(&ecstore, bucket_name.as_str(), object_name, Duration::from_secs(1)).await,
|
||||||
|
"deleted object should remain absent after scanner cleanup"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ use rustfs_ecstore::{
|
|||||||
warm_backend::{WarmBackend, WarmBackendGetOpts},
|
warm_backend::{WarmBackend, WarmBackendGetOpts},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use rustfs_utils::http::{SUFFIX_FORCE_DELETE, insert_header};
|
||||||
use s3s::{S3Request, dto::*};
|
use s3s::{S3Request, dto::*};
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
use std::{
|
use std::{
|
||||||
@@ -141,12 +142,17 @@ async fn create_test_bucket(ecstore: &Arc<ECStore>, bucket_name: &str) {
|
|||||||
.expect("Failed to create test bucket");
|
.expect("Failed to create test bucket");
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn upload_test_object(ecstore: &Arc<ECStore>, bucket: &str, object: &str, data: &[u8]) {
|
async fn upload_test_object(
|
||||||
|
ecstore: &Arc<ECStore>,
|
||||||
|
bucket: &str,
|
||||||
|
object: &str,
|
||||||
|
data: &[u8],
|
||||||
|
) -> rustfs_ecstore::store_api::ObjectInfo {
|
||||||
let mut reader = PutObjReader::from_vec(data.to_vec());
|
let mut reader = PutObjReader::from_vec(data.to_vec());
|
||||||
(**ecstore)
|
(**ecstore)
|
||||||
.put_object(bucket, object, &mut reader, &ObjectOptions::default())
|
.put_object(bucket, object, &mut reader, &ObjectOptions::default())
|
||||||
.await
|
.await
|
||||||
.expect("Failed to upload test object");
|
.expect("Failed to upload test object")
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_bucket_lifecycle_transition_with_tier(
|
async fn set_bucket_lifecycle_transition_with_tier(
|
||||||
@@ -282,6 +288,42 @@ async fn wait_for_transition(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn wait_for_remote_absence(backend: &MockWarmBackend, object: &str, timeout: Duration) -> bool {
|
||||||
|
let deadline = tokio::time::Instant::now() + timeout;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if !backend.objects.lock().await.contains_key(object) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokio::time::Instant::now() >= deadline {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_for_object_absence(ecstore: &Arc<ECStore>, bucket: &str, object: &str, timeout: Duration) -> bool {
|
||||||
|
let deadline = tokio::time::Instant::now() + timeout;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if ecstore
|
||||||
|
.get_object_info(bucket, object, &ObjectOptions::default())
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokio::time::Instant::now() >= deadline {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn build_request<T>(input: T, method: Method) -> S3Request<T> {
|
fn build_request<T>(input: T, method: Method) -> S3Request<T> {
|
||||||
S3Request {
|
S3Request {
|
||||||
input,
|
input,
|
||||||
@@ -353,7 +395,7 @@ async fn put_and_copy_object_transition_immediately_via_usecases() {
|
|||||||
set_bucket_lifecycle_transition_with_tier(dst_bucket.as_str(), &tier_name)
|
set_bucket_lifecycle_transition_with_tier(dst_bucket.as_str(), &tier_name)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to set destination lifecycle configuration");
|
.expect("Failed to set destination lifecycle configuration");
|
||||||
upload_test_object(&ecstore, src_bucket.as_str(), src_object, copy_payload).await;
|
let _ = upload_test_object(&ecstore, src_bucket.as_str(), src_object, copy_payload).await;
|
||||||
|
|
||||||
let copy_input = CopyObjectInput::builder()
|
let copy_input = CopyObjectInput::builder()
|
||||||
.copy_source(CopySource::Bucket {
|
.copy_source(CopySource::Bucket {
|
||||||
@@ -437,3 +479,63 @@ async fn complete_multipart_upload_transitions_immediately_via_usecase() {
|
|||||||
assert_eq!(info.transitioned_object.tier, tier_name);
|
assert_eq!(info.transitioned_object.tier, tier_name);
|
||||||
assert!(backend.objects.lock().await.contains_key(&info.transitioned_object.name));
|
assert!(backend.objects.lock().await.contains_key(&info.transitioned_object.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
#[serial]
|
||||||
|
#[ignore = "requires isolated global object layer state"]
|
||||||
|
async fn delete_transitioned_object_removes_remote_tier_copy_via_usecase() {
|
||||||
|
let (_disk_paths, ecstore) = setup_test_env().await;
|
||||||
|
let usecase = DefaultObjectUsecase::without_context();
|
||||||
|
|
||||||
|
let tier_name = format!("COLDTIER{}", &Uuid::new_v4().simple().to_string()[..8]).to_uppercase();
|
||||||
|
let backend = register_mock_tier(&tier_name).await;
|
||||||
|
|
||||||
|
let bucket = format!("test-api-delete-{}", &Uuid::new_v4().simple().to_string()[..8]);
|
||||||
|
let object = "test/object.txt";
|
||||||
|
let payload = b"delete transitioned object through delete API";
|
||||||
|
|
||||||
|
create_test_bucket(&ecstore, bucket.as_str()).await;
|
||||||
|
set_bucket_lifecycle_transition_with_tier(bucket.as_str(), &tier_name)
|
||||||
|
.await
|
||||||
|
.expect("Failed to set lifecycle configuration");
|
||||||
|
let _ = upload_test_object(&ecstore, bucket.as_str(), object, payload).await;
|
||||||
|
|
||||||
|
rustfs_ecstore::bucket::lifecycle::bucket_lifecycle_ops::enqueue_transition_for_existing_objects(
|
||||||
|
ecstore.clone(),
|
||||||
|
bucket.as_str(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to enqueue transitioned object");
|
||||||
|
|
||||||
|
let transitioned = wait_for_transition(&ecstore, bucket.as_str(), object, TRANSITION_WAIT_TIMEOUT)
|
||||||
|
.await
|
||||||
|
.expect("object should transition before delete usecase runs");
|
||||||
|
let remote_object = transitioned.transitioned_object.name.clone();
|
||||||
|
|
||||||
|
assert!(backend.objects.lock().await.contains_key(&remote_object));
|
||||||
|
|
||||||
|
let mut req = build_request(
|
||||||
|
DeleteObjectInput::builder()
|
||||||
|
.bucket(bucket.clone())
|
||||||
|
.key(object.to_string())
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
Method::DELETE,
|
||||||
|
);
|
||||||
|
insert_header(&mut req.headers, SUFFIX_FORCE_DELETE, "true");
|
||||||
|
|
||||||
|
usecase
|
||||||
|
.execute_delete_object(req)
|
||||||
|
.await
|
||||||
|
.expect("Failed to delete object through usecase");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
wait_for_object_absence(&ecstore, bucket.as_str(), object, TRANSITION_WAIT_TIMEOUT).await,
|
||||||
|
"object should be removed from hot tier after delete usecase"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
wait_for_remote_absence(&backend, &remote_object, TRANSITION_WAIT_TIMEOUT).await,
|
||||||
|
"transitioned object should be removed from remote tier after delete usecase"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -142,6 +142,42 @@ impl Drop for DeadlockRequestGuard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn enqueue_transitioned_delete_cleanup(bucket: &str, object: &str, opts: &ObjectOptions, existing: Option<&ObjectInfo>) {
|
||||||
|
let Some(existing) = existing else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let je = if opts.delete_prefix {
|
||||||
|
rustfs_ecstore::bucket::lifecycle::tier_sweeper::transitioned_force_delete_journal_entry(&existing.transitioned_object)
|
||||||
|
} else {
|
||||||
|
let version_id = opts.version_id.as_ref().and_then(|v| Uuid::parse_str(v).ok());
|
||||||
|
rustfs_ecstore::bucket::lifecycle::tier_sweeper::transitioned_delete_journal_entry(
|
||||||
|
version_id,
|
||||||
|
opts.versioned,
|
||||||
|
opts.version_suspended,
|
||||||
|
&existing.transitioned_object,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let Some(je) = je else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut expiry_state = rustfs_ecstore::bucket::lifecycle::bucket_lifecycle_ops::GLOBAL_ExpiryState
|
||||||
|
.write()
|
||||||
|
.await;
|
||||||
|
if let Err(err) = expiry_state.enqueue_tier_journal_entry(&je).await {
|
||||||
|
warn!(
|
||||||
|
bucket,
|
||||||
|
object,
|
||||||
|
remote_object = %existing.transitioned_object.name,
|
||||||
|
remote_version_id = %existing.transitioned_object.version_id,
|
||||||
|
tier = %existing.transitioned_object.tier,
|
||||||
|
error = ?err,
|
||||||
|
"failed to enqueue transitioned object cleanup"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pin_project! {
|
pin_project! {
|
||||||
struct ExtractArchiveEtagReader<R> {
|
struct ExtractArchiveEtagReader<R> {
|
||||||
#[pin]
|
#[pin]
|
||||||
@@ -2794,6 +2830,7 @@ impl DefaultObjectUsecase {
|
|||||||
let mut object_to_delete = Vec::new();
|
let mut object_to_delete = Vec::new();
|
||||||
let mut object_to_delete_idx = Vec::new();
|
let mut object_to_delete_idx = Vec::new();
|
||||||
let mut object_sizes = Vec::new();
|
let mut object_sizes = Vec::new();
|
||||||
|
let mut existing_object_infos = Vec::new();
|
||||||
for (idx, obj_id) in delete.objects.iter().enumerate() {
|
for (idx, obj_id) in delete.objects.iter().enumerate() {
|
||||||
let raw_version_id = obj_id.version_id.clone();
|
let raw_version_id = obj_id.version_id.clone();
|
||||||
let (version_id, version_uuid) = match normalize_delete_objects_version_id(raw_version_id.clone()) {
|
let (version_id, version_uuid) = match normalize_delete_objects_version_id(raw_version_id.clone()) {
|
||||||
@@ -2893,6 +2930,7 @@ impl DefaultObjectUsecase {
|
|||||||
|
|
||||||
object_to_delete_idx.push(idx);
|
object_to_delete_idx.push(idx);
|
||||||
object_to_delete.push(object);
|
object_to_delete.push(object);
|
||||||
|
existing_object_infos.push(gerr.is_none().then_some(goi));
|
||||||
}
|
}
|
||||||
|
|
||||||
let (mut dobjs, errs) = store
|
let (mut dobjs, errs) = store
|
||||||
@@ -2944,6 +2982,18 @@ impl DefaultObjectUsecase {
|
|||||||
dobjs[i].replication_state = Some(object_to_delete[i].replication_state());
|
dobjs[i].replication_state = Some(object_to_delete[i].replication_state());
|
||||||
}
|
}
|
||||||
delete_results[didx].delete_object = Some(dobjs[i].clone());
|
delete_results[didx].delete_object = Some(dobjs[i].clone());
|
||||||
|
enqueue_transitioned_delete_cleanup(
|
||||||
|
&bucket,
|
||||||
|
&object_to_delete[i].object_name,
|
||||||
|
&ObjectOptions {
|
||||||
|
version_id: object_to_delete[i].version_id.map(|v| v.to_string()),
|
||||||
|
versioned: version_cfg.prefix_enabled(object_to_delete[i].object_name.as_str()),
|
||||||
|
version_suspended: version_cfg.suspended(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
existing_object_infos[i].as_ref(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
let size = object_sizes[i];
|
let size = object_sizes[i];
|
||||||
if size > 0 {
|
if size > 0 {
|
||||||
rustfs_ecstore::data_usage::decrement_bucket_usage_memory(&bucket, size as u64).await;
|
rustfs_ecstore::data_usage::decrement_bucket_usage_memory(&bucket, size as u64).await;
|
||||||
@@ -3121,7 +3171,7 @@ impl DefaultObjectUsecase {
|
|||||||
.await
|
.await
|
||||||
.map_err(ApiError::from)?;
|
.map_err(ApiError::from)?;
|
||||||
|
|
||||||
match store.get_object_info(&bucket, &key, &get_opts).await {
|
let existing_object_info = match store.get_object_info(&bucket, &key, &get_opts).await {
|
||||||
Ok(obj_info) => {
|
Ok(obj_info) => {
|
||||||
// Check for bypass governance retention header (permission already verified in access.rs)
|
// Check for bypass governance retention header (permission already verified in access.rs)
|
||||||
let bypass_governance = has_bypass_governance_header(&req.headers);
|
let bypass_governance = has_bypass_governance_header(&req.headers);
|
||||||
@@ -3129,17 +3179,19 @@ impl DefaultObjectUsecase {
|
|||||||
if let Some(block_reason) = check_object_lock_for_deletion(&bucket, &obj_info, bypass_governance).await {
|
if let Some(block_reason) = check_object_lock_for_deletion(&bucket, &obj_info, bypass_governance).await {
|
||||||
return Err(S3Error::with_message(S3ErrorCode::AccessDenied, block_reason.error_message()));
|
return Err(S3Error::with_message(S3ErrorCode::AccessDenied, block_reason.error_message()));
|
||||||
}
|
}
|
||||||
|
Some(obj_info)
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// If object not found, allow deletion to proceed (will return 204 No Content)
|
// If object not found, allow deletion to proceed (will return 204 No Content)
|
||||||
if !is_err_object_not_found(&err) && !is_err_version_not_found(&err) {
|
if !is_err_object_not_found(&err) && !is_err_version_not_found(&err) {
|
||||||
return Err(ApiError::from(err).into());
|
return Err(ApiError::from(err).into());
|
||||||
}
|
}
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
let obj_info = {
|
let obj_info = {
|
||||||
match store.delete_object(&bucket, &key, opts).await {
|
match store.delete_object(&bucket, &key, opts.clone()).await {
|
||||||
Ok(obj) => obj,
|
Ok(obj) => obj,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
if is_err_bucket_not_found(&err) {
|
if is_err_bucket_not_found(&err) {
|
||||||
@@ -3157,6 +3209,8 @@ impl DefaultObjectUsecase {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enqueue_transitioned_delete_cleanup(&bucket, &key, &opts, existing_object_info.as_ref()).await;
|
||||||
|
|
||||||
// Fast in-memory update for immediate quota consistency
|
// Fast in-memory update for immediate quota consistency
|
||||||
rustfs_ecstore::data_usage::decrement_bucket_usage_memory(&bucket, obj_info.size as u64).await;
|
rustfs_ecstore::data_usage::decrement_bucket_usage_memory(&bucket, obj_info.size as u64).await;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user