use std::sync::Arc;
use std::time::{Duration, Instant};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use tokio::sync::{mpsc, RwLock};
use tokio::task::JoinHandle;
use tracing::{debug, info, warn};
use crate::app::cache::{CacheManager, ReservationStatus};
use crate::app::client::CedaClient;
use crate::app::models::FileInfo;
use crate::app::queue::WorkQueue;
use crate::constants::workers;
use crate::errors::{DownloadError, DownloadResult};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkerConfig {
pub worker_count: usize,
pub max_retries: u32,
pub retry_base_delay: Duration,
pub retry_max_delay: Duration,
pub idle_sleep_duration: Duration,
pub progress_buffer_size: usize,
pub download_timeout: Duration,
pub detailed_progress: bool,
}
impl Default for WorkerConfig {
fn default() -> Self {
Self {
worker_count: workers::DEFAULT_WORKER_COUNT,
max_retries: 3,
retry_base_delay: Duration::from_millis(100),
retry_max_delay: Duration::from_secs(30),
idle_sleep_duration: Duration::from_millis(100),
progress_buffer_size: workers::CHANNEL_BUFFER_SIZE,
download_timeout: Duration::from_secs(600), detailed_progress: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkerProgress {
pub worker_id: u32,
pub file_info: Option<FileInfo>,
pub bytes_downloaded: u64,
pub total_bytes: Option<u64>,
pub download_speed: f64,
pub eta_seconds: Option<f64>,
pub status: WorkerStatus,
pub timestamp: DateTime<Utc>,
pub files_completed: u64,
pub total_bytes_downloaded: u64,
pub error_message: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum WorkerStatus {
Idle,
RequestingWork,
CheckingCache,
Downloading,
Saving,
Error { retry_count: u32 },
Shutdown,
}
#[derive(Debug, Clone, Default)]
pub struct WorkerPoolStats {
pub active_workers: usize,
pub files_completed: u64,
pub files_failed: u64,
pub total_bytes_downloaded: u64,
pub average_download_speed: f64,
pub estimated_completion_time: Option<Duration>,
pub workers_by_status: std::collections::HashMap<WorkerStatus, usize>,
}
#[derive(Debug)]
pub struct DownloadWorker {
id: u32,
config: WorkerConfig,
queue: Arc<WorkQueue>,
cache: Arc<CacheManager>,
client: Arc<CedaClient>,
progress_tx: mpsc::Sender<WorkerProgress>,
stats: WorkerStats,
shutdown_rx: Arc<RwLock<Option<mpsc::Receiver<()>>>>,
}
#[derive(Debug, Default)]
struct WorkerStats {
files_completed: u64,
total_bytes_downloaded: u64,
current_download_start: Option<Instant>,
current_bytes_downloaded: u64,
speed_samples: Vec<(Instant, u64)>, consecutive_empty_polls: u32, last_empty_poll: Option<Instant>,
}
impl DownloadWorker {
pub fn new(
id: u32,
config: WorkerConfig,
queue: Arc<WorkQueue>,
cache: Arc<CacheManager>,
client: Arc<CedaClient>,
progress_tx: mpsc::Sender<WorkerProgress>,
shutdown_rx: mpsc::Receiver<()>,
) -> Self {
Self {
id,
config,
queue,
cache,
client,
progress_tx,
stats: WorkerStats::default(),
shutdown_rx: Arc::new(RwLock::new(Some(shutdown_rx))),
}
}
pub async fn run(mut self) -> DownloadResult<()> {
info!("Worker {} starting", self.id);
self.report_progress(WorkerStatus::Idle, None).await;
debug!("Worker {} ready for work", self.id);
loop {
if self.check_shutdown().await {
info!("Worker {} received shutdown signal", self.id);
break;
}
match self.worker_iteration().await {
Ok(work_found) => {
if !work_found {
self.report_progress(WorkerStatus::Idle, None).await;
self.stats.consecutive_empty_polls += 1;
self.stats.last_empty_poll = Some(Instant::now());
let backoff_multiplier =
std::cmp::min(self.stats.consecutive_empty_polls, 6); let base_sleep = self.config.idle_sleep_duration.as_millis() as u64;
let exponential_sleep = base_sleep * (1u64 << backoff_multiplier); let capped_sleep = std::cmp::min(exponential_sleep, 2000);
let jitter_range = capped_sleep / 4;
let jitter =
fastrand::u64(0..=jitter_range * 2).saturating_sub(jitter_range);
let final_sleep = capped_sleep.saturating_add(jitter);
let sleep_duration = Duration::from_millis(final_sleep);
debug!(
"Worker {} idle (attempt {}), sleeping for {:?}",
self.id, self.stats.consecutive_empty_polls, sleep_duration
);
tokio::time::sleep(sleep_duration).await;
} else {
if self.stats.consecutive_empty_polls > 0 {
debug!(
"Worker {} found work after {} empty polls",
self.id, self.stats.consecutive_empty_polls
);
self.stats.consecutive_empty_polls = 0;
}
}
}
Err(e) => {
debug!("Worker {} encountered error: {}", self.id, e);
self.report_progress(
WorkerStatus::Error { retry_count: 0 },
Some(format!("Worker error: {}", e)),
)
.await;
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
}
self.report_progress(WorkerStatus::Shutdown, None).await;
info!("Worker {} shutting down", self.id);
Ok(())
}
async fn worker_iteration(&mut self) -> DownloadResult<bool> {
self.report_progress(WorkerStatus::RequestingWork, None)
.await;
let work_request_start = std::time::Instant::now();
let work_info = match self.queue.get_next_work().await {
Some(work) => {
let queue_wait_time = work_request_start.elapsed();
if queue_wait_time > std::time::Duration::from_millis(50) {
debug!(
"Worker {} waited {:?} for work from queue",
self.id, queue_wait_time
);
}
work
}
None => {
let queue_wait_time = work_request_start.elapsed();
if queue_wait_time > std::time::Duration::from_millis(10) {
debug!(
"Worker {} found no work after {:?} queue wait",
self.id, queue_wait_time
);
}
return Ok(false); }
};
let file_info = work_info.file_info;
debug!("Worker {} got work: {}", self.id, file_info.file_name);
self.report_progress(WorkerStatus::CheckingCache, None)
.await;
match self.cache.check_and_reserve(&file_info).await {
Ok(ReservationStatus::AlreadyExists) => {
debug!(
"Worker {} found file already cached: {}",
self.id, file_info.file_name
);
self.queue.mark_completed(&file_info.hash).await?;
return Ok(true);
}
Ok(ReservationStatus::ReservedByOther { worker_id }) => {
debug!(
"Worker {} found file reserved by worker {}: {} - immediately seeking new work",
self.id, worker_id, file_info.file_name
);
return Ok(true);
}
Ok(ReservationStatus::Reserved) => {
debug!("Worker {} reserved file: {}", self.id, file_info.file_name);
}
Err(e) => {
warn!("Worker {} cache reservation failed: {}", self.id, e);
self.queue
.mark_failed(&file_info.hash, &format!("Cache reservation failed: {}", e))
.await?;
return Err(e.into());
}
}
let result = self.download_and_save_file(&file_info).await;
match &result {
Ok(_) => {
self.queue.mark_completed(&file_info.hash).await?;
}
Err(e) => {
self.queue
.mark_failed(&file_info.hash, &e.to_string())
.await?;
}
}
match &result {
Ok(_) => {
self.stats.files_completed += 1;
info!(
"Worker {} completed download: {}",
self.id, file_info.file_name
);
}
Err(e) => {
debug!(
"Worker {} failed to download: {} - {}",
self.id, file_info.file_name, e
);
}
}
result.map(|_| true)
}
async fn download_and_save_file(&mut self, file_info: &FileInfo) -> DownloadResult<()> {
let mut retry_count = 0;
let mut retry_delay = self.config.retry_base_delay;
loop {
self.stats.current_download_start = Some(Instant::now());
self.stats.current_bytes_downloaded = 0;
self.stats.speed_samples.clear();
self.report_progress(WorkerStatus::Downloading, None).await;
match self.attempt_download(file_info).await {
Ok(content) => {
self.report_progress(WorkerStatus::Saving, None).await;
match self.cache.save_file_atomic(&content, file_info).await {
Ok(()) => {
self.stats.total_bytes_downloaded += content.len() as u64;
return Ok(());
}
Err(e) => {
debug!("Worker {} failed to save file: {}", self.id, e);
retry_count += 1;
if retry_count >= self.config.max_retries {
return Err(DownloadError::MaxRetriesExceeded {
max_retries: retry_count,
});
}
self.report_progress(
WorkerStatus::Error { retry_count },
Some(format!("Save failed, retrying: {}", e)),
)
.await;
}
}
}
Err(e) => {
debug!("Worker {} download failed: {}", self.id, e);
retry_count += 1;
if retry_count >= self.config.max_retries {
let _ = self.cache.release_reservation(&file_info.hash).await;
return Err(DownloadError::MaxRetriesExceeded {
max_retries: retry_count,
});
}
self.report_progress(
WorkerStatus::Error { retry_count },
Some(format!("Download failed, retrying: {}", e)),
)
.await;
}
}
debug!(
"Worker {} retrying in {:?} (attempt {})",
self.id, retry_delay, retry_count
);
tokio::time::sleep(retry_delay).await;
retry_delay = std::cmp::min(retry_delay * 2, self.config.retry_max_delay);
}
}
async fn attempt_download(&mut self, file_info: &FileInfo) -> DownloadResult<Vec<u8>> {
let url = file_info.download_url("https://data.ceda.ac.uk");
debug!("Worker {} downloading: {}", self.id, url);
let download_future = self.client.download_file_content(&url);
match tokio::time::timeout(self.config.download_timeout, download_future).await {
Ok(Ok(content)) => {
debug!("Worker {} downloaded {} bytes", self.id, content.len());
Ok(content)
}
Ok(Err(e)) => Err(e),
Err(_) => Err(DownloadError::Timeout {
seconds: self.config.download_timeout.as_secs(),
}),
}
}
async fn check_shutdown(&self) -> bool {
let mut shutdown_rx_guard = self.shutdown_rx.write().await;
if let Some(rx) = shutdown_rx_guard.as_mut() {
match rx.try_recv() {
Ok(()) => {
*shutdown_rx_guard = None; true
}
Err(mpsc::error::TryRecvError::Empty) => false,
Err(mpsc::error::TryRecvError::Disconnected) => true,
}
} else {
true }
}
async fn report_progress(&self, status: WorkerStatus, error_message: Option<String>) {
let progress = WorkerProgress {
worker_id: self.id,
file_info: None, bytes_downloaded: self.stats.current_bytes_downloaded,
total_bytes: None, download_speed: self.calculate_download_speed(),
eta_seconds: None, status,
timestamp: Utc::now(),
files_completed: self.stats.files_completed,
total_bytes_downloaded: self.stats.total_bytes_downloaded,
error_message,
};
if let Err(e) = self.progress_tx.try_send(progress) {
match e {
mpsc::error::TrySendError::Full(_) => {
debug!("Worker {} progress channel full, skipping update", self.id);
}
mpsc::error::TrySendError::Closed(_) => {
debug!("Worker {} progress channel closed", self.id);
}
}
}
}
fn calculate_download_speed(&self) -> f64 {
if self.stats.speed_samples.len() < 2 {
return 0.0;
}
let recent_samples: Vec<_> = self
.stats
.speed_samples
.iter()
.rev()
.take(10) .collect();
if recent_samples.len() < 2 {
return 0.0;
}
let (latest_time, latest_bytes) = recent_samples[0];
let (earliest_time, earliest_bytes) = recent_samples[recent_samples.len() - 1];
let time_diff = latest_time.duration_since(*earliest_time).as_secs_f64();
let bytes_diff = latest_bytes.saturating_sub(*earliest_bytes) as f64;
if time_diff > 0.0 {
bytes_diff / time_diff
} else {
0.0
}
}
}
#[derive(Debug)]
pub struct WorkerPool {
config: WorkerConfig,
queue: Arc<WorkQueue>,
cache: Arc<CacheManager>,
client: Arc<CedaClient>,
worker_handles: Vec<JoinHandle<DownloadResult<()>>>,
shutdown_senders: Vec<mpsc::Sender<()>>,
stats: Arc<RwLock<WorkerPoolStats>>,
}
impl WorkerPool {
pub fn new(
config: WorkerConfig,
queue: Arc<WorkQueue>,
cache: Arc<CacheManager>,
client: Arc<CedaClient>,
) -> Self {
Self {
config,
queue,
cache,
client,
worker_handles: Vec::new(),
shutdown_senders: Vec::new(),
stats: Arc::new(RwLock::new(WorkerPoolStats::default())),
}
}
pub async fn start(&mut self, progress_tx: mpsc::Sender<WorkerProgress>) -> DownloadResult<()> {
info!("Starting {} workers", self.config.worker_count);
for worker_id in 0..self.config.worker_count {
let (shutdown_tx, shutdown_rx) = mpsc::channel(1);
let worker = DownloadWorker::new(
worker_id as u32,
self.config.clone(),
self.queue.clone(),
self.cache.clone(),
self.client.clone(),
progress_tx.clone(),
shutdown_rx,
);
let handle = tokio::spawn(async move { worker.run().await });
self.worker_handles.push(handle);
self.shutdown_senders.push(shutdown_tx);
}
let mut stats = self.stats.write().await;
stats.active_workers = self.config.worker_count;
info!(
"Worker pool started with {} workers",
self.config.worker_count
);
Ok(())
}
pub async fn shutdown(self) -> DownloadResult<()> {
info!("Shutting down worker pool");
for shutdown_tx in self.shutdown_senders {
let _ = shutdown_tx.send(()).await; }
let mut results = Vec::new();
for handle in self.worker_handles {
results.push(handle.await);
}
let mut error_count = 0;
for result in results {
match result {
Ok(Ok(())) => {
}
Ok(Err(e)) => {
debug!("Worker failed: {}", e);
error_count += 1;
}
Err(e) => {
debug!("Worker panicked: {}", e);
error_count += 1;
}
}
}
if error_count > 0 {
warn!("{} workers encountered errors during shutdown", error_count);
}
info!("Worker pool shutdown complete");
Ok(())
}
pub async fn get_stats(&self) -> WorkerPoolStats {
self.stats.read().await.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::cache::CacheConfig;
use crate::app::client::ClientConfig;
use tempfile::TempDir;
fn create_test_worker_config() -> WorkerConfig {
WorkerConfig {
worker_count: 2,
max_retries: 2,
retry_base_delay: Duration::from_millis(10),
retry_max_delay: Duration::from_millis(100),
idle_sleep_duration: Duration::from_millis(10),
progress_buffer_size: 10,
download_timeout: Duration::from_secs(5),
detailed_progress: true,
}
}
#[tokio::test]
async fn test_worker_config_default() {
let config = WorkerConfig::default();
assert_eq!(config.worker_count, workers::DEFAULT_WORKER_COUNT);
assert_eq!(config.max_retries, 3);
assert!(config.retry_base_delay > Duration::ZERO);
assert!(config.retry_max_delay > config.retry_base_delay);
}
#[tokio::test]
async fn test_worker_progress_serialization() {
let progress = WorkerProgress {
worker_id: 1,
file_info: None,
bytes_downloaded: 1024,
total_bytes: Some(2048),
download_speed: 1024.0,
eta_seconds: Some(1.0),
status: WorkerStatus::Downloading,
timestamp: Utc::now(),
files_completed: 5,
total_bytes_downloaded: 10240,
error_message: None,
};
let serialized = serde_json::to_string(&progress).unwrap();
let deserialized: WorkerProgress = serde_json::from_str(&serialized).unwrap();
assert_eq!(progress.worker_id, deserialized.worker_id);
assert_eq!(progress.bytes_downloaded, deserialized.bytes_downloaded);
assert_eq!(progress.status, deserialized.status);
}
#[tokio::test]
async fn test_worker_pool_creation() {
let temp_dir = TempDir::new().unwrap();
let cache_config = CacheConfig {
cache_root: Some(temp_dir.path().to_path_buf()),
..Default::default()
};
let queue = Arc::new(WorkQueue::new());
let cache = Arc::new(CacheManager::new(cache_config).await.unwrap());
let client = Arc::new(
CedaClient::new_simple_with_config(ClientConfig::default())
.await
.unwrap(),
);
let config = create_test_worker_config();
let pool = WorkerPool::new(config.clone(), queue, cache, client);
assert_eq!(pool.config.worker_count, config.worker_count);
assert_eq!(pool.worker_handles.len(), 0); assert_eq!(pool.shutdown_senders.len(), 0);
}
#[tokio::test]
async fn test_worker_status_variants() {
let statuses = vec![
WorkerStatus::Idle,
WorkerStatus::RequestingWork,
WorkerStatus::CheckingCache,
WorkerStatus::Downloading,
WorkerStatus::Saving,
WorkerStatus::Error { retry_count: 2 },
WorkerStatus::Shutdown,
];
for status in statuses {
let serialized = serde_json::to_string(&status).unwrap();
let deserialized: WorkerStatus = serde_json::from_str(&serialized).unwrap();
assert_eq!(status, deserialized);
}
}
#[tokio::test]
async fn test_worker_pool_stats_default() {
let stats = WorkerPoolStats::default();
assert_eq!(stats.active_workers, 0);
assert_eq!(stats.files_completed, 0);
assert_eq!(stats.files_failed, 0);
assert_eq!(stats.total_bytes_downloaded, 0);
assert_eq!(stats.average_download_speed, 0.0);
assert!(stats.estimated_completion_time.is_none());
assert!(stats.workers_by_status.is_empty());
}
#[tokio::test]
#[ignore = "Requires CEDA authentication and network access"]
async fn test_worker_integration() {
use crate::app::{ManifestConfig, ManifestStreamer};
use crate::constants::env as env_constants;
use futures::StreamExt;
use std::{env, path::Path, time::Duration};
use tempfile::TempDir;
tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.with_target(false)
.with_thread_ids(false)
.with_file(false)
.with_line_number(false)
.try_init()
.ok();
println!("🚀 Starting worker integration test");
if Path::new(".env").exists() {
println!("📁 Loading credentials from .env file...");
let env_content = std::fs::read_to_string(".env").expect("Failed to read .env file");
for line in env_content.lines() {
if let Some((key, value)) = line.split_once('=') {
unsafe {
if key.trim() == env_constants::USERNAME && !value.trim().is_empty() {
env::set_var(env_constants::USERNAME, value.trim());
}
if key.trim() == env_constants::PASSWORD && !value.trim().is_empty() {
env::set_var(env_constants::PASSWORD, value.trim());
}
}
}
}
}
let username = env::var(env_constants::USERNAME)
.expect("CEDA_USERNAME environment variable not set. Please set credentials.");
let _password = env::var(env_constants::PASSWORD)
.expect("CEDA_PASSWORD environment variable not set. Please set credentials.");
println!("🔐 Testing worker integration with user: {}", username);
let temp_dir = TempDir::new().unwrap();
println!(
"📁 Using temporary cache directory: {}",
temp_dir.path().display()
);
let cache_config = CacheConfig {
cache_root: Some(temp_dir.path().to_path_buf()),
..Default::default()
};
println!("🗄️ Initializing cache manager...");
let cache = Arc::new(CacheManager::new(cache_config).await.unwrap());
println!("🌐 Creating authenticated CEDA client...");
let client = Arc::new(
CedaClient::new()
.await
.expect("Failed to create CEDA client"),
);
println!("📋 Initializing work queue...");
let queue = Arc::new(WorkQueue::new());
let manifest_path = Path::new("examples/midas-open-v202407-md5s.txt");
if !manifest_path.exists() {
panic!(
"Example manifest file not found at: {}",
manifest_path.display()
);
}
println!("📄 Reading manifest file: {}", manifest_path.display());
let manifest_config = ManifestConfig {
destination_root: temp_dir.path().to_path_buf(),
..Default::default()
};
let mut streamer = ManifestStreamer::with_config(manifest_config);
let mut stream = streamer.stream(manifest_path).await.unwrap();
let mut test_files = Vec::new();
let mut file_count = 0;
while let Some(result) = stream.next().await {
if file_count >= 3 {
break;
}
match result {
Ok(file_info) => {
println!(
"📋 Adding to queue: {} ({})",
file_info.file_name, file_info.hash
);
queue.add_work(file_info.clone()).await.unwrap();
test_files.push(file_info);
file_count += 1;
}
Err(e) => {
eprintln!("❌ Error parsing manifest line: {}", e);
continue;
}
}
}
println!("📊 Added {} files to work queue", test_files.len());
assert!(!test_files.is_empty(), "No files loaded from manifest");
let worker_config = WorkerConfig {
worker_count: 2,
max_retries: 2,
retry_base_delay: Duration::from_millis(100),
retry_max_delay: Duration::from_secs(5),
idle_sleep_duration: Duration::from_millis(50),
progress_buffer_size: 10,
download_timeout: Duration::from_secs(60), detailed_progress: true,
};
println!(
"👷 Creating worker pool with {} workers",
worker_config.worker_count
);
let mut pool = WorkerPool::new(worker_config, queue.clone(), cache.clone(), client);
let (progress_tx, mut progress_rx) = tokio::sync::mpsc::channel(100);
println!("🚀 Starting worker pool...");
pool.start(progress_tx).await.unwrap();
let queue_clone = queue.clone();
let progress_task = tokio::spawn(async move {
let mut progress_updates = Vec::new();
let start = std::time::Instant::now();
let timeout = Duration::from_secs(180);
while start.elapsed() < timeout {
tokio::select! {
progress = progress_rx.recv() => {
match progress {
Some(update) => {
println!(
"📈 Worker {}: {:?} status, {} files completed, {} total bytes",
update.worker_id,
update.status,
update.files_completed,
update.total_bytes_downloaded
);
progress_updates.push(update);
}
None => {
println!("📈 Progress channel closed");
break;
}
}
}
_ = tokio::time::sleep(Duration::from_secs(1)) => {
if queue_clone.is_finished().await {
println!("✅ All work completed!");
break;
}
}
}
}
if start.elapsed() >= timeout {
println!("⚠️ Test timeout reached after 3 minutes");
}
progress_updates
});
let progress_updates = progress_task.await.unwrap();
println!("🛑 Shutting down worker pool...");
pool.shutdown().await.unwrap();
println!("🔍 Verifying integration test results...");
assert!(!progress_updates.is_empty(), "No progress updates received");
println!("📊 Received {} progress updates", progress_updates.len());
let queue_stats = queue.stats().await;
println!(
"📋 Queue stats: {} completed, {} failed, {} pending",
queue_stats.completed_count, queue_stats.failed_count, queue_stats.pending_count
);
assert!(
queue_stats.completed_count > 0 || queue_stats.failed_count > 0,
"No files were processed by workers"
);
let mut cached_files = 0;
let mut verified_files = 0;
for file_info in &test_files {
let cache_path = cache.get_file_path(file_info);
if cache_path.exists() {
cached_files += 1;
println!("📁 Found cached file: {}", cache_path.display());
match cache.verify_cache_integrity(vec![file_info.clone()]).await {
Ok(report) if report.files_verified > 0 => {
verified_files += 1;
println!("✅ Cache verification passed for: {}", file_info.file_name);
}
Ok(_) => {
println!("⚠️ Cache verification failed for: {}", file_info.file_name);
}
Err(e) => {
println!(
"❌ Cache verification error for {}: {}",
file_info.file_name, e
);
}
}
}
}
println!("📊 Integration test summary:");
println!(
" Files processed: {}",
queue_stats.completed_count + queue_stats.failed_count
);
println!(" Files cached: {}", cached_files);
println!(" Files verified: {}", verified_files);
println!(" Progress updates: {}", progress_updates.len());
assert!(cached_files > 0, "No files were successfully cached");
assert!(verified_files > 0, "No files passed cache verification");
let worker_statuses: std::collections::HashSet<_> = progress_updates
.iter()
.map(|p| std::mem::discriminant(&p.status))
.collect();
assert!(
worker_statuses.len() > 1,
"Workers should progress through multiple states"
);
println!("🎉 Worker integration test completed successfully!");
println!(" ✅ Workers successfully claimed work from queue");
println!(" ✅ Cache reservations and atomic operations working");
println!(" ✅ Real CEDA downloads completed with authentication");
println!(" ✅ Progress reporting system functional");
println!(" ✅ Worker pool coordination and shutdown successful");
println!(" ✅ File integrity verified through cache system");
}
}