mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-22 04:16:38 +00:00
feat: improve legacy metadata and admin compatibility (#2202)
This commit is contained in:
+698
-62
@@ -13,6 +13,17 @@
|
||||
// limitations under the License.
|
||||
|
||||
use crate::bucket::versioning_sys::BucketVersioningSys;
|
||||
use crate::bucket::{
|
||||
lifecycle::{
|
||||
bucket_lifecycle_audit::LcEventSrc,
|
||||
bucket_lifecycle_ops::{
|
||||
LifecycleOps, apply_expiry_on_transitioned_object, apply_expiry_rule, eval_action_from_lifecycle,
|
||||
},
|
||||
lifecycle::IlmAction,
|
||||
},
|
||||
metadata_sys,
|
||||
object_lock::objectlock_sys::BucketObjectLockSys,
|
||||
};
|
||||
use crate::cache_value::metacache_set::{ListPathRawOptions, list_path_raw};
|
||||
use crate::config::com::{CONFIG_PREFIX, read_config, save_config};
|
||||
use crate::data_usage::DATA_USAGE_CACHE_NAME;
|
||||
@@ -30,25 +41,32 @@ use crate::store_api::{
|
||||
BucketOperations, BucketOptions, CompletePart, GetObjectReader, HealOperations, MakeBucketOptions, MultipartOperations,
|
||||
ObjectIO, ObjectOperations, ObjectOptions, PutObjReader, StorageAPI,
|
||||
};
|
||||
use crate::{sets::Sets, store::ECStore};
|
||||
use crate::{global::GLOBAL_LifecycleSys, sets::Sets, store::ECStore};
|
||||
use byteorder::{ByteOrder, LittleEndian, WriteBytesExt};
|
||||
use bytes::Bytes;
|
||||
use futures::future::BoxFuture;
|
||||
use http::HeaderMap;
|
||||
use rmp_serde::{Deserializer, Serializer};
|
||||
use rustfs_common::defer;
|
||||
use rustfs_common::heal_channel::HealOpts;
|
||||
use rustfs_filemeta::{MetaCacheEntries, MetaCacheEntry, MetadataResolutionParams};
|
||||
use rustfs_rio::{HashReader, WarpReader};
|
||||
use rustfs_filemeta::{FileInfoVersions, MetaCacheEntries, MetaCacheEntry, MetadataResolutionParams};
|
||||
use rustfs_rio::{EtagResolvable, HashReader, HashReaderDetector, Index, Reader, TryGetIndex, WarpReader};
|
||||
use rustfs_utils::path::{SLASH_SEPARATOR, encode_dir_object, path_join};
|
||||
use rustfs_workers::workers::Workers;
|
||||
use s3s::dto::{BucketLifecycleConfiguration, DefaultRetention, ReplicationConfiguration};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::Display;
|
||||
use std::io::{Cursor, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::pin::Pin;
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
use std::task::{Context, Poll};
|
||||
use time::{Duration, OffsetDateTime};
|
||||
use tokio::io::{AsyncReadExt, BufReader};
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, BufReader, ReadBuf};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
@@ -75,6 +93,147 @@ pub struct PoolMeta {
|
||||
pub dont_save: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct PersistedPoolMeta {
|
||||
pub version: u16,
|
||||
pub pools: Vec<PersistedPoolStatus>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct PersistedPoolStatus {
|
||||
#[serde(rename = "id")]
|
||||
pub id: usize,
|
||||
#[serde(rename = "cmdline")]
|
||||
pub cmd_line: String,
|
||||
#[serde(rename = "lastUpdate", with = "time::serde::rfc3339")]
|
||||
pub last_update: OffsetDateTime,
|
||||
#[serde(rename = "decommissionInfo")]
|
||||
pub decommission: Option<PersistedPoolDecommissionInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
struct PersistedPoolDecommissionInfo {
|
||||
#[serde(rename = "startTime", with = "time::serde::rfc3339::option")]
|
||||
pub start_time: Option<OffsetDateTime>,
|
||||
#[serde(rename = "startSize")]
|
||||
pub start_size: usize,
|
||||
#[serde(rename = "totalSize")]
|
||||
pub total_size: usize,
|
||||
#[serde(rename = "currentSize")]
|
||||
pub current_size: usize,
|
||||
#[serde(rename = "complete")]
|
||||
pub complete: bool,
|
||||
#[serde(rename = "failed")]
|
||||
pub failed: bool,
|
||||
#[serde(rename = "canceled")]
|
||||
pub canceled: bool,
|
||||
#[serde(rename = "queuedBuckets", default)]
|
||||
pub queued_buckets: Vec<String>,
|
||||
#[serde(rename = "decommissionedBuckets", default)]
|
||||
pub decommissioned_buckets: Vec<String>,
|
||||
#[serde(rename = "bucket", default)]
|
||||
pub bucket: String,
|
||||
#[serde(rename = "prefix", default)]
|
||||
pub prefix: String,
|
||||
#[serde(rename = "object", default)]
|
||||
pub object: String,
|
||||
#[serde(rename = "objectsDecommissioned")]
|
||||
pub items_decommissioned: usize,
|
||||
#[serde(rename = "objectsDecommissionedFailed")]
|
||||
pub items_decommission_failed: usize,
|
||||
#[serde(rename = "bytesDecommissioned")]
|
||||
pub bytes_done: usize,
|
||||
#[serde(rename = "bytesDecommissionedFailed")]
|
||||
pub bytes_failed: usize,
|
||||
}
|
||||
|
||||
impl From<PersistedPoolMeta> for PoolMeta {
|
||||
fn from(value: PersistedPoolMeta) -> Self {
|
||||
Self {
|
||||
version: value.version,
|
||||
pools: value.pools.into_iter().map(Into::into).collect(),
|
||||
dont_save: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PersistedPoolStatus> for PoolStatus {
|
||||
fn from(value: PersistedPoolStatus) -> Self {
|
||||
Self {
|
||||
id: value.id,
|
||||
cmd_line: value.cmd_line,
|
||||
last_update: value.last_update,
|
||||
decommission: value.decommission.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PersistedPoolDecommissionInfo> for PoolDecommissionInfo {
|
||||
fn from(value: PersistedPoolDecommissionInfo) -> Self {
|
||||
Self {
|
||||
start_time: value.start_time,
|
||||
start_size: value.start_size,
|
||||
total_size: value.total_size,
|
||||
current_size: value.current_size,
|
||||
complete: value.complete,
|
||||
failed: value.failed,
|
||||
canceled: value.canceled,
|
||||
queued_buckets: value.queued_buckets,
|
||||
decommissioned_buckets: value.decommissioned_buckets,
|
||||
bucket: value.bucket,
|
||||
prefix: value.prefix,
|
||||
object: value.object,
|
||||
items_decommissioned: value.items_decommissioned,
|
||||
items_decommission_failed: value.items_decommission_failed,
|
||||
bytes_done: value.bytes_done,
|
||||
bytes_failed: value.bytes_failed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PoolMeta> for PersistedPoolMeta {
|
||||
fn from(value: &PoolMeta) -> Self {
|
||||
Self {
|
||||
version: value.version,
|
||||
pools: value.pools.iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PoolStatus> for PersistedPoolStatus {
|
||||
fn from(value: &PoolStatus) -> Self {
|
||||
Self {
|
||||
id: value.id,
|
||||
cmd_line: value.cmd_line.clone(),
|
||||
last_update: value.last_update,
|
||||
decommission: value.decommission.as_ref().map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PoolDecommissionInfo> for PersistedPoolDecommissionInfo {
|
||||
fn from(value: &PoolDecommissionInfo) -> Self {
|
||||
Self {
|
||||
start_time: value.start_time,
|
||||
start_size: value.start_size,
|
||||
total_size: value.total_size,
|
||||
current_size: value.current_size,
|
||||
complete: value.complete,
|
||||
failed: value.failed,
|
||||
canceled: value.canceled,
|
||||
queued_buckets: value.queued_buckets.clone(),
|
||||
decommissioned_buckets: value.decommissioned_buckets.clone(),
|
||||
bucket: value.bucket.clone(),
|
||||
prefix: value.prefix.clone(),
|
||||
object: value.object.clone(),
|
||||
items_decommissioned: value.items_decommissioned,
|
||||
items_decommission_failed: value.items_decommission_failed,
|
||||
bytes_done: value.bytes_done,
|
||||
bytes_failed: value.bytes_failed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PoolMeta {
|
||||
pub fn new(pools: &[Arc<Sets>], prev_meta: &PoolMeta) -> Self {
|
||||
let mut new_meta = Self {
|
||||
@@ -144,8 +303,8 @@ impl PoolMeta {
|
||||
}
|
||||
|
||||
let mut buf = Deserializer::new(Cursor::new(&data[4..]));
|
||||
let meta: PoolMeta = Deserialize::deserialize(&mut buf)?;
|
||||
*self = meta;
|
||||
let meta: PersistedPoolMeta = Deserialize::deserialize(&mut buf)?;
|
||||
*self = meta.into();
|
||||
|
||||
if self.version != POOL_META_VERSION {
|
||||
return Err(Error::other(format!("unexpected PoolMeta version: {}", self.version)));
|
||||
@@ -161,7 +320,7 @@ impl PoolMeta {
|
||||
data.write_u16::<LittleEndian>(POOL_META_FORMAT)?;
|
||||
data.write_u16::<LittleEndian>(POOL_META_VERSION)?;
|
||||
let mut buf = Vec::new();
|
||||
self.serialize(&mut Serializer::new(&mut buf))?;
|
||||
PersistedPoolMeta::from(self).serialize(&mut Serializer::new(&mut buf))?;
|
||||
data.write_all(&buf)?;
|
||||
|
||||
for pool in pools {
|
||||
@@ -178,7 +337,6 @@ impl PoolMeta {
|
||||
stats.last_update = OffsetDateTime::now_utc();
|
||||
|
||||
let mut pd = d.clone();
|
||||
pd.start_time = None;
|
||||
pd.canceled = true;
|
||||
pd.failed = false;
|
||||
pd.complete = false;
|
||||
@@ -202,7 +360,6 @@ impl PoolMeta {
|
||||
stats.last_update = OffsetDateTime::now_utc();
|
||||
|
||||
let mut pd = d.clone();
|
||||
pd.start_time = None;
|
||||
pd.canceled = false;
|
||||
pd.failed = true;
|
||||
pd.complete = false;
|
||||
@@ -226,7 +383,6 @@ impl PoolMeta {
|
||||
stats.last_update = OffsetDateTime::now_utc();
|
||||
|
||||
let mut pd = d.clone();
|
||||
pd.start_time = None;
|
||||
pd.canceled = false;
|
||||
pd.failed = false;
|
||||
pd.complete = true;
|
||||
@@ -576,6 +732,132 @@ impl Display for DecomBucketInfo {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum DecommissionFinalState {
|
||||
Complete,
|
||||
Failed,
|
||||
}
|
||||
|
||||
fn determine_decommission_final_state(items_failed: usize, was_cancelled: bool) -> DecommissionFinalState {
|
||||
if items_failed > 0 || was_cancelled {
|
||||
DecommissionFinalState::Failed
|
||||
} else {
|
||||
DecommissionFinalState::Complete
|
||||
}
|
||||
}
|
||||
|
||||
fn remaining_versions_after_decommission(fivs: &FileInfoVersions) -> usize {
|
||||
fivs.versions.iter().filter(|version| !version.deleted).count()
|
||||
}
|
||||
|
||||
fn decommission_delete_marker_opts(
|
||||
version: &rustfs_filemeta::FileInfo,
|
||||
version_id: Option<String>,
|
||||
src_pool_idx: usize,
|
||||
) -> ObjectOptions {
|
||||
ObjectOptions {
|
||||
versioned: true,
|
||||
version_id,
|
||||
mod_time: version.mod_time,
|
||||
src_pool_idx,
|
||||
data_movement: true,
|
||||
delete_marker: true,
|
||||
skip_decommissioned: true,
|
||||
delete_replication: version.replication_state_internal.clone(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
async fn should_skip_lifecycle_for_decommission(
|
||||
store: Arc<ECStore>,
|
||||
bucket: &str,
|
||||
version: &rustfs_filemeta::FileInfo,
|
||||
lifecycle_config: Option<&BucketLifecycleConfiguration>,
|
||||
lock_retention: Option<DefaultRetention>,
|
||||
replication_config: Option<(ReplicationConfiguration, OffsetDateTime)>,
|
||||
apply_actions: bool,
|
||||
) -> bool {
|
||||
let Some(lifecycle_config) = lifecycle_config else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let versioned = BucketVersioningSys::prefix_enabled(bucket, &version.name).await;
|
||||
let object_info = crate::store_api::ObjectInfo::from_file_info(version, bucket, &version.name, versioned);
|
||||
let event = eval_action_from_lifecycle(lifecycle_config, lock_retention, replication_config, &object_info).await;
|
||||
|
||||
match event.action {
|
||||
IlmAction::DeleteRestoredAction | IlmAction::DeleteRestoredVersionAction => {
|
||||
if apply_actions && object_info.is_remote() {
|
||||
let _ = apply_expiry_on_transitioned_object(store, &object_info, &event, &LcEventSrc::Decom).await;
|
||||
}
|
||||
false
|
||||
}
|
||||
IlmAction::DeleteAction
|
||||
| IlmAction::DeleteVersionAction
|
||||
| IlmAction::DeleteAllVersionsAction
|
||||
| IlmAction::DelMarkerDeleteAllVersionsAction => {
|
||||
if apply_actions {
|
||||
let _ = apply_expiry_rule(&event, &LcEventSrc::Decom, &object_info).await;
|
||||
}
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
struct IndexedDecommissionReader<R> {
|
||||
inner: R,
|
||||
index: Option<Index>,
|
||||
}
|
||||
|
||||
impl<R> IndexedDecommissionReader<R> {
|
||||
fn new(inner: R, index: Option<Index>) -> Self {
|
||||
Self { inner, index }
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: AsyncRead + Unpin + Send + Sync> AsyncRead for IndexedDecommissionReader<R> {
|
||||
fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll<std::io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: AsyncRead + Unpin + Send + Sync> EtagResolvable for IndexedDecommissionReader<R> {}
|
||||
|
||||
impl<R: AsyncRead + Unpin + Send + Sync> HashReaderDetector for IndexedDecommissionReader<R> {}
|
||||
|
||||
impl<R: AsyncRead + Unpin + Send + Sync> TryGetIndex for IndexedDecommissionReader<R> {
|
||||
fn try_get_index(&self) -> Option<&Index> {
|
||||
self.index.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: AsyncRead + Unpin + Send + Sync> Reader for IndexedDecommissionReader<R> {}
|
||||
|
||||
fn decode_part_index(index: Option<&Bytes>) -> Option<Index> {
|
||||
let bytes = index?;
|
||||
let mut decoded = Index::new();
|
||||
if decoded.load(bytes.as_ref()).is_ok() {
|
||||
Some(decoded)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn put_obj_reader_from_chunk(chunk: Vec<u8>, size: i64, actual_size: i64, index: Option<Index>) -> Result<PutObjReader> {
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
let sha256hex = if !chunk.is_empty() {
|
||||
Some(hex_simd::encode_to_string(Sha256::digest(&chunk), hex_simd::AsciiCase::Lower))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let reader = IndexedDecommissionReader::new(WarpReader::new(Cursor::new(chunk)), index);
|
||||
let hash_reader = HashReader::new(Box::new(reader), size, actual_size, None, sha256hex, false)?;
|
||||
Ok(PutObjReader::new(hash_reader))
|
||||
}
|
||||
|
||||
impl ECStore {
|
||||
pub async fn status(&self, idx: usize) -> Result<PoolStatus> {
|
||||
let space_info = self.get_decommission_pool_space_info(idx).await?;
|
||||
@@ -621,13 +903,18 @@ impl ECStore {
|
||||
return Err(Error::other("InvalidArgument"));
|
||||
}
|
||||
|
||||
let Some(has_canceler) = self.decommission_cancelers.get(idx) else {
|
||||
return Err(Error::other("InvalidArgument"));
|
||||
};
|
||||
let canceler = {
|
||||
let mut cancelers = self.decommission_cancelers.write().await;
|
||||
let Some(slot) = cancelers.get_mut(idx) else {
|
||||
return Err(Error::other("InvalidArgument"));
|
||||
};
|
||||
|
||||
if has_canceler.is_none() {
|
||||
return Err(StorageError::DecommissionNotStarted);
|
||||
}
|
||||
let Some(canceler) = slot.take() else {
|
||||
return Err(StorageError::DecommissionNotStarted);
|
||||
};
|
||||
|
||||
canceler
|
||||
};
|
||||
|
||||
let mut lock = self.pool_meta.write().await;
|
||||
if lock.decommission_cancel(idx) {
|
||||
@@ -640,6 +927,8 @@ impl ECStore {
|
||||
}
|
||||
}
|
||||
|
||||
canceler.cancel();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub async fn is_decommission_running(&self) -> bool {
|
||||
@@ -684,8 +973,8 @@ impl ECStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(unused_assignments)]
|
||||
#[tracing::instrument(skip(self, set, wk, rcfg))]
|
||||
#[allow(unused_assignments, clippy::too_many_arguments)]
|
||||
#[tracing::instrument(skip(self, set, wk, lifecycle_config, lock_retention, replication_config))]
|
||||
async fn decommission_entry(
|
||||
self: &Arc<Self>,
|
||||
idx: usize,
|
||||
@@ -693,7 +982,9 @@ impl ECStore {
|
||||
bucket: String,
|
||||
set: Arc<SetDisks>,
|
||||
wk: Arc<Workers>,
|
||||
rcfg: Option<String>,
|
||||
lifecycle_config: Option<BucketLifecycleConfiguration>,
|
||||
lock_retention: Option<DefaultRetention>,
|
||||
replication_config: Option<(ReplicationConfiguration, OffsetDateTime)>,
|
||||
) {
|
||||
warn!("decommission_entry: {} {}", &bucket, &entry.name);
|
||||
wk.give().await;
|
||||
@@ -713,12 +1004,27 @@ impl ECStore {
|
||||
fivs.versions.sort_by(|a, b| b.mod_time.cmp(&a.mod_time));
|
||||
|
||||
let mut decommissioned: usize = 0;
|
||||
let expired: usize = 0;
|
||||
let mut expired: usize = 0;
|
||||
|
||||
for version in fivs.versions.iter() {
|
||||
// TODO: filterLifecycle
|
||||
if should_skip_lifecycle_for_decommission(
|
||||
self.clone(),
|
||||
&bucket,
|
||||
version,
|
||||
lifecycle_config.as_ref(),
|
||||
lock_retention.clone(),
|
||||
replication_config.clone(),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
{
|
||||
expired += 1;
|
||||
decommissioned += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let remaining_versions = fivs.versions.len() - expired;
|
||||
if version.deleted && remaining_versions == 1 && rcfg.is_none() {
|
||||
if version.deleted && remaining_versions == 1 && replication_config.is_none() {
|
||||
//
|
||||
decommissioned += 1;
|
||||
info!("decommission_pool: DELETE marked object with no other non-current versions will be skipped");
|
||||
@@ -731,25 +1037,19 @@ impl ECStore {
|
||||
let mut failure = false;
|
||||
let mut error = None;
|
||||
if version.deleted {
|
||||
// TODO: other params
|
||||
if let Err(err) = self
|
||||
.delete_object(
|
||||
bucket.as_str(),
|
||||
&version.name,
|
||||
ObjectOptions {
|
||||
versioned: true,
|
||||
version_id: version_id.clone(),
|
||||
mod_time: version.mod_time,
|
||||
src_pool_idx: idx,
|
||||
data_movement: true,
|
||||
delete_marker: true,
|
||||
skip_decommissioned: true,
|
||||
..Default::default()
|
||||
},
|
||||
decommission_delete_marker_opts(version, version_id.clone(), idx),
|
||||
)
|
||||
.await
|
||||
{
|
||||
if is_err_object_not_found(&err) || is_err_version_not_found(&err) || is_err_data_movement_overwrite(&err) {
|
||||
warn!(
|
||||
"decommission_pool: ignore delete-marker copy for {}/{} version {:?}: {:?}",
|
||||
&bucket, &version.name, &version_id, &err
|
||||
);
|
||||
ignore = true;
|
||||
continue;
|
||||
}
|
||||
@@ -776,7 +1076,33 @@ impl ECStore {
|
||||
|
||||
for _i in 0..3 {
|
||||
if version.is_remote() {
|
||||
// TODO: DecomTieredObject
|
||||
if let Err(err) = self
|
||||
.decommission_tiered_object(
|
||||
bucket.as_str(),
|
||||
&version.name,
|
||||
version,
|
||||
&ObjectOptions {
|
||||
version_id: version_id.clone(),
|
||||
mod_time: version.mod_time,
|
||||
user_defined: version.metadata.clone(),
|
||||
src_pool_idx: idx,
|
||||
data_movement: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
if is_err_object_not_found(&err) || is_err_version_not_found(&err) || is_err_data_movement_overwrite(&err)
|
||||
{
|
||||
ignore = true;
|
||||
break;
|
||||
}
|
||||
|
||||
failure = true;
|
||||
error!("decommission_pool: decommission_tiered_object err {:?}", &err);
|
||||
error = Some(err);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
let bucket = bucket.clone();
|
||||
@@ -847,7 +1173,8 @@ impl ECStore {
|
||||
}
|
||||
|
||||
{
|
||||
self.pool_meta.write().await.count_item(idx, decommissioned, failure);
|
||||
let size = usize::try_from(version.size).unwrap_or_default();
|
||||
self.pool_meta.write().await.count_item(idx, size, failure);
|
||||
}
|
||||
|
||||
if failure {
|
||||
@@ -872,6 +1199,14 @@ impl ECStore {
|
||||
.await
|
||||
{
|
||||
error!("decommission_pool: delete_object err {:?}", &err);
|
||||
} else if decommissioned != fivs.versions.len() {
|
||||
warn!(
|
||||
"decommission_pool: source object retained for {}/{} because only {}/{} versions were decommissioned",
|
||||
&bucket,
|
||||
&entry.name,
|
||||
decommissioned,
|
||||
fivs.versions.len()
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -903,16 +1238,19 @@ impl ECStore {
|
||||
) -> Result<()> {
|
||||
let wk = Workers::new(pool.disk_set.len() * 2).map_err(Error::other)?;
|
||||
|
||||
// let mut vc = None;
|
||||
// replication
|
||||
let rcfg: Option<String> = None;
|
||||
let mut lifecycle_config = None;
|
||||
let mut lock_retention = None;
|
||||
let mut replication_config = None;
|
||||
|
||||
if bi.name != RUSTFS_META_BUCKET {
|
||||
let _versioning = BucketVersioningSys::get(&bi.name).await?;
|
||||
// vc = Some(versioning);
|
||||
// TODO: LifecycleSys
|
||||
// TODO: BucketObjectLockSys
|
||||
// TODO: ReplicationConfig
|
||||
let _ = BucketVersioningSys::get(&bi.name).await?;
|
||||
lifecycle_config = GLOBAL_LifecycleSys.get(&bi.name).await;
|
||||
lock_retention = BucketObjectLockSys::get(&bi.name).await;
|
||||
replication_config = match metadata_sys::get_replication_config(&bi.name).await {
|
||||
Ok(config) => Some(config),
|
||||
Err(Error::ConfigNotFound) => None,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
}
|
||||
|
||||
for (set_idx, set) in pool.disk_set.iter().enumerate() {
|
||||
@@ -925,17 +1263,22 @@ impl ECStore {
|
||||
let bucket = bi.name.clone();
|
||||
let wk = wk.clone();
|
||||
let set = set.clone();
|
||||
let rcfg = rcfg.clone();
|
||||
let lifecycle_config = lifecycle_config.clone();
|
||||
let lock_retention = lock_retention.clone();
|
||||
let replication_config = replication_config.clone();
|
||||
move |entry: MetaCacheEntry| {
|
||||
let this = this.clone();
|
||||
let bucket = bucket.clone();
|
||||
let wk = wk.clone();
|
||||
let set = set.clone();
|
||||
let rcfg = rcfg.clone();
|
||||
let lifecycle_config = lifecycle_config.clone();
|
||||
let lock_retention = lock_retention.clone();
|
||||
let replication_config = replication_config.clone();
|
||||
|
||||
Box::pin(async move {
|
||||
wk.take().await;
|
||||
this.decommission_entry(idx, entry, bucket, set, wk, rcfg).await
|
||||
this.decommission_entry(idx, entry, bucket, set, wk, lifecycle_config, lock_retention, replication_config)
|
||||
.await
|
||||
})
|
||||
}
|
||||
});
|
||||
@@ -988,7 +1331,15 @@ impl ECStore {
|
||||
|
||||
#[tracing::instrument(skip(self, rx))]
|
||||
pub async fn do_decommission_in_routine(self: &Arc<Self>, rx: CancellationToken, idx: usize) {
|
||||
if let Err(err) = self.decommission_in_background(rx, idx).await {
|
||||
let decommission_token = rx.child_token();
|
||||
{
|
||||
let mut cancelers = self.decommission_cancelers.write().await;
|
||||
if let Some(slot) = cancelers.get_mut(idx) {
|
||||
*slot = Some(decommission_token.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = self.decommission_in_background(decommission_token.clone(), idx).await {
|
||||
error!("decom err {:?}", &err);
|
||||
if let Err(er) = self.decommission_failed(idx).await {
|
||||
error!("decom failed err {:?}", &er);
|
||||
@@ -1001,29 +1352,49 @@ impl ECStore {
|
||||
|
||||
warn!("decommission: decommission_in_background complete {}", idx);
|
||||
|
||||
let (failed, cmd_line) = {
|
||||
let (final_state, cmd_line) = {
|
||||
let pool_meta = self.pool_meta.read().await;
|
||||
let failed = {
|
||||
let final_state = {
|
||||
if let Some(info) = &pool_meta.pools[idx].decommission {
|
||||
info.items_decommission_failed > 0
|
||||
determine_decommission_final_state(info.items_decommission_failed, info.canceled)
|
||||
} else {
|
||||
false
|
||||
DecommissionFinalState::Failed
|
||||
}
|
||||
};
|
||||
let cmd_line = pool_meta.pools[idx].cmd_line.clone();
|
||||
(failed, cmd_line)
|
||||
(final_state, cmd_line)
|
||||
};
|
||||
|
||||
if !failed {
|
||||
let mut completed_successfully = false;
|
||||
|
||||
if final_state == DecommissionFinalState::Complete {
|
||||
warn!("Decommissioning complete for pool {}, verifying for any pending objects", cmd_line);
|
||||
if let Err(er) = self.decommission_failed(idx).await {
|
||||
error!("decom failed err {:?}", &er);
|
||||
if let Err(err) = self.check_after_decommission(idx).await {
|
||||
error!("decom post-check err {:?}", &err);
|
||||
if let Err(er) = self.decommission_failed(idx).await {
|
||||
error!("decom failed err {:?}", &er);
|
||||
}
|
||||
} else if let Err(er) = self.complete_decommission(idx).await {
|
||||
error!("decom complete err {:?}", &er);
|
||||
} else {
|
||||
completed_successfully = true;
|
||||
}
|
||||
} else if let Err(er) = self.complete_decommission(idx).await {
|
||||
error!("decom complete err {:?}", &er);
|
||||
} else if let Err(er) = self.decommission_failed(idx).await {
|
||||
error!("decom failed err {:?}", &er);
|
||||
}
|
||||
|
||||
warn!("Decommissioning complete for pool {}", cmd_line);
|
||||
{
|
||||
let mut cancelers = self.decommission_cancelers.write().await;
|
||||
if let Some(slot) = cancelers.get_mut(idx) {
|
||||
*slot = None;
|
||||
}
|
||||
}
|
||||
|
||||
if completed_successfully {
|
||||
warn!("Decommissioning complete for pool {}", cmd_line);
|
||||
} else {
|
||||
warn!("Decommissioning finished in failed state for pool {}", cmd_line);
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
@@ -1043,6 +1414,14 @@ impl ECStore {
|
||||
}
|
||||
}
|
||||
|
||||
let canceler = {
|
||||
let mut cancelers = self.decommission_cancelers.write().await;
|
||||
cancelers.get_mut(idx).and_then(Option::take)
|
||||
};
|
||||
if let Some(canceler) = canceler {
|
||||
canceler.cancel();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1061,6 +1440,14 @@ impl ECStore {
|
||||
}
|
||||
}
|
||||
|
||||
let canceler = {
|
||||
let mut cancelers = self.decommission_cancelers.write().await;
|
||||
cancelers.get_mut(idx).and_then(Option::take)
|
||||
};
|
||||
if let Some(canceler) = canceler {
|
||||
canceler.cancel();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1190,6 +1577,94 @@ impl ECStore {
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
async fn check_after_decommission(self: &Arc<Self>, idx: usize) -> Result<()> {
|
||||
let buckets = self.get_buckets_to_decommission().await?;
|
||||
let pool = self.pools[idx].clone();
|
||||
|
||||
for set in &pool.disk_set {
|
||||
for bucket_info in &buckets {
|
||||
let mut lifecycle_config = None;
|
||||
let mut lock_retention = None;
|
||||
let mut replication_config = None;
|
||||
if bucket_info.name != RUSTFS_META_BUCKET {
|
||||
lifecycle_config = GLOBAL_LifecycleSys.get(&bucket_info.name).await;
|
||||
lock_retention = BucketObjectLockSys::get(&bucket_info.name).await;
|
||||
replication_config = match metadata_sys::get_replication_config(&bucket_info.name).await {
|
||||
Ok(config) => Some(config),
|
||||
Err(Error::ConfigNotFound) => None,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
}
|
||||
|
||||
let versions_found = Arc::new(AtomicUsize::new(0));
|
||||
let versions_found_cb = versions_found.clone();
|
||||
let bucket_name = bucket_info.name.clone();
|
||||
let lifecycle_config_cb = lifecycle_config.clone();
|
||||
let lock_retention_cb = lock_retention.clone();
|
||||
let replication_config_cb = replication_config.clone();
|
||||
let store = Arc::clone(self);
|
||||
|
||||
let callback: ListCallback = Arc::new(move |entry: MetaCacheEntry| {
|
||||
let versions_found = versions_found_cb.clone();
|
||||
let bucket_name = bucket_name.clone();
|
||||
let lifecycle_config = lifecycle_config_cb.clone();
|
||||
let lock_retention = lock_retention_cb.clone();
|
||||
let replication_config = replication_config_cb.clone();
|
||||
let store = Arc::clone(&store);
|
||||
Box::pin(async move {
|
||||
if !entry.is_object() {
|
||||
return;
|
||||
}
|
||||
|
||||
if bucket_name == RUSTFS_META_BUCKET && entry.name.contains(DATA_USAGE_CACHE_NAME) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Ok(fivs) = entry.file_info_versions(&bucket_name) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut remaining = 0;
|
||||
for version in &fivs.versions {
|
||||
if version.deleted {
|
||||
continue;
|
||||
}
|
||||
if should_skip_lifecycle_for_decommission(
|
||||
Arc::clone(&store),
|
||||
&bucket_name,
|
||||
version,
|
||||
lifecycle_config.as_ref(),
|
||||
lock_retention.clone(),
|
||||
replication_config.clone(),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
{
|
||||
continue;
|
||||
}
|
||||
remaining += 1;
|
||||
}
|
||||
|
||||
versions_found.fetch_add(remaining, Ordering::Relaxed);
|
||||
})
|
||||
});
|
||||
|
||||
set.list_objects_to_decommission(CancellationToken::new(), bucket_info.clone(), callback)
|
||||
.await?;
|
||||
|
||||
let versions_found = versions_found.load(Ordering::Relaxed);
|
||||
if versions_found > 0 {
|
||||
return Err(Error::other(format!(
|
||||
"at least {versions_found} object(s)/version(s) were found in bucket `{}` after decommissioning",
|
||||
bucket_info.name
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, rd))]
|
||||
async fn decommission_object(self: Arc<Self>, pool_idx: usize, bucket: String, rd: GetObjectReader) -> Result<()> {
|
||||
warn!("decommission_object: start {} {}", &bucket, &rd.object_info.name);
|
||||
@@ -1238,7 +1713,10 @@ impl ECStore {
|
||||
|
||||
reader.read_exact(&mut chunk).await?;
|
||||
|
||||
let mut data = PutObjReader::from_vec(chunk);
|
||||
let part_size = i64::try_from(part.size).map_err(|_| Error::other("part size overflow"))?;
|
||||
let part_actual_size = if part.actual_size > 0 { part.actual_size } else { part_size };
|
||||
let index = decode_part_index(part.index.as_ref());
|
||||
let mut data = put_obj_reader_from_chunk(chunk, part_size, part_actual_size, index)?;
|
||||
|
||||
let pi = match self
|
||||
.put_object_part(
|
||||
@@ -1294,8 +1772,13 @@ impl ECStore {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let reader = BufReader::new(rd.stream);
|
||||
let hrd = HashReader::new(Box::new(WarpReader::new(reader)), object_info.size, object_info.size, None, None, false)?;
|
||||
let actual_size = object_info.get_actual_size()?;
|
||||
let index = object_info
|
||||
.parts
|
||||
.first()
|
||||
.and_then(|part| decode_part_index(part.index.as_ref()));
|
||||
let reader = IndexedDecommissionReader::new(WarpReader::new(BufReader::new(rd.stream)), index);
|
||||
let hrd = HashReader::new(Box::new(reader), object_info.size, actual_size, object_info.etag.clone(), None, false)?;
|
||||
let mut data = PutObjReader::new(hrd);
|
||||
|
||||
if let Err(err) = self
|
||||
@@ -1325,6 +1808,159 @@ impl ECStore {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::items_after_test_module)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn determine_decommission_final_state_marks_failures_and_cancellations() {
|
||||
assert_eq!(determine_decommission_final_state(0, false), DecommissionFinalState::Complete);
|
||||
assert_eq!(determine_decommission_final_state(1, false), DecommissionFinalState::Failed);
|
||||
assert_eq!(determine_decommission_final_state(0, true), DecommissionFinalState::Failed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remaining_versions_after_decommission_ignores_delete_markers() {
|
||||
let fivs = FileInfoVersions {
|
||||
versions: vec![
|
||||
rustfs_filemeta::FileInfo {
|
||||
deleted: false,
|
||||
size: 128,
|
||||
..Default::default()
|
||||
},
|
||||
rustfs_filemeta::FileInfo {
|
||||
deleted: true,
|
||||
size: 0,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(remaining_versions_after_decommission(&fivs), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decommission_delete_marker_opts_preserves_replication_state() {
|
||||
let mod_time = OffsetDateTime::now_utc();
|
||||
let version = rustfs_filemeta::FileInfo {
|
||||
mod_time: Some(mod_time),
|
||||
replication_state_internal: Some(rustfs_filemeta::ReplicationState {
|
||||
replica_status: rustfs_filemeta::ReplicationStatusType::Replica,
|
||||
delete_marker: true,
|
||||
replicate_decision_str: "existing".to_string(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let opts = decommission_delete_marker_opts(&version, Some("version-id".to_string()), 7);
|
||||
let replication = opts.delete_replication.expect("replication state should be preserved");
|
||||
|
||||
assert!(opts.versioned);
|
||||
assert!(opts.data_movement);
|
||||
assert!(opts.delete_marker);
|
||||
assert!(opts.skip_decommissioned);
|
||||
assert_eq!(opts.src_pool_idx, 7);
|
||||
assert_eq!(opts.version_id.as_deref(), Some("version-id"));
|
||||
assert_eq!(opts.mod_time, Some(mod_time));
|
||||
assert_eq!(replication.replica_status, rustfs_filemeta::ReplicationStatusType::Replica);
|
||||
assert!(replication.delete_marker);
|
||||
assert_eq!(replication.replicate_decision_str, "existing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decommission_state_transitions_preserve_start_time() {
|
||||
let start_time = OffsetDateTime::now_utc();
|
||||
let mut pool_meta = PoolMeta {
|
||||
version: POOL_META_VERSION,
|
||||
pools: vec![PoolStatus {
|
||||
id: 0,
|
||||
cmd_line: "/tmp/pool".to_string(),
|
||||
last_update: start_time,
|
||||
decommission: Some(PoolDecommissionInfo {
|
||||
start_time: Some(start_time),
|
||||
..Default::default()
|
||||
}),
|
||||
}],
|
||||
dont_save: true,
|
||||
};
|
||||
|
||||
assert!(pool_meta.decommission_failed(0));
|
||||
assert_eq!(
|
||||
pool_meta.pools[0].decommission.as_ref().and_then(|info| info.start_time),
|
||||
Some(start_time)
|
||||
);
|
||||
|
||||
assert!(pool_meta.decommission_complete(0));
|
||||
assert_eq!(
|
||||
pool_meta.pools[0].decommission.as_ref().and_then(|info| info.start_time),
|
||||
Some(start_time)
|
||||
);
|
||||
|
||||
assert!(pool_meta.decommission_cancel(0));
|
||||
assert_eq!(
|
||||
pool_meta.pools[0].decommission.as_ref().and_then(|info| info.start_time),
|
||||
Some(start_time)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_meta_persists_decommission_resume_queues() {
|
||||
let start_time = OffsetDateTime::now_utc();
|
||||
let pool_meta = PoolMeta {
|
||||
version: POOL_META_VERSION,
|
||||
pools: vec![PoolStatus {
|
||||
id: 1,
|
||||
cmd_line: "/data/pool1/disk{1...4}".to_string(),
|
||||
last_update: start_time,
|
||||
decommission: Some(PoolDecommissionInfo {
|
||||
start_time: Some(start_time),
|
||||
queued_buckets: vec!["bucket-a".to_string(), "bucket-b/prefix".to_string()],
|
||||
decommissioned_buckets: vec!["bucket-done".to_string()],
|
||||
bucket: "bucket-b".to_string(),
|
||||
prefix: "prefix".to_string(),
|
||||
object: "object.txt".to_string(),
|
||||
items_decommissioned: 7,
|
||||
items_decommission_failed: 1,
|
||||
bytes_done: 1024,
|
||||
bytes_failed: 128,
|
||||
..Default::default()
|
||||
}),
|
||||
}],
|
||||
dont_save: false,
|
||||
};
|
||||
|
||||
let mut buf = Vec::new();
|
||||
PersistedPoolMeta::from(&pool_meta)
|
||||
.serialize(&mut Serializer::new(&mut buf))
|
||||
.expect("pool meta should serialize");
|
||||
|
||||
let mut deserializer = Deserializer::new(Cursor::new(&buf));
|
||||
let restored: PoolMeta = PersistedPoolMeta::deserialize(&mut deserializer)
|
||||
.expect("pool meta should deserialize")
|
||||
.into();
|
||||
|
||||
let restored_decommission = restored.pools[0]
|
||||
.decommission
|
||||
.as_ref()
|
||||
.expect("decommission info should survive round-trip");
|
||||
assert_eq!(
|
||||
restored_decommission.queued_buckets,
|
||||
vec!["bucket-a".to_string(), "bucket-b/prefix".to_string()]
|
||||
);
|
||||
assert_eq!(restored_decommission.decommissioned_buckets, vec!["bucket-done".to_string()]);
|
||||
assert_eq!(restored_decommission.bucket, "bucket-b");
|
||||
assert_eq!(restored_decommission.prefix, "prefix");
|
||||
assert_eq!(restored_decommission.object, "object.txt");
|
||||
assert_eq!(restored_decommission.items_decommissioned, 7);
|
||||
assert_eq!(restored_decommission.items_decommission_failed, 1);
|
||||
assert_eq!(restored_decommission.bytes_done, 1024);
|
||||
assert_eq!(restored_decommission.bytes_failed, 128);
|
||||
}
|
||||
}
|
||||
|
||||
// impl Fn(MetaCacheEntry) -> impl Future<Output = Result<(), Error>>
|
||||
|
||||
pub type ListCallback = Arc<dyn Fn(MetaCacheEntry) -> BoxFuture<'static, ()> + Send + Sync + 'static>;
|
||||
|
||||
Reference in New Issue
Block a user