Files
Oxicloud/src/infrastructure/services/id_mapping_service.rs
T
2025-03-26 18:33:22 +01:00

586 lines
22 KiB
Rust

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};
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),
})
}
/// Crea un servicio de mapeo de IDs en memoria (para pruebas)
///
/// Similar functionality as new_in_memory but with a simpler signature for dummy use
pub fn dummy() -> Self {
Self {
map_path: PathBuf::from("/tmp/dummy_id_map.json"),
id_map: RwLock::new(IdMap::default()),
save_mutex: Mutex::new(()),
timeouts: TimeoutConfig::default(),
pending_save: RwLock::new(false),
}
}
/// Crea un servicio de mapeo de IDs en memoria (para pruebas - versión original)
pub fn new_in_memory() -> Self {
Self {
map_path: PathBuf::from("memory"),
id_map: RwLock::new(IdMap::default()),
save_mutex: Mutex::new(()),
timeouts: TimeoutConfig::default(),
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
.map_err(|_| DomainError::timeout("IdMapping", format!("Timeout reading ID map from {}", map_path.display())))?;
let content = read_result.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to read ID map from {}: {}", map_path.display(), e)))?;
// 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());
}
tracing::info!("Creating new empty map after error");
return Ok(IdMap {
path_to_id: HashMap::new(),
id_to_path: HashMap::new(),
version: 1, // Iniciar con versión 1
});
}
}
}
// Devolver un mapa vacío si el archivo no existe y crear el archivo
tracing::info!("No existing ID map found, creating new empty map");
let empty_map = IdMap {
path_to_id: HashMap::new(),
id_to_path: HashMap::new(),
version: 1, // Iniciar con versión 1
};
// Ensure directory exists
if let Some(parent) = map_path.parent() {
if !parent.exists() {
if let Err(e) = fs::create_dir_all(parent).await {
tracing::error!("Failed to create directory for ID map: {}", e);
}
}
}
// Write empty map to file
match serde_json::to_string_pretty(&empty_map) {
Ok(json) => {
if let Err(e) = fs::write(map_path, json).await {
tracing::error!("Failed to write initial empty ID map: {}", e);
} else {
tracing::info!("Created initial empty ID map at {}", map_path.display());
}
},
Err(e) => {
tracing::error!("Failed to serialize empty ID map: {}", e);
}
}
Ok(empty_map)
}
/// 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
.map_err(|_| DomainError::timeout("IdMapping", "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
.map_err(|_| DomainError::timeout("IdMapping", "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)
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to serialize ID map to JSON: {}", e)))?
};
// 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
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to write temporary ID map to {}: {}", temp_path.display(), e)))?;
// Realizar el rename atómico
fs::rename(&temp_path, &self.map_path).await
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to rename temporary ID map to {}: {}", self.map_path.display(), e)))?;
// 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
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to get or create ID for path: {}: {}", path.to_string(), e)))
}
/// 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
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to get path for ID: {}: {}", id, e)))
}
/// 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
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to update path for ID: {} to {}: {}", id, new_path.to_string(), e)))
}
/// Elimina un ID del mapa
async fn remove_id(&self, id: &str) -> Result<(), DomainError> {
self.remove_id(id).await
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to remove ID: {}: {}", id, e)))
}
/// Guarda cambios pendientes al disco
async fn save_changes(&self) -> Result<(), DomainError> {
self.save_pending_changes().await
.map_err(|e| DomainError::internal_error("IdMapping", format!("Failed to save pending ID mapping changes: {}", e)))
}
}
// The extension methods were moved to the IdMappingPort trait as default implementations
// 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();
}
}