use super::backend::{FsBackend, FsEntry, FsMetadata};
use async_trait::async_trait;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
#[derive(Debug, Clone)]
pub struct SlowFsConfig {
pub read_dir_delay: Duration,
pub metadata_delay: Duration,
pub exists_delay: Duration,
pub is_dir_delay: Duration,
pub get_entry_delay: Duration,
pub canonicalize_delay: Duration,
}
impl SlowFsConfig {
pub fn uniform(delay: Duration) -> Self {
Self {
read_dir_delay: delay,
metadata_delay: delay,
exists_delay: delay,
is_dir_delay: delay,
get_entry_delay: delay,
canonicalize_delay: delay,
}
}
pub fn none() -> Self {
Self::uniform(Duration::ZERO)
}
pub fn slow_network() -> Self {
Self {
read_dir_delay: Duration::from_millis(500),
metadata_delay: Duration::from_millis(50),
exists_delay: Duration::from_millis(30),
is_dir_delay: Duration::from_millis(30),
get_entry_delay: Duration::from_millis(100),
canonicalize_delay: Duration::from_millis(50),
}
}
pub fn slow_disk() -> Self {
Self {
read_dir_delay: Duration::from_millis(200),
metadata_delay: Duration::from_millis(20),
exists_delay: Duration::from_millis(10),
is_dir_delay: Duration::from_millis(10),
get_entry_delay: Duration::from_millis(50),
canonicalize_delay: Duration::from_millis(20),
}
}
}
impl Default for SlowFsConfig {
fn default() -> Self {
Self::none()
}
}
#[derive(Debug, Clone, Default)]
pub struct BackendMetrics {
pub read_dir_calls: usize,
pub metadata_batch_calls: usize,
pub metadata_items: usize,
pub exists_calls: usize,
pub is_dir_calls: usize,
pub get_entry_calls: usize,
pub canonicalize_calls: usize,
pub total_delay_time: Duration,
}
impl BackendMetrics {
pub fn new() -> Self {
Self::default()
}
pub fn reset(&mut self) {
*self = Self::default();
}
pub fn total_calls(&self) -> usize {
self.read_dir_calls
+ self.metadata_batch_calls
+ self.exists_calls
+ self.is_dir_calls
+ self.get_entry_calls
+ self.canonicalize_calls
}
}
pub struct SlowFsBackend {
inner: Arc<dyn FsBackend>,
config: SlowFsConfig,
metrics: Arc<Mutex<BackendMetrics>>,
}
impl SlowFsBackend {
pub fn new(inner: Arc<dyn FsBackend>, config: SlowFsConfig) -> Self {
Self {
inner,
config,
metrics: Arc::new(Mutex::new(BackendMetrics::new())),
}
}
pub fn with_uniform_delay(inner: Arc<dyn FsBackend>, delay: Duration) -> Self {
Self::new(inner, SlowFsConfig::uniform(delay))
}
pub async fn metrics(&self) -> BackendMetrics {
self.metrics.lock().await.clone()
}
pub async fn reset_metrics(&self) {
self.metrics.lock().await.reset();
}
pub fn metrics_arc(&self) -> Arc<Mutex<BackendMetrics>> {
Arc::clone(&self.metrics)
}
async fn add_delay(&self, delay: Duration) {
if !delay.is_zero() {
tokio::time::sleep(delay).await;
self.metrics.lock().await.total_delay_time += delay;
}
}
}
#[async_trait]
impl FsBackend for SlowFsBackend {
async fn read_dir(&self, path: &Path) -> io::Result<Vec<FsEntry>> {
self.add_delay(self.config.read_dir_delay).await;
self.metrics.lock().await.read_dir_calls += 1;
self.inner.read_dir(path).await
}
async fn get_metadata_batch(&self, paths: &[PathBuf]) -> Vec<io::Result<FsMetadata>> {
let total_delay = self.config.metadata_delay * paths.len() as u32;
self.add_delay(total_delay).await;
let mut metrics = self.metrics.lock().await;
metrics.metadata_batch_calls += 1;
metrics.metadata_items += paths.len();
drop(metrics);
self.inner.get_metadata_batch(paths).await
}
async fn exists(&self, path: &Path) -> bool {
self.add_delay(self.config.exists_delay).await;
self.metrics.lock().await.exists_calls += 1;
self.inner.exists(path).await
}
async fn is_dir(&self, path: &Path) -> io::Result<bool> {
self.add_delay(self.config.is_dir_delay).await;
self.metrics.lock().await.is_dir_calls += 1;
self.inner.is_dir(path).await
}
async fn get_entry(&self, path: &Path) -> io::Result<FsEntry> {
self.add_delay(self.config.get_entry_delay).await;
self.metrics.lock().await.get_entry_calls += 1;
self.inner.get_entry(path).await
}
async fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
self.add_delay(self.config.canonicalize_delay).await;
self.metrics.lock().await.canonicalize_calls += 1;
self.inner.canonicalize(path).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::services::fs::LocalFsBackend;
use std::time::Instant;
use tempfile::TempDir;
#[tokio::test]
async fn test_slow_backend_adds_delay() {
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path();
let local = Arc::new(LocalFsBackend::new());
let slow_config = SlowFsConfig::uniform(Duration::from_millis(100));
let slow = SlowFsBackend::new(local, slow_config);
let start = Instant::now();
let _ = slow.read_dir(temp_path).await;
let elapsed = start.elapsed();
assert!(
elapsed >= Duration::from_millis(100),
"Expected at least 100ms delay, got {:?}",
elapsed
);
let metrics = slow.metrics().await;
assert_eq!(metrics.read_dir_calls, 1);
assert!(metrics.total_delay_time >= Duration::from_millis(100));
}
#[tokio::test]
async fn test_metrics_tracking() {
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path();
let local = Arc::new(LocalFsBackend::new());
let slow = SlowFsBackend::new(local, SlowFsConfig::none());
let _ = slow.read_dir(temp_path).await;
let _ = slow.exists(temp_path).await;
let _ = slow.is_dir(temp_path).await;
let metrics = slow.metrics().await;
assert_eq!(metrics.read_dir_calls, 1);
assert_eq!(metrics.exists_calls, 1);
assert_eq!(metrics.is_dir_calls, 1);
assert_eq!(metrics.total_calls(), 3);
}
#[tokio::test]
async fn test_metadata_batch_delay() {
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path();
std::fs::write(temp_path.join("file1.txt"), "test").unwrap();
std::fs::write(temp_path.join("file2.txt"), "test").unwrap();
let local = Arc::new(LocalFsBackend::new());
let slow_config = SlowFsConfig {
metadata_delay: Duration::from_millis(50),
..SlowFsConfig::none()
};
let slow = SlowFsBackend::new(local, slow_config);
let paths = vec![temp_path.join("file1.txt"), temp_path.join("file2.txt")];
let start = Instant::now();
let _ = slow.get_metadata_batch(&paths).await;
let elapsed = start.elapsed();
assert!(
elapsed >= Duration::from_millis(100),
"Expected at least 100ms delay, got {:?}",
elapsed
);
let metrics = slow.metrics().await;
assert_eq!(metrics.metadata_batch_calls, 1);
assert_eq!(metrics.metadata_items, 2);
}
#[tokio::test]
async fn test_reset_metrics() {
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path();
let local = Arc::new(LocalFsBackend::new());
let slow = SlowFsBackend::new(local, SlowFsConfig::none());
let _ = slow.read_dir(temp_path).await;
let _ = slow.exists(temp_path).await;
let metrics_before = slow.metrics().await;
assert!(metrics_before.total_calls() > 0);
slow.reset_metrics().await;
let metrics_after = slow.metrics().await;
assert_eq!(metrics_after.total_calls(), 0);
}
#[tokio::test]
async fn test_preset_configs() {
let local = Arc::new(LocalFsBackend::new());
let network_config = SlowFsConfig::slow_network();
assert_eq!(network_config.read_dir_delay, Duration::from_millis(500));
let disk_config = SlowFsConfig::slow_disk();
assert_eq!(disk_config.read_dir_delay, Duration::from_millis(200));
let none_config = SlowFsConfig::none();
assert_eq!(none_config.read_dir_delay, Duration::ZERO);
let _slow_network = SlowFsBackend::new(local.clone(), network_config);
let _slow_disk = SlowFsBackend::new(local.clone(), disk_config);
let _no_delay = SlowFsBackend::new(local, none_config);
}
}