// 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}, path::{Path, PathBuf}, sync::{ Arc, atomic::{AtomicU8, AtomicU64, AtomicUsize, Ordering}, }, time::{Duration, SystemTime}, }; use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; use tokio_util::sync::CancellationToken; use tracing::{debug, error, info, warn}; use rustfs_common::data_usage::DataUsageInfo; use rustfs_ecstore::StorageAPI; use rustfs_ecstore::disk::{DiskAPI, DiskStore}; // Add this import use super::checkpoint::CheckpointManager; use super::io_monitor::{AdvancedIOMonitor, IOMonitorConfig}; use super::io_throttler::{AdvancedIOThrottler, IOThrottlerConfig, MetricsSnapshot}; use super::local_stats::{BatchScanResult, LocalStatsManager, ScanResultEntry}; use crate::error::Result; /// SystemTime serde mod system_time_serde { use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::time::{SystemTime, UNIX_EPOCH}; pub fn serialize(time: &SystemTime, serializer: S) -> Result where S: Serializer, { let duration = time.duration_since(UNIX_EPOCH).unwrap_or_default(); duration.as_secs().serialize(serializer) } pub fn deserialize<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { let secs = u64::deserialize(deserializer)?; Ok(UNIX_EPOCH + std::time::Duration::from_secs(secs)) } } /// Option serde mod option_system_time_serde { use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::time::{SystemTime, UNIX_EPOCH}; pub fn serialize(time: &Option, serializer: S) -> Result where S: Serializer, { match time { Some(t) => { let duration = t.duration_since(UNIX_EPOCH).unwrap_or_default(); Some(duration.as_secs()).serialize(serializer) } None => None::.serialize(serializer), } } pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { let secs = Option::::deserialize(deserializer)?; Ok(secs.map(|s| UNIX_EPOCH + std::time::Duration::from_secs(s))) } } /// temporary BucketStats definition, for backward compatibility #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BucketStats { pub name: String, pub object_count: u64, pub total_size: u64, #[serde(with = "system_time_serde")] pub last_update: SystemTime, } impl Default for BucketStats { fn default() -> Self { Self { name: String::new(), object_count: 0, total_size: 0, last_update: SystemTime::now(), } } } /// business load level enum #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum LoadLevel { /// low load (<30%) Low, /// medium load (30-60%) Medium, /// high load (60-80%) High, /// ultra high load (>80%) Critical, } /// node scanner config #[derive(Debug, Clone)] pub struct NodeScannerConfig { /// scan interval pub scan_interval: Duration, /// disk scan delay pub disk_scan_delay: Duration, /// whether to enable smart scheduling pub enable_smart_scheduling: bool, /// whether to enable checkpoint resume pub enable_checkpoint: bool, /// checkpoint save interval pub checkpoint_save_interval: Duration, /// data directory path pub data_dir: PathBuf, /// max scan retry attempts pub max_retry_attempts: u32, } impl Default for NodeScannerConfig { fn default() -> Self { // use a user-writable directory for scanner data let data_dir = std::env::temp_dir().join("rustfs_scanner"); Self { scan_interval: Duration::from_secs(300), // 5 minutes base interval disk_scan_delay: Duration::from_secs(10), // disk scan delay 10 seconds enable_smart_scheduling: true, enable_checkpoint: true, checkpoint_save_interval: Duration::from_secs(30), // checkpoint save interval 30 seconds data_dir, max_retry_attempts: 3, } } } /// local scan stats data #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LocalScanStats { /// scanned objects count pub objects_scanned: u64, /// healthy objects count pub healthy_objects: u64, /// corrupted objects count pub corrupted_objects: u64, /// data usage pub data_usage: DataUsageInfo, /// last update time #[serde(with = "system_time_serde")] pub last_update: SystemTime, /// buckets stats pub buckets_stats: HashMap, /// disks stats pub disks_stats: HashMap, /// scan progress pub scan_progress: ScanProgress, /// checkpoint timestamp #[serde(with = "system_time_serde")] pub checkpoint_timestamp: SystemTime, } impl Default for LocalScanStats { fn default() -> Self { Self { objects_scanned: 0, healthy_objects: 0, corrupted_objects: 0, data_usage: DataUsageInfo::default(), last_update: SystemTime::now(), buckets_stats: HashMap::new(), disks_stats: HashMap::new(), scan_progress: ScanProgress::default(), checkpoint_timestamp: SystemTime::now(), } } } /// disk stats info #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DiskStats { /// disk id pub disk_id: String, /// scanned objects count pub objects_scanned: u64, /// errors count pub errors_count: u64, /// last scan time #[serde(with = "system_time_serde")] pub last_scan_time: SystemTime, /// scan duration pub scan_duration: Duration, /// whether scan is completed pub scan_completed: bool, } impl Default for DiskStats { fn default() -> Self { Self { disk_id: String::new(), objects_scanned: 0, errors_count: 0, last_scan_time: SystemTime::now(), scan_duration: Duration::from_secs(0), scan_completed: false, } } } /// scan progress state #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ScanProgress { /// current scan cycle pub current_cycle: u64, /// current disk index pub current_disk_index: usize, /// current bucket pub current_bucket: Option, /// current object prefix pub current_object_prefix: Option, /// completed disks pub completed_disks: HashSet, /// completed buckets scan state pub completed_buckets: HashMap, /// last scanned object key pub last_scan_key: Option, /// scan start time #[serde(with = "system_time_serde")] pub scan_start_time: SystemTime, /// estimated completion time #[serde(with = "option_system_time_serde")] pub estimated_completion: Option, /// data usage statistics pub data_usage: Option, } impl Default for ScanProgress { fn default() -> Self { Self { current_cycle: 0, current_disk_index: 0, current_bucket: None, current_object_prefix: None, completed_disks: HashSet::new(), completed_buckets: HashMap::new(), last_scan_key: None, scan_start_time: SystemTime::now(), estimated_completion: None, data_usage: None, } } } /// bucket scan state #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BucketScanState { /// whether completed pub completed: bool, /// last scanned object key pub last_object_key: Option, /// scanned objects count pub objects_scanned: u64, /// scan timestamp pub scan_timestamp: SystemTime, } /// IO monitor pub struct IOMonitor { /// current disk IOPS current_iops: Arc, /// disk queue depth queue_depth: Arc, /// business request latency (milliseconds) business_latency: Arc, /// CPU usage cpu_usage: Arc, /// memory usage /// memory usage (reserved field) #[allow(dead_code)] memory_usage: Arc, } impl IOMonitor { pub fn new() -> Self { Self { current_iops: Arc::new(AtomicU64::new(0)), queue_depth: Arc::new(AtomicU64::new(0)), business_latency: Arc::new(AtomicU64::new(0)), cpu_usage: Arc::new(AtomicU8::new(0)), memory_usage: Arc::new(AtomicU8::new(0)), } } /// get current business load level pub async fn get_business_load_level(&self) -> LoadLevel { let iops = self.current_iops.load(Ordering::Relaxed); let queue_depth = self.queue_depth.load(Ordering::Relaxed); let latency = self.business_latency.load(Ordering::Relaxed); let cpu = self.cpu_usage.load(Ordering::Relaxed); // comprehensive evaluation of load level let load_score = self.calculate_load_score(iops, queue_depth, latency, cpu); match load_score { 0..=30 => LoadLevel::Low, 31..=60 => LoadLevel::Medium, 61..=80 => LoadLevel::High, _ => LoadLevel::Critical, } } fn calculate_load_score(&self, iops: u64, queue_depth: u64, latency: u64, cpu: u8) -> u8 { // simplified load score algorithm, actual implementation needs to adjust based on specific hardware metrics let iops_score = std::cmp::min(iops / 100, 25) as u8; // IOPS weight 25% let queue_score = std::cmp::min(queue_depth * 5, 25) as u8; // queue depth weight 25% let latency_score = std::cmp::min(latency / 10, 25) as u8; // latency weight 25% let cpu_score = std::cmp::min(cpu / 4, 25); // CPU weight 25% iops_score + queue_score + latency_score + cpu_score } /// update system metrics pub async fn update_metrics(&self, iops: u64, queue_depth: u64, latency: u64, cpu: u8) { self.current_iops.store(iops, Ordering::Relaxed); self.queue_depth.store(queue_depth, Ordering::Relaxed); self.business_latency.store(latency, Ordering::Relaxed); self.cpu_usage.store(cpu, Ordering::Relaxed); } } /// IO throttler pub struct IOThrottler { /// max IOPS limit (reserved field) #[allow(dead_code)] max_iops: Arc, /// current IOPS usage (reserved field) #[allow(dead_code)] current_iops: Arc, /// business priority weight (0-100) business_priority: Arc, /// scan operation delay (milliseconds) scan_delay: Arc, } impl IOThrottler { pub fn new() -> Self { Self { max_iops: Arc::new(AtomicU64::new(1000)), // default max 1000 IOPS current_iops: Arc::new(AtomicU64::new(0)), business_priority: Arc::new(AtomicU8::new(95)), // business priority 95% scan_delay: Arc::new(AtomicU64::new(100)), // default 100ms delay } } /// adjust scanning delay based on load level pub async fn adjust_for_load_level(&self, load_level: LoadLevel) -> Duration { let delay_ms = match load_level { LoadLevel::Low => { self.scan_delay.store(100, Ordering::Relaxed); // 100ms delay self.business_priority.store(90, Ordering::Relaxed); 100 } LoadLevel::Medium => { self.scan_delay.store(500, Ordering::Relaxed); // 500ms delay self.business_priority.store(95, Ordering::Relaxed); 500 } LoadLevel::High => { self.scan_delay.store(2000, Ordering::Relaxed); // 2s self.business_priority.store(98, Ordering::Relaxed); 2000 } LoadLevel::Critical => { self.scan_delay.store(10000, Ordering::Relaxed); // 10s delay (actually will pause scanning) self.business_priority.store(99, Ordering::Relaxed); 10000 } }; Duration::from_millis(delay_ms) } /// check whether should pause scanning pub async fn should_pause_scanning(&self, load_level: LoadLevel) -> bool { matches!(load_level, LoadLevel::Critical) } } /// decentralized node scanner /// /// responsible for serial scanning of local disks, implementing smart scheduling and checkpoint resume functionality pub struct NodeScanner { /// node id node_id: String, /// local disks list local_disks: Arc>>>, /// IO monitor io_monitor: Arc, /// IO throttler throttler: Arc, /// config config: Arc>, /// current scanned disk index current_disk_index: Arc, /// local stats data local_stats: Arc>, /// stats data manager stats_manager: Arc, /// scan progress state scan_progress: Arc>, /// checkpoint manager mapping (one for each disk) checkpoint_managers: Arc>>>, /// cancel token cancel_token: CancellationToken, } impl NodeScanner { /// create a new node scanner pub fn new(node_id: String, config: NodeScannerConfig) -> Self { // Ensure data directory exists if !config.data_dir.exists() { if let Err(e) = std::fs::create_dir_all(&config.data_dir) { error!("create data directory failed {:?}: {}", config.data_dir, e); } } let stats_manager = Arc::new(LocalStatsManager::new(&node_id, &config.data_dir)); let monitor_config = IOMonitorConfig { monitor_interval: Duration::from_secs(1), enable_system_monitoring: true, ..Default::default() }; let io_monitor = Arc::new(AdvancedIOMonitor::new(monitor_config)); let throttler_config = IOThrottlerConfig { max_iops: 1000, base_business_priority: 95, min_scan_delay: 5000, max_scan_delay: 60000, enable_dynamic_adjustment: true, adjustment_response_time: 5, }; let throttler = Arc::new(AdvancedIOThrottler::new(throttler_config)); Self { node_id, local_disks: Arc::new(RwLock::new(Vec::new())), io_monitor, throttler, config: Arc::new(RwLock::new(config)), current_disk_index: Arc::new(AtomicUsize::new(0)), local_stats: Arc::new(RwLock::new(LocalScanStats::default())), stats_manager, scan_progress: Arc::new(RwLock::new(ScanProgress::default())), checkpoint_managers: Arc::new(RwLock::new(HashMap::new())), cancel_token: CancellationToken::new(), } } /// add local disk and create checkpoint manager for it pub async fn add_local_disk(&self, disk: Arc) { // get disk path and create corresponding scanner directory let disk_path = disk.path(); let sys_dir = disk_path.join(".rustfs.sys").join("scanner"); // ensure directory exists if let Err(e) = std::fs::create_dir_all(&sys_dir) { error!("Failed to create scanner directory on disk {:?}: {}", disk_path, e); return; } // create checkpoint manager for the disk let disk_id = disk_path.to_string_lossy().to_string(); let checkpoint_manager = Arc::new(CheckpointManager::new(&self.node_id, &sys_dir)); // store checkpoint manager self.checkpoint_managers .write() .await .insert(disk_id.clone(), checkpoint_manager); // add disk to local disks list self.local_disks.write().await.push(disk.clone()); info!("Added disk {} with checkpoint manager to node {}", disk_id, self.node_id); } /// get checkpoint manager for the disk async fn get_checkpoint_manager_for_disk(&self, disk_id: &str) -> Option> { self.checkpoint_managers.read().await.get(disk_id).cloned() } /// get default checkpoint manager (for the case when there is no local disk) async fn get_default_checkpoint_manager(&self) -> Option> { let config = self.config.read().await; let data_dir = &config.data_dir; // ensure data directory exists if let Err(e) = std::fs::create_dir_all(data_dir) { error!("Failed to create data directory {:?}: {}", data_dir, e); return None; } // create default checkpoint manager Some(Arc::new(CheckpointManager::new(&self.node_id, data_dir))) } /// create checkpoint manager for all disks (called during initialization) pub async fn initialize_checkpoint_managers(&self) { let local_disks = self.local_disks.read().await; let mut checkpoint_managers = self.checkpoint_managers.write().await; for disk in local_disks.iter() { let disk_path = disk.path(); let sys_dir = disk_path.join(".rustfs.sys").join("scanner"); // ensure directory exists if let Err(e) = std::fs::create_dir_all(&sys_dir) { error!("Failed to create scanner directory on disk {:?}: {}", disk_path, e); continue; } // create checkpoint manager for the disk let disk_id = disk_path.to_string_lossy().to_string(); let checkpoint_manager = Arc::new(CheckpointManager::new(&self.node_id, &sys_dir)); checkpoint_managers.insert(disk_id, checkpoint_manager); } } /// set data directory pub async fn set_data_dir(&self, data_dir: &Path) { // Update the checkpoint manager with the new data directory let _new_checkpoint_manager = CheckpointManager::new(&self.node_id, data_dir); // Note: We can't directly replace the Arc, so we would need to redesign this info!("Would set data directory to: {:?}", data_dir); // TODO: Implement proper data directory management } /// get node id pub fn node_id(&self) -> &str { &self.node_id } /// get local stats data pub async fn get_local_stats(&self) -> LocalScanStats { self.local_stats.read().await.clone() } /// get scan progress pub async fn get_scan_progress(&self) -> ScanProgress { self.scan_progress.read().await.clone() } /// start scanner pub async fn start(&self) -> Result<()> { info!("start scanner for node {}", self.node_id); // try to resume from checkpoint self.start_with_resume().await?; Ok(()) } /// try to resume from checkpoint when node starts pub async fn start_with_resume(&self) -> Result<()> { info!("node {} start, try to resume checkpoint", self.node_id); // initialize checkpoint managers self.initialize_checkpoint_managers().await; // try to resume scanning progress (from the first disk) let local_disks = self.local_disks.read().await; if !local_disks.is_empty() { let first_disk = &local_disks[0]; let disk_id = first_disk.path().to_string_lossy().to_string(); if let Some(checkpoint_manager) = self.get_checkpoint_manager_for_disk(&disk_id).await { match checkpoint_manager.load_checkpoint().await { Ok(Some(progress)) => { info!( "success to resume scanning from disk {}: cycle={}, disk={}, last_key={:?}", disk_id, progress.current_cycle, progress.current_disk_index, progress.last_scan_key ); *self.scan_progress.write().await = progress; // use the resumed progress to start scanning self.resume_scanning_from_checkpoint().await } Ok(None) => { info!("disk {} has no valid checkpoint, start fresh scanning", disk_id); self.start_fresh_scanning().await } Err(e) => { warn!("failed to resume scanning from disk {}: {}, start fresh scanning", disk_id, e); self.start_fresh_scanning().await } } } else { info!("cannot get checkpoint manager for disk {}, start fresh scanning", disk_id); self.start_fresh_scanning().await } } else { info!("no local disk, try to resume from default checkpoint manager"); // try to load from default checkpoint manager if let Some(default_checkpoint_manager) = self.get_default_checkpoint_manager().await { match default_checkpoint_manager.load_checkpoint().await { Ok(Some(scan_progress)) => { info!( "resume scanning from default checkpoint: cycle={}, last_key={:?}", scan_progress.current_cycle, scan_progress.last_scan_key ); // resume scanning progress *self.scan_progress.write().await = scan_progress; return self.resume_scanning_from_checkpoint().await; } Ok(None) => { info!("no default checkpoint file"); } Err(e) => { warn!("load default checkpoint failed: {}", e); } } } // if no checkpoint, check if there is scanning progress in memory (for test scenario) let current_progress = self.scan_progress.read().await; if current_progress.current_cycle > 0 || current_progress.last_scan_key.is_some() { info!( "found scanning progress in memory: cycle={}, disk={}, last_key={:?}", current_progress.current_cycle, current_progress.current_disk_index, current_progress.last_scan_key ); drop(current_progress); self.resume_scanning_from_checkpoint().await } else { drop(current_progress); self.start_fresh_scanning().await } } } /// resume scanning from checkpoint async fn resume_scanning_from_checkpoint(&self) -> Result<()> { let progress = self.scan_progress.read().await; let disk_index = progress.current_disk_index; let last_scan_key = progress.last_scan_key.clone(); drop(progress); info!("resume scanning from disk {}: last_scan_key={:?}", disk_index, last_scan_key); // update current disk index self.current_disk_index .store(disk_index, std::sync::atomic::Ordering::Relaxed); // start IO monitoring self.start_io_monitoring().await?; // start scanning loop let scanner_clone = self.clone_for_background(); tokio::spawn(async move { if let Err(e) = scanner_clone.scan_loop_with_resume(last_scan_key).await { error!("scanning loop failed: {}", e); } }); Ok(()) } /// start fresh scanning async fn start_fresh_scanning(&self) -> Result<()> { info!("start fresh scanning loop"); // initialize scanning progress { let mut progress = self.scan_progress.write().await; progress.current_cycle += 1; progress.current_disk_index = 0; progress.scan_start_time = SystemTime::now(); progress.last_scan_key = None; progress.completed_disks.clear(); progress.completed_buckets.clear(); } self.current_disk_index.store(0, std::sync::atomic::Ordering::Relaxed); // start IO monitoring self.start_io_monitoring().await?; // start scanning loop let scanner_clone = self.clone_for_background(); tokio::spawn(async move { if let Err(e) = scanner_clone.scan_loop_with_resume(None).await { error!("scanning loop failed: {}", e); } }); Ok(()) } /// stop scanner pub async fn stop(&self) -> Result<()> { info!("stop scanner for node {}", self.node_id); self.cancel_token.cancel(); Ok(()) } /// clone for background task fn clone_for_background(&self) -> Self { Self { node_id: self.node_id.clone(), local_disks: self.local_disks.clone(), io_monitor: self.io_monitor.clone(), throttler: self.throttler.clone(), config: self.config.clone(), current_disk_index: self.current_disk_index.clone(), local_stats: self.local_stats.clone(), stats_manager: self.stats_manager.clone(), scan_progress: self.scan_progress.clone(), checkpoint_managers: self.checkpoint_managers.clone(), cancel_token: self.cancel_token.clone(), } } /// start IO monitoring async fn start_io_monitoring(&self) -> Result<()> { info!("start advanced IO monitoring"); self.io_monitor.start().await?; Ok(()) } /// main scanning loop (not supported checkpoint resume) #[allow(dead_code)] async fn scan_loop(&self) -> Result<()> { self.scan_loop_with_resume(None).await } /// main scanning loop with checkpoint resume async fn scan_loop_with_resume(&self, resume_from_key: Option) -> Result<()> { info!("node {} start scanning loop, resume from key: {:?}", self.node_id, resume_from_key); while !self.cancel_token.is_cancelled() { // check business load let load_level = self.io_monitor.get_business_load_level().await; // get current system metrics let current_metrics = self.io_monitor.get_current_metrics().await; let metrics_snapshot = MetricsSnapshot { iops: current_metrics.iops, latency: current_metrics.avg_latency, cpu_usage: current_metrics.cpu_usage, memory_usage: current_metrics.memory_usage, }; // get throttle decision let throttle_decision = self .throttler .make_throttle_decision(load_level, Some(metrics_snapshot)) .await; // according to decision action if throttle_decision.should_pause { warn!("pause scanning according to throttle decision: {}", throttle_decision.reason); tokio::time::sleep(Duration::from_secs(600)).await; // pause 10 minutes continue; } // execute serial disk scanning if let Err(e) = self.scan_all_disks_serially().await { error!("disk scanning failed: {}", e); } // save checkpoint if let Err(e) = self.save_checkpoint().await { warn!("save checkpoint failed: {}", e); } // use throttle decision suggested delay let scan_interval = throttle_decision.suggested_delay; info!("scan completed, wait {:?} for next round", scan_interval); info!( "resource allocation: business {}%, scanner {}%", throttle_decision.resource_allocation.business_percentage, throttle_decision.resource_allocation.scanner_percentage ); tokio::select! { _ = self.cancel_token.cancelled() => { info!("scanning loop cancelled"); break; } _ = tokio::time::sleep(scan_interval) => { // continue next round scanning } } } Ok(()) } /// serial scanning all local disks async fn scan_all_disks_serially(&self) -> Result<()> { let local_disks = self.local_disks.read().await; info!("start serial scanning node {} of {} disks", self.node_id, local_disks.len()); for (index, disk) in local_disks.iter().enumerate() { // check again whether should pause let load_level = self.io_monitor.get_business_load_level().await; let should_pause = self.throttler.should_pause_scanning(load_level).await; if should_pause { warn!("business load too high, interrupt disk scanning"); break; } info!("start scanning disk {:?} (index: {})", disk.path(), index); // scan single disk if let Err(e) = self.scan_single_disk(disk.clone()).await { error!("scan disk {:?} failed: {}", disk.path(), e); continue; } // update scan progress self.update_disk_scan_progress(index, &disk.path().to_string_lossy()).await; // disk inter-delay (using smart throttle decision) if index < local_disks.len() - 1 { let delay = self.throttler.adjust_for_load_level(load_level).await; info!("disk {:?} scan completed, smart delay {:?} for next", disk.path(), delay); tokio::time::sleep(delay).await; } } // update cycle scan progress self.complete_scan_cycle().await; Ok(()) } /// scan single disk async fn scan_single_disk(&self, disk: Arc) -> Result<()> { info!("scan disk: path={:?}", disk.path()); let scan_start = SystemTime::now(); let mut scan_entries = Vec::new(); // get ECStore instance for real disk scanning if let Some(ecstore) = rustfs_ecstore::new_object_layer_fn() { // get all buckets on disk match ecstore .list_bucket(&rustfs_ecstore::store_api::BucketOptions::default()) .await { Ok(buckets) => { for bucket_info in buckets { let bucket_name = &bucket_info.name; // skip system internal buckets if bucket_name == ".minio.sys" { continue; } // scan objects in bucket match self.scan_bucket_on_disk(&disk, bucket_name, &mut scan_entries).await { Ok(object_count) => { debug!( "disk {:?} bucket {} scan completed, found {} objects", disk.path(), bucket_name, object_count ); } Err(e) => { warn!("disk {:?} bucket {} scan failed: {}", disk.path(), bucket_name, e); } } } } Err(e) => { warn!("get bucket list failed: {}", e); // Fallback: simulate some scan results for testing to continue self.generate_fallback_scan_results(&disk, &mut scan_entries).await; } } } else { warn!("ECStore instance not available, use simulated scan data"); // Fallback: simulate some scan results for testing to continue self.generate_fallback_scan_results(&disk, &mut scan_entries).await; } let scan_end = SystemTime::now(); let scan_duration = scan_end.duration_since(scan_start).unwrap_or(Duration::ZERO); // create batch scan result let batch_result = BatchScanResult { disk_id: disk.path().to_string_lossy().to_string(), entries: scan_entries, scan_start, scan_end, scan_duration, }; // update stats data self.stats_manager.update_disk_scan_result(&batch_result).await?; // sync update local stats data structure self.update_local_disk_stats(&disk.path().to_string_lossy(), batch_result.entries.len() as u64) .await; Ok(()) } /// update disk scan progress async fn update_disk_scan_progress(&self, disk_index: usize, disk_id: &str) { let mut progress = self.scan_progress.write().await; progress.current_disk_index = disk_index; progress.completed_disks.insert(disk_id.to_string()); debug!("update scan progress: disk index {}, disk id {}", disk_index, disk_id); } /// complete scan cycle async fn complete_scan_cycle(&self) { let mut progress = self.scan_progress.write().await; progress.current_cycle += 1; progress.current_disk_index = 0; progress.completed_disks.clear(); progress.scan_start_time = SystemTime::now(); info!("complete scan cycle {}", progress.current_cycle); } /// update local disk stats async fn update_local_disk_stats(&self, disk_id: &str, objects_scanned: u64) { let mut stats = self.local_stats.write().await; let disk_stat = stats.disks_stats.entry(disk_id.to_string()).or_insert_with(|| DiskStats { disk_id: disk_id.to_string(), ..Default::default() }); disk_stat.objects_scanned += objects_scanned; disk_stat.last_scan_time = SystemTime::now(); disk_stat.scan_completed = true; stats.last_update = SystemTime::now(); stats.objects_scanned += objects_scanned; stats.healthy_objects += objects_scanned; // assume most objects are healthy debug!("update disk {} stats, scanned objects {}", disk_id, objects_scanned); } /// scan objects in specified bucket on disk async fn scan_bucket_on_disk( &self, disk: &DiskStore, bucket_name: &str, scan_entries: &mut Vec, ) -> Result { let disk_path = disk.path(); let bucket_path = disk_path.join(bucket_name); if !bucket_path.exists() { return Ok(0); } let mut object_count = 0; // iterate all objects in bucket directory if let Ok(entries) = std::fs::read_dir(&bucket_path) { for entry in entries.flatten() { let entry_path = entry.path(); if entry_path.is_dir() { let object_name = entry_path.file_name().and_then(|name| name.to_str()).unwrap_or("unknown"); // skip hidden files and system files if object_name.starts_with('.') { continue; } // get object size (simplified processing) let object_size = self.estimate_object_size(&entry_path).await; let entry = ScanResultEntry { object_path: format!("{bucket_name}/{object_name}"), bucket_name: bucket_name.to_string(), object_size, is_healthy: true, // assume most objects are healthy error_message: None, scan_time: SystemTime::now(), disk_id: disk_path.to_string_lossy().to_string(), }; scan_entries.push(entry); object_count += 1; } } } Ok(object_count) } /// estimate object size async fn estimate_object_size(&self, object_dir: &std::path::Path) -> u64 { let mut total_size = 0u64; if let Ok(entries) = std::fs::read_dir(object_dir) { for entry in entries.flatten() { if let Ok(metadata) = entry.metadata() { total_size += metadata.len(); } } } total_size } /// generate fallback scan results (for testing or exceptional cases) async fn generate_fallback_scan_results(&self, disk: &DiskStore, scan_entries: &mut Vec) { debug!("generate fallback scan results for disk: {:?}", disk.path()); // simulate some scan results for i in 0..5 { let entry = ScanResultEntry { object_path: format!("/fallback-bucket/object_{i}"), bucket_name: "fallback-bucket".to_string(), object_size: 1024 * (i + 1), is_healthy: true, error_message: None, scan_time: SystemTime::now(), disk_id: disk.path().to_string_lossy().to_string(), }; scan_entries.push(entry); } } /// get adaptive scan interval #[allow(dead_code)] async fn get_adaptive_scan_interval(&self, load_level: LoadLevel) -> Duration { let base_interval = self.config.read().await.scan_interval; match load_level { LoadLevel::Low => base_interval, LoadLevel::Medium => base_interval * 2, LoadLevel::High => base_interval * 4, LoadLevel::Critical => base_interval * 10, } } /// save checkpoint to current disk async fn save_checkpoint(&self) -> Result<()> { let progress = self.scan_progress.read().await; // get current scanned disk let current_disk_index = self.current_disk_index.load(std::sync::atomic::Ordering::Relaxed); let local_disks = self.local_disks.read().await; if current_disk_index < local_disks.len() { let current_disk = &local_disks[current_disk_index]; let disk_id = current_disk.path().to_string_lossy().to_string(); // get checkpoint manager for disk if let Some(checkpoint_manager) = self.get_checkpoint_manager_for_disk(&disk_id).await { checkpoint_manager.save_checkpoint(&progress).await?; debug!("save checkpoint to disk {}", disk_id); } else { warn!("cannot get checkpoint manager for disk {}", disk_id); } } Ok(()) } /// force save checkpoint to current disk pub async fn force_save_checkpoint(&self) -> Result<()> { let progress = self.scan_progress.read().await; // get current scanned disk let current_disk_index = self.current_disk_index.load(std::sync::atomic::Ordering::Relaxed); let local_disks = self.local_disks.read().await; let checkpoint_manager = if current_disk_index < local_disks.len() { // when there is local disk, use corresponding disk's checkpoint manager let current_disk = &local_disks[current_disk_index]; let disk_id = current_disk.path().to_string_lossy().to_string(); self.get_checkpoint_manager_for_disk(&disk_id).await } else { // when there is no local disk, use default checkpoint manager self.get_default_checkpoint_manager().await }; if let Some(checkpoint_manager) = checkpoint_manager { checkpoint_manager.force_save_checkpoint(&progress).await?; info!("force save checkpoint"); } else { warn!("cannot get checkpoint manager"); } Ok(()) } /// get checkpoint info (from current disk) pub async fn get_checkpoint_info(&self) -> Result> { // get current scanned disk let current_disk_index = self.current_disk_index.load(std::sync::atomic::Ordering::Relaxed); let local_disks = self.local_disks.read().await; if current_disk_index < local_disks.len() { let current_disk = &local_disks[current_disk_index]; let disk_id = current_disk.path().to_string_lossy().to_string(); // get checkpoint manager for disk if let Some(checkpoint_manager) = self.get_checkpoint_manager_for_disk(&disk_id).await { checkpoint_manager.get_checkpoint_info().await } else { warn!("cannot get checkpoint manager for disk {}", disk_id); Ok(None) } } else if let Some(default_checkpoint_manager) = self.get_default_checkpoint_manager().await { default_checkpoint_manager.get_checkpoint_info().await } else { Ok(None) } } /// cleanup all checkpoints on all disks pub async fn cleanup_checkpoint(&self) -> Result<()> { let checkpoint_managers = self.checkpoint_managers.read().await; for (disk_id, checkpoint_manager) in checkpoint_managers.iter() { if let Err(e) = checkpoint_manager.cleanup_checkpoint().await { warn!("cleanup checkpoint on disk {} failed: {}", disk_id, e); } else { info!("cleanup checkpoint on disk {}", disk_id); } } Ok(()) } /// get stats data manager pub fn get_stats_manager(&self) -> Arc { self.stats_manager.clone() } /// get stats summary pub async fn get_stats_summary(&self) -> super::local_stats::StatsSummary { self.stats_manager.get_stats_summary().await } /// record heal triggered pub async fn record_heal_triggered(&self, object_path: &str, error_message: &str) { self.stats_manager.record_heal_triggered(object_path, error_message).await; } /// update data usage stats pub async fn update_data_usage(&self, data_usage: DataUsageInfo) { self.stats_manager.update_data_usage(data_usage).await; } /// initialize stats data pub async fn initialize_stats(&self) -> Result<()> { self.stats_manager.load_stats().await } /// get IO monitor pub fn get_io_monitor(&self) -> Arc { self.io_monitor.clone() } /// get IO throttler pub fn get_io_throttler(&self) -> Arc { self.throttler.clone() } /// update business metrics pub async fn update_business_metrics(&self, latency: u64, qps: u64, error_rate: u64, connections: u64) { self.io_monitor .update_business_metrics(latency, qps, error_rate, connections) .await; } /// get current IO metrics pub async fn get_current_io_metrics(&self) -> super::io_monitor::IOMetrics { self.io_monitor.get_current_metrics().await } /// get throttle stats pub async fn get_throttle_stats(&self) -> super::io_throttler::ThrottleStats { self.throttler.get_throttle_stats().await } /// run business pressure simulation pub async fn run_business_pressure_simulation(&self, duration: Duration) -> super::io_throttler::SimulationResult { self.throttler.simulate_business_pressure(duration).await } /// update scan progress (for testing) pub async fn update_scan_progress_for_test(&self, cycle: u64, disk_index: usize, last_key: Option) { let mut progress = self.scan_progress.write().await; progress.current_cycle = cycle; progress.current_disk_index = disk_index; progress.last_scan_key = last_key; } } impl Default for IOMonitor { fn default() -> Self { Self::new() } } impl Default for IOThrottler { fn default() -> Self { Self::new() } }