mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-23 12:49:04 +00:00
d6c62b9601
Removing the blanket exposes twenty-five items across tier, notification and rebalance. Only eight are deleted — the lowest ratio of this burn-down so far, and the reason is that these subsystems carry heavy test coverage, so the blanket was mostly hiding test-only seams rather than dead weight. Deleted: - crates/ecstore/src/services/tier/warm_backend_s3sdk.rs entirely (200 lines). Its WarmBackendS3 is never constructed; the type of the same name in warm_backend_s3.rs is the live one, wrapped by the Azure backend. Two implementations of one S3 warm backend, one of them never wired. - TierConfigMgr::begin_publish_transition and publish_candidate_inner, thin wrappers whose _with_allowed_mutation_blocks siblings carry every real caller, plus retire_driver. - The GCS backend's MAX_MULTIPART_PUT_OBJECT_SIZE, MAX_PARTS_COUNT and MIN_PART_SIZE, and its write-only storage_class field. - mark_started_rebalance_pools_stopped and the RStats alias. Two deletions were withdrawn after a per-name grep, both because of an inference rather than a check: AsyncBatchProcessor::new was deleted on the strength of grepping only BATCH_PROCESSOR_OPERATION_CUSTOM, whose two hits are its definition and its use inside new. That looked like a self-contained dead pair; new in fact has seven test callers. The warning listed both items, and only one of them was actually checked. Deleting the two dead publish wrappers then revealed a second layer — publish_candidate_owned, remove_and_save_with, clear_and_save_with, save_tiering_config_if_current. These are not dead: publish_candidate, their caller, is #[cfg(test)], so a callee that lives in the main body has no caller in the lib build and a live one in the test build. rustc reports the roots of a dead subgraph, and the next layer down can have a different character, so each layer needs its own grep. Kept with allows: the tier mutation-intent record helpers (asserted by store::init tests), affected_targets, tier_object_blocks_target_rebind, the rebalance snapshot and retry-wait helpers, notification_sys's tier_config_reload_worker_active and call_peer_with_timeout, and active_operation_lease_count, whose only caller sits behind #[cfg(feature = "test-util")]. Also kept, with a module note rather than removal: the ecstore-side EventNotifier. All four of its methods are unreachable and init_bucket_targets logs that it is a no-op in this build; the working stack is rustfs-notify, whose own EventNotifier drives bucket configuration. Removing it means also retiring the InstanceContext slot that holds it (backlog#939 Phase 5), which belongs in its own PR. Worth a separate issue: MAX_MULTIPART_PUT_OBJECT_SIZE, MAX_PARTS_COUNT and MIN_PART_SIZE are declared independently in eight warm-backend files plus client/constants.rs. Only the GCS copies were dead; the other seven backends each use their own. Verification, four lanes warning-free: default, --tests, --features rio-v2 --tests, --features test-util --tests. cargo nextest run -p rustfs-ecstore 4041 passed; clippy --lib --tests -D warnings clean; make pre-commit exit 0. Ref rustfs/backlog#1823 (step 2).
427 lines
13 KiB
Rust
427 lines
13 KiB
Rust
use super::worker::{is_transient_rebalance_error, rebalance_migration_retry_delay, sleep_rebalance_migration_retry};
|
|
use crate::bucket::replication::replication_state_from_filemeta;
|
|
use crate::data_usage::DATA_USAGE_CACHE_NAME;
|
|
use crate::error::{Error, Result, is_err_object_not_found, is_err_version_not_found};
|
|
use crate::object_api::{GetObjectReader, ObjectInfo, ObjectOptions};
|
|
use crate::set_disk::SetDisks;
|
|
use crate::storage_api_contracts::{object::ObjectIO, range::HTTPRangeSpec};
|
|
use crate::store::ECStore;
|
|
use http::HeaderMap;
|
|
use rustfs_filemeta::FileInfo;
|
|
use rustfs_utils::path::encode_dir_object;
|
|
use std::future::Future;
|
|
use tokio::time::Duration;
|
|
|
|
#[derive(Debug, Default, Clone)]
|
|
pub(crate) struct MigrationVersionResult {
|
|
pub moved: bool,
|
|
pub ignored: bool,
|
|
pub cleanup_ignored: bool,
|
|
pub failed: bool,
|
|
pub stage: Option<&'static str>,
|
|
pub error: Option<Error>,
|
|
}
|
|
|
|
pub(super) fn rebalance_delete_marker_opts(
|
|
version: &FileInfo,
|
|
version_id: Option<String>,
|
|
src_pool_idx: usize,
|
|
expected_bucket_incarnation_id: Option<uuid::Uuid>,
|
|
) -> ObjectOptions {
|
|
let version_suspended = version.version_id.is_none() && version_id.is_none();
|
|
ObjectOptions {
|
|
versioned: !version_suspended,
|
|
version_suspended,
|
|
version_id: version_id.or_else(|| version_suspended.then(|| uuid::Uuid::nil().to_string())),
|
|
mod_time: version.mod_time,
|
|
src_pool_idx,
|
|
data_movement: true,
|
|
delete_marker: true,
|
|
skip_decommissioned: true,
|
|
expected_bucket_incarnation_id,
|
|
delete_replication: version
|
|
.replication_state_internal
|
|
.as_ref()
|
|
.map(replication_state_from_filemeta),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn rebalance_remote_tiered_opts(
|
|
version: &FileInfo,
|
|
version_id: Option<String>,
|
|
src_pool_idx: usize,
|
|
expected_bucket_incarnation_id: Option<uuid::Uuid>,
|
|
) -> ObjectOptions {
|
|
ObjectOptions {
|
|
versioned: version_id.is_some(),
|
|
version_id,
|
|
mod_time: version.mod_time,
|
|
user_defined: version.metadata.clone(),
|
|
src_pool_idx,
|
|
data_movement: true,
|
|
include_part_checksums: true,
|
|
http_preconditions: Some(crate::data_movement::data_movement_target_precondition()),
|
|
expected_bucket_incarnation_id,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
pub(super) fn rebalance_object_migration_read_opts(version_id: Option<String>) -> ObjectOptions {
|
|
ObjectOptions {
|
|
version_id,
|
|
no_lock: true,
|
|
data_movement: true,
|
|
raw_data_movement_read: true,
|
|
skip_decommissioned: true,
|
|
skip_rebalancing: true,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
pub(crate) trait MigrationBackend: Send + Sync {
|
|
async fn get_object_reader_for_migration(
|
|
&self,
|
|
bucket: &str,
|
|
object: &str,
|
|
range: Option<HTTPRangeSpec>,
|
|
h: HeaderMap,
|
|
opts: &ObjectOptions,
|
|
) -> Result<GetObjectReader>;
|
|
|
|
async fn move_remote_version_for_migration(
|
|
&self,
|
|
bucket: &str,
|
|
object: &str,
|
|
fi: &FileInfo,
|
|
opts: &ObjectOptions,
|
|
) -> Result<()>;
|
|
}
|
|
|
|
pub(crate) struct RebalanceMigrationBackend<'a> {
|
|
source: &'a SetDisks,
|
|
store: &'a ECStore,
|
|
}
|
|
|
|
impl<'a> RebalanceMigrationBackend<'a> {
|
|
pub(crate) fn new(source: &'a SetDisks, store: &'a ECStore) -> Self {
|
|
Self { source, store }
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl MigrationBackend for RebalanceMigrationBackend<'_> {
|
|
async fn get_object_reader_for_migration(
|
|
&self,
|
|
bucket: &str,
|
|
object: &str,
|
|
range: Option<HTTPRangeSpec>,
|
|
h: HeaderMap,
|
|
opts: &ObjectOptions,
|
|
) -> Result<GetObjectReader> {
|
|
self.source.get_object_reader(bucket, object, range, h, opts).await
|
|
}
|
|
|
|
async fn move_remote_version_for_migration(
|
|
&self,
|
|
bucket: &str,
|
|
object: &str,
|
|
fi: &FileInfo,
|
|
opts: &ObjectOptions,
|
|
) -> Result<()> {
|
|
self.store.decommission_tiered_object(bucket, object, fi, opts).await
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub(crate) async fn migrate_entry_version<Backend, F, Fut, D, DFut>(
|
|
set: &Backend,
|
|
bucket: String,
|
|
pool_index: usize,
|
|
version: &FileInfo,
|
|
version_id: Option<String>,
|
|
expected_bucket_incarnation_id: Option<uuid::Uuid>,
|
|
max_attempts: usize,
|
|
ignore_data_usage_cache: bool,
|
|
transfer: F,
|
|
delete_marker: D,
|
|
) -> MigrationVersionResult
|
|
where
|
|
Backend: MigrationBackend + ?Sized,
|
|
F: FnMut(usize, String, GetObjectReader) -> Fut + Send,
|
|
Fut: Future<Output = Result<()>> + Send,
|
|
D: FnMut(String, String, ObjectOptions) -> DFut + Send,
|
|
DFut: Future<Output = Result<ObjectInfo>> + Send,
|
|
{
|
|
migrate_entry_version_with_retry_wait_and_incarnation(
|
|
set,
|
|
bucket,
|
|
pool_index,
|
|
version,
|
|
version_id,
|
|
expected_bucket_incarnation_id,
|
|
max_attempts,
|
|
ignore_data_usage_cache,
|
|
transfer,
|
|
delete_marker,
|
|
sleep_rebalance_migration_retry,
|
|
)
|
|
.await
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
#[allow(dead_code, reason = "asserted by this file's tests (backlog#1823)")]
|
|
pub(super) async fn migrate_entry_version_with_retry_wait<Backend, F, Fut, D, DFut, W, WFut>(
|
|
set: &Backend,
|
|
bucket: String,
|
|
pool_index: usize,
|
|
version: &FileInfo,
|
|
version_id: Option<String>,
|
|
max_attempts: usize,
|
|
ignore_data_usage_cache: bool,
|
|
transfer: F,
|
|
delete_marker: D,
|
|
wait_retry: W,
|
|
) -> MigrationVersionResult
|
|
where
|
|
Backend: MigrationBackend + ?Sized,
|
|
F: FnMut(usize, String, GetObjectReader) -> Fut + Send,
|
|
Fut: Future<Output = Result<()>> + Send,
|
|
D: FnMut(String, String, ObjectOptions) -> DFut + Send,
|
|
DFut: Future<Output = Result<ObjectInfo>> + Send,
|
|
W: FnMut(Duration) -> WFut + Send,
|
|
WFut: Future<Output = ()> + Send,
|
|
{
|
|
migrate_entry_version_with_retry_wait_and_incarnation(
|
|
set,
|
|
bucket,
|
|
pool_index,
|
|
version,
|
|
version_id,
|
|
None,
|
|
max_attempts,
|
|
ignore_data_usage_cache,
|
|
transfer,
|
|
delete_marker,
|
|
wait_retry,
|
|
)
|
|
.await
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
async fn migrate_entry_version_with_retry_wait_and_incarnation<Backend, F, Fut, D, DFut, W, WFut>(
|
|
set: &Backend,
|
|
bucket: String,
|
|
pool_index: usize,
|
|
version: &FileInfo,
|
|
version_id: Option<String>,
|
|
expected_bucket_incarnation_id: Option<uuid::Uuid>,
|
|
max_attempts: usize,
|
|
ignore_data_usage_cache: bool,
|
|
mut transfer: F,
|
|
mut delete_marker: D,
|
|
mut wait_retry: W,
|
|
) -> MigrationVersionResult
|
|
where
|
|
Backend: MigrationBackend + ?Sized,
|
|
F: FnMut(usize, String, GetObjectReader) -> Fut + Send,
|
|
Fut: Future<Output = Result<()>> + Send,
|
|
D: FnMut(String, String, ObjectOptions) -> DFut + Send,
|
|
DFut: Future<Output = Result<ObjectInfo>> + Send,
|
|
W: FnMut(Duration) -> WFut + Send,
|
|
WFut: Future<Output = ()> + Send,
|
|
{
|
|
let max_attempts = max_attempts.max(1);
|
|
|
|
if ignore_data_usage_cache && bucket == crate::disk::RUSTFS_META_BUCKET && version.name.contains(DATA_USAGE_CACHE_NAME) {
|
|
return MigrationVersionResult {
|
|
moved: false,
|
|
ignored: true,
|
|
cleanup_ignored: false,
|
|
failed: false,
|
|
stage: None,
|
|
error: None,
|
|
};
|
|
}
|
|
|
|
if version.is_remote() {
|
|
if let Err(err) = set
|
|
.move_remote_version_for_migration(
|
|
&bucket,
|
|
&version.name,
|
|
version,
|
|
&rebalance_remote_tiered_opts(version, version_id, pool_index, expected_bucket_incarnation_id),
|
|
)
|
|
.await
|
|
{
|
|
if is_err_object_not_found(&err) || is_err_version_not_found(&err) {
|
|
return MigrationVersionResult {
|
|
moved: false,
|
|
ignored: true,
|
|
cleanup_ignored: true,
|
|
failed: false,
|
|
stage: Some("move_remote_version"),
|
|
error: None,
|
|
};
|
|
}
|
|
|
|
return MigrationVersionResult {
|
|
moved: false,
|
|
ignored: false,
|
|
cleanup_ignored: false,
|
|
failed: true,
|
|
stage: Some("move_remote_version"),
|
|
error: Some(err),
|
|
};
|
|
}
|
|
|
|
return MigrationVersionResult {
|
|
moved: true,
|
|
ignored: false,
|
|
cleanup_ignored: false,
|
|
failed: false,
|
|
stage: None,
|
|
error: None,
|
|
};
|
|
}
|
|
|
|
if version.deleted {
|
|
// Delete markers must be routed through the store layer (ECStore::delete_object /
|
|
// handle_delete_object), which honours data_movement/src_pool_idx/delete_marker and
|
|
// writes the marker to the cross-pool target. Writing via the source SetDisks would
|
|
// silently rewrite the marker back onto the source set and lose it during cleanup.
|
|
if let Err(err) = delete_marker(
|
|
bucket.clone(),
|
|
version.name.clone(),
|
|
rebalance_delete_marker_opts(version, version_id, pool_index, expected_bucket_incarnation_id),
|
|
)
|
|
.await
|
|
{
|
|
if is_err_object_not_found(&err) || is_err_version_not_found(&err) {
|
|
return MigrationVersionResult {
|
|
moved: false,
|
|
ignored: true,
|
|
cleanup_ignored: true,
|
|
failed: false,
|
|
stage: Some("delete_marker"),
|
|
error: None,
|
|
};
|
|
}
|
|
|
|
return MigrationVersionResult {
|
|
moved: false,
|
|
ignored: false,
|
|
cleanup_ignored: false,
|
|
failed: true,
|
|
stage: Some("delete_marker"),
|
|
error: Some(err),
|
|
};
|
|
}
|
|
|
|
return MigrationVersionResult {
|
|
moved: true,
|
|
ignored: false,
|
|
cleanup_ignored: false,
|
|
failed: false,
|
|
stage: None,
|
|
error: None,
|
|
};
|
|
}
|
|
|
|
let mut last_error: Option<Error> = None;
|
|
for attempt in 0..max_attempts {
|
|
let rd = match set
|
|
.get_object_reader_for_migration(
|
|
&bucket,
|
|
&encode_dir_object(&version.name),
|
|
None,
|
|
HeaderMap::new(),
|
|
&rebalance_object_migration_read_opts(version_id.clone()),
|
|
)
|
|
.await
|
|
{
|
|
Ok(rd) => rd,
|
|
Err(err) => {
|
|
if is_err_object_not_found(&err) || is_err_version_not_found(&err) {
|
|
return MigrationVersionResult {
|
|
moved: false,
|
|
ignored: true,
|
|
cleanup_ignored: true,
|
|
failed: false,
|
|
stage: Some("read_source"),
|
|
error: None,
|
|
};
|
|
}
|
|
|
|
last_error = Some(err);
|
|
let Some(err) = last_error.as_ref() else {
|
|
continue;
|
|
};
|
|
if attempt + 1 >= max_attempts || !is_transient_rebalance_error(err) {
|
|
return MigrationVersionResult {
|
|
moved: false,
|
|
ignored: false,
|
|
cleanup_ignored: false,
|
|
failed: true,
|
|
stage: Some("read_source"),
|
|
error: last_error,
|
|
};
|
|
}
|
|
|
|
wait_retry(rebalance_migration_retry_delay(attempt, err)).await;
|
|
continue;
|
|
}
|
|
};
|
|
|
|
if let Err(err) = transfer(pool_index, bucket.clone(), rd).await {
|
|
if is_err_object_not_found(&err) || is_err_version_not_found(&err) {
|
|
return MigrationVersionResult {
|
|
moved: false,
|
|
ignored: true,
|
|
cleanup_ignored: true,
|
|
failed: false,
|
|
stage: Some("write_target"),
|
|
error: None,
|
|
};
|
|
}
|
|
|
|
last_error = Some(err);
|
|
let Some(err) = last_error.as_ref() else {
|
|
continue;
|
|
};
|
|
if attempt + 1 >= max_attempts || !is_transient_rebalance_error(err) {
|
|
return MigrationVersionResult {
|
|
moved: false,
|
|
ignored: false,
|
|
cleanup_ignored: false,
|
|
failed: true,
|
|
stage: Some("write_target"),
|
|
error: last_error,
|
|
};
|
|
}
|
|
|
|
wait_retry(rebalance_migration_retry_delay(attempt, err)).await;
|
|
continue;
|
|
}
|
|
|
|
return MigrationVersionResult {
|
|
moved: true,
|
|
ignored: false,
|
|
cleanup_ignored: false,
|
|
failed: false,
|
|
stage: None,
|
|
error: None,
|
|
};
|
|
}
|
|
|
|
MigrationVersionResult {
|
|
moved: false,
|
|
ignored: false,
|
|
cleanup_ignored: false,
|
|
failed: true,
|
|
stage: Some("migrate"),
|
|
error: last_error,
|
|
}
|
|
}
|