adding several features

This commit is contained in:
DioCrafts
2025-03-19 00:44:27 +01:00
parent 7157762540
commit d9bbd575d2
66 changed files with 12137 additions and 1055 deletions
+487
View File
@@ -0,0 +1,487 @@
use std::cmp::min;
use std::collections::VecDeque;
use std::sync::Arc;
use tokio::sync::{Mutex, Semaphore};
use std::time::{Duration, Instant};
use tracing::debug;
/// Tamaño por defecto de los buffers en el pool
pub const DEFAULT_BUFFER_SIZE: usize = 64 * 1024; // 64KB
/// Número máximo por defecto de buffers en el pool
#[allow(dead_code)]
pub const DEFAULT_MAX_BUFFERS: usize = 100;
/// Tiempo de vida por defecto de un buffer inactivo (en segundos)
#[allow(dead_code)]
pub const DEFAULT_BUFFER_TTL: u64 = 60;
/// Buffer pooling para optimizar operaciones de lectura/escritura
pub struct BufferPool {
/// Pool de buffers disponibles
pool: Mutex<VecDeque<PooledBuffer>>,
/// Semáforo para limitar el número máximo de buffers
limit: Semaphore,
/// Tamaño de los buffers en el pool
buffer_size: usize,
/// Estadísticas del pool
stats: Mutex<BufferPoolStats>,
/// Tiempo de vida de un buffer inactivo
buffer_ttl: Duration,
}
/// Estructura para tracking de estadísticas del pool
#[derive(Debug, Clone, Default)]
pub struct BufferPoolStats {
/// Número total de operaciones de get
pub gets: usize,
/// Número de hits del pool (reutilización exitosa)
pub hits: usize,
/// Número de misses (creación de nuevo buffer)
pub misses: usize,
/// Número de retornos al pool
pub returns: usize,
/// Número de eviction por TTL
pub evictions: usize,
/// Número máximo de buffers alcanzado
pub max_buffers_reached: usize,
/// Esperas por semáforo
pub waits: usize,
}
/// Buffer del pool con metadatos para gestión
struct PooledBuffer {
/// Buffer real de bytes
buffer: Vec<u8>,
/// Timestamp de cuándo se añadió/retornó al pool
last_used: Instant,
}
/// Buffer prestado del pool con cleanup automático
#[derive(Clone)]
pub struct BorrowedBuffer {
/// Buffer actual
buffer: Vec<u8>,
/// Tamaño real utilizado del buffer
used_size: usize,
/// Referencia al pool para retornar
pool: Arc<BufferPool>,
/// Si el buffer debe o no retornarse al pool
return_to_pool: bool,
}
impl BufferPool {
/// Crea un nuevo pool de buffers
pub fn new(buffer_size: usize, max_buffers: usize, buffer_ttl_secs: u64) -> Arc<Self> {
Arc::new(Self {
pool: Mutex::new(VecDeque::with_capacity(max_buffers)),
limit: Semaphore::new(max_buffers),
buffer_size,
stats: Mutex::new(BufferPoolStats::default()),
buffer_ttl: Duration::from_secs(buffer_ttl_secs),
})
}
/// Crea un pool con configuración por defecto
#[allow(dead_code)]
pub fn default() -> Arc<Self> {
Self::new(
DEFAULT_BUFFER_SIZE,
DEFAULT_MAX_BUFFERS,
DEFAULT_BUFFER_TTL
)
}
/// Obtiene un buffer del pool o crea uno nuevo si es necesario
#[allow(unused_variables)]
pub async fn get_buffer(&self) -> BorrowedBuffer {
// Incrementar contador de gets
{
let mut stats = self.stats.lock().await;
stats.gets += 1;
}
// Control de concurrencia
// Usando el mecanismo RAII de Rust para gestión automática
// de recursos al finalizar la función
let _ = match self.limit.try_acquire() {
Ok(_permit) => _permit, // _ prefix para indicar que es intencional
Err(_) => {
// No hay permisos disponibles, esperamos
let mut stats = self.stats.lock().await;
stats.waits += 1;
stats.max_buffers_reached += 1;
drop(stats);
debug!("Buffer pool: waiting for available buffer");
let _permit = self.limit.acquire().await.expect("Semaphore should not be closed");
debug!("Buffer pool: acquired buffer after waiting");
_permit
}
};
// Intentar obtener un buffer existente del pool
let mut pool_locked = self.pool.lock().await;
if let Some(mut pooled_buffer) = pool_locked.pop_front() {
// Verificar si el buffer ha expirado
if pooled_buffer.last_used.elapsed() > self.buffer_ttl {
// Buffer expirado, descartamos y creamos uno nuevo
let mut stats = self.stats.lock().await;
stats.evictions += 1;
stats.misses += 1;
drop(stats);
debug!("Buffer pool: evicted expired buffer");
// Crear nuevo buffer (reutilizando el permiso)
drop(pool_locked); // Liberar el lock antes de retornar
BorrowedBuffer {
buffer: vec![0; self.buffer_size],
used_size: 0,
pool: Arc::new(self.clone()),
return_to_pool: true,
}
} else {
// Buffer válido, lo reutilizamos
let mut stats = self.stats.lock().await;
stats.hits += 1;
drop(stats);
// Liberar el lock antes de retornar
drop(pool_locked);
// Limpiar buffer por seguridad
pooled_buffer.buffer.fill(0);
BorrowedBuffer {
buffer: pooled_buffer.buffer,
used_size: 0,
pool: Arc::new(self.clone()),
return_to_pool: true,
}
}
} else {
// No hay buffers disponibles, creamos uno nuevo
let mut stats = self.stats.lock().await;
stats.misses += 1;
drop(stats);
// Liberar el lock antes de retornar
drop(pool_locked);
debug!("Buffer pool: creating new buffer");
BorrowedBuffer {
buffer: vec![0; self.buffer_size],
used_size: 0,
pool: Arc::new(self.clone()),
return_to_pool: true,
}
}
}
/// Retorna un buffer al pool
async fn return_buffer(&self, mut buffer: Vec<u8>) {
// Si el buffer es del tamaño incorrecto, lo descartamos
if buffer.capacity() != self.buffer_size {
debug!("Buffer pool: discarding buffer of wrong size: {} (expected {})",
buffer.capacity(), self.buffer_size);
return;
}
// Resize para asegurar capacidad correcta
buffer.resize(self.buffer_size, 0);
// Añadir al pool
let mut pool_locked = self.pool.lock().await;
pool_locked.push_back(PooledBuffer {
buffer,
last_used: Instant::now(),
});
// Actualizar estadísticas
let mut stats = self.stats.lock().await;
stats.returns += 1;
}
/// Limpia buffers expirados del pool
pub async fn clean_expired_buffers(&self) {
let _now = Instant::now();
let mut pool_locked = self.pool.lock().await;
// Contar expirados
let count_before = pool_locked.len();
// Filtrar manteniendo solo los no expirados
pool_locked.retain(|buffer| {
buffer.last_used.elapsed() <= self.buffer_ttl
});
// Contar cuántos se eliminaron
let removed = count_before - pool_locked.len();
if removed > 0 {
// Actualizar estadísticas
let mut stats = self.stats.lock().await;
stats.evictions += removed;
debug!("Buffer pool: cleaned {} expired buffers", removed);
}
}
/// Obtiene estadísticas actuales del pool
pub async fn get_stats(&self) -> BufferPoolStats {
self.stats.lock().await.clone()
}
/// Inicia la tarea periódica de limpieza
pub fn start_cleaner(pool: Arc<Self>) {
tokio::spawn(async move {
let interval = Duration::from_secs(30); // Limpiar cada 30 segundos
loop {
tokio::time::sleep(interval).await;
pool.clean_expired_buffers().await;
// Loguear estadísticas periódicamente
let stats = pool.get_stats().await;
debug!("Buffer pool stats: gets={}, hits={}, misses={}, hit_ratio={:.2}%, returns={}, \
evictions={}, max_reached={}, waits={}",
stats.gets,
stats.hits,
stats.misses,
if stats.gets > 0 { (stats.hits as f64 * 100.0) / stats.gets as f64 } else { 0.0 },
stats.returns,
stats.evictions,
stats.max_buffers_reached,
stats.waits);
}
});
}
}
impl Clone for BufferPool {
fn clone(&self) -> Self {
Self {
pool: Mutex::new(VecDeque::new()),
limit: Semaphore::new(self.limit.available_permits()),
buffer_size: self.buffer_size,
stats: Mutex::new(BufferPoolStats::default()),
buffer_ttl: self.buffer_ttl,
}
}
}
impl BorrowedBuffer {
/// Accede al buffer interno
pub fn as_mut_slice(&mut self) -> &mut [u8] {
&mut self.buffer
}
/// Obtiene una referencia a los datos utilizados
#[allow(dead_code)]
pub fn as_slice(&self) -> &[u8] {
&self.buffer[..self.used_size]
}
/// Establece cuántos bytes se utilizaron realmente
pub fn set_used(&mut self, size: usize) {
self.used_size = min(size, self.buffer.len());
}
/// Convierte en un Vec<u8> que incluye solo los datos utilizados
pub fn into_vec(mut self) -> Vec<u8> {
// Marcar para no devolver al pool
self.return_to_pool = false;
// Crear un nuevo vector solo con los datos utilizados
self.buffer[..self.used_size].to_vec()
}
/// Copia datos a este buffer y actualiza el tamaño usado
#[allow(dead_code)]
pub fn copy_from_slice(&mut self, data: &[u8]) -> usize {
let copy_size = min(data.len(), self.buffer.len());
self.buffer[..copy_size].copy_from_slice(&data[..copy_size]);
self.used_size = copy_size;
copy_size
}
/// Impide que el buffer se devuelva al pool al destruirse
#[allow(dead_code)]
pub fn do_not_return(mut self) -> Self {
self.return_to_pool = false;
self
}
/// Obtiene el tamaño total del buffer
pub fn capacity(&self) -> usize {
self.buffer.len()
}
/// Obtiene el tamaño usado del buffer
#[allow(dead_code)]
pub fn used_size(&self) -> usize {
self.used_size
}
}
// Cuando se hace drop de un BorrowedBuffer, lo devuelve al pool
impl Drop for BorrowedBuffer {
fn drop(&mut self) {
if self.return_to_pool {
// Tomar posesión del buffer y crear un clone del pool
let buffer = std::mem::take(&mut self.buffer);
let pool = self.pool.clone();
// Spawn del return para que el drop no bloquee
tokio::spawn(async move {
pool.return_buffer(buffer).await;
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_buffer_pooling() {
// Crear pool pequeño para testing
let pool = BufferPool::new(1024, 5, 60);
// Obtener un buffer
let mut buffer1 = pool.get_buffer().await;
buffer1.copy_from_slice(b"test data");
assert_eq!(buffer1.as_slice(), b"test data");
// Obtener otro buffer
let buffer2 = pool.get_buffer().await;
// Verificar stats
let stats = pool.get_stats().await;
assert_eq!(stats.gets, 2);
assert_eq!(stats.hits, 0); // sin hits todavía
assert_eq!(stats.misses, 2); // todos son misses
// Devolver buffer1 al pool (implícitamente por drop)
drop(buffer1);
// Permitir que el return asíncrono ocurra
tokio::time::sleep(Duration::from_millis(10)).await;
// Obtener otro buffer (debería reutilizar el retornado)
let buffer3 = pool.get_buffer().await;
// Verificar stats actualizados
let stats = pool.get_stats().await;
assert_eq!(stats.gets, 3);
assert_eq!(stats.hits, 1); // ahora debería haber un hit
assert_eq!(stats.returns, 1); // un buffer retornado
// Limpiar
drop(buffer2);
drop(buffer3);
}
#[tokio::test]
async fn test_buffer_operations() {
let pool = BufferPool::new(1024, 10, 60);
// Obtener buffer
let mut buffer = pool.get_buffer().await;
// Escribir datos
buffer.copy_from_slice(b"Hello, world!");
assert_eq!(buffer.used_size(), 13);
assert_eq!(buffer.as_slice(), b"Hello, world!");
// Convertir a vec y verificar
let vec = buffer.into_vec(); // Esto impide retornar al pool
assert_eq!(vec, b"Hello, world!");
// Verificar que no se incrementan los returns (buffer no retornado)
tokio::time::sleep(Duration::from_millis(10)).await;
let stats = pool.get_stats().await;
assert_eq!(stats.returns, 0);
}
#[tokio::test]
async fn test_pool_limit() {
// Pool con solo 3 buffers
let pool = BufferPool::new(1024, 3, 60);
// Obtener 3 buffers (alcanza el límite)
let buffer1 = pool.get_buffer().await;
let buffer2 = pool.get_buffer().await;
let buffer3 = pool.get_buffer().await;
// Verificar stats
let stats = pool.get_stats().await;
assert_eq!(stats.gets, 3);
assert_eq!(stats.waits, 0); // sin esperas todavía
// Intentar obtener un 4º buffer en una tarea separada (debería esperar)
let pool_clone = pool.clone();
let handle = tokio::spawn(async move {
let _buffer4 = pool_clone.get_buffer().await;
true
});
// Dar tiempo para que la tarea intente tomar el buffer
tokio::time::sleep(Duration::from_millis(50)).await;
// Verificar que hay una espera
let stats = pool.get_stats().await;
assert_eq!(stats.waits, 1);
// Liberar un buffer
drop(buffer1);
// Dar tiempo para el retorno asíncrono y para que la tarea en espera obtenga su buffer
tokio::time::sleep(Duration::from_millis(50)).await;
// Verificar que la tarea pudo continuar
assert!(handle.await.unwrap());
// Limpiar
drop(buffer2);
drop(buffer3);
}
#[tokio::test]
async fn test_ttl_expiration() {
// Pool con TTL muy corto para testing
let pool = BufferPool::new(1024, 5, 1); // 1 segundo TTL
// Obtener y devolver un buffer
let buffer = pool.get_buffer().await;
drop(buffer);
// Permitir que el return asíncrono ocurra
tokio::time::sleep(Duration::from_millis(50)).await;
// Verificar que hay un buffer en el pool
let stats = pool.get_stats().await;
assert_eq!(stats.returns, 1);
// Esperar a que expire el TTL
tokio::time::sleep(Duration::from_secs(2)).await;
// Limpiar expirados
pool.clean_expired_buffers().await;
// Obtener otro buffer (debería ser un miss ya que el anterior expiró)
let _buffer2 = pool.get_buffer().await;
// Verificar stats
let stats = pool.get_stats().await;
assert_eq!(stats.evictions, 1); // un buffer expirado
assert_eq!(stats.hits, 0); // sin hits (el buffer expiró)
assert_eq!(stats.misses, 2); // dos misses (1er y 3er get)
}
}
@@ -0,0 +1,212 @@
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::time;
use futures::future::BoxFuture;
use tokio::sync::RwLock;
/// Representación de metadatos en caché
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct CachedMetadata {
/// Si el archivo o directorio existe
pub exists: bool,
/// Tamaño en bytes (para archivos)
pub size: Option<u64>,
/// Timestamp de creación
pub created_at: Option<u64>,
/// Timestamp de modificación
pub modified_at: Option<u64>,
/// Tiempo de expiración de la caché
expires_at: Instant,
}
/// Estructura para gestionar la caché de metadatos de archivos y directorios
#[allow(dead_code)]
pub struct StorageCacheManager {
/// Caché de existencia y metadatos
cache: RwLock<HashMap<PathBuf, CachedMetadata>>,
/// TTL para entradas de archivos (milisegundos)
file_ttl_ms: u64,
/// TTL para entradas de directorios (milisegundos)
dir_ttl_ms: u64,
/// Tamaño máximo de caché
max_entries: usize,
}
impl StorageCacheManager {
/// Crea una nueva instancia del gestor de caché
#[allow(dead_code)]
pub fn new(file_ttl_ms: u64, dir_ttl_ms: u64, max_entries: usize) -> Self {
Self {
cache: RwLock::new(HashMap::with_capacity(max_entries)),
file_ttl_ms,
dir_ttl_ms,
max_entries,
}
}
/// Crea una instancia por defecto del gestor de caché
#[allow(dead_code)]
pub fn default() -> Self {
Self::new(
60_000, // 1 minuto para archivos
300_000, // 5 minutos para directorios
10_000, // máximo 10,000 entradas
)
}
/// Verifica si un archivo o directorio existe en caché
#[allow(dead_code)]
pub async fn check_exists(&self, path: &PathBuf, _is_dir: bool) -> Result<bool, ()> {
// Intentar obtener de la caché
if let Some(metadata) = self.get_cached_metadata(path).await {
return Ok(metadata.exists);
}
// No está en caché
Err(())
}
/// Obtiene los metadatos de un path desde la caché
#[allow(dead_code)]
async fn get_cached_metadata(&self, path: &PathBuf) -> Option<CachedMetadata> {
let cache = self.cache.read().await;
if let Some(metadata) = cache.get(path) {
// Verificar si la entrada expiró
if Instant::now() < metadata.expires_at {
return Some(metadata.clone());
}
}
None
}
/// Actualiza la caché con los metadatos de un path
#[allow(dead_code)]
pub async fn update_cache(&self, path: &PathBuf, exists: bool, size: Option<u64>,
created_at: Option<u64>, modified_at: Option<u64>, is_dir: bool) {
let mut cache = self.cache.write().await;
// Si la caché está llena, eliminar entradas aleatorias antes de agregar
if cache.len() >= self.max_entries {
self.evict_entries(&mut cache, 100).await;
}
// Determinar TTL basado en si es archivo o directorio
let ttl = if is_dir {
Duration::from_millis(self.dir_ttl_ms)
} else {
Duration::from_millis(self.file_ttl_ms)
};
// Crear metadatos y agregar a la caché
let metadata = CachedMetadata {
exists,
size,
created_at,
modified_at,
expires_at: Instant::now() + ttl,
};
cache.insert(path.clone(), metadata);
}
/// Elimina entradas aleatorias de la caché cuando está llena
#[allow(dead_code)]
async fn evict_entries(&self, cache: &mut HashMap<PathBuf, CachedMetadata>, count: usize) {
// Obtener las entradas más antiguas para eliminar
let mut entries: Vec<_> = cache.keys().cloned().collect();
// Limitar el número de entradas a eliminar
let to_remove = count.min(entries.len() / 10);
if to_remove == 0 {
return;
}
// Eliminar las primeras entradas (implementación simple)
entries.truncate(to_remove);
for path in entries {
cache.remove(&path);
}
}
/// Inicia una tarea de limpieza periódica
#[allow(dead_code)]
pub fn start_cleanup_task(cache_manager: Arc<Self>) -> BoxFuture<'static, ()> {
Box::pin(async move {
let interval = Duration::from_secs(60); // Ejecutar cada minuto
loop {
time::sleep(interval).await;
// Limpiar entradas expiradas
let now = Instant::now();
let mut cache = cache_manager.cache.write().await;
// Encontrar entradas expiradas
let expired: Vec<_> = cache
.iter()
.filter(|(_, metadata)| now > metadata.expires_at)
.map(|(path, _)| path.clone())
.collect();
// Eliminar entradas expiradas
for path in expired {
cache.remove(&path);
}
// Registrar estadísticas
let cache_size = cache.len();
drop(cache);
tracing::debug!("Cache cleanup completed. Entries remaining: {}", cache_size);
}
})
}
/// Invalida una entrada específica de la caché
#[allow(dead_code)]
pub async fn invalidate(&self, path: &PathBuf) {
let mut cache = self.cache.write().await;
cache.remove(path);
}
/// Invalida todas las entradas de la caché relacionadas con una carpeta
#[allow(dead_code)]
pub async fn invalidate_folder(&self, folder_path: &PathBuf) {
let mut cache = self.cache.write().await;
// Eliminar entradas que sean descendientes de la carpeta
let folder_str = folder_path.to_string_lossy().to_string();
// Encontrar entradas a eliminar
let to_remove: Vec<_> = cache
.keys()
.filter_map(|path| {
let path_str = path.to_string_lossy().to_string();
if path_str.starts_with(&folder_str) {
Some(path.clone())
} else {
None
}
})
.collect();
// Eliminar las entradas
for path in to_remove {
cache.remove(&path);
}
}
/// Obtiene el número actual de entradas en la caché
#[allow(dead_code)]
pub async fn cache_size(&self) -> usize {
let cache = self.cache.read().await;
cache.len()
}
}
@@ -0,0 +1,425 @@
use std::io::{Read};
use std::sync::Arc;
use async_trait::async_trait;
use bytes::Bytes;
use futures::{Stream, StreamExt};
use tracing::error;
use std::io;
use flate2::Compression;
use flate2::read::GzEncoder as GzEncoderRead;
use flate2::bufread::GzDecoder;
use crate::infrastructure::services::buffer_pool::BufferPool;
/// Nivel de compresión para ficheros
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompressionLevel {
/// Sin compresión (solo para transferencia)
None = 0,
/// Compresión rápida con menor ratio
Fast = 1,
/// Compresión balanceada (por defecto)
Default = 6,
/// Compresión máxima (más lenta)
Best = 9,
}
impl From<CompressionLevel> for Compression {
fn from(level: CompressionLevel) -> Self {
match level {
CompressionLevel::None => Compression::none(),
CompressionLevel::Fast => Compression::fast(),
CompressionLevel::Default => Compression::default(),
CompressionLevel::Best => Compression::best(),
}
}
}
/// Umbral de tamaño para decidir si se comprime o no
const COMPRESSION_SIZE_THRESHOLD: u64 = 1024 * 50; // 50KB
/// Interfaz para servicios de compresión
#[async_trait]
pub trait CompressionService: Send + Sync {
/// Comprime datos en memoria
async fn compress_data(&self, data: &[u8], level: CompressionLevel) -> io::Result<Vec<u8>>;
/// Descomprime datos en memoria
async fn decompress_data(&self, compressed_data: &[u8]) -> io::Result<Vec<u8>>;
/// Comprime un stream de datos
#[allow(dead_code)]
fn compress_stream<S>(&self, stream: S, level: CompressionLevel)
-> impl Stream<Item = io::Result<Bytes>> + Send
where
S: Stream<Item = io::Result<Bytes>> + Send + 'static + Unpin;
/// Descomprime un stream de datos
#[allow(dead_code)]
fn decompress_stream<S>(&self, compressed_stream: S)
-> impl Stream<Item = io::Result<Bytes>> + Send
where
S: Stream<Item = io::Result<Bytes>> + Send + 'static + Unpin;
/// Determina si un archivo debe ser comprimido basado en su tipo MIME y tamaño
fn should_compress(&self, mime_type: &str, size: u64) -> bool;
}
/// Implementación de servicios de compresión usando Gzip
pub struct GzipCompressionService {
/// Pool de buffers para optimización de memoria
buffer_pool: Option<Arc<BufferPool>>,
}
impl GzipCompressionService {
/// Crea una nueva instancia del servicio
pub fn new() -> Self {
Self {
buffer_pool: None,
}
}
/// Crea una nueva instancia del servicio con buffer pool
pub fn new_with_buffer_pool(buffer_pool: Arc<BufferPool>) -> Self {
Self {
buffer_pool: Some(buffer_pool),
}
}
}
#[async_trait]
impl CompressionService for GzipCompressionService {
/// Comprime datos en memoria usando Gzip
async fn compress_data(&self, data: &[u8], level: CompressionLevel) -> io::Result<Vec<u8>> {
// Si tenemos un buffer pool, usar un buffer prestado para la compresión
if let Some(pool) = &self.buffer_pool {
// Estimar el tamaño de la compresión (aproximadamente 80% del original para casos típicos)
let estimated_size = (data.len() as f64 * 0.8) as usize;
// Obtener un buffer del pool
let buffer = pool.get_buffer().await;
// Comprobar si el buffer es suficientemente grande
if buffer.capacity() >= estimated_size {
// Ejecutar la compresión en un worker thread usando el buffer
let buffer_ptr = Arc::new(tokio::sync::Mutex::new(buffer));
let buffer_clone = buffer_ptr.clone();
// Comprimir datos
// Clonar los datos para evitar problemas de lifetime
let data_owned = data.to_vec();
let result = tokio::task::spawn_blocking(move || {
let mut encoder = GzEncoderRead::new(&data_owned[..], level.into());
// Intentar bloquear el mutex (no debería fallar ya que estamos en un hilo separado)
let mut buffer_guard = match futures::executor::block_on(buffer_clone.lock()) {
buffer => buffer,
};
// Leer directamente en el buffer
let read_bytes = encoder.read(buffer_guard.as_mut_slice())?;
buffer_guard.set_used(read_bytes);
Ok(()) as io::Result<()>
}).await;
// Verificar resultado
match result {
Ok(Ok(())) => {
// Obtener el buffer y convertirlo a Vec<u8>
let buffer = buffer_ptr.lock().await;
let cloned_buffer = buffer.clone();
drop(buffer); // Liberar el mutex primero
return Ok(cloned_buffer.into_vec());
},
Ok(Err(e)) => {
error!("Error en compresión con buffer pool: {}", e);
// Continuar con implementación estándar
},
Err(e) => {
error!("Error en task de compresión con buffer pool: {}", e);
// Continuar con implementación estándar
}
}
}
}
// Implementación estándar si no hay buffer pool o el buffer es insuficiente
// Clonar los datos para evitar problemas de lifetime
let data_owned = data.to_vec();
tokio::task::spawn_blocking(move || {
let mut encoder = GzEncoderRead::new(&data_owned[..], level.into());
let mut compressed = Vec::new();
encoder.read_to_end(&mut compressed)?;
Ok(compressed)
}).await.unwrap_or_else(|e| {
error!("Error en task de compresión: {}", e);
Err(io::Error::new(io::ErrorKind::Other, e.to_string()))
})
}
/// Descomprime datos en memoria
async fn decompress_data(&self, compressed_data: &[u8]) -> io::Result<Vec<u8>> {
// Si tenemos un buffer pool, usar un buffer prestado para la descompresión
if let Some(pool) = &self.buffer_pool {
// Estimar el tamaño de la descompresión (aproximadamente 5x del comprimido para casos típicos)
let estimated_size = compressed_data.len() * 5;
// Obtener un buffer del pool
let buffer = pool.get_buffer().await;
// Comprobar si el buffer es suficientemente grande
if buffer.capacity() >= estimated_size {
// Clonar datos comprimidos para mover al worker
let data = compressed_data.to_vec();
let buffer_ptr = Arc::new(tokio::sync::Mutex::new(buffer));
let buffer_clone = buffer_ptr.clone();
// Descomprimir datos
let result = tokio::task::spawn_blocking(move || {
let mut decoder = GzDecoder::new(&data[..]);
// Intentar bloquear el mutex
let mut buffer_guard = match futures::executor::block_on(buffer_clone.lock()) {
buffer => buffer,
};
// Leer directamente en el buffer
let read_bytes = decoder.read(buffer_guard.as_mut_slice())?;
buffer_guard.set_used(read_bytes);
Ok(()) as io::Result<()>
}).await;
// Verificar resultado
match result {
Ok(Ok(())) => {
// Obtener el buffer y convertirlo a Vec<u8>
let buffer = buffer_ptr.lock().await;
let cloned_buffer = buffer.clone();
drop(buffer); // Liberar el mutex primero
return Ok(cloned_buffer.into_vec());
},
Ok(Err(e)) => {
error!("Error en descompresión con buffer pool: {}", e);
// Continuar con implementación estándar
},
Err(e) => {
error!("Error en task de descompresión con buffer pool: {}", e);
// Continuar con implementación estándar
}
}
}
}
// Implementación estándar si no hay buffer pool o el buffer es insuficiente
let data = compressed_data.to_vec(); // Clonar para mover al worker
tokio::task::spawn_blocking(move || {
let mut decoder = GzDecoder::new(&data[..]);
let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed)?;
Ok(decompressed)
}).await.unwrap_or_else(|e| {
error!("Error en task de descompresión: {}", e);
Err(io::Error::new(io::ErrorKind::Other, e.to_string()))
})
}
/// Comprime un stream de bytes
fn compress_stream<S>(&self, stream: S, level: CompressionLevel)
-> impl Stream<Item = io::Result<Bytes>> + Send
where
S: Stream<Item = io::Result<Bytes>> + Send + 'static + Unpin
{
// For now, simplify the implementation to avoid complex pinning issues
// This implementation collects all stream data and then compresses it at once
// Future optimization would be to implement true streaming compression
let compression_level = level;
Box::pin(async_stream::stream! {
let mut data = Vec::new();
// Collect all bytes from the stream
let mut stream = Box::pin(stream);
while let Some(result) = stream.next().await {
match result {
Ok(bytes) => {
data.extend_from_slice(&bytes);
},
Err(e) => {
yield Err(e);
return;
}
}
}
// Compress collected data
match self.compress_data(&data, compression_level).await {
Ok(compressed) => {
// Return compressed data as a single chunk
yield Ok(Bytes::from(compressed));
},
Err(e) => {
yield Err(e);
}
}
})
}
/// Descomprime un stream de bytes
fn decompress_stream<S>(&self, compressed_stream: S)
-> impl Stream<Item = io::Result<Bytes>> + Send
where
S: Stream<Item = io::Result<Bytes>> + Send + 'static + Unpin
{
// For now, simplify the implementation to avoid complex pinning issues
// This implementation collects all stream data and then decompresses it at once
// Future optimization would be to implement streaming decompression correctly
Box::pin(async_stream::stream! {
let mut compressed_data = Vec::new();
// Collect all bytes from the stream
let mut stream = Box::pin(compressed_stream);
while let Some(result) = stream.next().await {
match result {
Ok(bytes) => {
compressed_data.extend_from_slice(&bytes);
},
Err(e) => {
yield Err(e);
return;
}
}
}
// Decompress collected data
match self.decompress_data(&compressed_data).await {
Ok(decompressed) => {
// Return decompressed data as a single chunk
yield Ok(Bytes::from(decompressed));
},
Err(e) => {
yield Err(e);
}
}
})
}
/// Determina si un archivo debe ser comprimido basado en su tipo MIME y tamaño
fn should_compress(&self, mime_type: &str, size: u64) -> bool {
// No comprimir archivos muy pequeños (overhead)
if size < COMPRESSION_SIZE_THRESHOLD {
return false;
}
// No comprimir archivos ya comprimidos
if mime_type.starts_with("image/")
&& !mime_type.contains("svg")
&& !mime_type.contains("bmp") {
return false;
}
if mime_type.starts_with("audio/")
|| mime_type.starts_with("video/")
|| mime_type.contains("zip")
|| mime_type.contains("gzip")
|| mime_type.contains("compressed")
|| mime_type.contains("7z")
|| mime_type.contains("rar")
|| mime_type.contains("bz2")
|| mime_type.contains("xz")
|| mime_type.contains("jpg")
|| mime_type.contains("jpeg")
|| mime_type.contains("png")
|| mime_type.contains("gif")
|| mime_type.contains("webp")
|| mime_type.contains("mp3")
|| mime_type.contains("mp4")
|| mime_type.contains("ogg")
|| mime_type.contains("webm") {
return false;
}
// Comprimir archivos de texto, documentos, y otros tipos compresibles
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio_stream::StreamExt;
use futures::TryStreamExt;
#[tokio::test]
async fn test_compress_decompress_data() {
let service = GzipCompressionService::new();
// Datos de prueba
let data = "Hello, world! ".repeat(1000).into_bytes();
// Comprimir
let compressed = service.compress_data(&data, CompressionLevel::Default).await.unwrap();
// Verificar que la compresión reduce el tamaño
assert!(compressed.len() < data.len());
// Descomprimir
let decompressed = service.decompress_data(&compressed).await.unwrap();
// Verificar que los datos originales se recuperan correctamente
assert_eq!(decompressed, data);
}
#[tokio::test]
async fn test_compress_decompress_stream() {
let service = GzipCompressionService::new();
// Crear datos de prueba
let chunks = vec![
Ok(Bytes::from("Hello, ")),
Ok(Bytes::from("world! ")),
Ok(Bytes::from("This is a test of streaming compression.")),
];
// Convertir a stream
let input_stream = futures::stream::iter(chunks);
// Comprimir el stream
let compressed_stream = service.compress_stream(input_stream, CompressionLevel::Default);
// Recolectar los bytes comprimidos
let compressed_bytes = compressed_stream
.try_fold(Vec::new(), |mut acc, chunk| async move {
acc.extend_from_slice(&chunk);
Ok(acc)
}).await.unwrap();
// Descomprimir los datos
let decompressed = service.decompress_data(&compressed_bytes).await.unwrap();
// Verificar resultado
let expected = "Hello, world! This is a test of streaming compression.";
assert_eq!(String::from_utf8(decompressed).unwrap(), expected);
}
#[test]
fn test_should_compress() {
let service = GzipCompressionService::new();
// Casos que no deberían comprimirse
assert!(!service.should_compress("image/jpeg", 100 * 1024));
assert!(!service.should_compress("video/mp4", 10 * 1024 * 1024));
assert!(!service.should_compress("application/zip", 5 * 1024 * 1024));
// Casos que sí deberían comprimirse
assert!(service.should_compress("text/html", 100 * 1024));
assert!(service.should_compress("application/json", 200 * 1024));
assert!(service.should_compress("text/plain", 1024 * 1024));
// Archivos pequeños no deberían comprimirse independientemente del tipo
assert!(!service.should_compress("text/html", 10 * 1024));
}
}
@@ -0,0 +1,659 @@
use std::collections::{HashMap, VecDeque};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant, UNIX_EPOCH};
use tokio::fs;
use tokio::sync::RwLock;
use tokio::time;
use futures::future::BoxFuture;
use tracing::debug;
use mime_guess::from_path;
use crate::common::config::AppConfig;
/// Tipos de entradas en caché
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CacheEntryType {
/// Archivo
File,
/// Directorio
Directory,
/// Tipo desconocido
Unknown,
}
/// Estadísticas de caché para monitoreo
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
/// Número de hits en caché
pub hits: usize,
/// Número de misses en caché
pub misses: usize,
/// Número de invalidaciones manuales
pub invalidations: usize,
/// Número de expiraciones automáticas
pub expirations: usize,
/// Número de inserciones en caché
pub inserts: usize,
/// Tiempo total ahorrado (milisegundos)
pub time_saved_ms: u64,
}
/// Metadatos completos de archivo en caché
#[derive(Debug, Clone)]
pub struct FileMetadata {
/// Ruta absoluta del archivo
pub path: PathBuf,
/// Si el archivo existe físicamente
#[allow(dead_code)]
pub exists: bool,
/// Tipo de entrada (archivo, directorio)
pub entry_type: CacheEntryType,
/// Tamaño en bytes (para archivos)
pub size: Option<u64>,
/// Tipo MIME (para archivos)
#[allow(dead_code)]
pub mime_type: Option<String>,
/// Timestamp de creación (UNIX epoch seconds)
pub created_at: Option<u64>,
/// Timestamp de modificación (UNIX epoch seconds)
pub modified_at: Option<u64>,
/// Acceso previo (usado para LRU)
pub last_access: Instant,
/// Tiempo de expiración de la caché
pub expires_at: Instant,
/// Número de accesos a esta entrada
pub access_count: usize,
}
impl FileMetadata {
/// Crea una nueva entrada de metadatos
pub fn new(
path: PathBuf,
exists: bool,
entry_type: CacheEntryType,
size: Option<u64>,
mime_type: Option<String>,
created_at: Option<u64>,
modified_at: Option<u64>,
ttl: Duration,
) -> Self {
let now = Instant::now();
Self {
path,
exists,
entry_type,
size,
mime_type,
created_at,
modified_at,
last_access: now,
expires_at: now + ttl,
access_count: 1,
}
}
/// Actualiza el tiempo de último acceso
pub fn touch(&mut self) {
self.last_access = Instant::now();
self.access_count += 1;
}
/// Verifica si la entrada ha expirado
pub fn is_expired(&self) -> bool {
Instant::now() > self.expires_at
}
/// Actualiza el tiempo de expiración con un nuevo TTL
pub fn update_expiry(&mut self, ttl: Duration) {
self.expires_at = Instant::now() + ttl;
}
}
/// Caché avanzada de metadatos de archivos
pub struct FileMetadataCache {
/// Caché principal de metadatos
metadata_cache: RwLock<HashMap<PathBuf, FileMetadata>>,
/// Cola LRU para administración de caché
lru_queue: RwLock<VecDeque<PathBuf>>,
/// Estadísticas de uso del caché
stats: RwLock<CacheStats>,
/// Configuración global de la aplicación
config: AppConfig,
/// TTL adaptativo para entradas populares
ttl_multiplier: f64,
/// Umbral de popularidad para TTL extendido
popularity_threshold: usize,
/// Tamaño máximo de caché
max_entries: usize,
}
impl FileMetadataCache {
/// Crea una nueva instancia de caché de metadatos
pub fn new(config: AppConfig, max_entries: usize) -> Self {
Self {
metadata_cache: RwLock::new(HashMap::with_capacity(max_entries)),
lru_queue: RwLock::new(VecDeque::with_capacity(max_entries)),
stats: RwLock::new(CacheStats::default()),
config,
ttl_multiplier: 5.0, // Entradas populares tienen 5x TTL
popularity_threshold: 10, // Después de 10 accesos se considera popular
max_entries,
}
}
/// Crea una instancia de caché con configuración por defecto
pub fn default_with_config(config: AppConfig) -> Self {
Self::new(config, 50_000) // Caché más grande para sistema en producción
}
/// Obtiene los metadatos de un archivo si están en caché
pub async fn get_metadata(&self, path: &Path) -> Option<FileMetadata> {
let start_time = Instant::now();
let mut cache = self.metadata_cache.write().await;
if let Some(metadata) = cache.get_mut(path) {
// Verificar si ha expirado
if metadata.is_expired() {
// Eliminar de caché si expiró
cache.remove(path);
// Actualizar estadísticas
let mut stats = self.stats.write().await;
stats.misses += 1;
stats.expirations += 1;
debug!("Cache entry expired for: {}", path.display());
return None;
}
// Actualizar tiempo de acceso
metadata.touch();
// Para entradas populares, extender TTL
if metadata.access_count >= self.popularity_threshold {
let new_ttl = match metadata.entry_type {
CacheEntryType::File => Duration::from_millis(
(self.config.timeouts.file_operation_ms as f64 * self.ttl_multiplier) as u64
),
CacheEntryType::Directory => Duration::from_millis(
(self.config.timeouts.dir_operation_ms as f64 * self.ttl_multiplier) as u64
),
_ => Duration::from_secs(60), // 1 minuto por defecto
};
metadata.update_expiry(new_ttl);
debug!("Extended TTL for popular entry: {}", path.display());
}
// Calcular tiempo ahorrado aproximado
let elapsed = start_time.elapsed().as_millis() as u64;
let estimated_io_time: u64 = 10; // Asumimos 10ms mínimo para operación de IO
let time_saved = estimated_io_time.saturating_sub(elapsed);
// Actualizar estadísticas
let mut stats = self.stats.write().await;
stats.hits += 1;
stats.time_saved_ms += time_saved;
debug!("Cache hit for: {}", path.display());
// Mantener también la cola LRU actualizada
self.update_lru(path.to_path_buf()).await;
// Clonar para retornar
return Some(metadata.clone());
}
// No encontrado en caché
let mut stats = self.stats.write().await;
stats.misses += 1;
debug!("Cache miss for: {}", path.display());
None
}
/// Actualiza la cola LRU
async fn update_lru(&self, path: PathBuf) {
let mut lru = self.lru_queue.write().await;
// Eliminar si ya existe
if let Some(pos) = lru.iter().position(|p| p == &path) {
lru.remove(pos);
}
// Agregar al final (más reciente)
lru.push_back(path);
}
/// Verifica si un archivo existe
#[allow(dead_code)]
pub async fn exists(&self, path: &Path) -> Option<bool> {
if let Some(metadata) = self.get_metadata(path).await {
return Some(metadata.exists);
}
None
}
/// Verifica si un path es un directorio
#[allow(dead_code)]
pub async fn is_dir(&self, path: &Path) -> Option<bool> {
if let Some(metadata) = self.get_metadata(path).await {
return Some(metadata.entry_type == CacheEntryType::Directory);
}
None
}
/// Verifica si un path es un archivo
pub async fn is_file(&self, path: &Path) -> Option<bool> {
if let Some(metadata) = self.get_metadata(path).await {
return Some(metadata.entry_type == CacheEntryType::File);
}
None
}
/// Obtiene el tamaño de un archivo
#[allow(dead_code)]
pub async fn get_size(&self, path: &Path) -> Option<u64> {
if let Some(metadata) = self.get_metadata(path).await {
return metadata.size;
}
None
}
/// Obtiene el tipo MIME de un archivo
#[allow(dead_code)]
pub async fn get_mime_type(&self, path: &Path) -> Option<String> {
if let Some(metadata) = self.get_metadata(path).await {
return metadata.mime_type;
}
None
}
/// Refresca los metadatos de un path
pub async fn refresh_metadata(&self, path: &Path) -> Result<FileMetadata, std::io::Error> {
// Realizar lectura real del sistema de archivos
let metadata = fs::metadata(path).await?;
// Determinar tipo de entrada
let entry_type = if metadata.is_dir() {
CacheEntryType::Directory
} else if metadata.is_file() {
CacheEntryType::File
} else {
CacheEntryType::Unknown
};
// Obtener tamaño para archivos
let size = if metadata.is_file() {
Some(metadata.len())
} else {
None
};
// Obtener tipo MIME para archivos
let mime_type = if metadata.is_file() {
Some(from_path(path).first_or_octet_stream().to_string())
} else {
None
};
// Obtener timestamps
let created_at = metadata.created()
.map(|time| time.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs())
.ok();
let modified_at = metadata.modified()
.map(|time| time.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs())
.ok();
// Determinar TTL apropiado
let ttl = if metadata.is_dir() {
Duration::from_millis(self.config.timeouts.dir_operation_ms)
} else {
Duration::from_millis(self.config.timeouts.file_operation_ms)
};
// Crear entrada de metadatos
let file_metadata = FileMetadata::new(
path.to_path_buf(),
true,
entry_type,
size,
mime_type,
created_at,
modified_at,
ttl,
);
// Actualizar caché
self.update_cache(file_metadata.clone()).await;
Ok(file_metadata)
}
/// Actualiza la caché con nuevos metadatos
pub async fn update_cache(&self, metadata: FileMetadata) {
// Evitar caché llena antes de insertar
self.ensure_capacity().await;
let path = metadata.path.clone();
// Insertar en caché
{
let mut cache = self.metadata_cache.write().await;
cache.insert(path.clone(), metadata);
// Actualizar estadísticas
let mut stats = self.stats.write().await;
stats.inserts += 1;
}
// Actualizar la cola LRU
self.update_lru(path).await;
}
/// Asegura que hay espacio en la caché
async fn ensure_capacity(&self) {
let cache_size = {
let cache = self.metadata_cache.read().await;
cache.len()
};
if cache_size >= self.max_entries {
self.evict_lru_entries(cache_size / 10).await; // Liberar 10%
}
}
/// Elimina entradas menos recientemente usadas
async fn evict_lru_entries(&self, count: usize) {
let mut paths_to_remove = Vec::with_capacity(count);
// Obtener entries a eliminar de la cola LRU
{
let mut lru = self.lru_queue.write().await;
for _ in 0..count {
if let Some(path) = lru.pop_front() {
paths_to_remove.push(path);
} else {
break;
}
}
}
// Eliminar de la caché principal
{
let mut cache = self.metadata_cache.write().await;
for path in paths_to_remove {
cache.remove(&path);
}
}
debug!("Evicted {} LRU entries from cache", count);
}
/// Invalidar una entrada específica de caché
pub async fn invalidate(&self, path: &Path) {
// Eliminar de la caché principal
{
let mut cache = self.metadata_cache.write().await;
cache.remove(path);
// Actualizar estadísticas
let mut stats = self.stats.write().await;
stats.invalidations += 1;
}
// Eliminar de la cola LRU
let path_buf = path.to_path_buf();
{
let mut lru = self.lru_queue.write().await;
if let Some(pos) = lru.iter().position(|p| p == &path_buf) {
lru.remove(pos);
}
}
debug!("Invalidated cache entry for: {}", path.display());
}
/// Invalidar recursivamente entradas bajo un directorio
pub async fn invalidate_directory(&self, dir_path: &Path) {
let dir_str = dir_path.to_string_lossy().to_string();
let mut paths_to_remove = Vec::new();
// Encontrar todos los paths que comienzan con el directorio
{
let cache = self.metadata_cache.read().await;
for path in cache.keys() {
let path_str = path.to_string_lossy().to_string();
if path_str.starts_with(&dir_str) {
paths_to_remove.push(path.clone());
}
}
}
// Actualizar estadísticas
{
let mut stats = self.stats.write().await;
stats.invalidations += paths_to_remove.len();
}
// Eliminar cada path encontrado
for path in paths_to_remove {
self.invalidate(&path).await;
}
debug!("Invalidated directory and contents: {}", dir_path.display());
}
/// Obtener estadísticas actuales de la caché
pub async fn get_stats(&self) -> CacheStats {
let stats = self.stats.read().await;
stats.clone()
}
/// Limpia todas las entradas expiradas de la caché
pub async fn clear_expired(&self) {
let now = Instant::now();
let mut paths_to_remove = Vec::new();
// Encontrar entradas expiradas
{
let cache = self.metadata_cache.read().await;
for (path, metadata) in cache.iter() {
if now > metadata.expires_at {
paths_to_remove.push(path.clone());
}
}
}
// Actualizar estadísticas
{
let mut stats = self.stats.write().await;
stats.expirations += paths_to_remove.len();
}
// Guardar la cantidad de entradas para el logging
let num_paths = paths_to_remove.len();
// Eliminar entradas expiradas
for path in paths_to_remove {
self.invalidate(&path).await;
}
debug!("Cleared {} expired entries from cache", num_paths);
}
/// Inicia el proceso de limpieza periódica
pub fn start_cleanup_task(cache: Arc<Self>) -> BoxFuture<'static, ()> {
Box::pin(async move {
let cleanup_interval = Duration::from_secs(60); // Cada minuto
loop {
// Esperar el intervalo
time::sleep(cleanup_interval).await;
// Limpiar entradas expiradas
cache.clear_expired().await;
// Registrar estadísticas
let stats = cache.get_stats().await;
let cache_size = {
let cache_map = cache.metadata_cache.read().await;
cache_map.len()
};
debug!(
"Cache stats: size={}, hits={}, misses={}, hit_ratio={:.2}%, time_saved={}ms",
cache_size,
stats.hits,
stats.misses,
if stats.hits + stats.misses > 0 {
(stats.hits as f64 * 100.0) / (stats.hits + stats.misses) as f64
} else {
0.0
},
stats.time_saved_ms
);
}
})
}
/// Precarga metadatos de directorios completos (útil para inicialización)
pub async fn preload_directory(&self, dir_path: &Path, recursive: bool, max_depth: usize) -> Result<usize, std::io::Error> {
self._preload_directory_internal(dir_path, recursive, max_depth, 0).await
}
/// Implementación interna de precarga con seguimiento de profundidad
async fn _preload_directory_internal(
&self,
dir_path: &Path,
recursive: bool,
max_depth: usize,
current_depth: usize
) -> Result<usize, std::io::Error> {
Box::pin(async move {
if current_depth > max_depth {
return Ok(0);
}
// Obtener entradas del directorio
let mut entries = fs::read_dir(dir_path).await?;
let mut count = 0;
// Procesar cada entrada
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
let metadata = fs::metadata(&path).await?;
// Refrescar metadatos de esta entrada
self.refresh_metadata(&path).await?;
count += 1;
// Recursivamente procesar subdirectorios si es necesario
if recursive && metadata.is_dir() {
// Box to break recursion
count += self._preload_directory_internal(
&path,
recursive,
max_depth,
current_depth + 1
).await?;
}
}
Ok(count)
}).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
#[tokio::test]
async fn test_cache_operations() {
// Crear directorio temporal para pruebas
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("test_file.txt");
// Crear un archivo de prueba
let mut file = File::create(&file_path).await.unwrap();
file.write_all(b"test content").await.unwrap();
file.flush().await.unwrap();
drop(file);
// Crear caché
let config = AppConfig::default();
let cache = FileMetadataCache::new(config, 1000);
// Verificar miss inicial
assert!(cache.exists(&file_path).await.is_none());
// Refrescar y verificar hit
let metadata = cache.refresh_metadata(&file_path).await.unwrap();
assert_eq!(metadata.entry_type, CacheEntryType::File);
assert_eq!(metadata.size, Some(12)); // "test content" = 12 bytes
// Verificar que ahora existe en caché
assert_eq!(cache.exists(&file_path).await, Some(true));
assert_eq!(cache.is_file(&file_path).await, Some(true));
// Invalidar y verificar que ya no existe en caché
cache.invalidate(&file_path).await;
assert!(cache.exists(&file_path).await.is_none());
// Verificar estadísticas
let stats = cache.get_stats().await;
assert_eq!(stats.inserts, 1);
assert_eq!(stats.invalidations, 1);
assert!(stats.hits > 0);
}
#[tokio::test]
async fn test_directory_operations() {
// Crear estructura de directorios para pruebas
let temp_dir = tempdir().unwrap();
let sub_dir = temp_dir.path().join("subdir");
fs::create_dir(&sub_dir).await.unwrap();
let file1 = temp_dir.path().join("file1.txt");
let file2 = sub_dir.join("file2.txt");
File::create(&file1).await.unwrap();
File::create(&file2).await.unwrap();
// Crear caché
let config = AppConfig::default();
let cache = FileMetadataCache::new(config, 1000);
// Precargar directorio recursivamente
let count = cache.preload_directory(temp_dir.path(), true, 2).await.unwrap();
assert_eq!(count, 3); // dir, subdir, 2 files
// Verificar existencia en caché
assert_eq!(cache.is_dir(temp_dir.path()).await, Some(true));
assert_eq!(cache.is_dir(&sub_dir).await, Some(true));
assert_eq!(cache.is_file(&file1).await, Some(true));
assert_eq!(cache.is_file(&file2).await, Some(true));
// Invalidar directorio y contenido
cache.invalidate_directory(temp_dir.path()).await;
// Verificar que nada existe en caché
assert!(cache.exists(temp_dir.path()).await.is_none());
assert!(cache.exists(&sub_dir).await.is_none());
assert!(cache.exists(&file1).await.is_none());
assert!(cache.exists(&file2).await.is_none());
}
}
@@ -0,0 +1,643 @@
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::{Mutex, RwLock, Semaphore};
use tracing::{debug, error, info, warn};
use async_trait::async_trait;
use crate::domain::services::path_service::StoragePath;
use crate::infrastructure::services::id_mapping_service::{IdMappingService, IdMappingError};
use crate::common::errors::DomainError;
use crate::application::ports::outbound::IdMappingPort;
/// Tamaño máximo de entradas en el caché
const MAX_CACHE_SIZE: usize = 10_000;
/// Tiempo de vida del caché (en segundos)
const CACHE_TTL_SECONDS: u64 = 60 * 5; // 5 minutos
/// Optimizador para operaciones masivas de mapeo de IDs
pub struct IdMappingOptimizer {
/// Servicio base de mapeo de IDs
base_service: Arc<IdMappingService>,
/// Caché de ID por ruta (path -> id)
path_to_id_cache: RwLock<HashMap<String, (String, Instant)>>,
/// Caché de ruta por ID (id -> path)
id_to_path_cache: RwLock<HashMap<String, (String, Instant)>>,
/// Contador de hits
stats: RwLock<OptimizerStats>,
/// Semáforo para limitar operaciones de batch
batch_limiter: Semaphore,
/// Cola de batch pendientes
pending_batch: Mutex<BatchQueue>,
}
/// Estadísticas del optimizador
#[derive(Debug, Default, Clone)]
pub struct OptimizerStats {
/// Número total de consultas get_path_by_id
pub path_by_id_queries: usize,
/// Número de hits en caché get_path_by_id
pub path_by_id_hits: usize,
/// Número total de consultas get_or_create_id
pub get_id_queries: usize,
/// Número de hits en caché get_or_create_id
pub get_id_hits: usize,
/// Número de batch realizados
pub batch_operations: usize,
/// Número total de IDs procesados en batch
pub batch_items_processed: usize,
/// Último momento de limpieza de caché
pub last_cleanup: Option<Instant>,
}
/// Cola para operaciones batch
struct BatchQueue {
/// Rutas pendientes para obtener/crear ID
path_to_id_requests: HashSet<String>,
/// IDs pendientes para obtener ruta
id_to_path_requests: HashSet<String>,
}
impl Default for BatchQueue {
fn default() -> Self {
Self {
path_to_id_requests: HashSet::new(),
id_to_path_requests: HashSet::new(),
}
}
}
/// Resultado de una operación batch
struct BatchResult {
/// Mapeo de ruta a ID
path_to_id: HashMap<String, String>,
/// Mapeo de ID a ruta
id_to_path: HashMap<String, String>,
}
impl IdMappingOptimizer {
/// Crea un nuevo optimizador para el servicio de mapeo de IDs
pub fn new(base_service: Arc<IdMappingService>) -> Self {
Self {
base_service,
path_to_id_cache: RwLock::new(HashMap::with_capacity(1000)),
id_to_path_cache: RwLock::new(HashMap::with_capacity(1000)),
stats: RwLock::new(OptimizerStats::default()),
batch_limiter: Semaphore::new(2), // Limitar a 2 operaciones batch concurrentes
pending_batch: Mutex::new(BatchQueue::default()),
}
}
/// Obtiene estadísticas del optimizador
pub async fn get_stats(&self) -> OptimizerStats {
self.stats.read().await.clone()
}
/// Limpia entradas expiradas del caché
pub async fn cleanup_cache(&self) {
let now = Instant::now();
let ttl = Duration::from_secs(CACHE_TTL_SECONDS);
// Limpiar caché path_to_id
{
let mut cache = self.path_to_id_cache.write().await;
let initial_size = cache.len();
// Retener solo entradas no expiradas
cache.retain(|_, (_, timestamp)| {
now.duration_since(*timestamp) < ttl
});
let removed = initial_size - cache.len();
if removed > 0 {
debug!("Cleaned {} expired entries from path_to_id cache", removed);
}
}
// Limpiar caché id_to_path
{
let mut cache = self.id_to_path_cache.write().await;
let initial_size = cache.len();
// Retener solo entradas no expiradas
cache.retain(|_, (_, timestamp)| {
now.duration_since(*timestamp) < ttl
});
let removed = initial_size - cache.len();
if removed > 0 {
debug!("Cleaned {} expired entries from id_to_path cache", removed);
}
}
// Actualizar estadísticas
{
let mut stats = self.stats.write().await;
stats.last_cleanup = Some(now);
}
}
/// Inicia tarea de limpieza periódica
pub fn start_cleanup_task(optimizer: Arc<Self>) {
tokio::spawn(async move {
let cleanup_interval = Duration::from_secs(CACHE_TTL_SECONDS / 2);
loop {
tokio::time::sleep(cleanup_interval).await;
optimizer.cleanup_cache().await;
// Loguear estadísticas periódicamente
let stats = optimizer.get_stats().await;
info!("ID Mapping Optimizer stats - Path queries: {}, hits: {} ({}%), ID queries: {}, hits: {} ({}%), Batch ops: {}, items: {}",
stats.path_by_id_queries,
stats.path_by_id_hits,
if stats.path_by_id_queries > 0 { stats.path_by_id_hits as f64 * 100.0 / stats.path_by_id_queries as f64 } else { 0.0 },
stats.get_id_queries,
stats.get_id_hits,
if stats.get_id_queries > 0 { stats.get_id_hits as f64 * 100.0 / stats.get_id_queries as f64 } else { 0.0 },
stats.batch_operations,
stats.batch_items_processed
);
}
});
}
/// Agrega una solicitud a la cola pendiente para procesamiento batch
async fn queue_path_to_id_request(&self, path: &StoragePath) -> Result<Option<String>, IdMappingError> {
let path_str = path.to_string();
// Verificar primero en el caché
{
let cache = self.path_to_id_cache.read().await;
if let Some((id, _)) = cache.get(&path_str) {
// Actualizar estadísticas
{
let mut stats = self.stats.write().await;
stats.get_id_hits += 1;
}
return Ok(Some(id.clone()));
}
}
// Si no está en caché, agregar a la cola de batch
{
let mut batch_queue = self.pending_batch.lock().await;
batch_queue.path_to_id_requests.insert(path_str);
}
// No encontrado en caché, debe procesarse en batch
Ok(None)
}
/// Procesa las solicitudes pendientes en batch
async fn process_batch(&self) -> Result<BatchResult, IdMappingError> {
// Adquirir permiso para operación batch
let _permit = self.batch_limiter.acquire().await.unwrap();
// Obtener las solicitudes pendientes
let (path_requests, id_requests) = {
let mut batch_queue = self.pending_batch.lock().await;
let paths = std::mem::take(&mut batch_queue.path_to_id_requests);
let ids = std::mem::take(&mut batch_queue.id_to_path_requests);
(paths, ids)
};
// Crear resultados
let mut result = BatchResult {
path_to_id: HashMap::with_capacity(path_requests.len()),
id_to_path: HashMap::with_capacity(id_requests.len()),
};
// Procesar solicitudes path->id en batch
for path_str in path_requests {
let path = StoragePath::from_string(&path_str);
match self.base_service.get_or_create_id(&path).await {
Ok(id) => {
result.path_to_id.insert(path_str.clone(), id.clone());
result.id_to_path.insert(id, path_str);
},
Err(e) => {
error!("Error batch-processing path {}: {}", path_str, e);
// Continuar con las demás solicitudes
}
}
}
// Procesar solicitudes id->path en batch
for id in id_requests {
match self.base_service.get_path_by_id(&id).await {
Ok(path) => {
let path_str = path.to_string();
result.id_to_path.insert(id.clone(), path_str.clone());
result.path_to_id.insert(path_str, id);
},
Err(e) => {
error!("Error batch-processing ID {}: {}", id, e);
// Continuar con las demás solicitudes
}
}
}
// Actualizar caché con los resultados del batch
{
let mut path_cache = self.path_to_id_cache.write().await;
let mut id_cache = self.id_to_path_cache.write().await;
let now = Instant::now();
for (path, id) in &result.path_to_id {
path_cache.insert(path.clone(), (id.clone(), now));
}
for (id, path) in &result.id_to_path {
id_cache.insert(id.clone(), (path.clone(), now));
}
}
// Actualizar estadísticas
{
let mut stats = self.stats.write().await;
stats.batch_operations += 1;
stats.batch_items_processed += result.path_to_id.len() + result.id_to_path.len();
}
// Guardar los cambios al disco en segundo plano
let service_clone = self.base_service.clone();
tokio::spawn(async move {
if let Err(e) = service_clone.save_pending_changes().await {
error!("Error saving ID mapping changes: {}", e);
}
});
Ok(result)
}
/// Fuerza el procesamiento de solicitudes pendientes si hay suficientes
async fn trigger_batch_if_needed(&self, min_batch_size: usize) -> Result<(), IdMappingError> {
// Verificar si hay suficientes solicitudes pendientes
let should_process = {
let batch_queue = self.pending_batch.lock().await;
batch_queue.path_to_id_requests.len() + batch_queue.id_to_path_requests.len() >= min_batch_size
};
// Procesar si es necesario
if should_process {
self.process_batch().await?;
}
Ok(())
}
/// Precargar un conjunto de rutas para obtener sus IDs en batch
#[allow(dead_code)]
pub async fn preload_paths(&self, paths: Vec<StoragePath>) -> Result<(), IdMappingError> {
// Solo proceder si hay rutas para cargar
if paths.is_empty() {
return Ok(());
}
// Rutas que debemos cargar (las que no están en caché)
let mut paths_to_load = Vec::new();
// Verificar primero el caché
{
let cache = self.path_to_id_cache.read().await;
for path in paths {
let path_str = path.to_string();
if !cache.contains_key(&path_str) {
paths_to_load.push(path_str);
}
}
}
// Si todos estaban en caché, terminar
if paths_to_load.is_empty() {
return Ok(());
}
// Agregar rutas a la cola para procesamiento batch
{
let mut batch_queue = self.pending_batch.lock().await;
for path in paths_to_load {
batch_queue.path_to_id_requests.insert(path);
}
}
// Ejecutar procesamiento batch inmediatamente
self.process_batch().await?;
Ok(())
}
/// Precargar un conjunto de IDs para obtener sus rutas en batch
#[allow(dead_code)]
pub async fn preload_ids(&self, ids: Vec<String>) -> Result<(), IdMappingError> {
// Solo proceder si hay IDs para cargar
if ids.is_empty() {
return Ok(());
}
// IDs que debemos cargar (los que no están en caché)
let mut ids_to_load = Vec::new();
// Verificar primero el caché
{
let cache = self.id_to_path_cache.read().await;
for id in ids {
if !cache.contains_key(&id) {
ids_to_load.push(id);
}
}
}
// Si todos estaban en caché, terminar
if ids_to_load.is_empty() {
return Ok(());
}
// Agregar IDs a la cola para procesamiento batch
{
let mut batch_queue = self.pending_batch.lock().await;
for id in ids_to_load {
batch_queue.id_to_path_requests.insert(id);
}
}
// Ejecutar procesamiento batch inmediatamente
self.process_batch().await?;
Ok(())
}
}
#[async_trait]
impl IdMappingPort for IdMappingOptimizer {
async fn get_or_create_id(&self, path: &StoragePath) -> Result<String, DomainError> {
// Actualizar estadísticas
{
let mut stats = self.stats.write().await;
stats.get_id_queries += 1;
}
let path_str = path.to_string();
// Verificar primero en el caché
{
let cache = self.path_to_id_cache.read().await;
if let Some((id, _)) = cache.get(&path_str) {
// Actualizar estadísticas
{
let mut stats = self.stats.write().await;
stats.get_id_hits += 1;
}
return Ok(id.clone());
}
}
// Si no está en caché, intentar agregar a cola de batch primero
let queued_result = self.queue_path_to_id_request(path).await?;
if let Some(id) = queued_result {
return Ok(id);
}
// Trigger batch processing if enough items accumulated
self.trigger_batch_if_needed(20).await?;
// Intentar obtener del servicio base
let id = self.base_service.get_or_create_id(path).await?;
// Actualizar caché con el nuevo ID
{
let mut path_cache = self.path_to_id_cache.write().await;
let mut id_cache = self.id_to_path_cache.write().await;
let now = Instant::now();
// Controlar tamaño del caché
if path_cache.len() >= MAX_CACHE_SIZE {
warn!("Path-to-ID cache size reached limit ({}), clearing oldest entries", MAX_CACHE_SIZE);
path_cache.clear();
}
if id_cache.len() >= MAX_CACHE_SIZE {
warn!("ID-to-path cache size reached limit ({}), clearing oldest entries", MAX_CACHE_SIZE);
id_cache.clear();
}
path_cache.insert(path_str.clone(), (id.clone(), now));
id_cache.insert(id.clone(), (path_str, now));
}
Ok(id)
}
async fn get_path_by_id(&self, id: &str) -> Result<StoragePath, DomainError> {
// Actualizar estadísticas
{
let mut stats = self.stats.write().await;
stats.path_by_id_queries += 1;
}
// Verificar primero en el caché
{
let cache = self.id_to_path_cache.read().await;
if let Some((path_str, _)) = cache.get(id) {
// Actualizar estadísticas
{
let mut stats = self.stats.write().await;
stats.path_by_id_hits += 1;
}
return Ok(StoragePath::from_string(path_str));
}
}
// Obtener del servicio base
let path = self.base_service.get_path_by_id(id).await?;
// Actualizar caché
{
let mut id_cache = self.id_to_path_cache.write().await;
let mut path_cache = self.path_to_id_cache.write().await;
let now = Instant::now();
let path_str = path.to_string();
// Controlar tamaño del caché
if id_cache.len() >= MAX_CACHE_SIZE {
warn!("ID-to-path cache size reached limit ({}), clearing oldest entries", MAX_CACHE_SIZE);
id_cache.clear();
}
if path_cache.len() >= MAX_CACHE_SIZE {
warn!("Path-to-ID cache size reached limit ({}), clearing oldest entries", MAX_CACHE_SIZE);
path_cache.clear();
}
id_cache.insert(id.to_string(), (path_str.clone(), now));
path_cache.insert(path_str, (id.to_string(), now));
}
Ok(path)
}
async fn update_path(&self, id: &str, new_path: &StoragePath) -> Result<(), DomainError> {
// Invalidar caché para este ID
{
let mut id_cache = self.id_to_path_cache.write().await;
let mut path_cache = self.path_to_id_cache.write().await;
// Eliminar la entrada del ID
if let Some((old_path, _)) = id_cache.remove(id) {
path_cache.remove(&old_path);
}
}
// Actualizar en el servicio base
let result = self.base_service.update_path(id, new_path).await?;
// Actualizar caché con el nuevo mapeo
{
let mut id_cache = self.id_to_path_cache.write().await;
let mut path_cache = self.path_to_id_cache.write().await;
let now = Instant::now();
let path_str = new_path.to_string();
id_cache.insert(id.to_string(), (path_str.clone(), now));
path_cache.insert(path_str, (id.to_string(), now));
}
Ok(result)
}
async fn remove_id(&self, id: &str) -> Result<(), DomainError> {
// Invalidar caché para este ID
{
let mut id_cache = self.id_to_path_cache.write().await;
let mut path_cache = self.path_to_id_cache.write().await;
// Eliminar la entrada del ID
if let Some((path, _)) = id_cache.remove(id) {
path_cache.remove(&path);
}
}
// Eliminar en el servicio base
self.base_service.remove_id(id).await?;
Ok(())
}
async fn save_changes(&self) -> Result<(), DomainError> {
// Delegar al servicio base
self.base_service.save_changes().await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
async fn create_test_service() -> (Arc<IdMappingService>, Arc<IdMappingOptimizer>) {
let temp_dir = tempdir().unwrap();
let map_path = temp_dir.path().join("id_map.json");
let base_service = Arc::new(IdMappingService::new(map_path).await.unwrap());
let optimizer = Arc::new(IdMappingOptimizer::new(base_service.clone()));
(base_service, optimizer)
}
#[tokio::test]
async fn test_basic_caching() {
let (_, optimizer) = create_test_service().await;
let path = StoragePath::from_string("/test/file.txt");
// Primera llamada debería usar el servicio base
let id = optimizer.get_or_create_id(&path).await.unwrap();
assert!(!id.is_empty(), "ID should not be empty");
// Segunda llamada debería usar caché
let id2 = optimizer.get_or_create_id(&path).await.unwrap();
assert_eq!(id, id2, "Same path should return same ID");
// Verificar estadísticas de caché
let stats = optimizer.get_stats().await;
assert_eq!(stats.get_id_queries, 2, "Should have 2 queries");
assert_eq!(stats.get_id_hits, 1, "Should have 1 hit");
}
#[tokio::test]
async fn test_batch_processing() {
let (_, optimizer) = create_test_service().await;
// Crear un lote de rutas
let mut paths = Vec::new();
for i in 0..50 {
paths.push(StoragePath::from_string(&format!("/test/batch/file{}.txt", i)));
}
// Precargar las rutas
optimizer.preload_paths(paths.clone()).await.unwrap();
// Verificar que todas están en caché
for path in &paths {
let id = optimizer.get_or_create_id(path).await.unwrap();
assert!(!id.is_empty(), "ID should be available for path");
}
// Verificar estadísticas
let stats = optimizer.get_stats().await;
assert_eq!(stats.batch_operations, 1, "Should have 1 batch operation");
assert!(stats.batch_items_processed >= 50, "Should have processed at least 50 items");
// Verificar que todas las consultas posteriores son hits en caché
assert_eq!(stats.get_id_hits, 50, "All subsequente queries should be cache hits");
}
#[tokio::test]
async fn test_cache_cleanup() {
let (_, optimizer) = create_test_service().await;
// Crear algunas entradas
let path = StoragePath::from_string("/test/cleanup.txt");
let id = optimizer.get_or_create_id(&path).await.unwrap();
// Verificar estadísticas iniciales
{
let stats = optimizer.get_stats().await;
assert_eq!(stats.get_id_queries, 1, "Should have 1 query");
assert_eq!(stats.get_id_hits, 0, "Should have 0 hits");
}
// Ejecutar limpieza (no debería eliminar nada todavía)
optimizer.cleanup_cache().await;
// Verificar que el caché sigue funcionando
let id2 = optimizer.get_or_create_id(&path).await.unwrap();
assert_eq!(id, id2, "Cache should still work after cleanup");
{
let stats = optimizer.get_stats().await;
assert_eq!(stats.get_id_hits, 1, "Should have 1 hit after cleanup");
}
}
}
@@ -0,0 +1,534 @@
use std::path::PathBuf;
use std::collections::HashMap;
use std::time::Duration;
use tokio::sync::{RwLock, Mutex};
use tokio::fs;
use tokio::time;
use uuid::Uuid;
use serde::{Serialize, Deserialize};
use async_trait::async_trait;
use crate::domain::services::path_service::StoragePath;
use crate::common::errors::{DomainError, ErrorKind, ErrorContext};
use crate::application::ports::outbound::IdMappingPort;
use crate::common::config::TimeoutConfig;
/// Error específico para el servicio de mapeo de IDs
#[derive(Debug, thiserror::Error)]
pub enum IdMappingError {
#[error("ID not found: {0}")]
NotFound(String),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Timeout error: {0}")]
Timeout(String),
#[error("Serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
#[error("Other error: {0}")]
#[allow(dead_code)]
Other(String),
}
// Implementar conversión de IdMappingError a DomainError
impl From<IdMappingError> for DomainError {
fn from(err: IdMappingError) -> Self {
match err {
IdMappingError::NotFound(id) => DomainError::not_found("IdMapping", id),
IdMappingError::IoError(e) => DomainError::new(
ErrorKind::InternalError,
"IdMapping",
format!("IO error: {}", e)
).with_source(e),
IdMappingError::Timeout(msg) => DomainError::timeout(
"IdMapping",
format!("Timeout: {}", msg)
),
IdMappingError::SerializationError(e) => DomainError::new(
ErrorKind::InternalError,
"IdMapping",
format!("Serialization error: {}", e)
).with_source(e),
IdMappingError::Other(msg) => DomainError::new(
ErrorKind::InternalError,
"IdMapping",
format!("Other error: {}", msg)
),
}
}
}
/// Estructura para almacenar IDs mapeados a sus rutas
#[derive(Serialize, Deserialize, Debug, Default)]
struct IdMap {
path_to_id: HashMap<String, String>,
id_to_path: HashMap<String, String>, // Campo para búsqueda bidireccional eficiente
version: u32, // Versión para detectar cambios
}
/// Constantes para configuración
const SAVE_DEBOUNCE_MS: u64 = 300; // Tiempo para agrupar operaciones de guardado
/// Servicio para gestionar mapeos entre rutas y IDs únicos
pub struct IdMappingService {
map_path: PathBuf,
id_map: RwLock<IdMap>,
save_mutex: Mutex<()>, // Para evitar múltiples guardados concurrentes
timeouts: TimeoutConfig,
pending_save: RwLock<bool>, // Indica si hay cambios pendientes
}
impl IdMappingService {
/// Crea un nuevo servicio de mapeo de IDs
pub async fn new(map_path: PathBuf) -> Result<Self, DomainError> {
let timeouts = TimeoutConfig::default();
let id_map = Self::load_id_map(&map_path, &timeouts).await?;
Ok(Self {
map_path,
id_map: RwLock::new(id_map),
save_mutex: Mutex::new(()),
timeouts,
pending_save: RwLock::new(false),
})
}
/// Carga el mapa de IDs desde disco con manejo robusto de errores
async fn load_id_map(map_path: &PathBuf, timeouts: &TimeoutConfig) -> Result<IdMap, DomainError> {
if map_path.exists() {
// Intentar leer con timeout para evitar bloqueos indefinidos
let read_result = time::timeout(
timeouts.lock_timeout(),
fs::read_to_string(map_path)
).await
.with_context(|| format!("Timeout reading ID map from {}", map_path.display()))?;
let content = read_result.with_context(|| format!("Failed to read ID map from {}", map_path.display()))?;
// Parsear el JSON
match serde_json::from_str::<IdMap>(&content) {
Ok(mut map) => {
// Reconstruir el mapa inverso si es necesario
if map.id_to_path.is_empty() && !map.path_to_id.is_empty() {
let mut rebuild_count = 0;
for (path, id) in &map.path_to_id {
map.id_to_path.insert(id.clone(), path.clone());
rebuild_count += 1;
}
tracing::info!("Rebuilt inverse mapping with {} entries", rebuild_count);
}
tracing::info!("Loaded ID map with {} entries (version: {})",
map.path_to_id.len(), map.version);
return Ok(map);
},
Err(e) => {
tracing::error!("Error parsing ID map: {}", e);
// Intentar hacer un respaldo del archivo corrupto
let backup_path = map_path.with_extension("json.bak");
if let Err(copy_err) = tokio::fs::copy(map_path, &backup_path).await {
tracing::error!("Failed to backup corrupted map file: {}", copy_err);
} else {
tracing::info!("Backed up corrupted ID map to {}", backup_path.display());
}
return Err(DomainError::new(
ErrorKind::InternalError,
"IdMapping",
format!("Error parsing ID map: {}", e)
).with_source(e));
}
}
}
// Devolver un mapa vacío si el archivo no existe
tracing::info!("No existing ID map found, creating new empty map");
Ok(IdMap {
path_to_id: HashMap::new(),
id_to_path: HashMap::new(),
version: 1, // Iniciar con versión 1
})
}
/// Guarda el mapa de IDs en disco de manera segura
async fn save_id_map(&self) -> Result<(), DomainError> {
// Adquirir bloqueo exclusivo para salvar
let _lock = time::timeout(
self.timeouts.lock_timeout(),
self.save_mutex.lock()
).await
.with_context(|| "Timeout acquiring save lock for ID mapping")?;
// Crear JSON con el lock de lectura para minimizar el tiempo de bloqueo
let json = {
let mut map = time::timeout(
self.timeouts.lock_timeout(),
self.id_map.write()
).await
.with_context(|| "Timeout acquiring write lock for ID mapping")?;
// Incrementar versión sólo si hay cambios por guardar
let pending = *self.pending_save.read().await;
if pending {
map.version += 1;
tracing::debug!("Incrementing ID map version to {}", map.version);
}
// Use serde with reasonably safe defaults
serde_json::to_string_pretty(&*map)
.with_context(|| "Failed to serialize ID map to JSON")?
};
// Escribir a un archivo temporal primero para evitar corrupción
let temp_path = self.map_path.with_extension("json.tmp");
fs::write(&temp_path, &json).await
.with_context(|| format!("Failed to write temporary ID map to {}", temp_path.display()))?;
// Realizar el rename atómico
fs::rename(&temp_path, &self.map_path).await
.with_context(|| format!("Failed to rename temporary ID map to {}", self.map_path.display()))?;
// Resetear flag de pendientes
{
let mut pending = self.pending_save.write().await;
*pending = false;
}
tracing::info!("Saved ID map successfully to {}", self.map_path.display());
Ok(())
}
/// Genera un ID único
fn generate_id(&self) -> String {
Uuid::new_v4().to_string()
}
/// Marca cambios como pendientes
async fn mark_pending(&self) {
let mut pending = self.pending_save.write().await;
*pending = true;
}
/// Obtiene el ID para una ruta o genera uno nuevo si no existe
pub async fn get_or_create_id(&self, path: &StoragePath) -> Result<String, IdMappingError> {
let path_str = path.to_string();
// Primer intento con lock de lectura (más eficiente)
{
let read_result = match time::timeout(
self.timeouts.lock_timeout(),
self.id_map.read()
).await {
Ok(guard) => guard,
Err(_) => return Err(IdMappingError::Timeout("Timeout acquiring read lock for ID mapping".to_string())),
};
if let Some(id) = read_result.path_to_id.get(&path_str) {
return Ok(id.clone());
}
}
// Si no se encuentra, adquirir lock de escritura
let write_result = match time::timeout(
self.timeouts.lock_timeout(),
self.id_map.write()
).await {
Ok(guard) => guard,
Err(_) => return Err(IdMappingError::Timeout("Timeout acquiring write lock for ID mapping".to_string())),
};
let mut map = write_result;
// Verificar nuevamente (podría haberse agregado mientras esperábamos el lock)
if let Some(id) = map.path_to_id.get(&path_str) {
return Ok(id.clone());
}
// Generar un nuevo ID y almacenarlo
let id = self.generate_id();
map.path_to_id.insert(path_str.clone(), id.clone());
map.id_to_path.insert(id.clone(), path_str);
// Marcar como pendiente para guardar
drop(map); // Liberar el write lock antes de adquirir otro
self.mark_pending().await;
tracing::debug!("Created new ID mapping: {} -> {}", path.to_string(), id);
Ok(id)
}
/// Obtiene una ruta por su ID con manejo de timeout
pub async fn get_path_by_id(&self, id: &str) -> Result<StoragePath, IdMappingError> {
let read_result = match time::timeout(
self.timeouts.lock_timeout(),
self.id_map.read()
).await {
Ok(guard) => guard,
Err(_) => return Err(IdMappingError::Timeout("Timeout acquiring read lock for ID lookup".to_string())),
};
if let Some(path_str) = read_result.id_to_path.get(id) {
return Ok(StoragePath::from_string(path_str));
}
Err(IdMappingError::NotFound(id.to_string()))
}
/// Actualiza el mapeo de un ID existente a una nueva ruta
pub async fn update_path(&self, id: &str, new_path: &StoragePath) -> Result<(), IdMappingError> {
let write_result = match time::timeout(
self.timeouts.lock_timeout(),
self.id_map.write()
).await {
Ok(guard) => guard,
Err(_) => return Err(IdMappingError::Timeout("Timeout acquiring write lock for ID update".to_string())),
};
let mut map = write_result;
// Buscar la ruta anterior para eliminarla
if let Some(old_path) = map.id_to_path.get(id).cloned() {
map.path_to_id.remove(&old_path);
// Registrar la nueva ruta
let new_path_str = new_path.to_string();
map.path_to_id.insert(new_path_str.clone(), id.to_string());
map.id_to_path.insert(id.to_string(), new_path_str);
// Marcar como pendiente
drop(map); // Liberar el write lock antes de adquirir otro
self.mark_pending().await;
tracing::debug!("Updated path mapping for ID {}: {} -> {}",
id, old_path, new_path.to_string());
Ok(())
} else {
Err(IdMappingError::NotFound(id.to_string()))
}
}
/// Elimina un ID del mapa
pub async fn remove_id(&self, id: &str) -> Result<(), IdMappingError> {
let write_result = match time::timeout(
self.timeouts.lock_timeout(),
self.id_map.write()
).await {
Ok(guard) => guard,
Err(_) => return Err(IdMappingError::Timeout("Timeout acquiring write lock for ID removal".to_string())),
};
let mut map = write_result;
// Buscar la ruta para eliminarla
if let Some(path) = map.id_to_path.remove(id) {
map.path_to_id.remove(&path);
// Marcar como pendiente
drop(map); // Liberar el write lock antes de adquirir otro
self.mark_pending().await;
tracing::debug!("Removed ID mapping: {} -> {}", id, path);
Ok(())
} else {
Err(IdMappingError::NotFound(id.to_string()))
}
}
/// Guarda cambios pendientes al disco
pub async fn save_pending_changes(&self) -> Result<(), IdMappingError> {
// Verificar si hay cambios pendientes
{
let pending = self.pending_save.read().await;
if !*pending {
return Ok(());
}
}
// Implementar debounce para agrupación de guardados
let map_path = self.map_path.clone();
let self_clone = self.clone();
tokio::spawn(async move {
// Esperar un poco para permitir la agrupación de operaciones
time::sleep(Duration::from_millis(SAVE_DEBOUNCE_MS)).await;
if let Err(e) = self_clone.save_id_map().await {
tracing::error!("Failed to save ID map to {}: {}", map_path.display(), e);
}
});
Ok(())
}
}
#[async_trait]
impl IdMappingPort for IdMappingService {
/// Obtiene el ID para una ruta o genera uno nuevo si no existe
async fn get_or_create_id(&self, path: &StoragePath) -> Result<String, DomainError> {
self.get_or_create_id(path).await
.with_context(|| format!("Failed to get or create ID for path: {}", path.to_string()))
}
/// Obtiene una ruta por su ID con manejo de timeout
async fn get_path_by_id(&self, id: &str) -> Result<StoragePath, DomainError> {
self.get_path_by_id(id).await
.with_context(|| format!("Failed to get path for ID: {}", id))
}
/// Actualiza el mapeo de un ID existente a una nueva ruta
async fn update_path(&self, id: &str, new_path: &StoragePath) -> Result<(), DomainError> {
self.update_path(id, new_path).await
.with_context(|| format!("Failed to update path for ID: {} to {}", id, new_path.to_string()))
}
/// Elimina un ID del mapa
async fn remove_id(&self, id: &str) -> Result<(), DomainError> {
self.remove_id(id).await
.with_context(|| format!("Failed to remove ID: {}", id))
}
/// Guarda cambios pendientes al disco
async fn save_changes(&self) -> Result<(), DomainError> {
self.save_pending_changes().await
.with_context(|| "Failed to save pending ID mapping changes")
}
}
// Implementar Clone para poder usar en tokio::spawn
/// Synchronous helper for contexts where we can't use async
impl IdMappingService {
/// Create a new service synchronously (only for stubs and initialization)
#[allow(dead_code)]
pub fn new_sync(map_path: PathBuf) -> Self {
// Create a minimal implementation for initialization purposes
Self {
map_path,
id_map: RwLock::new(IdMap::default()),
save_mutex: Mutex::new(()),
timeouts: TimeoutConfig::default(),
pending_save: RwLock::new(false),
}
}
}
impl Clone for IdMappingService {
fn clone(&self) -> Self {
// No podemos clonar directamente los RwLock/Mutex,
// pero podemos crear nuevas instancias que apunten al mismo Arc interno
// Sin embargo, en este caso simplemente necesitamos la map_path
Self {
map_path: self.map_path.clone(),
id_map: RwLock::new(IdMap::default()), // Esto no se usa en el task asíncrono
save_mutex: Mutex::new(()), // Esto tampoco
timeouts: self.timeouts.clone(),
pending_save: RwLock::new(false),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[tokio::test]
async fn test_get_or_create_id() {
let temp_dir = tempdir().unwrap();
let map_path = temp_dir.path().join("id_map.json");
let service = IdMappingService::new(map_path).await.unwrap();
let path = StoragePath::from_string("/test/file.txt");
let id = service.get_or_create_id(&path).await.unwrap();
assert!(!id.is_empty(), "ID should not be empty");
// Verificar que el mismo ID se devuelve para la misma ruta
let id2 = service.get_or_create_id(&path).await.unwrap();
assert_eq!(id, id2, "Same path should return same ID");
}
#[tokio::test]
async fn test_update_path() {
let temp_dir = tempdir().unwrap();
let map_path = temp_dir.path().join("id_map.json");
let service = IdMappingService::new(map_path).await.unwrap();
let old_path = StoragePath::from_string("/test/old.txt");
let id = service.get_or_create_id(&old_path).await.unwrap();
let new_path = StoragePath::from_string("/test/new.txt");
service.update_path(&id, &new_path).await.unwrap();
let retrieved_path = service.get_path_by_id(&id).await.unwrap();
assert_eq!(retrieved_path, new_path, "Path should be updated");
}
#[tokio::test]
async fn test_save_and_load() {
let temp_dir = tempdir().unwrap();
let map_path = temp_dir.path().join("id_map.json");
// Crear y poblar el servicio
let service = IdMappingService::new(map_path.clone()).await.unwrap();
let path1 = StoragePath::from_string("/test/file1.txt");
let path2 = StoragePath::from_string("/test/file2.txt");
let id1 = service.get_or_create_id(&path1).await.unwrap();
let id2 = service.get_or_create_id(&path2).await.unwrap();
// Guardar cambios
service.save_pending_changes().await.unwrap();
// Esperar para asegurar que el guardado asíncrono termine
tokio::time::sleep(Duration::from_millis(500)).await;
// Crear un nuevo servicio que debería cargar el mismo mapa
let service2 = IdMappingService::new(map_path).await.unwrap();
// Verificar que los IDs coinciden
let loaded_id1 = service2.get_or_create_id(&path1).await.unwrap();
let loaded_id2 = service2.get_or_create_id(&path2).await.unwrap();
assert_eq!(id1, loaded_id1, "ID1 should be preserved");
assert_eq!(id2, loaded_id2, "ID2 should be preserved");
}
#[tokio::test]
async fn test_concurrent_operations() {
use futures::future::join_all;
let temp_dir = tempdir().unwrap();
let map_path = temp_dir.path().join("id_map.json");
let service = std::sync::Arc::new(IdMappingService::new(map_path).await.unwrap());
// Crear múltiples tareas que intentan acceder simultáneamente
let mut tasks = Vec::new();
for i in 0..100 {
let path = StoragePath::from_string(&format!("/test/concurrent/file{}.txt", i));
let service_clone = service.clone();
tasks.push(tokio::spawn(async move {
service_clone.get_or_create_id(&path).await
}));
}
// Esperar a que todas terminen
let results = join_all(tasks).await;
// Verificar que todas tuvieron éxito
for result in results {
assert!(result.unwrap().is_ok(), "Concurrent operations should succeed");
}
// Guardar cambios
service.save_pending_changes().await.unwrap();
}
}
+7 -1
View File
@@ -1 +1,7 @@
pub mod file_system_i18n_service;
pub mod file_system_i18n_service;
pub mod id_mapping_service;
pub mod id_mapping_optimizer;
pub mod cache_manager;
pub mod file_metadata_cache;
pub mod compression_service;
pub mod buffer_pool;