mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-23 12:49:04 +00:00
a247c79359
Cache heal queue dedup keys, avoid retry request double construction, clear task aliases after terminal completion, and age out stale scanner pending-heal ledger entries during retry sweeps. Co-authored-by: heihutu <heihutu@gmail.com>
2286 lines
96 KiB
Rust
2286 lines
96 KiB
Rust
// Copyright 2024 RustFS Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::fs::FileType;
|
|
use std::io::ErrorKind;
|
|
use std::sync::{Arc, Mutex, Once};
|
|
use std::time::{Duration, Instant, SystemTime};
|
|
|
|
use crate::ReplTargetSizeSummary;
|
|
use crate::data_usage_define::{
|
|
DATA_USAGE_SCAN_CHECKPOINT_VERSION, DataUsageCache, DataUsageEntry, DataUsageHash, DataUsageHashMap, DataUsageScanCheckpoint,
|
|
DataUsageScanCheckpointReason, PendingScannerHeal, PendingScannerHealKind, ScannerSizeSummaryExt, SizeSummary, hash_path,
|
|
};
|
|
use crate::error::ScannerError;
|
|
use crate::runtime_config::{
|
|
scanner_alert_excess_folders, scanner_alert_excess_version_size, scanner_alert_excess_versions, scanner_yield_every_n_objects,
|
|
};
|
|
use crate::scanner_budget::{ScannerCycleBudget, ScannerCycleBudgetReason};
|
|
use crate::scanner_io::{
|
|
SCANNER_SKIP_FILE_ERROR, ScannerIODisk as _, is_scanner_metadata_corrupt_error, is_scanner_metadata_transient_error,
|
|
};
|
|
use crate::sleeper::DynamicSleeper;
|
|
use crate::storage_api::owner::{EcstoreEventArgs, ecstore_send_event};
|
|
use metrics::{counter, describe_counter};
|
|
use rustfs_common::heal_channel::{
|
|
HEAL_DELETE_DANGLING, HealAdmissionDropReason, HealAdmissionResult, HealChannelPriority, HealChannelRequest,
|
|
HealRequestSource, HealScanMode, send_heal_request_with_admission,
|
|
};
|
|
use rustfs_common::metrics::{
|
|
CloseDiskGuard, IlmAction, Metric, Metrics, ScannerReplicationRepairKind, ScannerSourceWorkUpdate, ScannerWorkSource,
|
|
UpdateCurrentPathFn, current_path_updater, global_metrics,
|
|
};
|
|
use rustfs_common::trace_bus::{TraceEvent, TraceFunc, TraceKind, trace_emit, trace_subscriber_count};
|
|
use rustfs_filemeta::{MetaCacheEntries, MetaCacheEntry, MetadataResolutionParams};
|
|
use rustfs_utils::path::{SLASH_SEPARATOR, path_join_buf};
|
|
use s3s::dto::{BucketLifecycleConfiguration, ObjectLockConfiguration, VersioningConfiguration};
|
|
use time::OffsetDateTime;
|
|
use tokio::select;
|
|
use tokio::sync::mpsc;
|
|
use tokio_util::sync::CancellationToken;
|
|
use tracing::{debug, error, warn};
|
|
|
|
use crate::{
|
|
Disk, DiskError, DiskInfoOptions, Evaluator, Event, LcEventSrc, ListPathRawOptions, ObjectOpts, ReplicationConfig,
|
|
ReplicationHealObject, ReplicationQueueAdmission, ReplicationStatusType, STORAGE_FORMAT_FILE, ScannerDiskExt as _,
|
|
ScannerLifecycleConfigExt as _, ScannerVersioningConfigExt as _, StorageError, apply_expiry_rule, apply_transition_rule,
|
|
enqueue_runtime_newer_noncurrent, is_reserved_or_invalid_bucket, list_path_raw, path2_bucket_object,
|
|
path2_bucket_object_with_base_path, queue_replication_heal, scanner_is_erasure,
|
|
scanner_replication_config_for_lifecycle_eval,
|
|
};
|
|
use crate::{ScannerObjectInfo as ObjectInfo, ScannerObjectToDelete as ObjectToDelete};
|
|
|
|
const LOG_COMPONENT_SCANNER: &str = "scanner";
|
|
const LOG_SUBSYSTEM_FOLDER: &str = "folder";
|
|
const LOG_SUBSYSTEM_LIFECYCLE: &str = "lifecycle";
|
|
const LOG_SUBSYSTEM_HEAL: &str = "heal";
|
|
const EVENT_SCANNER_FOLDER_STATE: &str = "scanner_folder_state";
|
|
const EVENT_SCANNER_METADATA_CORRUPT: &str = "scanner_metadata_corrupt";
|
|
const EVENT_SCANNER_LIFECYCLE_ACTION: &str = "scanner_lifecycle_action";
|
|
const EVENT_SCANNER_HEAL_ADMISSION: &str = "scanner_heal_admission";
|
|
const EVENT_SCANNER_ALERT_STATE: &str = "scanner_alert_state";
|
|
|
|
const DATA_USAGE_UPDATE_DIR_CYCLES: u32 = 16;
|
|
const DATA_SCANNER_COMPACT_LEAST_OBJECT: usize = 500;
|
|
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_STALL_TIMEOUT: Duration = Duration::from_secs(60);
|
|
const SCANNER_ENTRY_PROGRESS_BATCH: u64 = 32;
|
|
const SCANNER_ENTRY_PROGRESS_INTERVAL: Duration = Duration::from_secs(30);
|
|
// Erasure data directories contain direct part.N files; keep namespace probes bounded.
|
|
const ERASURE_DATA_DIR_PROBE_ENTRY_LIMIT: usize = 64;
|
|
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";
|
|
const ENV_SCANNER_DEEP_VERIFY_COOLDOWN_SECS: &str = "RUSTFS_SCANNER_DEEP_VERIFY_COOLDOWN_SECS";
|
|
const ENV_FAILED_OBJECT_TTL_SECS: &str = "RUSTFS_DATA_USAGE_FAILED_OBJECT_TTL_SECS";
|
|
const ENV_FAILED_OBJECTS_MAX: &str = "RUSTFS_DATA_USAGE_FAILED_OBJECTS_MAX";
|
|
const DEFAULT_FAILED_OBJECT_TTL_SECS: u32 = 86_400;
|
|
const DEFAULT_FAILED_OBJECTS_MAX: u32 = 10_000;
|
|
const DEFAULT_SCANNER_DEEP_VERIFY_COOLDOWN_SECS: u64 = 60;
|
|
const METRIC_SCANNER_EXCESS_OBJECT_VERSIONS_TOTAL: &str = "rustfs_scanner_excess_object_versions_total";
|
|
const METRIC_SCANNER_EXCESS_OBJECT_VERSION_SIZE_TOTAL: &str = "rustfs_scanner_excess_object_version_size_total";
|
|
const METRIC_SCANNER_EXCESS_FOLDERS_TOTAL: &str = "rustfs_scanner_excess_folders_total";
|
|
const METRIC_SCANNER_PENDING_HEAL_PRUNE_TOTAL: &str = "rustfs_scanner_pending_heal_prune_total";
|
|
const METRIC_SCANNER_PENDING_HEAL_MALFORMED_TOTAL: &str = "rustfs_scanner_pending_heal_malformed_total";
|
|
const MAX_PENDING_SCANNER_HEAL_RETRIES_PER_BUCKET: usize = 128;
|
|
|
|
// --- scanner excess alerts as S3 notification events (rustfs/backlog#1868) --
|
|
//
|
|
// The excess-versions / excess-version-size / excess-folders alerts were
|
|
// metrics-and-logs only; subscribers (consoles, external auditors) had no way
|
|
// to hear them. MinIO emits s3:ObjectManyVersions / s3:ObjectLargeVersions /
|
|
// s3:PrefixManyFolders for the same conditions — RustFS carries those as
|
|
// EventName::Scanner* with the wire names below. Without a cooldown a single
|
|
// over-threshold object would re-emit on every scan cycle (~a minute), so
|
|
// emissions are edge-held per (kind, bucket, object) for 24h.
|
|
|
|
/// `s3:Scanner:ManyVersions` (MinIO `s3:ObjectManyVersions`).
|
|
pub const EVENT_SCANNER_MANY_VERSIONS: &str = "s3:Scanner:ManyVersions";
|
|
/// `s3:Scanner:LargeVersions` (MinIO `s3:ObjectLargeVersions`).
|
|
pub const EVENT_SCANNER_LARGE_VERSIONS: &str = "s3:Scanner:LargeVersions";
|
|
/// `s3:Scanner:BigPrefix` (MinIO `s3:PrefixManyFolders`).
|
|
pub const EVENT_SCANNER_BIG_PREFIX: &str = "s3:Scanner:BigPrefix";
|
|
const ENV_SCANNER_ALERT_COOLDOWN_SECS: &str = "RUSTFS_SCANNER_ALERT_COOLDOWN_SECS";
|
|
const DEFAULT_SCANNER_ALERT_COOLDOWN_SECS: u64 = 86_400;
|
|
/// Hard cap on distinct cooldown keys; a pathological number of over-threshold
|
|
/// objects clears the map wholesale instead of growing without bound (the
|
|
/// worst case is one re-emission per still-hot key per scan cycle).
|
|
const MAX_SCANNER_ALERT_COOLDOWN_KEYS: usize = 4096;
|
|
|
|
/// Distinct alert kinds sharing one cooldown map.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
enum ScannerAlertKind {
|
|
ManyVersions,
|
|
LargeVersions,
|
|
BigPrefix,
|
|
}
|
|
|
|
type ScannerAlertCooldownKey = (ScannerAlertKind, String, String);
|
|
type ScannerAlertCooldownMap = HashMap<ScannerAlertCooldownKey, Instant>;
|
|
|
|
static SCANNER_ALERT_EMISSION_COOLDOWN: Mutex<Option<ScannerAlertCooldownMap>> = Mutex::new(None);
|
|
|
|
fn scanner_alert_cooldown() -> Duration {
|
|
let raw = std::env::var(ENV_SCANNER_ALERT_COOLDOWN_SECS)
|
|
.ok()
|
|
.and_then(|v| v.parse::<u64>().ok());
|
|
Duration::from_secs(raw.unwrap_or(DEFAULT_SCANNER_ALERT_COOLDOWN_SECS))
|
|
}
|
|
|
|
/// Edge-held emission gate: returns `true` (and records the cooldown) only
|
|
/// when this (kind, bucket, object) last fired longer than the cooldown ago —
|
|
/// or never. Metrics and logs stay level-triggered every cycle; only the
|
|
/// notification events are held back.
|
|
fn scanner_alert_emission_allows(kind: ScannerAlertKind, bucket: &str, object: &str, cooldown: Duration) -> bool {
|
|
let key = (kind, bucket.to_string(), object.to_string());
|
|
let mut guard = SCANNER_ALERT_EMISSION_COOLDOWN
|
|
.lock()
|
|
.unwrap_or_else(|poison| poison.into_inner());
|
|
let guard = guard.get_or_insert_with(ScannerAlertCooldownMap::new);
|
|
let now = Instant::now();
|
|
// Expired entries leave first; the cap is still exceeded only when live
|
|
// keys alone overflow it, in which case a wholesale clear trades one
|
|
// extra emission per hot key for a hard memory bound.
|
|
if guard.len() >= MAX_SCANNER_ALERT_COOLDOWN_KEYS {
|
|
guard.retain(|_, fired_at| now.duration_since(*fired_at) < cooldown);
|
|
if guard.len() >= MAX_SCANNER_ALERT_COOLDOWN_KEYS {
|
|
guard.clear();
|
|
}
|
|
}
|
|
match guard.get(&key) {
|
|
Some(fired_at) if now.duration_since(*fired_at) < cooldown => false,
|
|
_ => {
|
|
guard.insert(key, now);
|
|
true
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Emit a scanner alert as an S3 notification event through the standard
|
|
/// dispatch pipeline. Fire-and-forget: the notify layer owns delivery,
|
|
/// retry, and target filtering; the scanner never waits on it.
|
|
fn emit_scanner_alert_event(event_name: &str, bucket: &str, object: &str, size: i64, details: &[(&str, String)]) {
|
|
let mut req_params = HashMap::with_capacity(details.len());
|
|
for (key, value) in details {
|
|
req_params.insert((*key).to_string(), value.clone());
|
|
}
|
|
ecstore_send_event(EcstoreEventArgs {
|
|
event_name: event_name.to_string(),
|
|
bucket_name: bucket.to_string(),
|
|
object: crate::ScannerObjectInfo {
|
|
bucket: bucket.to_string(),
|
|
name: object.to_string(),
|
|
size,
|
|
..Default::default()
|
|
},
|
|
req_params,
|
|
user_agent: "Scanner".to_string(),
|
|
..Default::default()
|
|
});
|
|
}
|
|
const MAX_PENDING_SCANNER_HEALS_PER_BUCKET: usize = 10_000;
|
|
const MAX_PENDING_SCANNER_HEAL_AGE_SECS: u64 = 24 * 60 * 60;
|
|
|
|
static SCANNER_ALERT_METRICS_ONCE: Once = Once::new();
|
|
|
|
#[cfg(test)]
|
|
type ListPathRawTimeoutSnapshot = (bool, Option<Duration>, Option<Duration>);
|
|
|
|
fn scanner_abandoned_child_list_options() -> ListPathRawOptions {
|
|
// A complete heal walk scales with bucket size and may legitimately take
|
|
// longer than a fixed wall-clock budget. Keep the total duration unbounded;
|
|
// Retain the scanner's per-read stall budget and keep cancellation controlled
|
|
// by the scanner cycle token.
|
|
ListPathRawOptions {
|
|
skip_walkdir_total_timeout: true,
|
|
walkdir_stall_timeout: Some(SCANNER_LIST_PATH_RAW_STALL_TIMEOUT),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
pub fn data_usage_update_dir_cycles() -> u32 {
|
|
rustfs_utils::get_env_u32(ENV_DATA_USAGE_UPDATE_DIR_CYCLES, DATA_USAGE_UPDATE_DIR_CYCLES)
|
|
}
|
|
|
|
pub fn heal_object_select_prob() -> u32 {
|
|
rustfs_utils::get_env_u32(ENV_HEAL_OBJECT_SELECT_PROB, DEFAULT_HEAL_OBJECT_SELECT_PROB)
|
|
}
|
|
|
|
fn deep_verify_cooldown() -> Duration {
|
|
Duration::from_secs(rustfs_utils::get_env_u64(
|
|
ENV_SCANNER_DEEP_VERIFY_COOLDOWN_SECS,
|
|
DEFAULT_SCANNER_DEEP_VERIFY_COOLDOWN_SECS,
|
|
))
|
|
}
|
|
|
|
fn object_is_within_deep_verify_cooldown(mod_time: Option<OffsetDateTime>, now: OffsetDateTime, cooldown: Duration) -> bool {
|
|
let Some(mod_time) = mod_time else {
|
|
return false;
|
|
};
|
|
let Ok(cooldown) = time::Duration::try_from(cooldown) else {
|
|
return false;
|
|
};
|
|
mod_time > now - cooldown
|
|
}
|
|
|
|
fn effective_object_heal_scan_mode(heal_bitrot: bool, mod_time: Option<OffsetDateTime>, now: OffsetDateTime) -> HealScanMode {
|
|
if !heal_bitrot {
|
|
return HealScanMode::Normal;
|
|
}
|
|
if object_is_within_deep_verify_cooldown(mod_time, now, deep_verify_cooldown()) {
|
|
HealScanMode::Normal
|
|
} else {
|
|
HealScanMode::Deep
|
|
}
|
|
}
|
|
|
|
fn ensure_scanner_alert_metrics_registered() {
|
|
SCANNER_ALERT_METRICS_ONCE.call_once(|| {
|
|
describe_counter!(
|
|
METRIC_SCANNER_EXCESS_OBJECT_VERSIONS_TOTAL,
|
|
"Total scanner alerts for objects with too many retained versions."
|
|
);
|
|
describe_counter!(
|
|
METRIC_SCANNER_EXCESS_OBJECT_VERSION_SIZE_TOTAL,
|
|
"Total scanner alerts for objects whose retained versions exceed the cumulative size threshold."
|
|
);
|
|
describe_counter!(
|
|
METRIC_SCANNER_EXCESS_FOLDERS_TOTAL,
|
|
"Total scanner alerts for folders with too many direct subfolders."
|
|
);
|
|
});
|
|
}
|
|
|
|
fn scanner_excess_versions_threshold() -> u64 {
|
|
scanner_alert_excess_versions()
|
|
}
|
|
|
|
fn scanner_excess_version_size_threshold() -> u64 {
|
|
scanner_alert_excess_version_size()
|
|
}
|
|
|
|
fn scanner_excess_folders_threshold() -> u64 {
|
|
scanner_alert_excess_folders()
|
|
}
|
|
|
|
fn should_yield_after_object(object_count: u64, yield_every: u64) -> bool {
|
|
yield_every > 0 && object_count.is_multiple_of(yield_every)
|
|
}
|
|
|
|
const SCANNER_FAILED_OBJECT_LOG_INITIAL_LIMIT: usize = 16;
|
|
const SCANNER_FAILED_OBJECT_LOG_EVERY: usize = 1024;
|
|
|
|
fn should_log_failed_object(failed_objects: usize) -> bool {
|
|
failed_objects <= SCANNER_FAILED_OBJECT_LOG_INITIAL_LIMIT || failed_objects.is_multiple_of(SCANNER_FAILED_OBJECT_LOG_EVERY)
|
|
}
|
|
|
|
fn record_scanner_ilm_action_if_queued(metrics: &Metrics, action: IlmAction, count: u64, queued: bool) -> bool {
|
|
if queued {
|
|
metrics.record_scanner_lifecycle_action(action, count);
|
|
}
|
|
queued
|
|
}
|
|
|
|
fn scanner_replication_work_update(admission: ReplicationQueueAdmission) -> ScannerSourceWorkUpdate {
|
|
match admission {
|
|
ReplicationQueueAdmission::Queued => ScannerSourceWorkUpdate::queued(1),
|
|
ReplicationQueueAdmission::Missed => ScannerSourceWorkUpdate::missed(1),
|
|
ReplicationQueueAdmission::Skipped => ScannerSourceWorkUpdate {
|
|
skipped: 1,
|
|
..Default::default()
|
|
},
|
|
}
|
|
}
|
|
|
|
fn scanner_replication_repair_kind(roi: &ReplicationHealObject) -> Option<ScannerReplicationRepairKind> {
|
|
if roi.is_empty_identity() {
|
|
return None;
|
|
}
|
|
|
|
if roi.is_existing_object_repair() {
|
|
Some(ScannerReplicationRepairKind::BucketExistingObject)
|
|
} else if roi.has_version_purge_status() {
|
|
Some(ScannerReplicationRepairKind::BucketVersionPurge)
|
|
} else if roi.delete_marker {
|
|
Some(ScannerReplicationRepairKind::BucketDeleteMarker)
|
|
} else {
|
|
Some(ScannerReplicationRepairKind::BucketObject)
|
|
}
|
|
}
|
|
|
|
fn record_scanner_replication_admission(metrics: &Metrics, roi: &ReplicationHealObject, admission: ReplicationQueueAdmission) {
|
|
let work = scanner_replication_work_update(admission);
|
|
metrics.record_scanner_source_work(ScannerWorkSource::BucketReplication, work);
|
|
if let Some(kind) = scanner_replication_repair_kind(roi) {
|
|
metrics.record_scanner_replication_repair_work(kind, work);
|
|
}
|
|
}
|
|
|
|
fn scanner_heal_source(scan_mode: HealScanMode) -> ScannerWorkSource {
|
|
match scan_mode {
|
|
HealScanMode::Deep => ScannerWorkSource::Bitrot,
|
|
HealScanMode::Unknown | HealScanMode::Normal => ScannerWorkSource::Heal,
|
|
}
|
|
}
|
|
|
|
fn record_scanner_heal_admission(metrics: &Metrics, scan_mode: HealScanMode, admission: Result<HealAdmissionResult, ()>) -> bool {
|
|
let (work, admitted) = match admission {
|
|
Ok(HealAdmissionResult::Accepted) => (ScannerSourceWorkUpdate::queued(1), true),
|
|
Ok(HealAdmissionResult::Merged) => (
|
|
ScannerSourceWorkUpdate {
|
|
skipped: 1,
|
|
..Default::default()
|
|
},
|
|
true,
|
|
),
|
|
Ok(HealAdmissionResult::Full | HealAdmissionResult::Dropped(_)) | Err(_) => (ScannerSourceWorkUpdate::missed(1), false),
|
|
};
|
|
metrics.record_scanner_source_work(scanner_heal_source(scan_mode), work);
|
|
admitted
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
struct PendingScannerAccounting<'a> {
|
|
object: &'a ObjectInfo,
|
|
retained_size: i64,
|
|
expired_size: i64,
|
|
}
|
|
|
|
impl PendingScannerAccounting<'_> {
|
|
fn apply(self, size_summary: &mut SizeSummary, cumulative_size: &mut i64, queued: bool) {
|
|
let size = if queued { self.expired_size } else { self.retained_size };
|
|
size_summary.actions_accounting(self.object, size, self.retained_size);
|
|
*cumulative_size += size;
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
enum FolderResumeMatch {
|
|
Exact,
|
|
Descendant,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
enum FolderResumeOrder {
|
|
NoHint,
|
|
Used,
|
|
Stale,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
enum FolderScanSource {
|
|
New,
|
|
Existing,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct QueuedFolder {
|
|
folder: CachedFolder,
|
|
source: FolderScanSource,
|
|
}
|
|
|
|
fn folder_resume_match(folder_name: &str, resume_after: &str) -> Option<FolderResumeMatch> {
|
|
if resume_after == folder_name {
|
|
return Some(FolderResumeMatch::Exact);
|
|
}
|
|
resume_after
|
|
.strip_prefix(folder_name)
|
|
.filter(|suffix| suffix.starts_with(SLASH_SEPARATOR))
|
|
.map(|_| FolderResumeMatch::Descendant)
|
|
}
|
|
|
|
fn order_items_for_resume<T, F>(items: &mut [T], resume_after: Option<&str>, name: F) -> FolderResumeOrder
|
|
where
|
|
F: Fn(&T) -> &str,
|
|
{
|
|
items.sort_by(|left, right| name(left).cmp(name(right)));
|
|
|
|
let Some(resume_after) = resume_after.filter(|resume_after| !resume_after.is_empty()) else {
|
|
return FolderResumeOrder::NoHint;
|
|
};
|
|
|
|
let Some((resume_index, resume_match)) = items
|
|
.iter()
|
|
.enumerate()
|
|
.find_map(|(index, item)| folder_resume_match(name(item), resume_after).map(|resume_match| (index, resume_match)))
|
|
else {
|
|
return FolderResumeOrder::Stale;
|
|
};
|
|
|
|
let rotate_by = match resume_match {
|
|
FolderResumeMatch::Exact => resume_index + 1,
|
|
FolderResumeMatch::Descendant => resume_index,
|
|
};
|
|
if rotate_by < items.len() {
|
|
items.rotate_left(rotate_by);
|
|
}
|
|
FolderResumeOrder::Used
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn order_folders_for_resume(folders: &mut [CachedFolder], resume_after: Option<&str>) -> FolderResumeOrder {
|
|
order_items_for_resume(folders, resume_after, |folder| folder.name.as_str())
|
|
}
|
|
|
|
fn order_queued_folders_for_resume(folders: &mut [QueuedFolder], resume_after: Option<&str>) -> FolderResumeOrder {
|
|
order_items_for_resume(folders, resume_after, |folder| folder.folder.name.as_str())
|
|
}
|
|
|
|
fn checkpoint_reason_from_budget(reason: Option<ScannerCycleBudgetReason>) -> DataUsageScanCheckpointReason {
|
|
match reason {
|
|
Some(ScannerCycleBudgetReason::Runtime) => DataUsageScanCheckpointReason::Runtime,
|
|
Some(ScannerCycleBudgetReason::Objects) => DataUsageScanCheckpointReason::Objects,
|
|
Some(ScannerCycleBudgetReason::Directories) => DataUsageScanCheckpointReason::Directories,
|
|
None => DataUsageScanCheckpointReason::Unknown,
|
|
}
|
|
}
|
|
|
|
fn data_usage_entry_has_progress(entry: &DataUsageEntry) -> bool {
|
|
data_usage_root_has_progress(entry)
|
|
}
|
|
|
|
fn set_scan_checkpoint(cache: &mut DataUsageCache, reason: DataUsageScanCheckpointReason) {
|
|
let resume_after = cache.info.scan_resume_after.clone().or_else(|| {
|
|
cache
|
|
.info
|
|
.scan_checkpoint
|
|
.as_ref()
|
|
.map(|checkpoint| checkpoint.resume_after.clone())
|
|
});
|
|
|
|
if let Some(resume_after) = resume_after {
|
|
let checkpoint = DataUsageScanCheckpoint::new(resume_after, reason);
|
|
global_metrics().record_scanner_checkpoint_set(
|
|
checkpoint.version,
|
|
checkpoint.resume_after.clone(),
|
|
checkpoint.reason.as_str(),
|
|
);
|
|
cache.info.scan_checkpoint = Some(checkpoint);
|
|
} else {
|
|
cache.info.scan_checkpoint = None;
|
|
}
|
|
}
|
|
|
|
fn should_alert_excessive_versions(remaining_versions: usize, cumulative_size: i64) -> (bool, bool) {
|
|
let too_many_versions = remaining_versions as u64 >= scanner_excess_versions_threshold();
|
|
let too_large_versions = cumulative_size > 0 && cumulative_size as u64 >= scanner_excess_version_size_threshold();
|
|
(too_many_versions, too_large_versions)
|
|
}
|
|
|
|
fn non_negative_i64_to_u64(value: i64) -> u64 {
|
|
value.max(0) as u64
|
|
}
|
|
|
|
fn trace_start_instant() -> Option<Instant> {
|
|
(trace_subscriber_count() > 0).then(Instant::now)
|
|
}
|
|
|
|
fn emit_scanner_folder_trace(root: &str, folder: &str, objects: u64, started_at: Option<Instant>, state: &'static str) {
|
|
let Some(started_at) = started_at else {
|
|
return;
|
|
};
|
|
|
|
trace_emit(|| {
|
|
let (bucket, prefix) = path2_bucket_object_with_base_path(root, folder);
|
|
TraceEvent::new(TraceKind::Scanner, TraceFunc::ScannerFolder)
|
|
.with_bucket(bucket)
|
|
.with_object(prefix)
|
|
.with_duration(started_at.elapsed())
|
|
.with_attr("state", state)
|
|
.with_attr("objects", objects)
|
|
});
|
|
}
|
|
|
|
fn emit_scanner_ilm_action_trace(
|
|
bucket: &str,
|
|
object: &str,
|
|
action: IlmAction,
|
|
count: u64,
|
|
queued: bool,
|
|
started_at: Option<Instant>,
|
|
) {
|
|
let Some(started_at) = started_at else {
|
|
return;
|
|
};
|
|
|
|
let state = if queued { "queued" } else { "not_queued" };
|
|
trace_emit(|| {
|
|
TraceEvent::new(TraceKind::Scanner, TraceFunc::ScannerIlmAction)
|
|
.with_bucket(bucket)
|
|
.with_object(object)
|
|
.with_duration(started_at.elapsed())
|
|
.with_attr("state", state)
|
|
.with_attr("action", action.as_str())
|
|
.with_attr("count", count)
|
|
.with_attr("queued", queued)
|
|
});
|
|
}
|
|
|
|
struct ScannerHealCandidateTraceContext {
|
|
bucket: String,
|
|
object: Option<String>,
|
|
version_id: Option<String>,
|
|
scan_mode: Option<HealScanMode>,
|
|
started_at: Instant,
|
|
}
|
|
|
|
fn scanner_heal_candidate_trace_context(request: &HealChannelRequest) -> Option<ScannerHealCandidateTraceContext> {
|
|
let started_at = trace_start_instant()?;
|
|
Some(ScannerHealCandidateTraceContext {
|
|
bucket: request.bucket.clone(),
|
|
object: request.object_prefix.clone(),
|
|
version_id: request.object_version_id.clone(),
|
|
scan_mode: request.scan_mode,
|
|
started_at,
|
|
})
|
|
}
|
|
|
|
struct ScannerHealCandidateTrace<'a> {
|
|
candidate_type: &'static str,
|
|
bucket: &'a str,
|
|
object: Option<&'a str>,
|
|
version_id: Option<&'a str>,
|
|
priority: HealChannelPriority,
|
|
scan_mode: Option<HealScanMode>,
|
|
result: Result<HealAdmissionResult, &'a str>,
|
|
started_at: Instant,
|
|
}
|
|
|
|
fn emit_scanner_heal_candidate_trace(trace: ScannerHealCandidateTrace<'_>) {
|
|
trace_emit(|| {
|
|
let (state, admission, error) = match trace.result {
|
|
Ok(result) if result.is_admitted() => ("admitted", describe_heal_admission(result), None),
|
|
Ok(result) => ("not_admitted", describe_heal_admission(result), None),
|
|
Err(error) => ("submit_failed", "channel_error".to_string(), Some(error)),
|
|
};
|
|
let mut event = TraceEvent::new(TraceKind::Scanner, TraceFunc::ScannerHealCandidate)
|
|
.with_bucket(trace.bucket)
|
|
.with_duration(trace.started_at.elapsed())
|
|
.with_attr("state", state)
|
|
.with_attr("candidate_type", trace.candidate_type)
|
|
.with_attr("priority", heal_priority_label(trace.priority))
|
|
.with_attr("admission", admission);
|
|
|
|
if let Some(object) = trace.object {
|
|
event = event.with_object(object);
|
|
}
|
|
if let Some(version_id) = trace.version_id {
|
|
event = event.with_attr("version_id", version_id);
|
|
}
|
|
if let Some(scan_mode) = trace.scan_mode {
|
|
event = event.with_attr("scan_mode", scan_mode.as_str());
|
|
}
|
|
if let Some(error) = error {
|
|
event = event.with_attr("error", error);
|
|
}
|
|
|
|
event
|
|
});
|
|
}
|
|
|
|
fn apply_scanner_size_summary(into: &mut DataUsageEntry, summary: &SizeSummary) {
|
|
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 = 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 = 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));
|
|
}
|
|
|
|
into.add_tier_sizes(&summary.tier_stats);
|
|
}
|
|
|
|
fn data_usage_root_has_progress(root: &DataUsageEntry) -> bool {
|
|
!root.children.is_empty()
|
|
|| root.size > 0
|
|
|| root.objects > 0
|
|
|| root.versions > 0
|
|
|| root.delete_markers > 0
|
|
|| root.failed_objects > 0
|
|
|| root.replication_stats.is_some()
|
|
}
|
|
|
|
fn partial_cache_is_useful(root: &DataUsageEntry, pending_heals_changed: bool) -> bool {
|
|
data_usage_root_has_progress(root) || pending_heals_changed
|
|
}
|
|
|
|
/// Folder scanner for scanning directory structures
|
|
pub struct FolderScanner {
|
|
root: String,
|
|
old_cache: DataUsageCache,
|
|
new_cache: DataUsageCache,
|
|
update_cache: DataUsageCache,
|
|
|
|
data_usage_scanner_debug: bool,
|
|
heal_object_select: u32,
|
|
scan_mode: HealScanMode,
|
|
is_erasure_mode: bool,
|
|
|
|
failed_object_ttl_secs: u64,
|
|
failed_objects_max: usize,
|
|
|
|
sleeper: DynamicSleeper,
|
|
// should_heal: Arc<dyn Fn() -> bool + Send + Sync>,
|
|
disks: Vec<Arc<Disk>>,
|
|
disks_quorum: usize,
|
|
|
|
updates: Option<mpsc::Sender<DataUsageEntry>>,
|
|
last_update: SystemTime,
|
|
|
|
update_current_path: UpdateCurrentPathFn,
|
|
|
|
budget: Arc<ScannerCycleBudget>,
|
|
skip_heal: Arc<std::sync::atomic::AtomicBool>,
|
|
local_disk: Arc<Disk>,
|
|
pending_heals_changed: bool,
|
|
#[cfg(test)]
|
|
list_path_raw_options_observer: Option<mpsc::UnboundedSender<ListPathRawTimeoutSnapshot>>,
|
|
}
|
|
|
|
impl FolderScanner {
|
|
fn now_secs() -> u64 {
|
|
SystemTime::now()
|
|
.duration_since(SystemTime::UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_secs()
|
|
}
|
|
|
|
fn should_skip_failed(&self, path: &str) -> bool {
|
|
let ttl = self.failed_object_ttl_secs;
|
|
if ttl == 0 {
|
|
return false;
|
|
}
|
|
|
|
let Some(last_failed) = self.new_cache.info.failed_objects.get(path) else {
|
|
return false;
|
|
};
|
|
|
|
let now = Self::now_secs();
|
|
now.saturating_sub(*last_failed) < ttl
|
|
}
|
|
|
|
fn record_failed(&mut self, path: &str) {
|
|
let ttl = self.failed_object_ttl_secs;
|
|
if ttl == 0 {
|
|
return;
|
|
}
|
|
|
|
let now = Self::now_secs();
|
|
self.new_cache.info.failed_objects.insert(path.to_string(), now);
|
|
|
|
let max_entries = self.failed_objects_max;
|
|
if max_entries > 0 && self.new_cache.info.failed_objects.len() > max_entries {
|
|
self.prune_failed_objects(now, ttl);
|
|
}
|
|
}
|
|
|
|
fn prune_failed_objects_cache(&mut self) {
|
|
let ttl = self.failed_object_ttl_secs;
|
|
if ttl == 0 {
|
|
return;
|
|
}
|
|
|
|
let now = Self::now_secs();
|
|
self.prune_failed_objects(now, ttl);
|
|
}
|
|
|
|
fn prune_failed_objects(&mut self, now: u64, ttl: u64) {
|
|
let max_entries = self.failed_objects_max;
|
|
let failed = &mut self.new_cache.info.failed_objects;
|
|
if failed.is_empty() {
|
|
return;
|
|
}
|
|
|
|
failed.retain(|_, ts| now.saturating_sub(*ts) < ttl);
|
|
|
|
if max_entries == 0 {
|
|
return;
|
|
}
|
|
|
|
if failed.len() <= max_entries {
|
|
return;
|
|
}
|
|
|
|
let mut entries: Vec<(String, u64)> = failed.iter().map(|(k, v)| (k.clone(), *v)).collect();
|
|
entries.sort_by(|(k1, ts1), (k2, ts2)| ts1.cmp(ts2).then_with(|| k1.cmp(k2)));
|
|
|
|
let remove_count = failed.len().saturating_sub(max_entries);
|
|
for (key, _) in entries.into_iter().take(remove_count) {
|
|
failed.remove(&key);
|
|
}
|
|
}
|
|
|
|
fn record_scan_resume_hint(&mut self, folder: &str) {
|
|
self.new_cache.info.scan_resume_after = Some(folder.to_string());
|
|
self.update_cache.info.scan_resume_after = Some(folder.to_string());
|
|
let checkpoint = DataUsageScanCheckpoint::new(folder.to_string(), DataUsageScanCheckpointReason::Unknown);
|
|
global_metrics().record_scanner_checkpoint_set(
|
|
checkpoint.version,
|
|
checkpoint.resume_after.clone(),
|
|
checkpoint.reason.as_str(),
|
|
);
|
|
self.new_cache.info.scan_checkpoint = Some(checkpoint.clone());
|
|
self.update_cache.info.scan_checkpoint = Some(checkpoint);
|
|
}
|
|
|
|
fn record_scan_resume_hint_if_not_ancestor(&mut self, folder: &str) {
|
|
let keep_existing = self
|
|
.new_cache
|
|
.info
|
|
.scan_resume_after
|
|
.as_deref()
|
|
.is_some_and(|existing| matches!(folder_resume_match(folder, existing), Some(FolderResumeMatch::Descendant)));
|
|
if !keep_existing {
|
|
self.record_scan_resume_hint(folder);
|
|
}
|
|
}
|
|
|
|
fn carry_forward_old_children(&mut self, parent_hash: &DataUsageHash, entry: &mut DataUsageEntry) {
|
|
if entry.compacted {
|
|
// Compacted entries store child totals directly; child links would be flattened twice.
|
|
return;
|
|
}
|
|
|
|
let Some(old_entry) = self.old_cache.cache.get(&parent_hash.key()) else {
|
|
return;
|
|
};
|
|
|
|
let old_children = old_entry.children.iter().cloned().collect::<Vec<_>>();
|
|
for child in old_children {
|
|
if entry.children.contains(&child) {
|
|
continue;
|
|
}
|
|
if !self.old_cache.cache.contains_key(&child) {
|
|
continue;
|
|
}
|
|
|
|
let child_hash = DataUsageHash(child.clone());
|
|
self.new_cache
|
|
.copy_with_children(&self.old_cache, &child_hash, &Some(parent_hash.clone()));
|
|
entry.children.insert(child);
|
|
}
|
|
}
|
|
|
|
async fn preserve_partial_child_progress(
|
|
&mut self,
|
|
parent: &Option<DataUsageHash>,
|
|
child_hash: &DataUsageHash,
|
|
parent_entry: &mut DataUsageEntry,
|
|
child_entry: &DataUsageEntry,
|
|
) {
|
|
if data_usage_entry_has_progress(child_entry) {
|
|
let mut child_entry = child_entry.clone();
|
|
self.carry_forward_old_children(child_hash, &mut child_entry);
|
|
self.record_scan_resume_hint_if_not_ancestor(&child_hash.key());
|
|
parent_entry.add_child(child_hash);
|
|
self.new_cache.replace_hashed(child_hash, parent, &child_entry);
|
|
self.update_cache.delete_recursive(child_hash);
|
|
self.update_cache.copy_with_children(&self.new_cache, child_hash, parent);
|
|
self.send_update().await;
|
|
}
|
|
}
|
|
|
|
fn alert_excessive_folders(&self, folder: &str, total_folders: usize) {
|
|
let threshold = scanner_excess_folders_threshold();
|
|
if u64::try_from(total_folders).unwrap_or(u64::MAX) <= threshold {
|
|
return;
|
|
}
|
|
|
|
ensure_scanner_alert_metrics_registered();
|
|
global_metrics().record_scanner_source_executed(ScannerWorkSource::Alerts, 1);
|
|
counter!(
|
|
METRIC_SCANNER_EXCESS_FOLDERS_TOTAL,
|
|
"root" => self.root.clone()
|
|
)
|
|
.increment(1);
|
|
if scanner_alert_emission_allows(ScannerAlertKind::BigPrefix, &self.root, folder, scanner_alert_cooldown()) {
|
|
emit_scanner_alert_event(
|
|
EVENT_SCANNER_BIG_PREFIX,
|
|
&self.root,
|
|
folder,
|
|
0,
|
|
&[("folders", total_folders.to_string()), ("threshold", threshold.to_string())],
|
|
);
|
|
}
|
|
warn!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_ALERT_STATE,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_FOLDER,
|
|
root = %self.root,
|
|
folder,
|
|
folders = total_folders,
|
|
threshold,
|
|
state = "excess_folders",
|
|
"Scanner alert recorded excessive direct subfolders"
|
|
);
|
|
}
|
|
|
|
pub async fn should_heal(&self) -> bool {
|
|
if self.skip_heal.load(std::sync::atomic::Ordering::Relaxed) {
|
|
return false;
|
|
}
|
|
if self.heal_object_select == 0 {
|
|
return false;
|
|
}
|
|
|
|
if self
|
|
.local_disk
|
|
.disk_info(&DiskInfoOptions::default())
|
|
.await
|
|
.unwrap_or_default()
|
|
.healing
|
|
{
|
|
self.skip_heal.store(true, std::sync::atomic::Ordering::Relaxed);
|
|
return false;
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
async fn send_required_scanner_heal_request(
|
|
&mut self,
|
|
kind: PendingScannerHealKind,
|
|
bucket: String,
|
|
object: Option<String>,
|
|
version_id: Option<String>,
|
|
request: HealChannelRequest,
|
|
) -> Result<(), ScannerError> {
|
|
let candidate_type = pending_scanner_heal_candidate_type(kind);
|
|
let priority = request.priority;
|
|
let scan_mode = request.scan_mode.unwrap_or(self.scan_mode);
|
|
let result = match send_scanner_heal_request(candidate_type, request).await {
|
|
Ok(result) => result,
|
|
Err(err) => {
|
|
self.update_pending_scanner_heal_after_admission(
|
|
kind,
|
|
&bucket,
|
|
object.as_deref(),
|
|
version_id.as_deref(),
|
|
scan_mode,
|
|
HealAdmissionResult::Full,
|
|
);
|
|
error!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_HEAL_ADMISSION,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_HEAL,
|
|
candidate_type,
|
|
bucket = %bucket,
|
|
object = object.as_deref().unwrap_or(""),
|
|
priority = heal_priority_label(priority),
|
|
state = "heal_channel_error",
|
|
error = %err,
|
|
"Scanner deferred heal request after channel error"
|
|
);
|
|
return Ok(());
|
|
}
|
|
};
|
|
self.update_pending_scanner_heal_after_admission(
|
|
kind,
|
|
&bucket,
|
|
object.as_deref(),
|
|
version_id.as_deref(),
|
|
scan_mode,
|
|
result,
|
|
);
|
|
if result.is_admitted() {
|
|
return Ok(());
|
|
}
|
|
|
|
record_high_priority_heal_escalation(candidate_type, priority, result);
|
|
let admission_error =
|
|
build_high_priority_heal_admission_error(candidate_type, &bucket, object.as_deref(), priority, result);
|
|
error!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_HEAL_ADMISSION,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_HEAL,
|
|
candidate_type,
|
|
bucket = %bucket,
|
|
object = object.as_deref().unwrap_or(""),
|
|
priority = heal_priority_label(priority),
|
|
admission = result.result_label(),
|
|
reason = result.reason_label(),
|
|
error = %admission_error,
|
|
state = "high_priority_not_admitted",
|
|
"Scanner high-priority heal admission failed"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn set_heal_object_select(&mut self, prob: u32) {
|
|
self.heal_object_select = prob;
|
|
}
|
|
|
|
/// Set debug mode
|
|
pub fn set_debug(&mut self, debug: bool) {
|
|
self.data_usage_scanner_debug = debug;
|
|
}
|
|
|
|
/// Send update if enough time has passed
|
|
/// Should be called on a regular basis when the new_cache contains more recent total than previously.
|
|
/// May or may not send an update upstream.
|
|
fn should_send_update(&self) -> bool {
|
|
if self.updates.is_none() {
|
|
return false;
|
|
}
|
|
|
|
let elapsed = self.last_update.elapsed().unwrap_or(Duration::from_secs(0));
|
|
elapsed >= Duration::from_secs(60)
|
|
}
|
|
|
|
pub async fn send_update(&mut self) {
|
|
// Send at most an update every minute.
|
|
if !self.should_send_update() {
|
|
return;
|
|
}
|
|
|
|
if let Some(flat) = self.update_cache.size_recursive(&self.new_cache.info.name)
|
|
&& let Some(ref updates) = self.updates
|
|
{
|
|
// Try to send without blocking
|
|
if let Err(e) = updates.send(flat.clone()).await {
|
|
error!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_FOLDER_STATE,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_FOLDER,
|
|
root = %self.new_cache.info.name,
|
|
state = "update_send_failed",
|
|
error = %e,
|
|
"Scanner folder update send failed"
|
|
);
|
|
}
|
|
self.last_update = SystemTime::now();
|
|
}
|
|
}
|
|
|
|
async fn send_update_for_entry(&mut self, hash: &DataUsageHash, parent: &Option<DataUsageHash>, entry: &DataUsageEntry) {
|
|
if !self.should_send_update() {
|
|
return;
|
|
}
|
|
|
|
self.update_cache.replace_hashed(hash, parent, entry);
|
|
self.send_update().await;
|
|
}
|
|
|
|
/// Scan a folder recursively
|
|
/// Files found in the folders will be added to new_cache.
|
|
#[allow(clippy::never_loop)]
|
|
#[allow(unused_assignments)]
|
|
pub async fn scan_folder(
|
|
&mut self,
|
|
ctx: CancellationToken,
|
|
folder: CachedFolder,
|
|
into: &mut DataUsageEntry,
|
|
) -> Result<(), ScannerError> {
|
|
let done_folder = Metrics::time(Metric::ScanFolder);
|
|
let trace_started_at = trace_start_instant();
|
|
|
|
if ctx.is_cancelled() {
|
|
return Err(ScannerError::Other("Operation cancelled".to_string()));
|
|
}
|
|
if !self.budget.try_start_directory() {
|
|
return Err(ScannerError::Other("Operation cancelled".to_string()));
|
|
}
|
|
|
|
let this_hash = hash_path(&folder.name);
|
|
// Store initial compaction state.
|
|
let was_compacted = into.compacted;
|
|
|
|
loop {
|
|
if ctx.is_cancelled() {
|
|
return Err(ScannerError::Other("Operation cancelled".to_string()));
|
|
}
|
|
|
|
self.prune_failed_objects_cache();
|
|
|
|
let mut abandoned_children: DataUsageHashMap = HashSet::new();
|
|
if !into.compacted {
|
|
abandoned_children = self.old_cache.find_children_copy(this_hash.clone());
|
|
}
|
|
|
|
debug!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_FOLDER_STATE,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_FOLDER,
|
|
root = %self.root,
|
|
folder = %folder.name,
|
|
state = "scan_started",
|
|
"Scanner folder state updated"
|
|
);
|
|
let (_, prefix) = path2_bucket_object_with_base_path(&self.root, &folder.name);
|
|
|
|
let active_life_cycle = if self
|
|
.old_cache
|
|
.info
|
|
.lifecycle
|
|
.as_ref()
|
|
.is_some_and(|v| v.has_active_rules(&prefix))
|
|
{
|
|
self.old_cache.info.lifecycle.clone()
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let active_replication = if self
|
|
.old_cache
|
|
.info
|
|
.replication
|
|
.as_ref()
|
|
.is_some_and(|v| v.has_active_rules(&prefix, true))
|
|
{
|
|
self.old_cache.info.replication.clone()
|
|
} else {
|
|
None
|
|
};
|
|
let active_object_lock = self.old_cache.info.object_lock.clone();
|
|
|
|
self.sleeper.sleep_folder().await;
|
|
|
|
let mut existing_folders: Vec<CachedFolder> = Vec::new();
|
|
let mut new_folders: Vec<CachedFolder> = Vec::new();
|
|
let mut found_object_metadata = false;
|
|
let mut erasure_data_directory_candidates: Vec<(CachedFolder, bool, String)> = Vec::new();
|
|
let mut object_count: u64 = 0;
|
|
let yield_every_objects = scanner_yield_every_n_objects();
|
|
|
|
let dir_path = path_join_buf(&[&self.root, &folder.name]);
|
|
|
|
debug!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_FOLDER_STATE,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_FOLDER,
|
|
dir_path = ?dir_path,
|
|
state = "dir_open",
|
|
"Scanner folder state updated"
|
|
);
|
|
|
|
let mut dir_reader = match tokio::fs::read_dir(&dir_path).await {
|
|
Ok(dir_reader) => dir_reader,
|
|
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 {
|
|
Ok(Some(entry)) => entry,
|
|
Ok(None) => break,
|
|
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_during_iteration",
|
|
error = %e,
|
|
"Scanner folder state updated"
|
|
);
|
|
break;
|
|
}
|
|
Err(e) if e.kind() == ErrorKind::NotADirectory => {
|
|
warn!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_FOLDER_STATE,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_FOLDER,
|
|
dir_path = %dir_path,
|
|
state = "dir_became_non_directory",
|
|
error = %e,
|
|
"Scanner folder state updated"
|
|
);
|
|
break;
|
|
}
|
|
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;
|
|
}
|
|
let is_storage_format_entry = file_name == STORAGE_FORMAT_FILE;
|
|
|
|
let file_path = entry.path().to_string_lossy().to_string();
|
|
|
|
let trim_dir_name = file_path.strip_prefix(&dir_path).unwrap_or(&file_path);
|
|
|
|
let entry_name = path_join_buf(&[&folder.name, trim_dir_name]);
|
|
|
|
if entry_name.is_empty() || entry_name == folder.name {
|
|
continue;
|
|
}
|
|
|
|
// Ignore entries that disappeared during traversal or hit symlink
|
|
// loops, but propagate other walk errors.
|
|
let mut entry_type = match entry.file_type().await {
|
|
Ok(entry_type) => entry_type,
|
|
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,
|
|
entry = %entry_name,
|
|
state = "entry_missing_before_type_lookup",
|
|
error = %e,
|
|
"Scanner folder state updated"
|
|
);
|
|
continue;
|
|
}
|
|
Err(e) if e.kind() == ErrorKind::TooManyLinks => {
|
|
warn!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_FOLDER_STATE,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_FOLDER,
|
|
entry = %entry_name,
|
|
state = "entry_symlink_loop_before_type_lookup",
|
|
error = %e,
|
|
"Scanner folder state updated"
|
|
);
|
|
continue;
|
|
}
|
|
Err(e) => return Err(ScannerError::Io(e)),
|
|
};
|
|
|
|
// Metadata presence establishes an erasure object boundary;
|
|
// parsing failures still belong to accounting and healing. A
|
|
// directory named `xl.meta` remains a valid namespace prefix,
|
|
// and symlinks are classified after resolving their target.
|
|
if is_storage_format_entry && !entry_type.is_dir() && !entry_type.is_symlink() {
|
|
found_object_metadata = true;
|
|
}
|
|
|
|
if entry_type.is_symlink() {
|
|
let metadata = match tokio::fs::metadata(&file_path).await {
|
|
Ok(metadata) => metadata,
|
|
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,
|
|
file_path = %file_path,
|
|
state = "symlink_target_missing_before_metadata",
|
|
error = %e,
|
|
"Scanner folder state updated"
|
|
);
|
|
continue;
|
|
}
|
|
Err(e) if e.kind() == ErrorKind::TooManyLinks => {
|
|
warn!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_FOLDER_STATE,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_FOLDER,
|
|
file_path = %file_path,
|
|
state = "symlink_target_loop_before_metadata",
|
|
error = %e,
|
|
"Scanner folder state updated"
|
|
);
|
|
continue;
|
|
}
|
|
Err(e) => return Err(ScannerError::Io(e)),
|
|
};
|
|
|
|
if metadata.is_dir() {
|
|
warn!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_FOLDER_STATE,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_FOLDER,
|
|
file_path = %file_path,
|
|
state = "symlink_directory_ignored",
|
|
"Scanner folder state updated"
|
|
);
|
|
continue;
|
|
}
|
|
|
|
entry_type = metadata.file_type();
|
|
if is_storage_format_entry {
|
|
found_object_metadata = true;
|
|
}
|
|
}
|
|
|
|
// ok
|
|
|
|
let (bucket, prefix) = path2_bucket_object_with_base_path(self.root.as_str(), &entry_name);
|
|
if bucket.is_empty() {
|
|
break;
|
|
}
|
|
|
|
if is_reserved_or_invalid_bucket(&bucket, false) {
|
|
break;
|
|
}
|
|
|
|
if ctx.is_cancelled() {
|
|
break;
|
|
}
|
|
|
|
if entry_type.is_dir() {
|
|
let h = hash_path(&entry_name);
|
|
|
|
if h == this_hash {
|
|
continue;
|
|
}
|
|
|
|
let exists = self.old_cache.cache.contains_key(&h.key());
|
|
|
|
let this = CachedFolder {
|
|
name: entry_name.clone(),
|
|
parent: Some(this_hash.clone()),
|
|
object_heal_prob_div: folder.object_heal_prob_div,
|
|
};
|
|
|
|
if self.is_erasure_mode && uuid::Uuid::parse_str(&file_name).is_ok_and(|data_dir_id| !data_dir_id.is_nil()) {
|
|
erasure_data_directory_candidates.push((this, exists, file_path));
|
|
continue;
|
|
}
|
|
|
|
abandoned_children.remove(&h.key());
|
|
|
|
if exists {
|
|
existing_folders.push(this);
|
|
self.update_cache
|
|
.copy_with_children(&self.old_cache, &h, &Some(this_hash.clone()));
|
|
} else {
|
|
new_folders.push(this);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
let timer = self.sleeper.timer();
|
|
|
|
let heal_enabled = this_hash.mod_alt(
|
|
self.old_cache.info.next_cycle as u32 / folder.object_heal_prob_div,
|
|
self.heal_object_select / folder.object_heal_prob_div,
|
|
) && self.should_heal().await;
|
|
|
|
let mut item = ScannerItem {
|
|
path: file_path,
|
|
bucket,
|
|
prefix: rustfs_utils::path::dir(&prefix),
|
|
object_name: file_name,
|
|
lifecycle: active_life_cycle.clone(),
|
|
object_lock: active_object_lock.clone(),
|
|
replication: active_replication.clone(),
|
|
heal_enabled,
|
|
heal_bitrot: self.scan_mode == HealScanMode::Deep,
|
|
debug: self.data_usage_scanner_debug,
|
|
file_type: entry_type,
|
|
};
|
|
|
|
// If this path is already known as failed, just skip it.
|
|
// We intentionally do NOT call `record_failed` or bump `failed_objects` here,
|
|
// because the failure was recorded when the original error occurred
|
|
// (e.g. in the get_size error branch below). This branch only accounts
|
|
// for subsequent skips of already-failed paths.
|
|
if self.should_skip_failed(&item.path) {
|
|
continue;
|
|
}
|
|
|
|
let sz = match self.local_disk.get_size(item.clone()).await {
|
|
Ok(sz) => sz,
|
|
Err(e) => {
|
|
let failure_action = classify_get_size_failure(&item, &e);
|
|
|
|
if failure_action != GetSizeFailureAction::Skip {
|
|
// Track failed objects to prevent infinite retry loops
|
|
into.failed_objects += 1;
|
|
self.record_failed(&item.path);
|
|
|
|
if should_log_failed_object(into.failed_objects) {
|
|
if let GetSizeFailureAction::HealMetadata { object } = &failure_action {
|
|
error!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_METADATA_CORRUPT,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_FOLDER,
|
|
drive = %self.local_disk.path().display(),
|
|
bucket = %item.bucket,
|
|
object = %object,
|
|
metadata_path = %item.path,
|
|
failed_objects = into.failed_objects,
|
|
state = "metadata_corrupt",
|
|
error = %e,
|
|
"Scanner detected corrupt object metadata"
|
|
);
|
|
} else {
|
|
warn!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_FOLDER_STATE,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_FOLDER,
|
|
path = %item.path,
|
|
failed_objects = into.failed_objects,
|
|
state = "get_size_failed",
|
|
error = %e,
|
|
"Scanner folder failed to get object size"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if let GetSizeFailureAction::HealMetadata { object } = failure_action {
|
|
// Single-flight (backlog#1894 axis A) — the
|
|
// recording mode and its guarantees are pinned by
|
|
// corrupt_metadata_recording below.
|
|
let mrf_accepted = rustfs_common::mrf_channel::try_send_mrf_intent(
|
|
rustfs_common::mrf_channel::MrfKind::MetadataCorruption,
|
|
&item.bucket,
|
|
&object,
|
|
None,
|
|
);
|
|
match corrupt_metadata_recording(mrf_accepted) {
|
|
CorruptMetadataRecording::LedgerOnly => {
|
|
// Recorded as Full (retry-later): admission
|
|
// for this target happens in the MRF
|
|
// consumer, not in the manager's queue here.
|
|
self.update_pending_scanner_heal_after_admission(
|
|
PendingScannerHealKind::Object,
|
|
&item.bucket,
|
|
Some(&object),
|
|
None,
|
|
self.scan_mode,
|
|
HealAdmissionResult::Full,
|
|
);
|
|
}
|
|
CorruptMetadataRecording::ImmediateAndLedger => {
|
|
self.send_required_scanner_heal_request(
|
|
PendingScannerHealKind::Object,
|
|
item.bucket.clone(),
|
|
Some(object.clone()),
|
|
None,
|
|
build_object_heal_request(
|
|
item.bucket.clone(),
|
|
object.clone(),
|
|
None,
|
|
self.scan_mode,
|
|
HealChannelPriority::High,
|
|
),
|
|
)
|
|
.await?;
|
|
}
|
|
}
|
|
}
|
|
|
|
timer.sleep().await;
|
|
continue;
|
|
}
|
|
};
|
|
|
|
found_object_metadata = true;
|
|
|
|
item.transform_meta_dir();
|
|
|
|
abandoned_children.remove(&path_join_buf(&[&item.bucket, &item.object_path()]));
|
|
|
|
apply_scanner_size_summary(into, &sz);
|
|
into.objects += 1;
|
|
object_count += 1;
|
|
self.budget.record_object_scanned();
|
|
|
|
timer.sleep().await;
|
|
|
|
if ctx.is_cancelled() {
|
|
return Err(ScannerError::Other("Operation cancelled".to_string()));
|
|
}
|
|
|
|
if should_yield_after_object(object_count, yield_every_objects) {
|
|
self.send_update_for_entry(&this_hash, &folder.parent, into).await;
|
|
let yield_start = Instant::now();
|
|
tokio::task::yield_now().await;
|
|
global_metrics().record_scanner_yield(yield_start.elapsed());
|
|
}
|
|
}
|
|
self.budget.record_entries_visited(pending_entry_progress);
|
|
|
|
let mut found_erasure_data_directory = false;
|
|
if self.is_erasure_mode && !found_object_metadata {
|
|
for (_, _, path) in &erasure_data_directory_candidates {
|
|
if contains_erasure_part_file(path).await? {
|
|
found_erasure_data_directory = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if !found_object_metadata && !found_erasure_data_directory {
|
|
for (candidate, exists, _) in erasure_data_directory_candidates {
|
|
let h = hash_path(&candidate.name);
|
|
abandoned_children.remove(&h.key());
|
|
if exists {
|
|
self.update_cache.copy_with_children(&self.old_cache, &h, &candidate.parent);
|
|
existing_folders.push(candidate);
|
|
} else {
|
|
new_folders.push(candidate);
|
|
}
|
|
}
|
|
}
|
|
|
|
if self.is_erasure_mode && found_erasure_data_directory && !found_object_metadata {
|
|
found_object_metadata = true;
|
|
let metadata_path = path_join_buf(&[&dir_path, STORAGE_FORMAT_FILE]);
|
|
|
|
if !self.should_skip_failed(&metadata_path) {
|
|
into.failed_objects = into.failed_objects.saturating_add(1);
|
|
self.record_failed(&metadata_path);
|
|
|
|
let failed_cache_entries = self.new_cache.info.failed_objects.len();
|
|
if failed_cache_entries > 0 && should_log_failed_object(failed_cache_entries) {
|
|
warn!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_FOLDER_STATE,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_FOLDER,
|
|
path = %metadata_path,
|
|
failed_objects = failed_cache_entries,
|
|
state = "object_metadata_missing",
|
|
"Scanner found erasure object data without metadata"
|
|
);
|
|
}
|
|
|
|
let (bucket, object) = path2_bucket_object_with_base_path(&self.root, &folder.name);
|
|
if !bucket.is_empty() && !object.is_empty() {
|
|
self.send_required_scanner_heal_request(
|
|
PendingScannerHealKind::Object,
|
|
bucket.clone(),
|
|
Some(object.clone()),
|
|
None,
|
|
build_object_heal_request(bucket, object, None, self.scan_mode, HealChannelPriority::High),
|
|
)
|
|
.await?;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ctx.is_cancelled() {
|
|
return Err(ScannerError::Other("Operation cancelled".to_string()));
|
|
}
|
|
|
|
if found_object_metadata && self.is_erasure_mode {
|
|
// If we found an object in erasure mode, we skip subdirs (only datadirs)...
|
|
debug!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_FOLDER_STATE,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_FOLDER,
|
|
folder = %folder.name,
|
|
state = "erasure_object_found",
|
|
"Scanner folder descent stopped after erasure object"
|
|
);
|
|
break;
|
|
}
|
|
|
|
// If we have many subfolders, compact ourself.
|
|
let should_compact = (self.new_cache.info.name != folder.name
|
|
&& existing_folders.len() + new_folders.len() >= DATA_SCANNER_COMPACT_AT_FOLDERS)
|
|
|| existing_folders.len() + new_folders.len() >= DATA_SCANNER_FORCE_COMPACT_AT_FOLDERS;
|
|
|
|
let total_folders = existing_folders.len() + new_folders.len();
|
|
self.alert_excessive_folders(&folder.name, total_folders);
|
|
|
|
if !into.compacted && should_compact {
|
|
into.compacted = true;
|
|
new_folders.append(&mut existing_folders);
|
|
|
|
existing_folders.clear();
|
|
|
|
if self.data_usage_scanner_debug {
|
|
debug!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_FOLDER_STATE,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_FOLDER,
|
|
folder = %folder.name,
|
|
entry_count = new_folders.len(),
|
|
state = "preemptive_compaction",
|
|
"Scanner folder switched to compacted mode"
|
|
);
|
|
}
|
|
}
|
|
|
|
if !into.compacted {
|
|
for folder_item in &existing_folders {
|
|
let h = hash_path(&folder_item.name);
|
|
self.update_cache.copy_with_children(&self.old_cache, &h, &folder_item.parent);
|
|
}
|
|
}
|
|
|
|
let is_scan_root = folder.name == self.old_cache.info.name;
|
|
let scan_checkpoint = self.old_cache.info.scan_checkpoint.as_ref();
|
|
let checkpoint_resume_after = scan_checkpoint.and_then(|checkpoint| {
|
|
if is_scan_root {
|
|
global_metrics().record_scanner_checkpoint_set(
|
|
checkpoint.version,
|
|
checkpoint.resume_after.clone(),
|
|
checkpoint.reason.as_str(),
|
|
);
|
|
}
|
|
if checkpoint.version != DATA_USAGE_SCAN_CHECKPOINT_VERSION || checkpoint.resume_after.is_empty() {
|
|
if is_scan_root {
|
|
global_metrics().record_scanner_checkpoint_ignored();
|
|
}
|
|
None
|
|
} else {
|
|
Some(checkpoint.resume_after.as_str())
|
|
}
|
|
});
|
|
let checkpoint_tracks_child_order = checkpoint_resume_after
|
|
.and_then(|resume_after| folder_resume_match(&folder.name, resume_after))
|
|
.is_some_and(|resume_match| matches!(resume_match, FolderResumeMatch::Descendant));
|
|
let scan_resume_after = checkpoint_resume_after.or(self.old_cache.info.scan_resume_after.as_deref());
|
|
let mut queued_folders = Vec::with_capacity(new_folders.len() + existing_folders.len());
|
|
queued_folders.extend(new_folders.into_iter().map(|folder| QueuedFolder {
|
|
folder,
|
|
source: FolderScanSource::New,
|
|
}));
|
|
queued_folders.extend(existing_folders.into_iter().map(|folder| QueuedFolder {
|
|
folder,
|
|
source: FolderScanSource::Existing,
|
|
}));
|
|
let has_queued_folders = !queued_folders.is_empty();
|
|
let resume_order = order_queued_folders_for_resume(&mut queued_folders, scan_resume_after);
|
|
if checkpoint_tracks_child_order && has_queued_folders {
|
|
match resume_order {
|
|
FolderResumeOrder::Used => global_metrics().record_scanner_checkpoint_used(),
|
|
FolderResumeOrder::Stale => global_metrics().record_scanner_checkpoint_stale(),
|
|
FolderResumeOrder::NoHint => {}
|
|
}
|
|
}
|
|
|
|
// Scan child folders in the combined resume order.
|
|
for queued_folder in queued_folders {
|
|
if ctx.is_cancelled() {
|
|
return Err(ScannerError::Other("Operation cancelled".to_string()));
|
|
}
|
|
|
|
let mut folder_item = queued_folder.folder;
|
|
let h = hash_path(&folder_item.name);
|
|
|
|
match queued_folder.source {
|
|
FolderScanSource::New => {
|
|
// Add new folders to the update tree so totals update for these.
|
|
if !into.compacted {
|
|
let mut found_any = false;
|
|
let mut parent = this_hash.clone();
|
|
let update_cache_name_hash = hash_path(&self.update_cache.info.name);
|
|
|
|
while parent != update_cache_name_hash {
|
|
let parent_key = parent.key();
|
|
let e = self.update_cache.find(&parent_key);
|
|
if e.is_none_or(|v| v.compacted) {
|
|
found_any = true;
|
|
break;
|
|
}
|
|
if let Some(next) = self.update_cache.search_parent(&parent) {
|
|
parent = next;
|
|
} else {
|
|
found_any = true;
|
|
break;
|
|
}
|
|
}
|
|
if !found_any {
|
|
// Add non-compacted empty entry.
|
|
self.update_cache
|
|
.replace_hashed(&h, &Some(this_hash.clone()), &DataUsageEntry::default());
|
|
}
|
|
}
|
|
}
|
|
FolderScanSource::Existing => {
|
|
if !into.compacted && self.old_cache.is_compacted(&h) {
|
|
let next_cycle = self.old_cache.info.next_cycle as u32;
|
|
if !h.mod_(next_cycle, data_usage_update_dir_cycles()) {
|
|
// Transfer and add as child...
|
|
self.new_cache.copy_with_children(&self.old_cache, &h, &folder_item.parent);
|
|
into.add_child(&h);
|
|
self.record_scan_resume_hint(&folder_item.name);
|
|
continue;
|
|
}
|
|
|
|
folder_item.object_heal_prob_div = data_usage_update_dir_cycles();
|
|
}
|
|
}
|
|
}
|
|
|
|
(self.update_current_path)(&folder_item.name).await;
|
|
|
|
if into.compacted {
|
|
// In compacted mode child totals are accumulated directly into the parent entry.
|
|
let fut = Box::pin(self.scan_folder(ctx.clone(), folder_item.clone(), into));
|
|
fut.await.map_err(|e| ScannerError::Other(e.to_string()))?;
|
|
self.record_scan_resume_hint(&folder_item.name);
|
|
self.send_update_for_entry(&this_hash, &folder.parent, into).await;
|
|
tokio::task::yield_now().await;
|
|
} else {
|
|
let mut dst = DataUsageEntry::default();
|
|
|
|
// Use Box::pin for recursive async call
|
|
let fut = Box::pin(self.scan_folder(ctx.clone(), folder_item.clone(), &mut dst));
|
|
if let Err(e) = fut.await {
|
|
if ctx.is_cancelled() {
|
|
self.preserve_partial_child_progress(&folder_item.parent, &h, into, &dst)
|
|
.await;
|
|
return Err(e);
|
|
}
|
|
warn!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_FOLDER_STATE,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_FOLDER,
|
|
folder = %folder.name,
|
|
child_folder = %folder_item.name,
|
|
state = "child_scan_failed",
|
|
error = %e,
|
|
"Scanner child folder scan failed"
|
|
);
|
|
continue;
|
|
}
|
|
tokio::task::yield_now().await;
|
|
|
|
into.add_child(&h);
|
|
self.record_scan_resume_hint(&folder_item.name);
|
|
// We scanned a folder, optionally send update.
|
|
self.update_cache.delete_recursive(&h);
|
|
self.update_cache.copy_with_children(&self.new_cache, &h, &folder_item.parent);
|
|
self.send_update().await;
|
|
}
|
|
|
|
if queued_folder.source == FolderScanSource::New
|
|
&& !into.compacted
|
|
&& self.update_cache.find(&this_hash.key()).is_some_and(|v| !v.compacted)
|
|
{
|
|
self.update_cache.delete_recursive(&h);
|
|
self.update_cache
|
|
.copy_with_children(&self.new_cache, &h, &Some(this_hash.clone()));
|
|
}
|
|
}
|
|
|
|
// Scan for healing
|
|
if abandoned_children.is_empty() || !self.should_heal().await {
|
|
debug!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_FOLDER_STATE,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_FOLDER,
|
|
folder = %folder.name,
|
|
state = "heal_skip_no_abandoned_children",
|
|
"Scanner folder skipped heal scan for abandoned children"
|
|
);
|
|
// If we are not heal scanning, return now.
|
|
break;
|
|
}
|
|
|
|
if self.disks.is_empty() || self.disks_quorum == 0 {
|
|
debug!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_FOLDER_STATE,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_FOLDER,
|
|
folder = %folder.name,
|
|
disks = self.disks.len(),
|
|
quorum = self.disks_quorum,
|
|
state = "heal_skip_no_quorum",
|
|
"Scanner folder skipped heal scan because quorum is unavailable"
|
|
);
|
|
break;
|
|
}
|
|
|
|
let mut resolver = MetadataResolutionParams {
|
|
dir_quorum: self.disks_quorum,
|
|
obj_quorum: self.disks_quorum,
|
|
bucket: "".to_string(),
|
|
strict: false,
|
|
..Default::default()
|
|
};
|
|
|
|
for name in abandoned_children {
|
|
if !self.should_heal().await {
|
|
break;
|
|
}
|
|
|
|
let (bucket, prefix) = path2_bucket_object(name.as_str());
|
|
|
|
if bucket != resolver.bucket {
|
|
self.send_required_scanner_heal_request(
|
|
PendingScannerHealKind::Bucket,
|
|
bucket.clone(),
|
|
None,
|
|
None,
|
|
build_bucket_heal_request(bucket.clone(), HealChannelPriority::High),
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
resolver.bucket = bucket.clone();
|
|
|
|
let child_ctx = ctx.child_token();
|
|
|
|
let (agreed_tx, mut agreed_rx) = mpsc::channel::<String>(1);
|
|
let (partial_tx, mut partial_rx) = mpsc::channel::<MetaCacheEntries>(1);
|
|
let (finished_tx, mut finished_rx) = mpsc::channel::<Vec<Option<DiskError>>>(1);
|
|
|
|
let disks = self.disks.iter().cloned().map(Some).collect();
|
|
let disks_quorum = self.disks_quorum;
|
|
let bucket_clone = bucket.clone();
|
|
let prefix_clone = prefix.clone();
|
|
let child_ctx_clone = child_ctx.clone();
|
|
#[cfg(test)]
|
|
let list_path_raw_options_observer = self.list_path_raw_options_observer.clone();
|
|
|
|
tokio::spawn(async move {
|
|
let options = ListPathRawOptions {
|
|
disks,
|
|
bucket: bucket_clone.clone(),
|
|
path: prefix_clone.clone(),
|
|
recursive: true,
|
|
report_not_found: true,
|
|
min_disks: disks_quorum,
|
|
agreed: Some(Box::new(move |entry: MetaCacheEntry| {
|
|
let entry_name = entry.name.clone();
|
|
let agreed_tx = agreed_tx.clone();
|
|
Box::pin(async move {
|
|
if let Err(e) = agreed_tx.send(entry_name).await {
|
|
error!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_FOLDER_STATE,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_FOLDER,
|
|
entry = %entry.name,
|
|
state = "list_path_agreed_send_failed",
|
|
error = %e,
|
|
"Scanner list_path_raw agreed callback failed"
|
|
);
|
|
}
|
|
})
|
|
})),
|
|
partial: Some(Box::new(move |entries: MetaCacheEntries, _: &[Option<DiskError>]| {
|
|
let partial_tx = partial_tx.clone();
|
|
Box::pin(async move {
|
|
if let Err(e) = partial_tx.send(entries).await {
|
|
error!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_FOLDER_STATE,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_FOLDER,
|
|
state = "list_path_partial_send_failed",
|
|
error = %e,
|
|
"Scanner list_path_raw partial callback failed"
|
|
);
|
|
}
|
|
})
|
|
})),
|
|
finished: Some(Box::new(move |errs: &[Option<DiskError>]| {
|
|
let finished_tx = finished_tx.clone();
|
|
let errs_clone = errs.to_vec();
|
|
Box::pin(async move {
|
|
if let Err(e) = finished_tx.send(errs_clone).await {
|
|
error!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_FOLDER_STATE,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_FOLDER,
|
|
state = "list_path_finished_send_failed",
|
|
error = %e,
|
|
"Scanner list_path_raw finished callback failed"
|
|
);
|
|
}
|
|
})
|
|
})),
|
|
..scanner_abandoned_child_list_options()
|
|
};
|
|
#[cfg(test)]
|
|
if let Some(observer) = list_path_raw_options_observer {
|
|
let _ = observer.send((
|
|
options.skip_walkdir_total_timeout,
|
|
options.walkdir_timeout,
|
|
options.walkdir_stall_timeout,
|
|
));
|
|
}
|
|
if let Err(e) = list_path_raw(child_ctx_clone.clone(), options).await {
|
|
if is_missing_path_disk_error(&e) {
|
|
debug!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_FOLDER_STATE,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_FOLDER,
|
|
bucket = %bucket_clone,
|
|
prefix = %prefix_clone,
|
|
state = "list_path_missing",
|
|
error = %e,
|
|
"Scanner list_path_raw missing path skipped"
|
|
);
|
|
} else {
|
|
error!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_FOLDER_STATE,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_FOLDER,
|
|
bucket = %bucket_clone,
|
|
prefix = %prefix_clone,
|
|
state = "list_path_failed",
|
|
error = %e,
|
|
"Scanner list_path_raw failed"
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
let mut found_objects = false;
|
|
let mut agreed_closed = false;
|
|
let mut partial_closed = false;
|
|
let mut finished_closed = false;
|
|
|
|
loop {
|
|
if agreed_closed && partial_closed && finished_closed {
|
|
break;
|
|
}
|
|
|
|
select! {
|
|
entry_name = agreed_rx.recv(), if !agreed_closed => {
|
|
let Some(entry_name) = entry_name else {
|
|
agreed_closed = true;
|
|
continue;
|
|
};
|
|
(self.update_current_path)(&entry_name).await;
|
|
}
|
|
entries = partial_rx.recv(), if !partial_closed => {
|
|
let Some(entries) = entries else {
|
|
partial_closed = true;
|
|
continue;
|
|
};
|
|
if !self.should_heal().await {
|
|
child_ctx.cancel();
|
|
break;
|
|
}
|
|
|
|
let Some(entry) = resolve_object_heal_entry(&entries, resolver.clone()) else {
|
|
continue;
|
|
};
|
|
|
|
(self.update_current_path)(&entry.name).await;
|
|
|
|
if entry.is_dir() {
|
|
continue;
|
|
}
|
|
|
|
let fivs = match entry.file_info_versions(&bucket) {
|
|
Ok(fivs) => fivs,
|
|
Err(e) => {
|
|
error!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_FOLDER_STATE,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_FOLDER,
|
|
bucket = %bucket,
|
|
entry = %entry.name,
|
|
state = "file_info_versions_failed",
|
|
error = %e,
|
|
"Scanner list_path_raw failed to resolve file versions"
|
|
);
|
|
self.send_required_scanner_heal_request(
|
|
PendingScannerHealKind::Object,
|
|
bucket.clone(),
|
|
Some(entry.name.clone()),
|
|
None,
|
|
build_object_heal_request(
|
|
bucket.clone(),
|
|
entry.name.clone(),
|
|
None,
|
|
self.scan_mode,
|
|
HealChannelPriority::High,
|
|
),
|
|
)
|
|
.await?;
|
|
found_objects = true;
|
|
continue;
|
|
}
|
|
};
|
|
|
|
for fiv in fivs.versions {
|
|
let version_id = fiv.version_id.and_then(|v| if v.is_nil() { None } else { Some(v.to_string()) });
|
|
self.send_required_scanner_heal_request(
|
|
PendingScannerHealKind::Object,
|
|
bucket.clone(),
|
|
Some(entry.name.clone()),
|
|
version_id.clone(),
|
|
build_object_heal_request(
|
|
bucket.clone(),
|
|
entry.name.clone(),
|
|
version_id,
|
|
self.scan_mode,
|
|
HealChannelPriority::High,
|
|
),
|
|
)
|
|
.await?;
|
|
found_objects = true;
|
|
}
|
|
|
|
|
|
}
|
|
errs = finished_rx.recv(), if !finished_closed => {
|
|
let Some(errs) = errs else {
|
|
finished_closed = true;
|
|
continue;
|
|
};
|
|
if disk_errors_are_only_missing_paths(&errs) {
|
|
debug!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_FOLDER_STATE,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_FOLDER,
|
|
state = "list_path_finished_missing_paths",
|
|
errors = ?errs,
|
|
"Scanner list_path_raw finished with missing paths"
|
|
);
|
|
} else {
|
|
error!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_FOLDER_STATE,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_FOLDER,
|
|
state = "list_path_finished_with_errors",
|
|
errors = ?errs,
|
|
"Scanner list_path_raw finished with disk errors"
|
|
);
|
|
}
|
|
child_ctx.cancel();
|
|
}
|
|
_ = child_ctx.cancelled() => {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if found_objects {
|
|
let folder_item = CachedFolder {
|
|
name: name.clone(),
|
|
parent: Some(this_hash.clone()),
|
|
object_heal_prob_div: 1,
|
|
};
|
|
|
|
if into.compacted {
|
|
// In compacted mode child totals are accumulated directly into the parent entry.
|
|
let fut = Box::pin(self.scan_folder(ctx.clone(), folder_item.clone(), into));
|
|
fut.await.map_err(|e| ScannerError::Other(e.to_string()))?;
|
|
self.send_update_for_entry(&this_hash, &folder.parent, into).await;
|
|
tokio::task::yield_now().await;
|
|
} else {
|
|
let mut dst = DataUsageEntry::default();
|
|
let h = hash_path(&folder_item.name);
|
|
|
|
// Use Box::pin for recursive async call
|
|
let fut = Box::pin(self.scan_folder(ctx.clone(), folder_item.clone(), &mut dst));
|
|
if let Err(e) = fut.await {
|
|
if ctx.is_cancelled() {
|
|
self.preserve_partial_child_progress(&folder_item.parent, &h, into, &dst)
|
|
.await;
|
|
return Err(e);
|
|
}
|
|
warn!(
|
|
target: "rustfs::scanner::folder",
|
|
event = EVENT_SCANNER_FOLDER_STATE,
|
|
component = LOG_COMPONENT_SCANNER,
|
|
subsystem = LOG_SUBSYSTEM_FOLDER,
|
|
folder = %folder.name,
|
|
child_folder = %folder_item.name,
|
|
state = "heal_child_scan_failed",
|
|
error = %e,
|
|
"Scanner heal child folder scan failed"
|
|
);
|
|
continue;
|
|
}
|
|
tokio::task::yield_now().await;
|
|
|
|
into.add_child(&h);
|
|
// We scanned a folder, optionally send update.
|
|
self.update_cache.delete_recursive(&h);
|
|
self.update_cache.copy_with_children(&self.new_cache, &h, &folder_item.parent);
|
|
self.send_update().await;
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
if !was_compacted {
|
|
self.new_cache.replace_hashed(&this_hash, &folder.parent, into);
|
|
}
|
|
|
|
if !into.compacted
|
|
&& self.new_cache.info.name != folder.name
|
|
&& let Some(mut flat) = self.new_cache.size_recursive(&this_hash.key())
|
|
{
|
|
flat.compacted = true;
|
|
let mut should_compact = false;
|
|
|
|
if flat.objects < DATA_SCANNER_COMPACT_LEAST_OBJECT {
|
|
should_compact = true;
|
|
} else {
|
|
// Compact if we only have objects as children...
|
|
should_compact = true;
|
|
for k in &into.children {
|
|
if let Some(v) = self.new_cache.cache.get(k)
|
|
&& (!v.children.is_empty() || v.objects > 1)
|
|
{
|
|
should_compact = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if should_compact {
|
|
self.new_cache.delete_recursive(&this_hash);
|
|
self.new_cache.replace_hashed(&this_hash, &folder.parent, &flat);
|
|
}
|
|
}
|
|
|
|
// Compact if too many children...
|
|
if !into.compacted {
|
|
let done_compact = Metrics::time(Metric::CompactFolder);
|
|
self.new_cache.reduce_children_of(
|
|
&this_hash,
|
|
DATA_SCANNER_COMPACT_AT_CHILDREN,
|
|
self.new_cache.info.name != folder.name,
|
|
);
|
|
done_compact();
|
|
}
|
|
|
|
if self.update_cache.cache.contains_key(&this_hash.key()) && !was_compacted {
|
|
// Replace if existed before.
|
|
if let Some(flat) = self.new_cache.size_recursive(&this_hash.key()) {
|
|
self.update_cache.delete_recursive(&this_hash);
|
|
self.update_cache.replace_hashed(&this_hash, &folder.parent, &flat);
|
|
}
|
|
}
|
|
|
|
done_folder();
|
|
let scanned_objects = u64::try_from(into.objects).unwrap_or(u64::MAX);
|
|
emit_scanner_folder_trace(&self.root, &folder.name, scanned_objects, trace_started_at, "completed");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn as_mut_new_cache(&mut self) -> &mut DataUsageCache {
|
|
&mut self.new_cache
|
|
}
|
|
}
|
|
|
|
/// Scan a data folder
|
|
/// This function scans the basepath+cache.info.name and returns an updated cache.
|
|
/// The returned cache will always be valid, but may not be updated from the existing.
|
|
/// Throttling between operations is controlled by the provided [`DynamicSleeper`].
|
|
/// If the supplied context is canceled the function will return at the first chance.
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn scan_data_folder(
|
|
ctx: CancellationToken,
|
|
budget: Arc<ScannerCycleBudget>,
|
|
disks: Vec<Arc<Disk>>,
|
|
local_disk: Arc<Disk>,
|
|
cache: DataUsageCache,
|
|
updates: Option<mpsc::Sender<DataUsageEntry>>,
|
|
scan_mode: HealScanMode,
|
|
sleeper: DynamicSleeper,
|
|
) -> Result<DataUsageCache, ScannerError> {
|
|
use crate::data_usage_define::DATA_USAGE_ROOT;
|
|
|
|
// Check that we're not trying to scan the root
|
|
if cache.info.name.is_empty() || cache.info.name == DATA_USAGE_ROOT {
|
|
return Err(ScannerError::Other("internal error: root scan attempted".to_string()));
|
|
}
|
|
|
|
// 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).await;
|
|
let mut close_disk_guard = CloseDiskGuard::new(close_disk);
|
|
|
|
// Create skip_heal flag
|
|
let is_erasure_mode = scanner_is_erasure().await;
|
|
let skip_heal = Arc::new(std::sync::atomic::AtomicBool::new(!is_erasure_mode || cache.info.skip_healing));
|
|
|
|
// Create heal_object_select flag
|
|
let heal_object_select = if is_erasure_mode && !cache.info.skip_healing {
|
|
heal_object_select_prob()
|
|
} else {
|
|
0
|
|
};
|
|
|
|
let disks_quorum = disks.len() / 2;
|
|
|
|
let failed_object_ttl = rustfs_utils::get_env_u32(ENV_FAILED_OBJECT_TTL_SECS, DEFAULT_FAILED_OBJECT_TTL_SECS) as u64;
|
|
let failed_objects_max = rustfs_utils::get_env_u32(ENV_FAILED_OBJECTS_MAX, DEFAULT_FAILED_OBJECTS_MAX) as usize;
|
|
|
|
// Create folder scanner
|
|
let mut scanner = FolderScanner {
|
|
root: base_path,
|
|
old_cache: cache.clone(),
|
|
new_cache: DataUsageCache {
|
|
info: cache.info.clone(),
|
|
..Default::default()
|
|
},
|
|
update_cache: DataUsageCache {
|
|
info: cache.info.clone(),
|
|
..Default::default()
|
|
},
|
|
data_usage_scanner_debug: false,
|
|
heal_object_select,
|
|
scan_mode,
|
|
is_erasure_mode,
|
|
failed_object_ttl_secs: failed_object_ttl,
|
|
failed_objects_max,
|
|
sleeper,
|
|
disks,
|
|
disks_quorum,
|
|
updates,
|
|
last_update: SystemTime::UNIX_EPOCH,
|
|
update_current_path,
|
|
budget: budget.clone(),
|
|
skip_heal,
|
|
local_disk,
|
|
pending_heals_changed: false,
|
|
#[cfg(test)]
|
|
list_path_raw_options_observer: None,
|
|
};
|
|
|
|
// Check if context is cancelled
|
|
if ctx.is_cancelled() {
|
|
return Err(ScannerError::Other("Operation cancelled".to_string()));
|
|
}
|
|
|
|
scanner.retry_pending_scanner_heals().await?;
|
|
|
|
// Read top level in bucket
|
|
let mut root = DataUsageEntry::default();
|
|
let folder = CachedFolder {
|
|
name: cache.info.name.clone(),
|
|
parent: None,
|
|
object_heal_prob_div: 1,
|
|
};
|
|
|
|
// Scan the folder
|
|
match scanner.scan_folder(ctx.clone(), folder, &mut root).await {
|
|
Ok(()) => {
|
|
// Get the new cache and finalize it
|
|
let new_cache = scanner.as_mut_new_cache();
|
|
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;
|
|
if had_scan_checkpoint {
|
|
global_metrics().record_scanner_checkpoint_cleared();
|
|
}
|
|
|
|
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() {
|
|
let root_hash = hash_path(&cache.info.name);
|
|
let root_has_progress = data_usage_root_has_progress(&root);
|
|
let pending_heals_changed = scanner.pending_heals_changed;
|
|
if root_has_progress {
|
|
scanner.carry_forward_old_children(&root_hash, &mut root);
|
|
}
|
|
let new_cache = scanner.as_mut_new_cache();
|
|
if root_has_progress {
|
|
new_cache.replace_hashed(&root_hash, &None, &root);
|
|
}
|
|
if partial_cache_is_useful(&root, pending_heals_changed) {
|
|
if new_cache.root().is_some() {
|
|
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;
|
|
new_cache.info.snapshot_complete = false;
|
|
if root_has_progress {
|
|
set_scan_checkpoint(new_cache, checkpoint_reason_from_budget(budget.reason()));
|
|
}
|
|
close_disk_guard.close().await;
|
|
return Err(ScannerError::PartialCache(Box::new(new_cache.clone())));
|
|
}
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
mod item_actions;
|
|
mod ledger;
|
|
|
|
use item_actions::*;
|
|
pub use item_actions::{GetSizeFn, ScannerItem};
|
|
use ledger::*;
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|