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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1
View File
@@ -1,3 +1,4 @@
pub mod file_fs_repository;
pub mod folder_fs_repository;
pub mod parallel_file_processor;
@@ -0,0 +1,525 @@
use std::path::PathBuf;
use std::sync::Arc;
use std::io::{self, SeekFrom};
use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
use tokio::task;
use tokio::sync::{Semaphore, Mutex};
use futures::future::join_all;
use tracing::{info, debug, error};
use bytes::{Bytes, BytesMut};
use crate::common::config::AppConfig;
use crate::domain::repositories::file_repository::FileRepositoryError;
use crate::infrastructure::services::buffer_pool::BufferPool;
/// Estructura para el rango de bytes a procesar
#[derive(Debug, Clone, Copy)]
pub struct ChunkRange {
/// Índice del chunk
pub index: usize,
/// Posición de inicio en bytes
pub start: u64,
/// Tamaño del chunk en bytes
pub size: usize,
}
/// Buffer pooling específico para BytesMut
pub struct BytesBufferPool {
buffers: Mutex<Vec<BytesMut>>,
buffer_size: usize,
max_buffers: usize,
}
impl BytesBufferPool {
pub fn new(buffer_size: usize, max_buffers: usize) -> Self {
Self {
buffers: Mutex::new(Vec::with_capacity(max_buffers)),
buffer_size,
max_buffers,
}
}
/// Obtener un buffer del pool o crear uno nuevo
pub async fn get_buffer(&self) -> BytesMut {
let mut buffers = self.buffers.lock().await;
if let Some(mut buffer) = buffers.pop() {
// Reutilizar buffer existente
buffer.clear(); // Mantener capacidad, limpiar contenido
buffer
} else {
// Crear nuevo buffer si el pool está vacío
BytesMut::with_capacity(self.buffer_size)
}
}
/// Devolver un buffer al pool para reutilización
pub async fn return_buffer(&self, mut buffer: BytesMut) {
// Restablece el buffer para reutilización
buffer.clear();
let mut buffers = self.buffers.lock().await;
// Solo mantener hasta max_buffers
if buffers.len() < self.max_buffers {
buffers.push(buffer);
}
// Si ya tenemos suficientes buffers, este se descartará
}
}
/// Procesador paralelo de archivos para operaciones IO intensivas
pub struct ParallelFileProcessor {
/// Configuración de la aplicación
config: AppConfig,
/// Semáforo para limitar concurrencia global
concurrency_limiter: Arc<Semaphore>,
/// Pool de buffers para optimizar memoria
buffer_pool: Option<Arc<BufferPool>>,
/// Pool de buffers BytesMut para operaciones zero-copy
bytes_pool: Arc<BytesBufferPool>,
}
impl ParallelFileProcessor {
/// Crea una nueva instancia del procesador
pub fn new(config: AppConfig) -> Self {
let concurrency_limiter = Arc::new(Semaphore::new(config.concurrency.max_concurrent_io));
// Crear pool de BytesMut para operaciones eficientes
let chunk_size = config.resources.chunk_size_bytes;
let max_chunks = config.concurrency.max_parallel_chunks;
let bytes_pool = Arc::new(BytesBufferPool::new(chunk_size, max_chunks * 2));
Self {
config,
concurrency_limiter,
buffer_pool: None,
bytes_pool,
}
}
/// Crea una nueva instancia del procesador con un pool de buffers
pub fn new_with_buffer_pool(config: AppConfig, buffer_pool: Arc<BufferPool>) -> Self {
let concurrency_limiter = Arc::new(Semaphore::new(config.concurrency.max_concurrent_io));
// Crear pool de BytesMut para operaciones eficientes
let chunk_size = config.resources.chunk_size_bytes;
let max_chunks = config.concurrency.max_parallel_chunks;
let bytes_pool = Arc::new(BytesBufferPool::new(chunk_size, max_chunks * 2));
Self {
config,
concurrency_limiter,
buffer_pool: Some(buffer_pool),
bytes_pool,
}
}
/// Divide un archivo en chunks para procesamiento paralelo
pub fn calculate_chunks(&self, file_size: u64) -> Vec<ChunkRange> {
// Determinar si el archivo necesita procesamiento paralelo
let needs_parallel = self.config.resources.needs_parallel_processing(
file_size, &self.config.concurrency
);
if !needs_parallel {
// Para archivos pequeños, usar un solo chunk
return vec![ChunkRange {
index: 0,
start: 0,
size: file_size as usize
}];
}
// Calcular número óptimo de chunks
let chunk_count = self.config.resources.calculate_optimal_chunks(
file_size, &self.config.concurrency
);
// Calcular tamaño de cada chunk
let chunk_size = self.config.resources.calculate_chunk_size(file_size, chunk_count);
// Crear los rangos de chunks
let mut chunks = Vec::with_capacity(chunk_count);
let mut start = 0;
for i in 0..chunk_count {
let current_chunk_size = if i == chunk_count - 1 {
// Último chunk puede ser más pequeño
(file_size - start) as usize
} else {
chunk_size
};
chunks.push(ChunkRange {
index: i,
start,
size: current_chunk_size,
});
start += current_chunk_size as u64;
}
debug!("File size: {} bytes, divided into {} chunks of ~{} bytes each",
file_size, chunks.len(), chunk_size);
chunks
}
/// Lee un archivo en paralelo y devuelve el contenido completo
/// Implementación optimizada usando BytesMut para reducir copias de memoria
pub async fn read_file_parallel(&self, file_path: &PathBuf) -> Result<Vec<u8>, FileRepositoryError> {
// Obtener tamaño del archivo
let metadata = tokio::fs::metadata(file_path).await
.map_err(FileRepositoryError::IoError)?;
let file_size = metadata.len();
// Verificar si el archivo es demasiado grande para memoria
if !self.config.resources.can_load_in_memory(file_size) {
return Err(FileRepositoryError::Other(
format!("File too large to load in memory: {} MB (max: {} MB)",
file_size / (1024 * 1024),
self.config.resources.max_in_memory_file_size_mb)
));
}
// Calcular chunks
let chunks = self.calculate_chunks(file_size);
if chunks.len() == 1 {
// Para un solo chunk, usar lectura simple con buffer pool si está disponible
info!("Reading file with size {}MB as a single chunk", file_size / (1024 * 1024));
if let Some(pool) = &self.buffer_pool {
// Usar buffer del pool para lectura eficiente
debug!("Using buffer pool for single chunk read");
let mut buffer = pool.get_buffer().await;
// Si el buffer es demasiado pequeño, revertir a la implementación estándar
if buffer.capacity() < file_size as usize {
debug!("Buffer from pool too small ({}), using standard read", buffer.capacity());
let content = tokio::fs::read(file_path).await
.map_err(FileRepositoryError::IoError)?;
return Ok(content);
}
// Usar el buffer de memoria del pool
let mut file = File::open(file_path).await
.map_err(FileRepositoryError::IoError)?;
let read_size = file.read(buffer.as_mut_slice()).await
.map_err(FileRepositoryError::IoError)?;
buffer.set_used(read_size);
// Convertir en Vec<u8>
let content = buffer.into_vec();
return Ok(content);
} else {
// Implementación estándar sin pool
let content = tokio::fs::read(file_path).await
.map_err(FileRepositoryError::IoError)?;
return Ok(content);
}
}
// Para múltiples chunks, usar lectura paralela
info!("Reading file with size {}MB in {} parallel chunks using BytesMut",
file_size / (1024 * 1024), chunks.len());
// Crear buffer de resultado final (pre-allocated)
let mut result = BytesMut::with_capacity(file_size as usize);
result.resize(file_size as usize, 0);
let result_mutex = Arc::new(Mutex::new(result));
// Crear tareas para cada chunk
let mut tasks = Vec::with_capacity(chunks.len());
// Abrir archivo una sola vez y compartirlo
let file = Arc::new(File::open(file_path).await
.map_err(FileRepositoryError::IoError)?);
// Referencia al pool de BytesMut
let bytes_pool = self.bytes_pool.clone();
// Procesar chunks en paralelo
for chunk in chunks {
let file_clone = file.clone();
let result_clone = result_mutex.clone();
let semaphore_clone = self.concurrency_limiter.clone();
let bytes_pool_clone = bytes_pool.clone();
// Spawn task para este chunk - no hay necesidad de copiar los datos originales
let task = task::spawn(async move {
// Adquirir permiso del semáforo
let _permit = semaphore_clone.acquire().await.unwrap();
// Obtener un buffer reusable del pool de BytesMut
let mut chunk_buffer = bytes_pool_clone.get_buffer().await;
// Asegurar que tenga suficiente capacidad
if chunk_buffer.capacity() < chunk.size {
chunk_buffer = BytesMut::with_capacity(chunk.size);
}
// Resize al tamaño exacto necesario
chunk_buffer.resize(chunk.size, 0);
// Crear un descriptor de archivo duplicado para uso independiente
let mut file_handle = file_clone.try_clone().await?;
// Posicionar y leer directamente en el BytesMut
file_handle.seek(SeekFrom::Start(chunk.start)).await?;
let bytes_read = file_handle.read_exact(&mut chunk_buffer[..chunk.size]).await?;
if bytes_read != chunk.size {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!("Expected to read {} bytes but got {}", chunk.size, bytes_read)
));
}
// Escribir en resultado final
let mut result_lock = result_clone.lock().await;
let start_pos = chunk.start as usize;
let end_pos = start_pos + chunk.size;
// Usar copy_from_slice para copiar desde BytesMut al buffer de resultado
result_lock[start_pos..end_pos].copy_from_slice(&chunk_buffer[..chunk.size]);
// Devolver el buffer al pool para su reutilización
bytes_pool_clone.return_buffer(chunk_buffer).await;
// Registrar progreso
debug!("Chunk {} processed: {} bytes from offset {}",
chunk.index, chunk.size, chunk.start);
Ok::<_, io::Error>(())
});
tasks.push(task);
}
// Esperar a que todas las tareas terminen
let results = join_all(tasks).await;
// Verificar errores
for (i, task_result) in results.into_iter().enumerate() {
match task_result {
Ok(Ok(())) => {},
Ok(Err(e)) => {
error!("Error in chunk {}: {}", i, e);
return Err(FileRepositoryError::IoError(e));
},
Err(e) => {
error!("Task error in chunk {}: {}", i, e);
return Err(FileRepositoryError::Other(format!("Task error: {}", e)));
}
}
}
// Obtener el resultado final y convertir a Vec<u8>
let result_buffer = result_mutex.lock().await;
let result_vec = result_buffer.to_vec();
info!("Successfully read file of {}MB in parallel with optimized BytesMut", file_size / (1024 * 1024));
Ok(result_vec)
}
/// Escribe un archivo en paralelo desde un buffer
/// Implementación optimizada usando BytesMut/Bytes para reducir copias de memoria
pub async fn write_file_parallel(
&self,
file_path: &PathBuf,
content: &[u8]
) -> Result<(), FileRepositoryError> {
let file_size = content.len() as u64;
// Calcular chunks
let chunks = self.calculate_chunks(file_size);
if chunks.len() == 1 {
// Para un solo chunk, usar escritura simple
info!("Writing file with size {}MB as a single chunk", file_size / (1024 * 1024));
// Implementación estándar (el buffer pooling no ofrece ventajas para escritura simple)
tokio::fs::write(file_path, content).await
.map_err(FileRepositoryError::IoError)?;
return Ok(());
}
// Para múltiples chunks, usar escritura paralela
info!("Writing file with size {}MB in {} parallel chunks using Bytes",
file_size / (1024 * 1024), chunks.len());
// Crear archivo (no usamos Mutex para reducir contención)
let file = File::create(file_path).await
.map_err(FileRepositoryError::IoError)?;
// Convertir contenido a Bytes (un solo paso de copia)
let content_bytes = Bytes::copy_from_slice(content);
// Crear tareas para cada chunk
let mut tasks = Vec::with_capacity(chunks.len());
// Procesar chunks en paralelo
for chunk in chunks {
let file_clone = file.try_clone().await
.map_err(FileRepositoryError::IoError)?;
let semaphore_clone = self.concurrency_limiter.clone();
// Crear slice de Bytes (no copia datos, solo referencia)
let start_idx = chunk.start as usize;
let end_idx = start_idx + chunk.size;
let chunk_data = content_bytes.slice(start_idx..end_idx);
// Crear y lanzar tarea
let task = task::spawn(async move {
// Adquirir permiso del semáforo
let _permit = semaphore_clone.acquire().await.unwrap();
// Posicionar y escribir
let mut file_handle = file_clone;
file_handle.seek(SeekFrom::Start(chunk.start)).await?;
file_handle.write_all(&chunk_data).await?;
// Registrar progreso
debug!("Chunk {} written: {} bytes at offset {}",
chunk.index, chunk.size, chunk.start);
Ok::<_, io::Error>(())
});
tasks.push(task);
}
// Esperar a que todas las tareas terminen
let results = join_all(tasks).await;
// Verificar errores
for (i, task_result) in results.into_iter().enumerate() {
match task_result {
Ok(Ok(())) => {},
Ok(Err(e)) => {
error!("Error in chunk {}: {}", i, e);
return Err(FileRepositoryError::IoError(e));
},
Err(e) => {
error!("Task error in chunk {}: {}", i, e);
return Err(FileRepositoryError::Other(format!("Task error: {}", e)));
}
}
}
// Garantizar que todo se ha escrito correctamente
let mut file_handle = file;
file_handle.flush().await.map_err(FileRepositoryError::IoError)?;
info!("Successfully wrote file of {}MB in parallel with optimized Bytes", file_size / (1024 * 1024));
Ok(())
}
/// Escribe un chunk en un archivo en una posición específica
#[allow(dead_code)]
async fn write_chunk_optimized(
file: &mut File,
offset: u64,
data: Bytes
) -> Result<(), std::io::Error> {
// Preparar la escritura en la posición correcta
file.seek(SeekFrom::Start(offset)).await?;
// Escribir datos sin copias adicionales
file.write_all(&data).await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[tokio::test]
async fn test_parallel_read_write() {
// Crear configuración con umbral bajo para testing
let mut config = AppConfig::default();
config.concurrency.min_size_for_parallel_chunks_mb = 1; // 1MB para testing
config.concurrency.max_parallel_chunks = 4;
let processor = ParallelFileProcessor::new(config);
// Crear directorio temporal
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("test_file.bin");
// Crear datos de prueba (2MB)
let size = 2 * 1024 * 1024;
let mut test_data = Vec::with_capacity(size);
for i in 0..size {
test_data.push((i % 256) as u8);
}
// Escribir archivo en paralelo
processor.write_file_parallel(&file_path, &test_data).await.unwrap();
// Leer archivo en paralelo
let read_data = processor.read_file_parallel(&file_path).await.unwrap();
// Verificar que los datos son idénticos
assert_eq!(test_data.len(), read_data.len());
assert_eq!(test_data, read_data);
}
#[tokio::test]
async fn test_bytesmut_pool() {
// Crear pool
let pool = BytesBufferPool::new(1024, 5);
// Obtener buffer
let mut buffer1 = pool.get_buffer().await;
buffer1.put_slice(b"test data");
assert_eq!(&buffer1[..9], b"test data");
// Devolver buffer al pool
pool.return_buffer(buffer1).await;
// Obtener otro buffer (debería ser el mismo)
let buffer2 = pool.get_buffer().await;
assert_eq!(buffer2.capacity(), 1024);
// El buffer debería estar vacío (clear)
assert_eq!(buffer2.len(), 0);
}
#[test]
fn test_chunk_calculation() {
// Crear configuración de prueba
let mut config = AppConfig::default();
config.concurrency.min_size_for_parallel_chunks_mb = 100; // 100MB
config.concurrency.max_parallel_chunks = 4;
config.concurrency.parallel_chunk_size_bytes = 50 * 1024 * 1024; // 50MB
let processor = ParallelFileProcessor::new(config);
// Archivo pequeño (10MB)
let small_file_size = 10 * 1024 * 1024;
let chunks = processor.calculate_chunks(small_file_size);
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0].size as u64, small_file_size);
// Archivo grande (300MB)
let large_file_size = 300 * 1024 * 1024;
let chunks = processor.calculate_chunks(large_file_size);
assert_eq!(chunks.len(), 4); // Limitado a max_parallel_chunks
// Verificar que todos los chunks suman el tamaño total
let total_size: u64 = chunks.iter().map(|c| c.size as u64).sum();
assert_eq!(total_size, large_file_size);
}
}
+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;