mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-13 16:46:55 +00:00
fix(scanner): defer cycles during data movement (#5970)
* fix(scanner): defer cycles during data movement * fix(scanner): distinguish deferred scan cycles --------- Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com> Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
+106
-42
@@ -30,8 +30,8 @@ use crate::runtime_config::{
|
||||
use crate::scanner_budget::{ScannerCycleBudget, ScannerCycleBudgetConfig, ScannerCycleBudgetReason};
|
||||
use crate::scanner_folder::{data_usage_update_dir_cycles, heal_object_select_prob};
|
||||
use crate::scanner_io::{
|
||||
ScannerCycleStatus, ScannerIOCycle, dirty_usage_bucket_notified, dirty_usage_buckets_pending, dirty_usage_generation,
|
||||
scanner_dirty_usage_state, scanner_maintenance_changed, scanner_maintenance_generation,
|
||||
ScannerCycleDeferReason, ScannerCycleStatus, ScannerIOCycle, dirty_usage_bucket_notified, dirty_usage_buckets_pending,
|
||||
dirty_usage_generation, scanner_dirty_usage_state, scanner_maintenance_changed, scanner_maintenance_generation,
|
||||
};
|
||||
use crate::sleeper::{SCANNER_SLEEPER, set_scanner_default_speed};
|
||||
use crate::{DataUsageInfo, ScannerActivityGuard, ScannerError, ScannerRuntimeGuard};
|
||||
@@ -41,7 +41,8 @@ use chrono::{DateTime, Utc};
|
||||
use rustfs_common::heal_channel::HealScanMode;
|
||||
use rustfs_common::metrics::{
|
||||
CurrentCycle, Metric, Metrics, ScanCyclePartialReason, ScanCycleWorkSnapshot, ScannerUsageSaveResult, ScannerWorkSource,
|
||||
emit_scan_cycle_complete, emit_scan_cycle_partial_with_source, emit_scan_cycle_superseded, global_metrics,
|
||||
emit_scan_cycle_complete, emit_scan_cycle_deferred, emit_scan_cycle_partial_with_source, emit_scan_cycle_superseded,
|
||||
global_metrics,
|
||||
};
|
||||
use rustfs_config::ScannerSpeed;
|
||||
#[cfg(test)]
|
||||
@@ -84,7 +85,7 @@ const METRIC_SCANNER_LEADER_LOCK_TOTAL: &str = "rustfs_scanner_leader_lock_total
|
||||
const CLEAN_IDLE_MAX_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60);
|
||||
const MAX_SCANNER_SCHEDULE_DELAY: Duration = Duration::from_secs(365 * 24 * 60 * 60);
|
||||
const CLEAN_IDLE_BACKOFF_FACTOR: u32 = 2;
|
||||
/// First-retry delay after a usage snapshot is superseded by concurrent writes.
|
||||
/// First-retry delay after a scanner cycle cannot publish authoritative usage.
|
||||
///
|
||||
/// A superseded cycle is the *expected* outcome of the dirty-usage fast path:
|
||||
/// a write burst marks buckets dirty, the scanner wakes within milliseconds,
|
||||
@@ -94,14 +95,15 @@ const CLEAN_IDLE_BACKOFF_FACTOR: u32 = 2;
|
||||
/// otherwise idle instance whose clean-idle backoff had doubled a 60 s
|
||||
/// interval), which defeats the fast path it is meant to protect.
|
||||
///
|
||||
/// The exponential growth in [`ScannerSupersededBackoff::retry_interval`] is
|
||||
/// The exponential growth in [`ScannerRetryBackoff::retry_interval`] is
|
||||
/// what protects against a persistently hot bucket driving an unbroken
|
||||
/// full-scan loop, so it can start small: 5 s, 10 s, 20 s … capped by
|
||||
/// [`SUPERSEDED_RETRY_MAX_INTERVAL`]. A one-off race recovers in seconds; a
|
||||
/// [`SCANNER_RETRY_MAX_INTERVAL`]. A one-off race recovers in seconds; a
|
||||
/// genuinely hot bucket still reaches minute-scale backoff within a handful of
|
||||
/// cycles.
|
||||
const SUPERSEDED_RETRY_BASE_INTERVAL: Duration = Duration::from_secs(5);
|
||||
const SUPERSEDED_RETRY_MAX_INTERVAL: Duration = Duration::from_secs(30 * 60);
|
||||
/// cycles. Preflight deferrals use the same bounded schedule so a temporarily
|
||||
/// unavailable peer cannot drive a tight retry loop.
|
||||
const SCANNER_RETRY_BASE_INTERVAL: Duration = Duration::from_secs(5);
|
||||
const SCANNER_RETRY_MAX_INTERVAL: Duration = Duration::from_secs(30 * 60);
|
||||
const SCANNER_LEADER_LOCK_POLL_INTERVAL: Duration = Duration::from_secs(1);
|
||||
#[cfg(not(test))]
|
||||
const SCANNER_LOCK_LOSS_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
@@ -338,6 +340,7 @@ pub(crate) enum ScannerCycleOutcome {
|
||||
CompletedWithPendingMaintenance,
|
||||
Partial,
|
||||
Superseded,
|
||||
Deferred(ScannerCycleDeferReason),
|
||||
Failed,
|
||||
}
|
||||
|
||||
@@ -382,22 +385,16 @@ struct ScannerCleanIdleBackoff {
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
struct ScannerSupersededBackoff {
|
||||
struct ScannerRetryBackoff {
|
||||
consecutive_cycles: u32,
|
||||
}
|
||||
|
||||
impl ScannerSupersededBackoff {
|
||||
fn record_cycle(&mut self, outcome: ScannerCycleOutcome) {
|
||||
match outcome {
|
||||
ScannerCycleOutcome::Superseded => {
|
||||
self.consecutive_cycles = self.consecutive_cycles.saturating_add(1);
|
||||
}
|
||||
ScannerCycleOutcome::Completed
|
||||
| ScannerCycleOutcome::CompletedWithPendingMaintenance
|
||||
| ScannerCycleOutcome::Partial
|
||||
| ScannerCycleOutcome::Failed => {
|
||||
self.consecutive_cycles = 0;
|
||||
}
|
||||
impl ScannerRetryBackoff {
|
||||
fn record_retryable_cycle(&mut self, retryable: bool) {
|
||||
if retryable {
|
||||
self.consecutive_cycles = self.consecutive_cycles.saturating_add(1);
|
||||
} else {
|
||||
self.consecutive_cycles = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,8 +403,8 @@ impl ScannerSupersededBackoff {
|
||||
let multiplier = 1u32.checked_shl(exponent).unwrap_or(u32::MAX);
|
||||
let base_interval = configured_interval
|
||||
.max(Duration::from_secs(1))
|
||||
.min(SUPERSEDED_RETRY_BASE_INTERVAL);
|
||||
let cap = SUPERSEDED_RETRY_MAX_INTERVAL.max(configured_interval.max(Duration::from_secs(1)));
|
||||
.min(SCANNER_RETRY_BASE_INTERVAL);
|
||||
let cap = SCANNER_RETRY_MAX_INTERVAL.max(configured_interval.max(Duration::from_secs(1)));
|
||||
Some(base_interval.saturating_mul(multiplier).min(cap))
|
||||
}
|
||||
}
|
||||
@@ -3008,6 +3005,21 @@ async fn run_data_scanner_cycle(
|
||||
ScannerCycleOutcome::Failed
|
||||
};
|
||||
}
|
||||
ScannerCycleOutcome::Deferred(reason) => {
|
||||
info!(
|
||||
target: "rustfs::scanner",
|
||||
event = EVENT_SCANNER_CYCLE_STATE,
|
||||
component = LOG_COMPONENT_SCANNER,
|
||||
subsystem = LOG_SUBSYSTEM_RUNTIME,
|
||||
cycle = cycle_info.current,
|
||||
reason = reason.as_str(),
|
||||
state = "deferred",
|
||||
"Scanner cycle deferred before usage scanning began"
|
||||
);
|
||||
emit_scan_cycle_deferred(cycle_start.elapsed());
|
||||
mark_scan_cycle_idle(cycle_info, &mut cycle_metrics_guard).await;
|
||||
return ScannerCycleOutcome::Deferred(reason);
|
||||
}
|
||||
ScannerCycleOutcome::Superseded => {
|
||||
info!(
|
||||
target: "rustfs::scanner",
|
||||
@@ -3201,7 +3213,8 @@ async fn run_data_scanner_with_maintenance_state(
|
||||
let mut dirty_usage_generation_seen = dirty_usage_generation();
|
||||
let mut runtime_config_generation_seen = scanner_runtime_config_generation();
|
||||
let mut clean_idle_backoff = ScannerCleanIdleBackoff::default();
|
||||
let mut superseded_backoff = ScannerSupersededBackoff::default();
|
||||
let mut superseded_backoff = ScannerRetryBackoff::default();
|
||||
let mut deferred_backoff = ScannerRetryBackoff::default();
|
||||
let initial_runtime_config = resolve_scanner_runtime_config();
|
||||
if clean_idle_topology_supported
|
||||
&& scanner_clean_idle_backoff_configured(&initial_runtime_config)
|
||||
@@ -3331,7 +3344,8 @@ async fn run_data_scanner_with_maintenance_state(
|
||||
)
|
||||
.await
|
||||
.unwrap_or(ScannerCycleOutcome::Failed);
|
||||
superseded_backoff.record_cycle(initial_outcome);
|
||||
superseded_backoff.record_retryable_cycle(initial_outcome == ScannerCycleOutcome::Superseded);
|
||||
deferred_backoff.record_retryable_cycle(matches!(initial_outcome, ScannerCycleOutcome::Deferred(_)));
|
||||
dirty_usage_generation_seen = dirty_generation_before_cycle;
|
||||
if guard.is_lock_lost() {
|
||||
record_scanner_leader_lock_lost("Scanner leader lock lost during the initial cycle").await;
|
||||
@@ -3416,7 +3430,9 @@ async fn run_data_scanner_with_maintenance_state(
|
||||
let mut wait_plan =
|
||||
scanner_cycle_wait_plan(&runtime_config, clean_idle_backoff, backoff_enabled, randomized_cycle_delay_for);
|
||||
let superseded_retry_interval = superseded_backoff.retry_interval(runtime_config.cycle_interval);
|
||||
if let Some(retry_interval) = superseded_retry_interval {
|
||||
let deferred_retry_interval = deferred_backoff.retry_interval(runtime_config.cycle_interval);
|
||||
let convergence_retry_interval = superseded_retry_interval.or(deferred_retry_interval);
|
||||
if let Some(retry_interval) = convergence_retry_interval {
|
||||
wait_plan.effective_interval = retry_interval;
|
||||
wait_plan.delay = randomized_cycle_delay_for(retry_interval).min(retry_interval);
|
||||
}
|
||||
@@ -3443,6 +3459,8 @@ async fn run_data_scanner_with_maintenance_state(
|
||||
clean_idle_backoff_enabled = backoff_enabled,
|
||||
superseded_retry_backoff_enabled = superseded_retry_interval.is_some(),
|
||||
superseded_cycles = superseded_backoff.consecutive_cycles,
|
||||
deferred_retry_backoff_enabled = deferred_retry_interval.is_some(),
|
||||
deferred_cycles = deferred_backoff.consecutive_cycles,
|
||||
lifecycle_active = maintenance_features.lifecycle,
|
||||
replication_active = maintenance_features.replication,
|
||||
feature_inspection_failed = maintenance_features.inspection_failed,
|
||||
@@ -3457,13 +3475,12 @@ async fn run_data_scanner_with_maintenance_state(
|
||||
activity_poll_interval,
|
||||
&mut scanner_activity_seen,
|
||||
ScannerCycleObservedGenerations {
|
||||
// A superseded cycle already observed concurrent writes. Hold
|
||||
// further dirty notifications until the bounded retry timer so
|
||||
// a hot bucket cannot drive an unbroken full-scan loop.
|
||||
dirty_usage: superseded_retry_interval.is_none().then_some(dirty_usage_generation_seen),
|
||||
// A non-converged cycle holds further activity notifications
|
||||
// until its bounded retry timer to avoid an unbroken scan loop.
|
||||
dirty_usage: convergence_retry_interval.is_none().then_some(dirty_usage_generation_seen),
|
||||
runtime_config: runtime_config_generation_seen,
|
||||
maintenance: maintenance_generation_before_wait,
|
||||
defer_cluster_activity: superseded_retry_interval.is_some(),
|
||||
defer_cluster_activity: convergence_retry_interval.is_some(),
|
||||
},
|
||||
|| guard.is_lock_lost(),
|
||||
|| probe_scanner_activity(storeapi.as_ref(), distributed),
|
||||
@@ -3540,7 +3557,8 @@ async fn run_data_scanner_with_maintenance_state(
|
||||
)
|
||||
.await
|
||||
.unwrap_or(ScannerCycleOutcome::Failed);
|
||||
superseded_backoff.record_cycle(outcome);
|
||||
superseded_backoff.record_retryable_cycle(outcome == ScannerCycleOutcome::Superseded);
|
||||
deferred_backoff.record_retryable_cycle(matches!(outcome, ScannerCycleOutcome::Deferred(_)));
|
||||
dirty_usage_generation_seen = dirty_generation_before_cycle;
|
||||
if guard.is_lock_lost() {
|
||||
record_scanner_leader_lock_lost("Scanner leader lock lost during a scanner cycle").await;
|
||||
@@ -3710,6 +3728,12 @@ fn scanner_cycle_completion_outcome(
|
||||
) -> ScannerCycleOutcome {
|
||||
match (scan_status, usage_persist_outcome) {
|
||||
(_, DataUsagePersistOutcome::Failed) => ScannerCycleOutcome::Failed,
|
||||
(ScannerCycleStatus::Deferred(reason), DataUsagePersistOutcome::NoUpdate)
|
||||
if !has_dirty_usage && !has_failed_dirty_usage =>
|
||||
{
|
||||
ScannerCycleOutcome::Deferred(reason)
|
||||
}
|
||||
(ScannerCycleStatus::Deferred(_), _) => ScannerCycleOutcome::Failed,
|
||||
(ScannerCycleStatus::Superseded, _) if !has_failed_dirty_usage => ScannerCycleOutcome::Superseded,
|
||||
(ScannerCycleStatus::Superseded, _) => ScannerCycleOutcome::Failed,
|
||||
(
|
||||
@@ -6553,6 +6577,46 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_scanner_cycle_completion_prioritizes_persist_failure() {
|
||||
assert_eq!(
|
||||
scanner_cycle_completion_outcome(
|
||||
ScannerCycleStatus::Deferred(ScannerCycleDeferReason::ActivityBaselineUnavailable),
|
||||
DataUsagePersistOutcome::NoUpdate,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
ScannerCycleOutcome::Deferred(ScannerCycleDeferReason::ActivityBaselineUnavailable)
|
||||
);
|
||||
assert_eq!(
|
||||
scanner_cycle_completion_outcome(
|
||||
ScannerCycleStatus::Deferred(ScannerCycleDeferReason::DataMovement),
|
||||
DataUsagePersistOutcome::Saved,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
ScannerCycleOutcome::Failed
|
||||
);
|
||||
assert_eq!(
|
||||
scanner_cycle_completion_outcome(
|
||||
ScannerCycleStatus::Deferred(ScannerCycleDeferReason::DataMovement),
|
||||
DataUsagePersistOutcome::NoUpdate,
|
||||
true,
|
||||
false,
|
||||
),
|
||||
ScannerCycleOutcome::Failed
|
||||
);
|
||||
assert_eq!(
|
||||
scanner_cycle_completion_outcome(
|
||||
ScannerCycleStatus::Deferred(ScannerCycleDeferReason::DataMovement),
|
||||
DataUsagePersistOutcome::Failed,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
ScannerCycleOutcome::Failed
|
||||
);
|
||||
assert_eq!(
|
||||
scanner_cycle_completion_outcome(ScannerCycleStatus::Incomplete, DataUsagePersistOutcome::NoUpdate, false, false),
|
||||
ScannerCycleOutcome::Failed
|
||||
);
|
||||
assert_eq!(
|
||||
scanner_cycle_completion_outcome(ScannerCycleStatus::Incomplete, DataUsagePersistOutcome::Failed, true, true),
|
||||
ScannerCycleOutcome::Failed
|
||||
@@ -6940,47 +7004,47 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn superseded_retry_backoff_grows_caps_and_resets_after_convergence() {
|
||||
let mut backoff = ScannerSupersededBackoff::default();
|
||||
let mut backoff = ScannerRetryBackoff::default();
|
||||
assert_eq!(backoff.retry_interval(Duration::from_secs(24 * 60 * 60)), None);
|
||||
|
||||
for expected in [5, 10, 20, 40, 80, 160, 320] {
|
||||
backoff.record_cycle(ScannerCycleOutcome::Superseded);
|
||||
backoff.record_retryable_cycle(true);
|
||||
assert_eq!(
|
||||
backoff.retry_interval(Duration::from_secs(24 * 60 * 60)),
|
||||
Some(Duration::from_secs(expected))
|
||||
);
|
||||
}
|
||||
for _ in 0..20 {
|
||||
backoff.record_cycle(ScannerCycleOutcome::Superseded);
|
||||
backoff.record_retryable_cycle(true);
|
||||
}
|
||||
assert_eq!(
|
||||
backoff.retry_interval(Duration::from_secs(24 * 60 * 60)),
|
||||
Some(Duration::from_secs(24 * 60 * 60))
|
||||
);
|
||||
|
||||
backoff.record_cycle(ScannerCycleOutcome::Completed);
|
||||
backoff.record_retryable_cycle(false);
|
||||
assert_eq!(backoff.retry_interval(Duration::from_secs(24 * 60 * 60)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn superseded_retry_backoff_respects_a_faster_configured_cycle() {
|
||||
let mut backoff = ScannerSupersededBackoff::default();
|
||||
backoff.record_cycle(ScannerCycleOutcome::Superseded);
|
||||
let mut backoff = ScannerRetryBackoff::default();
|
||||
backoff.record_retryable_cycle(true);
|
||||
|
||||
// A configured cycle shorter than the base still wins: retrying sooner
|
||||
// than the operator's own cadence buys nothing.
|
||||
assert_eq!(backoff.retry_interval(Duration::from_secs(3)), Some(Duration::from_secs(3)));
|
||||
backoff.record_cycle(ScannerCycleOutcome::Superseded);
|
||||
backoff.record_retryable_cycle(true);
|
||||
assert_eq!(backoff.retry_interval(Duration::from_secs(3)), Some(Duration::from_secs(6)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn superseded_retry_backoff_grows_from_the_default_cycle() {
|
||||
let mut backoff = ScannerSupersededBackoff::default();
|
||||
let mut backoff = ScannerRetryBackoff::default();
|
||||
// The first race after a write burst retries in seconds, not a whole
|
||||
// cycle, while repeated supersedes still climb toward the cap.
|
||||
for expected in [5, 10, 20, 40] {
|
||||
backoff.record_cycle(ScannerCycleOutcome::Superseded);
|
||||
backoff.record_retryable_cycle(true);
|
||||
assert_eq!(backoff.retry_interval(Duration::from_secs(60)), Some(Duration::from_secs(expected)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2194,11 +2194,45 @@ pub(crate) async fn scanner_set_disk_inventory(set: &SetDisks) -> Vec<Arc<Disk>>
|
||||
disks
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum ScannerCycleDeferReason {
|
||||
ActivityBaselineUnavailable,
|
||||
DataMovement,
|
||||
}
|
||||
|
||||
impl ScannerCycleDeferReason {
|
||||
pub(crate) fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::ActivityBaselineUnavailable => "activity_baseline_unavailable",
|
||||
Self::DataMovement => "data_movement",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum ScannerCycleStatus {
|
||||
Complete,
|
||||
Incomplete,
|
||||
Superseded,
|
||||
Deferred(ScannerCycleDeferReason),
|
||||
}
|
||||
|
||||
enum ScannerActivityPreflight {
|
||||
Ready(crate::scanner::ScannerActivitySnapshot),
|
||||
ActivityBaselineUnavailable(String),
|
||||
DataMovement,
|
||||
}
|
||||
|
||||
fn scanner_activity_preflight(
|
||||
activity: std::result::Result<crate::scanner::ScannerActivitySnapshot, String>,
|
||||
) -> ScannerActivityPreflight {
|
||||
match activity {
|
||||
Err(error) => ScannerActivityPreflight::ActivityBaselineUnavailable(error),
|
||||
Ok(snapshot) if !crate::scanner::scanner_activity_allows_usage_publication(&snapshot) => {
|
||||
ScannerActivityPreflight::DataMovement
|
||||
}
|
||||
Ok(snapshot) => ScannerActivityPreflight::Ready(snapshot),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -2307,9 +2341,9 @@ impl ScannerIOCycle for ECStore {
|
||||
let child_token = ctx.child_token();
|
||||
|
||||
let distributed = self.setup_is_dist_erasure().await;
|
||||
let activity_before = match crate::scanner::probe_scanner_activity(self, distributed).await {
|
||||
Ok(snapshot) => snapshot,
|
||||
Err(err) => {
|
||||
let activity_before = match scanner_activity_preflight(crate::scanner::probe_scanner_activity(self, distributed).await) {
|
||||
ScannerActivityPreflight::Ready(snapshot) => snapshot,
|
||||
ScannerActivityPreflight::ActivityBaselineUnavailable(err) => {
|
||||
warn!(
|
||||
target: "rustfs::scanner::io",
|
||||
event = EVENT_SCANNER_SET_STATE,
|
||||
@@ -2319,20 +2353,26 @@ impl ScannerIOCycle for ECStore {
|
||||
error = %err,
|
||||
"Scanner cycle skipped because cluster activity could not be baselined"
|
||||
);
|
||||
return Ok(ScannerCycleResult::new(ScannerCycleStatus::Incomplete, None));
|
||||
return Ok(ScannerCycleResult::new(
|
||||
ScannerCycleStatus::Deferred(ScannerCycleDeferReason::ActivityBaselineUnavailable),
|
||||
None,
|
||||
));
|
||||
}
|
||||
ScannerActivityPreflight::DataMovement => {
|
||||
debug!(
|
||||
target: "rustfs::scanner::io",
|
||||
event = EVENT_SCANNER_SET_STATE,
|
||||
component = LOG_COMPONENT_SCANNER,
|
||||
subsystem = LOG_SUBSYSTEM_IO,
|
||||
state = "cycle_data_movement_active",
|
||||
"Scanner cycle deferred while rebalance or decommission data movement is active"
|
||||
);
|
||||
return Ok(ScannerCycleResult::new(
|
||||
ScannerCycleStatus::Deferred(ScannerCycleDeferReason::DataMovement),
|
||||
None,
|
||||
));
|
||||
}
|
||||
};
|
||||
if !crate::scanner::scanner_activity_allows_usage_publication(&activity_before) {
|
||||
debug!(
|
||||
target: "rustfs::scanner::io",
|
||||
event = EVENT_SCANNER_SET_STATE,
|
||||
component = LOG_COMPONENT_SCANNER,
|
||||
subsystem = LOG_SUBSYSTEM_IO,
|
||||
state = "cycle_data_movement_active",
|
||||
"Scanner cycle deferred while rebalance or decommission data movement is active"
|
||||
);
|
||||
return Ok(ScannerCycleResult::new(ScannerCycleStatus::Incomplete, None));
|
||||
}
|
||||
let dirty_generation_before_bucket_list = dirty_usage_generation();
|
||||
let bucket_listing = self.list_bucket_for_scanner(&BucketOptions::default()).await?;
|
||||
let mut bucket_plan_complete = bucket_listing.topology_complete;
|
||||
@@ -3982,6 +4022,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::scanner_budget::ScannerCycleBudgetConfig;
|
||||
use crate::scanner_folder::ScannerItem;
|
||||
use crate::storage_api::owner::{EcstoreRebalStatus, EcstoreRebalanceInfo, EcstoreRebalanceMeta, EcstoreRebalanceStats};
|
||||
use crate::storage_api::scan::{BucketOperations as _, MakeBucketOptions, ObjectIO as _};
|
||||
use crate::{
|
||||
DiskOption, ECStore, Endpoint, EndpointServerPools, Endpoints, InstanceContext, PoolEndpoints, ScannerObjectOptions,
|
||||
@@ -4004,6 +4045,20 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scanner_activity_preflight_defers_a_temporarily_offline_peer() {
|
||||
let preflight = scanner_activity_preflight(Err("peer rustfs-node3:9000 is temporarily offline".to_string()));
|
||||
|
||||
match preflight {
|
||||
ScannerActivityPreflight::ActivityBaselineUnavailable(error) => {
|
||||
assert_eq!(error, "peer rustfs-node3:9000 is temporarily offline");
|
||||
}
|
||||
ScannerActivityPreflight::Ready(_) | ScannerActivityPreflight::DataMovement => {
|
||||
panic!("an unavailable activity baseline must defer the scanner cycle");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn setup_two_pool_scanner_store() -> (tempfile::TempDir, Arc<ECStore>) {
|
||||
init_ecstore_config_for_scanner_tests();
|
||||
let temp_dir = tempfile::tempdir().expect("multi-pool scanner test directory should be created");
|
||||
@@ -4096,6 +4151,42 @@ mod tests {
|
||||
assert!(!second.is_lock_lost());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn scanner_cycle_is_deferred_while_rebalance_is_active() {
|
||||
let (_temp_dir, store) = setup_two_pool_scanner_store().await;
|
||||
let mut pool_stats = vec![EcstoreRebalanceStats::default(); store.pools.len()];
|
||||
pool_stats[0] = EcstoreRebalanceStats {
|
||||
participating: true,
|
||||
info: EcstoreRebalanceInfo {
|
||||
start_time: Some(OffsetDateTime::now_utc()),
|
||||
status: EcstoreRebalStatus::Started,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
*store.rebalance_meta.write().await = Some(EcstoreRebalanceMeta {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
pool_stats,
|
||||
..Default::default()
|
||||
});
|
||||
assert!(store.scanner_data_movement_active().await);
|
||||
|
||||
let ctx = CancellationToken::new();
|
||||
let budget = ScannerCycleBudget::new(&ctx, ScannerCycleBudgetConfig::default());
|
||||
let (updates, mut receiver) = mpsc::channel(1);
|
||||
let result = tokio::time::timeout(
|
||||
Duration::from_secs(30),
|
||||
ScannerIOCycle::nsscanner_with_status(store.as_ref(), ctx, budget, updates, 1, 1, HealScanMode::Normal),
|
||||
)
|
||||
.await
|
||||
.expect("rebalance-deferred scanner cycle should finish")
|
||||
.expect("rebalance-deferred scanner cycle should succeed");
|
||||
|
||||
assert_eq!(result.status, ScannerCycleStatus::Deferred(ScannerCycleDeferReason::DataMovement));
|
||||
assert!(receiver.recv().await.is_none(), "rebalance-deferred cycle must not publish usage");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn data_usage_publish_fails_when_receiver_is_closed() {
|
||||
let (updates, receiver) = mpsc::channel(1);
|
||||
|
||||
@@ -83,6 +83,11 @@ pub(crate) use rustfs_ecstore::api::layout::{
|
||||
EndpointServerPools as EcstoreEndpointServerPools, Endpoints as EcstoreEndpoints, PoolEndpoints as EcstorePoolEndpoints,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use rustfs_ecstore::api::rebalance::{
|
||||
RebalStatus as EcstoreRebalStatus, RebalanceInfo as EcstoreRebalanceInfo, RebalanceMeta as EcstoreRebalanceMeta,
|
||||
RebalanceStats as EcstoreRebalanceStats,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use rustfs_ecstore::api::runtime::InstanceContext as EcstoreInstanceContext;
|
||||
pub(crate) use rustfs_ecstore::api::runtime::{
|
||||
expiry_state_handle as ecstore_expiry_state_handle, global_tier_config_mgr as ecstore_get_global_tier_config_mgr,
|
||||
@@ -122,8 +127,9 @@ pub(crate) mod owner {
|
||||
#[cfg(test)]
|
||||
pub(crate) use super::{
|
||||
EcstoreDiskOption, EcstoreDiskStore, EcstoreEndpoint, EcstoreEndpointServerPools, EcstoreEndpoints,
|
||||
EcstoreInstanceContext, EcstorePoolEndpoints, ecstore_config_init, ecstore_init_bucket_metadata_sys,
|
||||
ecstore_init_local_disks_with_instance_ctx, ecstore_new_disk,
|
||||
EcstoreInstanceContext, EcstorePoolEndpoints, EcstoreRebalStatus, EcstoreRebalanceInfo, EcstoreRebalanceMeta,
|
||||
EcstoreRebalanceStats, ecstore_config_init, ecstore_init_bucket_metadata_sys, ecstore_init_local_disks_with_instance_ctx,
|
||||
ecstore_new_disk,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user