mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-12 08:06:54 +00:00
fix(scanner): make distributed usage convergence authoritative (#5151)
* fix(scanner): make distributed usage cycles authoritative * fix(scanner): close distributed refresh races * fix(config): align scanner reload integration * fix(admin): scope config test helpers * fix(scanner): harden distributed usage convergence * fix(scanner): preserve rolling activity compatibility * fix(admin): expose non-secret optional config values * fix(scanner): acknowledge distributed dirty usage * fix(ecstore): make bucket mutations cancellation safe * fix(scanner): preserve pending dirty acknowledgements * test(obs): account for superseded scanner metric * fix(api): reject excess detached bucket mutations * test: close scanner convergence coverage gaps * fix(scanner): make path tracking cleanup one-shot --------- Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com> Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
+1485
-128
File diff suppressed because it is too large
Load Diff
@@ -36,7 +36,23 @@ pub enum ScannerError {
|
||||
#[error("Scanner error: {0}")]
|
||||
Other(String),
|
||||
|
||||
/// A remote namespace scanner request ID was already accepted.
|
||||
#[error("Remote namespace scanner request replay detected")]
|
||||
RemoteRequestReplay,
|
||||
|
||||
/// The bounded remote namespace scanner replay cache cannot accept more IDs yet.
|
||||
#[error("Remote namespace scanner replay cache capacity exceeded")]
|
||||
RemoteReplayCapacity,
|
||||
|
||||
/// A remote namespace scanner request is already active for the target disk.
|
||||
#[error("Remote namespace scanner disk is already active")]
|
||||
RemoteDiskBusy,
|
||||
|
||||
/// Partial data usage cache produced before the scanner stopped.
|
||||
#[error("Scanner stopped with partial data usage cache")]
|
||||
PartialCache(Box<DataUsageCache>),
|
||||
|
||||
/// Partial cache retained because the bucket or scan root disappeared.
|
||||
#[error("Scanner namespace path disappeared during the scan")]
|
||||
NamespaceNotFoundCache(Box<DataUsageCache>),
|
||||
}
|
||||
|
||||
+104
-8
@@ -20,6 +20,7 @@
|
||||
rust_2018_idioms
|
||||
)]
|
||||
|
||||
use bytes::Bytes;
|
||||
use http::HeaderMap;
|
||||
use rustfs_config::server_config::{Config as ServerConfig, get_global_server_config as config_get_global_server_config};
|
||||
use std::path::PathBuf;
|
||||
@@ -29,11 +30,11 @@ use storage_api::owner::{
|
||||
ECSTORE_STORAGECLASS_STANDARD, ECSTORE_TRANSITION_COMPLETE, EcstoreBucketTargetSys, EcstoreBucketVersioningSys, EcstoreDisk,
|
||||
EcstoreDiskAPI, EcstoreDiskBytes, EcstoreDiskError, EcstoreDiskInfo, EcstoreDiskInfoOptions, EcstoreDiskLocation,
|
||||
EcstoreDiskResult, EcstoreErrorType, EcstoreEvaluator, EcstoreEvent, EcstoreLcEventSrc, EcstoreLifecycle,
|
||||
EcstoreListPathRawOptions, EcstoreObjectOpts, EcstoreReplicationConfigurationExt, EcstoreReplicationScannerBridge,
|
||||
EcstoreResultType, EcstoreScanGuard, EcstoreSetDisks, EcstoreStorageError, EcstoreStore, EcstoreTierConfig,
|
||||
EcstoreVersioningApi, HTTPRangeSpec, ObjectIO, ObjectOperations, ObjectToDelete, ScannerReplicationHealObject,
|
||||
ScannerReplicationHealResult, ScannerReplicationQueueAdmission, ecstore_apply_expiry_rule, ecstore_apply_transition_rule,
|
||||
ecstore_expiry_state_handle, ecstore_get_global_tier_config_mgr, ecstore_get_lifecycle_config,
|
||||
EcstoreListPathRawOptions, EcstoreNsScannerOpenRequest, EcstoreObjectOpts, EcstoreReplicationConfigurationExt,
|
||||
EcstoreReplicationScannerBridge, EcstoreResultType, EcstoreScanGuard, EcstoreSetDisks, EcstoreStorageError, EcstoreStore,
|
||||
EcstoreTierConfig, EcstoreVersioningApi, HTTPPreconditions, HTTPRangeSpec, ObjectIO, ObjectOperations, ObjectToDelete,
|
||||
ScannerReplicationHealObject, ScannerReplicationHealResult, ScannerReplicationQueueAdmission, ecstore_apply_expiry_rule,
|
||||
ecstore_apply_transition_rule, ecstore_expiry_state_handle, ecstore_get_global_tier_config_mgr, ecstore_get_lifecycle_config,
|
||||
ecstore_get_object_lock_config, ecstore_get_replication_config, ecstore_is_erasure, ecstore_is_erasure_sd,
|
||||
ecstore_is_reserved_or_invalid_bucket, ecstore_list_path_raw, ecstore_object_opts_from_object_info,
|
||||
ecstore_path2_bucket_object, ecstore_path2_bucket_object_with_base_path, ecstore_read_config,
|
||||
@@ -41,11 +42,16 @@ use storage_api::owner::{
|
||||
scanner_replication_config_for_lifecycle_eval,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use storage_api::owner::{EcstoreDiskOption, EcstoreDiskStore, EcstoreEndpoint, ecstore_config_init, ecstore_new_disk};
|
||||
use storage_api::owner::{
|
||||
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,
|
||||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub mod data_usage_define;
|
||||
pub mod error;
|
||||
mod remote_scanner;
|
||||
pub mod runtime_config;
|
||||
pub mod scanner;
|
||||
pub mod scanner_budget;
|
||||
@@ -56,16 +62,23 @@ pub(crate) mod storage_api;
|
||||
|
||||
pub use data_usage_define::*;
|
||||
pub use error::ScannerError;
|
||||
pub use remote_scanner::{
|
||||
NS_SCANNER_MAX_REQUEST_BODY_SIZE, RemoteScannerAdmission, RemoteScannerRequest, admit_remote_scanner_request,
|
||||
claim_remote_scanner_request, decode_remote_scanner_request, preflight_remote_scanner_request,
|
||||
remote_scanner_request_matches_envelope, serve_remote_scanner_request, validate_remote_scanner_request_fence,
|
||||
};
|
||||
pub use runtime_config::{apply_scanner_runtime_config, scanner_runtime_config_status, validate_scanner_runtime_config};
|
||||
pub use rustfs_common::last_minute;
|
||||
pub use scanner::{ScannerCycleScheduleStatus, init_data_scanner, scanner_cycle_schedule_status};
|
||||
pub use scanner::{ScannerCycleScheduleStatus, init_data_scanner, scanner_cycle_schedule_status, scanner_topology_digest};
|
||||
pub use scanner_io::{
|
||||
clear_dirty_usage_bucket, record_dirty_usage_bucket, record_scanner_maintenance_change, scanner_activity_epoch,
|
||||
ScannerDirtyUsageAckError, ScannerDirtyUsageState, acknowledge_dirty_usage_generation, clear_dirty_usage_bucket,
|
||||
record_dirty_usage_bucket, record_scanner_maintenance_change, scanner_activity_epoch, scanner_dirty_usage_state,
|
||||
scanner_maintenance_generation,
|
||||
};
|
||||
pub use sleeper::{DynamicSleeper, SCANNER_IDLE_MODE, SCANNER_SLEEPER};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
pub use storage_api::ScannerReplicationConfig as ReplicationConfig;
|
||||
pub use storage_api::scan::SCANNER_ACTIVITY_PROTOCOL_VERSION;
|
||||
|
||||
static SCANNER_ACTIVE_WORK_UNITS: AtomicU64 = AtomicU64::new(0);
|
||||
static SCANNER_FOREGROUND_READ_ACTIVITY: AtomicU64 = AtomicU64::new(0);
|
||||
@@ -150,6 +163,7 @@ pub(crate) type BucketTargetSys = EcstoreBucketTargetSys;
|
||||
pub(crate) type BucketVersioningSys = EcstoreBucketVersioningSys;
|
||||
pub(crate) type DiskInfo = EcstoreDiskInfo;
|
||||
pub(crate) type DiskInfoOptions = EcstoreDiskInfoOptions;
|
||||
pub(crate) type NsScannerOpenRequest = EcstoreNsScannerOpenRequest;
|
||||
pub(crate) type DiskBytes = EcstoreDiskBytes;
|
||||
pub(crate) type Evaluator = EcstoreEvaluator;
|
||||
pub(crate) type Event = EcstoreEvent;
|
||||
@@ -185,6 +199,27 @@ pub(crate) fn init_ecstore_config_for_scanner_tests() {
|
||||
pub(crate) type DiskOption = EcstoreDiskOption;
|
||||
#[cfg(test)]
|
||||
pub(crate) type Endpoint = EcstoreEndpoint;
|
||||
#[cfg(test)]
|
||||
pub(crate) type EndpointServerPools = EcstoreEndpointServerPools;
|
||||
#[cfg(test)]
|
||||
pub(crate) type Endpoints = EcstoreEndpoints;
|
||||
#[cfg(test)]
|
||||
pub(crate) type InstanceContext = EcstoreInstanceContext;
|
||||
#[cfg(test)]
|
||||
pub(crate) type PoolEndpoints = EcstorePoolEndpoints;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn init_local_disks_with_instance_ctx(
|
||||
ctx: &Arc<InstanceContext>,
|
||||
pools: EndpointServerPools,
|
||||
) -> EcstoreResult<()> {
|
||||
ecstore_init_local_disks_with_instance_ctx(ctx, pools).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn init_bucket_metadata_sys_for_scanner_tests(store: Arc<ECStore>) {
|
||||
ecstore_init_bucket_metadata_sys(store, Vec::new()).await;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn new_disk(ep: &Endpoint, opt: &DiskOption) -> DiskResult<DiskStore> {
|
||||
@@ -247,6 +282,8 @@ impl ScannerVersioningConfigExt for s3s::dto::VersioningConfiguration {
|
||||
pub(crate) trait ScannerDiskExt {
|
||||
async fn disk_info(&self, opts: &DiskInfoOptions) -> DiskResult<DiskInfo>;
|
||||
async fn read_metadata(&self, volume: &str, path: &str) -> DiskResult<DiskBytes>;
|
||||
fn is_local(&self) -> bool;
|
||||
fn host_name(&self) -> String;
|
||||
fn path(&self) -> PathBuf;
|
||||
fn get_disk_location(&self) -> DiskLocation;
|
||||
fn start_scan(&self) -> ScanGuard;
|
||||
@@ -264,6 +301,14 @@ where
|
||||
EcstoreDiskAPI::read_metadata(self, volume, path).await
|
||||
}
|
||||
|
||||
fn is_local(&self) -> bool {
|
||||
EcstoreDiskAPI::is_local(self)
|
||||
}
|
||||
|
||||
fn host_name(&self) -> String {
|
||||
EcstoreDiskAPI::host_name(self)
|
||||
}
|
||||
|
||||
fn path(&self) -> PathBuf {
|
||||
EcstoreDiskAPI::path(self)
|
||||
}
|
||||
@@ -344,6 +389,10 @@ pub(crate) async fn scanner_is_erasure_sd() -> bool {
|
||||
ecstore_is_erasure_sd().await
|
||||
}
|
||||
|
||||
pub(crate) async fn scanner_disk_is_online(disk: &Disk) -> bool {
|
||||
EcstoreDiskAPI::is_online(disk).await
|
||||
}
|
||||
|
||||
pub(crate) async fn read_config<S>(api: Arc<S>, file: &str) -> EcstoreResult<Vec<u8>>
|
||||
where
|
||||
S: ScannerObjectIO,
|
||||
@@ -358,6 +407,53 @@ where
|
||||
ecstore_save_config(api, file, data).await
|
||||
}
|
||||
|
||||
pub(crate) async fn save_config_with_preconditions<S>(
|
||||
api: Arc<S>,
|
||||
file: &str,
|
||||
data: Vec<u8>,
|
||||
preconditions: HTTPPreconditions,
|
||||
) -> EcstoreResult<ScannerObjectInfo>
|
||||
where
|
||||
S: ScannerObjectIO,
|
||||
{
|
||||
let mut reader = ScannerPutObjReader::from_vec(data);
|
||||
api.put_object(
|
||||
RUSTFS_META_BUCKET,
|
||||
file,
|
||||
&mut reader,
|
||||
&ScannerObjectOptions {
|
||||
max_parity: true,
|
||||
http_preconditions: Some(preconditions),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn save_config_shared_with_preconditions<S>(
|
||||
api: Arc<S>,
|
||||
file: &str,
|
||||
data: Bytes,
|
||||
sha256hex: Option<String>,
|
||||
preconditions: HTTPPreconditions,
|
||||
) -> EcstoreResult<ScannerObjectInfo>
|
||||
where
|
||||
S: ScannerObjectIO,
|
||||
{
|
||||
let mut reader = ScannerPutObjReader::from_prehashed_bytes(data, sha256hex)?;
|
||||
api.put_object(
|
||||
RUSTFS_META_BUCKET,
|
||||
file,
|
||||
&mut reader,
|
||||
&ScannerObjectOptions {
|
||||
max_parity: true,
|
||||
http_preconditions: Some(preconditions),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_path_raw(rx: CancellationToken, opts: ListPathRawOptions) -> std::result::Result<(), DiskError> {
|
||||
ecstore_list_path_raw(rx, opts).await
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+3089
-167
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@ use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicU8, AtomicU64, Ordering},
|
||||
};
|
||||
use std::time::Instant;
|
||||
|
||||
use tokio::time::Duration;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
@@ -61,15 +62,26 @@ impl ScannerCycleBudgetReason {
|
||||
pub struct ScannerCycleBudget {
|
||||
token: CancellationToken,
|
||||
reason: Arc<AtomicU8>,
|
||||
started_at: Instant,
|
||||
max_duration: Option<Duration>,
|
||||
max_objects: Option<u64>,
|
||||
max_directories: Option<u64>,
|
||||
track_progress: bool,
|
||||
objects_scanned: AtomicU64,
|
||||
directories_started: AtomicU64,
|
||||
entries_visited: AtomicU64,
|
||||
}
|
||||
|
||||
impl ScannerCycleBudget {
|
||||
pub(crate) fn new(parent: &CancellationToken, config: ScannerCycleBudgetConfig) -> Arc<Self> {
|
||||
Self::new_inner(parent, config, false)
|
||||
}
|
||||
|
||||
pub(crate) fn new_with_progress_tracking(parent: &CancellationToken, config: ScannerCycleBudgetConfig) -> Arc<Self> {
|
||||
Self::new_inner(parent, config, true)
|
||||
}
|
||||
|
||||
fn new_inner(parent: &CancellationToken, config: ScannerCycleBudgetConfig, track_progress: bool) -> Arc<Self> {
|
||||
let token = parent.child_token();
|
||||
let reason = Arc::new(AtomicU8::new(BUDGET_REASON_NONE));
|
||||
|
||||
@@ -92,11 +104,14 @@ impl ScannerCycleBudget {
|
||||
Arc::new(Self {
|
||||
token,
|
||||
reason,
|
||||
started_at: Instant::now(),
|
||||
max_duration: config.max_duration,
|
||||
max_objects: config.max_objects,
|
||||
max_directories: config.max_directories,
|
||||
track_progress,
|
||||
objects_scanned: AtomicU64::new(0),
|
||||
directories_started: AtomicU64::new(0),
|
||||
entries_visited: AtomicU64::new(0),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -124,27 +139,97 @@ impl ScannerCycleBudget {
|
||||
self.max_directories
|
||||
}
|
||||
|
||||
pub(crate) fn try_start_directory(&self) -> bool {
|
||||
let Some(max_directories) = self.max_directories else {
|
||||
return true;
|
||||
};
|
||||
pub(crate) fn requires_serial_progress_accounting(&self) -> bool {
|
||||
self.max_objects.is_some() || self.max_directories.is_some()
|
||||
}
|
||||
|
||||
let directories = self.directories_started.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
if directories <= max_directories {
|
||||
pub(crate) fn remaining_config(&self) -> ScannerCycleBudgetConfig {
|
||||
let max_duration = self
|
||||
.max_duration
|
||||
.map(|duration| duration.saturating_sub(self.started_at.elapsed()));
|
||||
if max_duration.is_some_and(|duration| duration.is_zero()) {
|
||||
self.cancel_for(ScannerCycleBudgetReason::Runtime);
|
||||
}
|
||||
|
||||
ScannerCycleBudgetConfig {
|
||||
max_duration,
|
||||
max_objects: self
|
||||
.max_objects
|
||||
.map(|max| max.saturating_sub(self.objects_scanned.load(Ordering::Relaxed))),
|
||||
max_directories: self
|
||||
.max_directories
|
||||
.map(|max| max.saturating_sub(self.directories_started.load(Ordering::Relaxed))),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn progress(&self) -> (u64, u64) {
|
||||
(
|
||||
self.objects_scanned.load(Ordering::Relaxed),
|
||||
self.directories_started.load(Ordering::Relaxed),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn entries_visited(&self) -> u64 {
|
||||
self.entries_visited.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub(crate) fn record_entries_visited(&self, entries_visited: u64) {
|
||||
if self.track_progress {
|
||||
saturating_fetch_add(&self.entries_visited, entries_visited);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn record_remote_progress(&self, objects_scanned: u64, directories_started: u64) {
|
||||
if self.track_progress || self.max_objects.is_some() {
|
||||
let objects = saturating_fetch_add(&self.objects_scanned, objects_scanned);
|
||||
if self.max_objects.is_some_and(|max_objects| objects >= max_objects) {
|
||||
self.cancel_for(ScannerCycleBudgetReason::Objects);
|
||||
}
|
||||
}
|
||||
|
||||
if self.track_progress || self.max_directories.is_some() {
|
||||
let directories = saturating_fetch_add(&self.directories_started, directories_started);
|
||||
if self
|
||||
.max_directories
|
||||
.is_some_and(|max_directories| directories > max_directories)
|
||||
{
|
||||
self.cancel_for(ScannerCycleBudgetReason::Directories);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn cancel_after_unreported_remote_progress(&self) {
|
||||
if self.max_objects.is_some() {
|
||||
self.cancel_for(ScannerCycleBudgetReason::Objects);
|
||||
} else if self.max_directories.is_some() {
|
||||
self.cancel_for(ScannerCycleBudgetReason::Directories);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn try_start_directory(&self) -> bool {
|
||||
if !self.track_progress && self.max_directories.is_none() {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.cancel_for(ScannerCycleBudgetReason::Directories);
|
||||
false
|
||||
let directories = saturating_fetch_add(&self.directories_started, 1);
|
||||
if self
|
||||
.max_directories
|
||||
.is_some_and(|max_directories| directories > max_directories)
|
||||
{
|
||||
self.cancel_for(ScannerCycleBudgetReason::Directories);
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn record_object_scanned(&self) {
|
||||
let Some(max_objects) = self.max_objects else {
|
||||
if !self.track_progress && self.max_objects.is_none() {
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
let objects = self.objects_scanned.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
if objects >= max_objects {
|
||||
let objects = saturating_fetch_add(&self.objects_scanned, 1);
|
||||
if self.max_objects.is_some_and(|max_objects| objects >= max_objects) {
|
||||
self.cancel_for(ScannerCycleBudgetReason::Objects);
|
||||
}
|
||||
}
|
||||
@@ -163,6 +248,17 @@ impl ScannerCycleBudget {
|
||||
}
|
||||
}
|
||||
|
||||
fn saturating_fetch_add(value: &AtomicU64, delta: u64) -> u64 {
|
||||
let mut current = value.load(Ordering::Relaxed);
|
||||
loop {
|
||||
let next = current.saturating_add(delta);
|
||||
match value.compare_exchange_weak(current, next, Ordering::Relaxed, Ordering::Relaxed) {
|
||||
Ok(_) => return next,
|
||||
Err(observed) => current = observed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ScannerCycleBudget {
|
||||
fn drop(&mut self) {
|
||||
self.token.cancel();
|
||||
@@ -250,4 +346,119 @@ mod tests {
|
||||
assert_eq!(budget.reason(), Some(ScannerCycleBudgetReason::Objects));
|
||||
assert!(budget.token().is_cancelled());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remaining_config_accounts_for_local_and_remote_progress() {
|
||||
let parent = CancellationToken::new();
|
||||
let budget = ScannerCycleBudget::new(
|
||||
&parent,
|
||||
ScannerCycleBudgetConfig {
|
||||
max_duration: Some(Duration::from_secs(60)),
|
||||
max_objects: Some(10),
|
||||
max_directories: Some(5),
|
||||
},
|
||||
);
|
||||
|
||||
budget.record_object_scanned();
|
||||
assert!(budget.try_start_directory());
|
||||
budget.record_remote_progress(3, 2);
|
||||
|
||||
let remaining = budget.remaining_config();
|
||||
assert!(
|
||||
remaining
|
||||
.max_duration
|
||||
.is_some_and(|duration| duration <= Duration::from_secs(60))
|
||||
);
|
||||
assert_eq!(remaining.max_objects, Some(6));
|
||||
assert_eq!(remaining.max_directories, Some(2));
|
||||
assert_eq!(budget.progress(), (4, 3));
|
||||
assert!(!budget.budget_elapsed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_progress_preserves_object_and_directory_limit_semantics() {
|
||||
let parent = CancellationToken::new();
|
||||
let object_budget = ScannerCycleBudget::new(
|
||||
&parent,
|
||||
ScannerCycleBudgetConfig {
|
||||
max_objects: Some(2),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
object_budget.record_remote_progress(2, 0);
|
||||
assert_eq!(object_budget.reason(), Some(ScannerCycleBudgetReason::Objects));
|
||||
|
||||
let directory_budget = ScannerCycleBudget::new(
|
||||
&parent,
|
||||
ScannerCycleBudgetConfig {
|
||||
max_directories: Some(2),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
directory_budget.record_remote_progress(0, 2);
|
||||
assert!(!directory_budget.budget_elapsed());
|
||||
directory_budget.record_remote_progress(0, 1);
|
||||
assert_eq!(directory_budget.reason(), Some(ScannerCycleBudgetReason::Directories));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_progress_tracking_counts_unbounded_remote_work_without_cancelling() {
|
||||
let parent = CancellationToken::new();
|
||||
let budget = ScannerCycleBudget::new_with_progress_tracking(&parent, ScannerCycleBudgetConfig::default());
|
||||
|
||||
assert!(budget.try_start_directory());
|
||||
budget.record_object_scanned();
|
||||
budget.record_remote_progress(2, 3);
|
||||
|
||||
assert_eq!(budget.progress(), (3, 4));
|
||||
assert!(!budget.budget_elapsed());
|
||||
assert!(!budget.token().is_cancelled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unreported_remote_progress_cancels_count_budget() {
|
||||
let parent = CancellationToken::new();
|
||||
let budget = ScannerCycleBudget::new(
|
||||
&parent,
|
||||
ScannerCycleBudgetConfig {
|
||||
max_objects: Some(10),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
budget.cancel_after_unreported_remote_progress();
|
||||
|
||||
assert_eq!(budget.reason(), Some(ScannerCycleBudgetReason::Objects));
|
||||
assert!(budget.token().is_cancelled());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn count_budgets_require_serial_progress_accounting() {
|
||||
let parent = CancellationToken::new();
|
||||
let runtime_only = ScannerCycleBudget::new(
|
||||
&parent,
|
||||
ScannerCycleBudgetConfig {
|
||||
max_duration: Some(Duration::from_secs(1)),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let object_limited = ScannerCycleBudget::new(
|
||||
&parent,
|
||||
ScannerCycleBudgetConfig {
|
||||
max_objects: Some(1),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let directory_limited = ScannerCycleBudget::new(
|
||||
&parent,
|
||||
ScannerCycleBudgetConfig {
|
||||
max_directories: Some(1),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert!(!runtime_only.requires_serial_progress_accounting());
|
||||
assert!(object_limited.requires_serial_progress_accounting());
|
||||
assert!(directory_limited.requires_serial_progress_accounting());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,8 +38,8 @@ use rustfs_common::heal_channel::{
|
||||
HealRequestSource, HealScanMode, send_heal_request_with_admission,
|
||||
};
|
||||
use rustfs_common::metrics::{
|
||||
IlmAction, Metric, Metrics, ScannerReplicationRepairKind, ScannerSourceWorkUpdate, ScannerWorkSource, UpdateCurrentPathFn,
|
||||
current_path_updater, global_metrics,
|
||||
CloseDiskGuard, IlmAction, Metric, Metrics, ScannerReplicationRepairKind, ScannerSourceWorkUpdate, ScannerWorkSource,
|
||||
UpdateCurrentPathFn, current_path_updater, global_metrics,
|
||||
};
|
||||
use rustfs_filemeta::{MetaCacheEntries, MetaCacheEntry, MetadataResolutionParams};
|
||||
use rustfs_utils::path::{SLASH_SEPARATOR, path_join_buf};
|
||||
@@ -76,6 +76,8 @@ const DATA_SCANNER_COMPACT_AT_CHILDREN: usize = 10000;
|
||||
const DATA_SCANNER_COMPACT_AT_FOLDERS: usize = DATA_SCANNER_COMPACT_AT_CHILDREN / 4;
|
||||
const DATA_SCANNER_FORCE_COMPACT_AT_FOLDERS: usize = 250_000;
|
||||
const SCANNER_LIST_PATH_RAW_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
const SCANNER_ENTRY_PROGRESS_BATCH: u64 = 32;
|
||||
const SCANNER_ENTRY_PROGRESS_INTERVAL: Duration = Duration::from_secs(30);
|
||||
const DEFAULT_HEAL_OBJECT_SELECT_PROB: u32 = 1024;
|
||||
const ENV_DATA_USAGE_UPDATE_DIR_CYCLES: &str = "RUSTFS_DATA_USAGE_UPDATE_DIR_CYCLES";
|
||||
const ENV_HEAL_OBJECT_SELECT_PROB: &str = "RUSTFS_HEAL_OBJECT_SELECT_PROB";
|
||||
@@ -411,24 +413,36 @@ fn non_negative_i64_to_u64(value: i64) -> u64 {
|
||||
}
|
||||
|
||||
fn apply_scanner_size_summary(into: &mut DataUsageEntry, summary: &SizeSummary) {
|
||||
into.size += summary.total_size;
|
||||
into.versions += summary.versions;
|
||||
into.delete_markers += summary.delete_markers;
|
||||
into.obj_sizes.add(summary.total_size as u64);
|
||||
into.obj_versions.add(summary.versions as u64);
|
||||
into.size = into.size.saturating_add(summary.total_size);
|
||||
into.versions = into.versions.saturating_add(summary.versions);
|
||||
into.delete_markers = into.delete_markers.saturating_add(summary.delete_markers);
|
||||
into.obj_sizes.add(u64::try_from(summary.total_size).unwrap_or(u64::MAX));
|
||||
into.obj_versions.add(u64::try_from(summary.versions).unwrap_or(u64::MAX));
|
||||
|
||||
let replication_stats = into.replication_stats.get_or_insert_with(Default::default);
|
||||
replication_stats.replica_size += non_negative_i64_to_u64(summary.replica_size);
|
||||
replication_stats.replica_count += summary.replica_count as u64;
|
||||
replication_stats.replica_size = replication_stats
|
||||
.replica_size
|
||||
.saturating_add(non_negative_i64_to_u64(summary.replica_size));
|
||||
replication_stats.replica_count = replication_stats
|
||||
.replica_count
|
||||
.saturating_add(u64::try_from(summary.replica_count).unwrap_or(u64::MAX));
|
||||
|
||||
for (arn, st) in &summary.repl_target_stats {
|
||||
let tgt_stat = replication_stats.targets.entry(arn.clone()).or_default();
|
||||
tgt_stat.pending_size += non_negative_i64_to_u64(st.pending_size);
|
||||
tgt_stat.failed_size += non_negative_i64_to_u64(st.failed_size);
|
||||
tgt_stat.replicated_size += non_negative_i64_to_u64(st.replicated_size);
|
||||
tgt_stat.replicated_count += st.replicated_count as u64;
|
||||
tgt_stat.failed_count += st.failed_count as u64;
|
||||
tgt_stat.pending_count += st.pending_count as u64;
|
||||
tgt_stat.pending_size = tgt_stat.pending_size.saturating_add(non_negative_i64_to_u64(st.pending_size));
|
||||
tgt_stat.failed_size = tgt_stat.failed_size.saturating_add(non_negative_i64_to_u64(st.failed_size));
|
||||
tgt_stat.replicated_size = tgt_stat
|
||||
.replicated_size
|
||||
.saturating_add(non_negative_i64_to_u64(st.replicated_size));
|
||||
tgt_stat.replicated_count = tgt_stat
|
||||
.replicated_count
|
||||
.saturating_add(u64::try_from(st.replicated_count).unwrap_or(u64::MAX));
|
||||
tgt_stat.failed_count = tgt_stat
|
||||
.failed_count
|
||||
.saturating_add(u64::try_from(st.failed_count).unwrap_or(u64::MAX));
|
||||
tgt_stat.pending_count = tgt_stat
|
||||
.pending_count
|
||||
.saturating_add(u64::try_from(st.pending_count).unwrap_or(u64::MAX));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1036,22 +1050,23 @@ impl ScannerItem {
|
||||
if let Some(repl_target_size_summary) = size_summary.repl_target_stats.get_mut(arn.as_str()) {
|
||||
match target_status {
|
||||
ReplicationStatusType::Pending => {
|
||||
repl_target_size_summary.pending_size += roi.size;
|
||||
repl_target_size_summary.pending_count += 1;
|
||||
size_summary.pending_size += roi.size;
|
||||
size_summary.pending_count += 1;
|
||||
repl_target_size_summary.pending_size = repl_target_size_summary.pending_size.saturating_add(roi.size);
|
||||
repl_target_size_summary.pending_count = repl_target_size_summary.pending_count.saturating_add(1);
|
||||
size_summary.pending_size = size_summary.pending_size.saturating_add(roi.size);
|
||||
size_summary.pending_count = size_summary.pending_count.saturating_add(1);
|
||||
}
|
||||
ReplicationStatusType::Failed => {
|
||||
repl_target_size_summary.failed_size += roi.size;
|
||||
repl_target_size_summary.failed_count += 1;
|
||||
size_summary.failed_size += roi.size;
|
||||
size_summary.failed_count += 1;
|
||||
repl_target_size_summary.failed_size = repl_target_size_summary.failed_size.saturating_add(roi.size);
|
||||
repl_target_size_summary.failed_count = repl_target_size_summary.failed_count.saturating_add(1);
|
||||
size_summary.failed_size = size_summary.failed_size.saturating_add(roi.size);
|
||||
size_summary.failed_count = size_summary.failed_count.saturating_add(1);
|
||||
}
|
||||
ReplicationStatusType::Completed | ReplicationStatusType::CompletedLegacy => {
|
||||
repl_target_size_summary.replicated_size += roi.size;
|
||||
repl_target_size_summary.replicated_count += 1;
|
||||
size_summary.replicated_size += roi.size;
|
||||
size_summary.replicated_count += 1;
|
||||
repl_target_size_summary.replicated_size =
|
||||
repl_target_size_summary.replicated_size.saturating_add(roi.size);
|
||||
repl_target_size_summary.replicated_count = repl_target_size_summary.replicated_count.saturating_add(1);
|
||||
size_summary.replicated_size = size_summary.replicated_size.saturating_add(roi.size);
|
||||
size_summary.replicated_count = size_summary.replicated_count.saturating_add(1);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -1059,8 +1074,8 @@ impl ScannerItem {
|
||||
}
|
||||
|
||||
if oi.replication_status == ReplicationStatusType::Replica {
|
||||
size_summary.replica_size += roi.size;
|
||||
size_summary.replica_count += 1;
|
||||
size_summary.replica_size = size_summary.replica_size.saturating_add(roi.size);
|
||||
size_summary.replica_count = size_summary.replica_count.saturating_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1835,21 +1850,10 @@ impl FolderScanner {
|
||||
|
||||
let mut dir_reader = match tokio::fs::read_dir(&dir_path).await {
|
||||
Ok(dir_reader) => dir_reader,
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => {
|
||||
debug!(
|
||||
target: "rustfs::scanner::folder",
|
||||
event = EVENT_SCANNER_FOLDER_STATE,
|
||||
component = LOG_COMPONENT_SCANNER,
|
||||
subsystem = LOG_SUBSYSTEM_FOLDER,
|
||||
dir_path = %dir_path,
|
||||
state = "dir_missing_before_read",
|
||||
error = %e,
|
||||
"Scanner folder state updated"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => return Err(ScannerError::Io(e)),
|
||||
};
|
||||
let mut pending_entry_progress = 0_u64;
|
||||
let mut last_entry_progress = Instant::now();
|
||||
|
||||
loop {
|
||||
let entry = match dir_reader.next_entry().await {
|
||||
@@ -1883,6 +1887,14 @@ impl FolderScanner {
|
||||
}
|
||||
Err(e) => return Err(ScannerError::Io(e)),
|
||||
};
|
||||
pending_entry_progress = pending_entry_progress.saturating_add(1);
|
||||
if pending_entry_progress >= SCANNER_ENTRY_PROGRESS_BATCH
|
||||
|| last_entry_progress.elapsed() >= SCANNER_ENTRY_PROGRESS_INTERVAL
|
||||
{
|
||||
self.budget.record_entries_visited(pending_entry_progress);
|
||||
pending_entry_progress = 0;
|
||||
last_entry_progress = Instant::now();
|
||||
}
|
||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||
if file_name.is_empty() || file_name == "." || file_name == ".." {
|
||||
continue;
|
||||
@@ -2122,6 +2134,7 @@ impl FolderScanner {
|
||||
global_metrics().record_scanner_yield(yield_start.elapsed());
|
||||
}
|
||||
}
|
||||
self.budget.record_entries_visited(pending_entry_progress);
|
||||
|
||||
if ctx.is_cancelled() {
|
||||
return Err(ScannerError::Other("Operation cancelled".to_string()));
|
||||
@@ -2759,7 +2772,8 @@ pub async fn scan_data_folder(
|
||||
// Get disk path
|
||||
let base_path = local_disk.path().to_string_lossy().to_string();
|
||||
|
||||
let (update_current_path, close_disk) = current_path_updater(&base_path, &cache.info.name);
|
||||
let (update_current_path, close_disk) = current_path_updater(&base_path, &cache.info.name).await;
|
||||
let mut close_disk_guard = CloseDiskGuard::new(close_disk);
|
||||
|
||||
// Create skip_heal flag
|
||||
let is_erasure_mode = scanner_is_erasure().await;
|
||||
@@ -2829,6 +2843,8 @@ pub async fn scan_data_folder(
|
||||
new_cache.force_compact(DATA_SCANNER_COMPACT_AT_CHILDREN);
|
||||
new_cache.info.last_update = Some(SystemTime::now());
|
||||
new_cache.info.next_cycle = cache.info.next_cycle;
|
||||
let unresolved_objects = root.failed_objects > 0 || !new_cache.info.failed_objects.is_empty();
|
||||
new_cache.info.snapshot_complete = !unresolved_objects;
|
||||
let had_scan_checkpoint = cache.info.scan_checkpoint.is_some() || new_cache.info.scan_checkpoint.is_some();
|
||||
new_cache.info.scan_resume_after = None;
|
||||
new_cache.info.scan_checkpoint = None;
|
||||
@@ -2836,8 +2852,12 @@ pub async fn scan_data_folder(
|
||||
global_metrics().record_scanner_checkpoint_cleared();
|
||||
}
|
||||
|
||||
close_disk().await;
|
||||
Ok(new_cache.clone())
|
||||
close_disk_guard.close().await;
|
||||
if unresolved_objects {
|
||||
Err(ScannerError::PartialCache(Box::new(new_cache.clone())))
|
||||
} else {
|
||||
Ok(new_cache.clone())
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if ctx.is_cancelled() {
|
||||
@@ -2857,14 +2877,23 @@ pub async fn scan_data_folder(
|
||||
}
|
||||
new_cache.info.last_update = Some(SystemTime::now());
|
||||
new_cache.info.next_cycle = cache.info.next_cycle;
|
||||
new_cache.info.snapshot_complete = false;
|
||||
if root_has_progress {
|
||||
set_scan_checkpoint(new_cache, checkpoint_reason_from_budget(budget.reason()));
|
||||
}
|
||||
close_disk().await;
|
||||
close_disk_guard.close().await;
|
||||
return Err(ScannerError::PartialCache(Box::new(new_cache.clone())));
|
||||
}
|
||||
}
|
||||
close_disk().await;
|
||||
if matches!(&e, ScannerError::Io(io) if io.kind() == ErrorKind::NotFound) {
|
||||
let mut partial_cache = scanner.old_cache.clone();
|
||||
partial_cache.info.last_update = Some(SystemTime::now());
|
||||
partial_cache.info.next_cycle = cache.info.next_cycle;
|
||||
partial_cache.info.snapshot_complete = false;
|
||||
close_disk_guard.close().await;
|
||||
return Err(ScannerError::NamespaceNotFoundCache(Box::new(partial_cache)));
|
||||
}
|
||||
close_disk_guard.close().await;
|
||||
// No useful information, return original cache
|
||||
Err(e)
|
||||
}
|
||||
@@ -2877,7 +2906,7 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::storage_api::VersionPurgeStatusType;
|
||||
use crate::{DiskOption, Endpoint, new_disk};
|
||||
use crate::{DiskOption, Endpoint, STORAGE_FORMAT_FILE, new_disk};
|
||||
use rustfs_filemeta::{FileInfo, FileMeta};
|
||||
use serial_test::serial;
|
||||
#[cfg(unix)]
|
||||
@@ -2886,6 +2915,76 @@ mod tests {
|
||||
use temp_env::{with_var, with_var_unset};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[test]
|
||||
fn scanner_size_summary_application_saturates_usage_counters() {
|
||||
let target = "arn:minio:replication::target".to_string();
|
||||
let mut entry = DataUsageEntry {
|
||||
size: usize::MAX,
|
||||
versions: usize::MAX,
|
||||
delete_markers: usize::MAX,
|
||||
replication_stats: Some(Default::default()),
|
||||
..Default::default()
|
||||
};
|
||||
let replication_stats = entry
|
||||
.replication_stats
|
||||
.as_mut()
|
||||
.expect("replication statistics should be initialized");
|
||||
replication_stats.replica_size = u64::MAX;
|
||||
replication_stats.replica_count = u64::MAX;
|
||||
let target_stats = replication_stats.targets.entry(target.clone()).or_default();
|
||||
target_stats.pending_size = u64::MAX;
|
||||
target_stats.failed_size = u64::MAX;
|
||||
target_stats.replicated_size = u64::MAX;
|
||||
target_stats.replicated_count = u64::MAX;
|
||||
target_stats.failed_count = u64::MAX;
|
||||
target_stats.pending_count = u64::MAX;
|
||||
|
||||
let mut summary = SizeSummary {
|
||||
total_size: 1,
|
||||
versions: 1,
|
||||
delete_markers: 1,
|
||||
replica_size: 1,
|
||||
replica_count: 1,
|
||||
..Default::default()
|
||||
};
|
||||
summary.repl_target_stats.insert(
|
||||
target.clone(),
|
||||
ReplTargetSizeSummary {
|
||||
replicated_size: 1,
|
||||
replicated_count: 1,
|
||||
pending_size: 1,
|
||||
failed_size: 1,
|
||||
pending_count: 1,
|
||||
failed_count: 1,
|
||||
},
|
||||
);
|
||||
|
||||
apply_scanner_size_summary(&mut entry, &summary);
|
||||
|
||||
assert_eq!(entry.size, usize::MAX);
|
||||
assert_eq!(entry.versions, usize::MAX);
|
||||
assert_eq!(entry.delete_markers, usize::MAX);
|
||||
assert_eq!(entry.obj_sizes.to_map()["LESS_THAN_1024_B"], 1);
|
||||
assert_eq!(entry.obj_versions.to_map()["SINGLE_VERSION"], 1);
|
||||
|
||||
let replication_stats = entry
|
||||
.replication_stats
|
||||
.as_ref()
|
||||
.expect("replication statistics should remain present");
|
||||
assert_eq!(replication_stats.replica_size, u64::MAX);
|
||||
assert_eq!(replication_stats.replica_count, u64::MAX);
|
||||
let target_stats = replication_stats
|
||||
.targets
|
||||
.get(&target)
|
||||
.expect("replication target statistics should remain present");
|
||||
assert_eq!(target_stats.pending_size, u64::MAX);
|
||||
assert_eq!(target_stats.failed_size, u64::MAX);
|
||||
assert_eq!(target_stats.replicated_size, u64::MAX);
|
||||
assert_eq!(target_stats.replicated_count, u64::MAX);
|
||||
assert_eq!(target_stats.failed_count, u64::MAX);
|
||||
assert_eq!(target_stats.pending_count, u64::MAX);
|
||||
}
|
||||
|
||||
async fn build_test_scanner() -> (FolderScanner, std::path::PathBuf) {
|
||||
let temp_dir = std::env::temp_dir().join(format!("rustfs-scanner-test-{}", Uuid::new_v4()));
|
||||
tokio::fs::create_dir_all(&temp_dir)
|
||||
@@ -4145,7 +4244,7 @@ mod tests {
|
||||
scanner.update_cache.info.name = "bucket".to_string();
|
||||
|
||||
let parent = CancellationToken::new();
|
||||
let budget = ScannerCycleBudget::new(
|
||||
let budget = ScannerCycleBudget::new_with_progress_tracking(
|
||||
&parent,
|
||||
crate::scanner_budget::ScannerCycleBudgetConfig {
|
||||
max_directories: Some(1),
|
||||
@@ -4168,6 +4267,7 @@ mod tests {
|
||||
assert!(budget.budget_elapsed());
|
||||
assert_eq!(budget.reason(), Some(crate::scanner_budget::ScannerCycleBudgetReason::Directories));
|
||||
assert!(budget.token().is_cancelled());
|
||||
assert!(budget.entries_visited() >= 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -4212,6 +4312,51 @@ mod tests {
|
||||
assert!(update.compacted, "partial update should preserve compacted state");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_scan_data_folder_cancelled_before_scan_clears_current_path() {
|
||||
let (scanner, temp_dir) = build_test_scanner().await;
|
||||
let _guard = TestGuard {
|
||||
temp_dir: Some(temp_dir),
|
||||
};
|
||||
let parent = CancellationToken::new();
|
||||
parent.cancel();
|
||||
let budget = ScannerCycleBudget::new(&parent, Default::default());
|
||||
let cache = DataUsageCache {
|
||||
info: crate::data_usage_define::DataUsageCacheInfo {
|
||||
name: "bucket".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let disk_path = scanner.local_disk.path().to_string_lossy().to_string();
|
||||
|
||||
let result = scan_data_folder(
|
||||
budget.token(),
|
||||
budget,
|
||||
vec![scanner.local_disk.clone()],
|
||||
scanner.local_disk,
|
||||
cache,
|
||||
None,
|
||||
HealScanMode::Normal,
|
||||
SCANNER_SLEEPER.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(matches!(result, Err(ScannerError::Other(message)) if message == "Operation cancelled"));
|
||||
tokio::time::timeout(Duration::from_secs(1), async {
|
||||
loop {
|
||||
let report = global_metrics().report().await;
|
||||
if report.active_paths.iter().all(|path| !path.starts_with(&disk_path)) {
|
||||
break;
|
||||
}
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("cancelled scan should deregister its active path");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_scan_data_folder_returns_partial_cache_on_budget_cancel() {
|
||||
@@ -4262,6 +4407,7 @@ mod tests {
|
||||
|
||||
assert!(partial_cache.info.last_update.is_some());
|
||||
assert_eq!(partial_cache.info.next_cycle, 7);
|
||||
assert!(!partial_cache.info.snapshot_complete);
|
||||
assert!(partial_cache.root().is_some(), "partial cache should keep completed scan progress");
|
||||
assert!(budget.budget_elapsed());
|
||||
assert_eq!(budget.reason(), Some(crate::scanner_budget::ScannerCycleBudgetReason::Directories));
|
||||
@@ -4386,6 +4532,104 @@ mod tests {
|
||||
assert_eq!(budget.reason(), Some(crate::scanner_budget::ScannerCycleBudgetReason::Directories));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn scan_data_folder_missing_bucket_returns_partial() {
|
||||
let (scanner, temp_dir) = build_test_scanner().await;
|
||||
let _guard = TestGuard {
|
||||
temp_dir: Some(temp_dir),
|
||||
};
|
||||
let parent = CancellationToken::new();
|
||||
let budget = ScannerCycleBudget::new(&parent, Default::default());
|
||||
let mut cache = DataUsageCache {
|
||||
info: crate::data_usage_define::DataUsageCacheInfo {
|
||||
name: "missing-bucket".to_string(),
|
||||
next_cycle: 9,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
cache.replace(
|
||||
"missing-bucket",
|
||||
crate::data_usage_define::DATA_USAGE_ROOT,
|
||||
DataUsageEntry {
|
||||
objects: 7,
|
||||
size: 70,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let result = scan_data_folder(
|
||||
budget.token(),
|
||||
budget,
|
||||
vec![scanner.local_disk.clone()],
|
||||
scanner.local_disk,
|
||||
cache,
|
||||
None,
|
||||
HealScanMode::Normal,
|
||||
SCANNER_SLEEPER.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let partial = match result {
|
||||
Err(ScannerError::NamespaceNotFoundCache(partial)) => partial,
|
||||
other => panic!("missing bucket should keep the scan incomplete, got {other:?}"),
|
||||
};
|
||||
assert!(!partial.info.snapshot_complete);
|
||||
assert_eq!(partial.info.next_cycle, 9);
|
||||
let root = partial
|
||||
.checked_flatten("missing-bucket")
|
||||
.expect("missing bucket partial must retain the last durable usage");
|
||||
assert_eq!(root.objects, 7);
|
||||
assert_eq!(root.size, 70);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn scan_data_folder_missing_scan_root_returns_partial() {
|
||||
let (scanner, temp_dir) = build_test_scanner().await;
|
||||
tokio::fs::remove_dir_all(&temp_dir)
|
||||
.await
|
||||
.expect("failed to remove scanner root");
|
||||
let _guard = TestGuard {
|
||||
temp_dir: Some(temp_dir),
|
||||
};
|
||||
let parent = CancellationToken::new();
|
||||
let budget = ScannerCycleBudget::new(&parent, Default::default());
|
||||
let cache = DataUsageCache {
|
||||
info: crate::data_usage_define::DataUsageCacheInfo {
|
||||
name: "missing-bucket".to_string(),
|
||||
next_cycle: 9,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = scan_data_folder(
|
||||
budget.token(),
|
||||
budget,
|
||||
vec![scanner.local_disk.clone()],
|
||||
scanner.local_disk,
|
||||
cache,
|
||||
None,
|
||||
HealScanMode::Normal,
|
||||
SCANNER_SLEEPER.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let partial = match result {
|
||||
Err(ScannerError::NamespaceNotFoundCache(partial)) => partial,
|
||||
other => panic!("missing scan root should keep the scan incomplete, got {other:?}"),
|
||||
};
|
||||
assert!(!partial.info.snapshot_complete);
|
||||
assert_eq!(partial.info.next_cycle, 9);
|
||||
let root = partial
|
||||
.checked_flatten("missing-bucket")
|
||||
.expect("missing scan root partial should retain a non-publishable root");
|
||||
assert_eq!(root.objects, 0);
|
||||
assert_eq!(root.size, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_scan_data_folder_resume_hint_orders_across_new_and_existing_folders() {
|
||||
@@ -4533,6 +4777,7 @@ mod tests {
|
||||
.size_recursive("bucket")
|
||||
.expect("completed cache should retain bucket usage");
|
||||
assert_eq!(root.objects, 5);
|
||||
assert!(result.info.snapshot_complete);
|
||||
assert!(result.info.scan_resume_after.is_none());
|
||||
assert!(result.info.scan_checkpoint.is_none());
|
||||
}
|
||||
@@ -4626,6 +4871,53 @@ mod tests {
|
||||
assert_eq!(result.info.next_cycle, 11);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_scan_data_folder_keeps_unresolved_objects_partial() {
|
||||
let (scanner, temp_dir) = build_test_scanner().await;
|
||||
let _guard = TestGuard {
|
||||
temp_dir: Some(temp_dir.clone()),
|
||||
};
|
||||
write_test_object_metadata(&temp_dir, "bucket", "object").await;
|
||||
|
||||
let failed_path = temp_dir
|
||||
.join("bucket")
|
||||
.join("object")
|
||||
.join(STORAGE_FORMAT_FILE)
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let mut cache = DataUsageCache {
|
||||
info: crate::data_usage_define::DataUsageCacheInfo {
|
||||
name: "bucket".to_string(),
|
||||
next_cycle: 12,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
cache.info.failed_objects.insert(failed_path, FolderScanner::now_secs());
|
||||
|
||||
let parent = CancellationToken::new();
|
||||
let budget = ScannerCycleBudget::new(&parent, Default::default());
|
||||
let result = scan_data_folder(
|
||||
budget.token(),
|
||||
budget,
|
||||
vec![scanner.local_disk.clone()],
|
||||
scanner.local_disk.clone(),
|
||||
cache,
|
||||
None,
|
||||
HealScanMode::Normal,
|
||||
SCANNER_SLEEPER.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let partial = match result {
|
||||
Err(ScannerError::PartialCache(partial)) => partial,
|
||||
other => panic!("expected unresolved object to keep the cache partial, got {other:?}"),
|
||||
};
|
||||
assert!(!partial.info.snapshot_complete);
|
||||
assert!(!partial.info.failed_objects.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
#[cfg(unix)]
|
||||
|
||||
+2407
-360
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,8 @@ pub(crate) use rustfs_ecstore::api::bucket::lifecycle::lifecycle::{
|
||||
Event as EcstoreEvent, Lifecycle as EcstoreLifecycle, ObjectOpts as EcstoreObjectOpts,
|
||||
TRANSITION_COMPLETE as ECSTORE_TRANSITION_COMPLETE, object_opts_from_object_info as ecstore_object_opts_from_object_info,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use rustfs_ecstore::api::bucket::metadata_sys::init_bucket_metadata_sys as ecstore_init_bucket_metadata_sys;
|
||||
pub(crate) use rustfs_ecstore::api::bucket::metadata_sys::{
|
||||
get_lifecycle_config as ecstore_get_lifecycle_config, get_object_lock_config as ecstore_get_object_lock_config,
|
||||
get_replication_config as ecstore_get_replication_config,
|
||||
@@ -62,8 +64,8 @@ pub(crate) use rustfs_ecstore::api::disk::error::{DiskError as EcstoreDiskError,
|
||||
pub(crate) use rustfs_ecstore::api::disk::{
|
||||
BUCKET_META_PREFIX as ECSTORE_BUCKET_META_PREFIX, Bytes as EcstoreDiskBytes, Disk as EcstoreDisk, DiskAPI as EcstoreDiskAPI,
|
||||
DiskInfo as EcstoreDiskInfo, DiskInfoOptions as EcstoreDiskInfoOptions, DiskLocation as EcstoreDiskLocation,
|
||||
RUSTFS_META_BUCKET as ECSTORE_RUSTFS_META_BUCKET, STORAGE_FORMAT_FILE as ECSTORE_STORAGE_FORMAT_FILE,
|
||||
ScanGuard as EcstoreScanGuard,
|
||||
NsScannerOpenRequest as EcstoreNsScannerOpenRequest, RUSTFS_META_BUCKET as ECSTORE_RUSTFS_META_BUCKET,
|
||||
STORAGE_FORMAT_FILE as ECSTORE_STORAGE_FORMAT_FILE, ScanGuard as EcstoreScanGuard,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use rustfs_ecstore::api::disk::{
|
||||
@@ -72,6 +74,12 @@ pub(crate) use rustfs_ecstore::api::disk::{
|
||||
pub(crate) use rustfs_ecstore::api::error::{
|
||||
Error as EcstoreErrorType, Result as EcstoreResultType, StorageError as EcstoreStorageError,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use rustfs_ecstore::api::layout::{
|
||||
EndpointServerPools as EcstoreEndpointServerPools, Endpoints as EcstoreEndpoints, PoolEndpoints as EcstorePoolEndpoints,
|
||||
};
|
||||
#[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,
|
||||
object_store_handle as ecstore_resolve_object_store_handle, setup_is_erasure as ecstore_is_erasure,
|
||||
@@ -79,30 +87,39 @@ pub(crate) use rustfs_ecstore::api::runtime::{
|
||||
};
|
||||
pub(crate) use rustfs_ecstore::api::set_disk::SetDisks as EcstoreSetDisks;
|
||||
pub(crate) use rustfs_ecstore::api::storage::ECStore as EcstoreStore;
|
||||
#[cfg(test)]
|
||||
pub(crate) use rustfs_ecstore::api::storage::init_local_disks_with_instance_ctx as ecstore_init_local_disks_with_instance_ctx;
|
||||
pub(crate) use rustfs_ecstore::api::tier::tier_config::TierConfig as EcstoreTierConfig;
|
||||
use rustfs_storage_api as storage_contracts;
|
||||
|
||||
pub(crate) mod owner {
|
||||
pub(crate) use super::storage_contracts::{HTTPRangeSpec, ObjectIO, ObjectOperations, ObjectToDelete};
|
||||
pub(crate) use super::storage_contracts::{
|
||||
HTTPPreconditions, HTTPRangeSpec, NS_SCANNER_PROTOCOL_VERSION, ObjectIO, ObjectOperations, ObjectToDelete,
|
||||
};
|
||||
|
||||
pub(crate) use super::{
|
||||
ECSTORE_BUCKET_META_PREFIX, ECSTORE_RUSTFS_META_BUCKET, ECSTORE_STORAGE_FORMAT_FILE, ECSTORE_STORAGECLASS_RRS,
|
||||
ECSTORE_STORAGECLASS_STANDARD, ECSTORE_TRANSITION_COMPLETE, EcstoreBucketTargetSys, EcstoreBucketVersioningSys,
|
||||
EcstoreDisk, EcstoreDiskAPI, EcstoreDiskBytes, EcstoreDiskError, EcstoreDiskInfo, EcstoreDiskInfoOptions,
|
||||
EcstoreDiskLocation, EcstoreDiskResult, EcstoreErrorType, EcstoreEvaluator, EcstoreEvent, EcstoreLcEventSrc,
|
||||
EcstoreLifecycle, EcstoreListPathRawOptions, EcstoreObjectOpts, EcstoreReplicationConfigurationExt,
|
||||
EcstoreReplicationScannerBridge, EcstoreResultType, EcstoreScanGuard, EcstoreSetDisks, EcstoreStorageError, EcstoreStore,
|
||||
EcstoreTierConfig, EcstoreVersioningApi, ScannerReplicationHealObject, ScannerReplicationHealResult,
|
||||
ScannerReplicationQueueAdmission, ecstore_apply_expiry_rule, ecstore_apply_transition_rule, ecstore_expiry_state_handle,
|
||||
ecstore_get_global_tier_config_mgr, ecstore_get_lifecycle_config, ecstore_get_object_lock_config,
|
||||
ecstore_get_replication_config, ecstore_is_erasure, ecstore_is_erasure_sd, ecstore_is_reserved_or_invalid_bucket,
|
||||
ecstore_list_path_raw, ecstore_object_opts_from_object_info, ecstore_path2_bucket_object,
|
||||
ecstore_path2_bucket_object_with_base_path, ecstore_read_config, ecstore_replace_bucket_usage_memory_from_info,
|
||||
ecstore_resolve_object_store_handle, ecstore_save_config, scanner_replication_config_for_lifecycle_eval,
|
||||
EcstoreLifecycle, EcstoreListPathRawOptions, EcstoreNsScannerOpenRequest, EcstoreObjectOpts,
|
||||
EcstoreReplicationConfigurationExt, EcstoreReplicationScannerBridge, EcstoreResultType, EcstoreScanGuard,
|
||||
EcstoreSetDisks, EcstoreStorageError, EcstoreStore, EcstoreTierConfig, EcstoreVersioningApi,
|
||||
ScannerReplicationHealObject, ScannerReplicationHealResult, ScannerReplicationQueueAdmission, ecstore_apply_expiry_rule,
|
||||
ecstore_apply_transition_rule, ecstore_expiry_state_handle, ecstore_get_global_tier_config_mgr,
|
||||
ecstore_get_lifecycle_config, ecstore_get_object_lock_config, ecstore_get_replication_config, ecstore_is_erasure,
|
||||
ecstore_is_erasure_sd, ecstore_is_reserved_or_invalid_bucket, ecstore_list_path_raw,
|
||||
ecstore_object_opts_from_object_info, ecstore_path2_bucket_object, ecstore_path2_bucket_object_with_base_path,
|
||||
ecstore_read_config, ecstore_replace_bucket_usage_memory_from_info, ecstore_resolve_object_store_handle,
|
||||
ecstore_save_config, scanner_replication_config_for_lifecycle_eval,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use super::{EcstoreDiskOption, EcstoreDiskStore, EcstoreEndpoint, ecstore_config_init, ecstore_new_disk};
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
@@ -244,11 +261,17 @@ impl From<EcstoreReplicationHealQueueResult> for ScannerReplicationHealResult {
|
||||
}
|
||||
|
||||
pub(crate) mod scan {
|
||||
pub(crate) use super::storage_contracts::{BucketOperations, BucketOptions, NamespaceLocking};
|
||||
pub use super::storage_contracts::SCANNER_ACTIVITY_PROTOCOL_VERSION;
|
||||
pub(crate) use super::storage_contracts::{
|
||||
BucketOperations, BucketOptions, NamespaceLocking, SCANNER_ACTIVITY_LEGACY_PROTOCOL_VERSION,
|
||||
SCANNER_ACTIVITY_PREVIOUS_PROTOCOL_VERSION,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use super::storage_contracts::{MakeBucketOptions, ObjectIO};
|
||||
}
|
||||
|
||||
pub(crate) mod scanner_io {
|
||||
pub(crate) use super::storage_contracts::{BucketInfo, BucketOperations, BucketOptions, DiskSetSelector, StorageAdminApi};
|
||||
pub(crate) use super::storage_contracts::{BucketInfo, BucketOptions};
|
||||
#[cfg(test)]
|
||||
pub(crate) use super::storage_contracts::{HTTPRangeSpec, ObjectIO};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user