perf: add TTL/TTI to content cache and invalidate on file mutations
This commit is contained in:
@@ -9,6 +9,7 @@ use crate::common::errors::DomainError;
|
||||
use crate::infrastructure::repositories::pg::file_blob_read_repository::FileBlobReadRepository;
|
||||
use crate::infrastructure::repositories::pg::file_blob_write_repository::FileBlobWriteRepository;
|
||||
use crate::infrastructure::repositories::pg::folder_db_repository::FolderDbRepository;
|
||||
use crate::infrastructure::services::file_content_cache::FileContentCache;
|
||||
use crate::infrastructure::services::thumbnail_service::ThumbnailService;
|
||||
use tracing::{error, info, warn};
|
||||
use uuid::Uuid;
|
||||
@@ -25,6 +26,7 @@ pub struct FileManagementService {
|
||||
folder_repo: Option<Arc<FolderDbRepository>>,
|
||||
trash_service: Option<Arc<TrashService>>,
|
||||
thumbnail_service: Option<Arc<ThumbnailService>>,
|
||||
content_cache: Option<Arc<FileContentCache>>,
|
||||
}
|
||||
|
||||
impl FileManagementService {
|
||||
@@ -36,6 +38,7 @@ impl FileManagementService {
|
||||
folder_repo: None,
|
||||
trash_service: None,
|
||||
thumbnail_service: None,
|
||||
content_cache: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +49,7 @@ impl FileManagementService {
|
||||
file_read: Option<Arc<FileBlobReadRepository>>,
|
||||
folder_repo: Option<Arc<FolderDbRepository>>,
|
||||
thumbnail_service: Option<Arc<ThumbnailService>>,
|
||||
content_cache: Option<Arc<FileContentCache>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
file_repository,
|
||||
@@ -53,6 +57,7 @@ impl FileManagementService {
|
||||
folder_repo,
|
||||
trash_service,
|
||||
thumbnail_service,
|
||||
content_cache,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +221,10 @@ impl FileManagementUseCase for FileManagementService {
|
||||
|
||||
async fn delete_file(&self, id: &str) -> Result<(), DomainError> {
|
||||
self.file_repository.delete_file(id).await?;
|
||||
// Invalidate content cache — file no longer exists.
|
||||
if let Some(cc) = &self.content_cache {
|
||||
cc.invalidate(id).await;
|
||||
}
|
||||
// Best-effort thumbnail cleanup
|
||||
if let Some(thumb) = &self.thumbnail_service
|
||||
&& let Err(e) = thumb.delete_thumbnails(id).await
|
||||
@@ -243,6 +252,10 @@ impl FileManagementUseCase for FileManagementService {
|
||||
match trash.move_to_trash(id, "file", user_id).await {
|
||||
Ok(_) => {
|
||||
info!("File successfully moved to trash: {}", id);
|
||||
// Invalidate content cache — trashed files must not be served.
|
||||
if let Some(cc) = &self.content_cache {
|
||||
cc.invalidate(id).await;
|
||||
}
|
||||
// Do NOT decrement blob ref here — the file row still exists
|
||||
// (is_trashed = TRUE). The trigger will decrement when the
|
||||
// row is actually DELETEd during trash emptying.
|
||||
@@ -261,6 +274,10 @@ impl FileManagementUseCase for FileManagementService {
|
||||
// Step 2: Permanent delete — trigger handles blob ref_count
|
||||
warn!("Permanently deleting file: {}", id);
|
||||
self.file_repository.delete_file(id).await?;
|
||||
// Invalidate content cache — file permanently removed.
|
||||
if let Some(cc) = &self.content_cache {
|
||||
cc.invalidate(id).await;
|
||||
}
|
||||
// Best-effort thumbnail cleanup
|
||||
if let Some(thumb) = &self.thumbnail_service
|
||||
&& let Err(e) = thumb.delete_thumbnails(id).await
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::application::services::storage_usage_service::StorageUsageService;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::infrastructure::repositories::pg::FileBlobReadRepository;
|
||||
use crate::infrastructure::repositories::pg::FileBlobWriteRepository;
|
||||
use crate::infrastructure::services::file_content_cache::FileContentCache;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
/// Helper function to extract username from folder path string.
|
||||
@@ -48,6 +49,8 @@ pub struct FileUploadService {
|
||||
file_read: Option<Arc<FileBlobReadRepository>>,
|
||||
/// Optional storage usage tracking
|
||||
storage_usage_service: Option<Arc<StorageUsageService>>,
|
||||
/// Content cache — invalidated on file update so stale content is never served.
|
||||
content_cache: Option<Arc<FileContentCache>>,
|
||||
}
|
||||
|
||||
impl FileUploadService {
|
||||
@@ -57,6 +60,7 @@ impl FileUploadService {
|
||||
file_write: file_repository,
|
||||
file_read: None,
|
||||
storage_usage_service: None,
|
||||
content_cache: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,9 +73,16 @@ impl FileUploadService {
|
||||
file_write,
|
||||
file_read: Some(file_read),
|
||||
storage_usage_service: None,
|
||||
content_cache: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Configures the content cache for invalidation on file updates.
|
||||
pub fn with_content_cache(mut self, cache: Arc<FileContentCache>) -> Self {
|
||||
self.content_cache = Some(cache);
|
||||
self
|
||||
}
|
||||
|
||||
/// Configures the storage usage service
|
||||
pub fn with_storage_usage_service(
|
||||
mut self,
|
||||
@@ -279,6 +290,10 @@ impl FileUploadUseCase for FileUploadService {
|
||||
modified_at,
|
||||
)
|
||||
.await?;
|
||||
// Invalidate content cache — file content has changed.
|
||||
if let Some(cc) = &self.content_cache {
|
||||
cc.invalidate(&file_id).await;
|
||||
}
|
||||
// Re-read to get fresh DTO with updated etag and timestamps.
|
||||
let updated = file_read.get_file(&file_id).await?;
|
||||
return Ok(FileDto::from(updated));
|
||||
|
||||
@@ -16,6 +16,7 @@ use crate::infrastructure::repositories::pg::file_blob_read_repository::FileBlob
|
||||
use crate::infrastructure::repositories::pg::file_blob_write_repository::FileBlobWriteRepository;
|
||||
use crate::infrastructure::repositories::pg::folder_db_repository::FolderDbRepository;
|
||||
use crate::infrastructure::repositories::pg::trash_db_repository::TrashDbRepository;
|
||||
use crate::infrastructure::services::file_content_cache::FileContentCache;
|
||||
use crate::infrastructure::services::thumbnail_service::ThumbnailService;
|
||||
|
||||
/**
|
||||
@@ -47,6 +48,9 @@ pub struct TrashService {
|
||||
/// Thumbnail service for cleaning up thumbnails on permanent delete
|
||||
thumbnail_service: Option<Arc<ThumbnailService>>,
|
||||
|
||||
/// Content cache — invalidated when files are permanently deleted from trash.
|
||||
content_cache: Option<Arc<FileContentCache>>,
|
||||
|
||||
/// Number of days items should be kept in trash before automatic cleanup
|
||||
retention_days: u32,
|
||||
}
|
||||
@@ -59,6 +63,7 @@ impl TrashService {
|
||||
folder_storage_port: Arc<FolderDbRepository>,
|
||||
retention_days: u32,
|
||||
thumbnail_service: Option<Arc<ThumbnailService>>,
|
||||
content_cache: Option<Arc<FileContentCache>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
trash_repository,
|
||||
@@ -66,6 +71,7 @@ impl TrashService {
|
||||
file_write_port,
|
||||
folder_storage_port,
|
||||
thumbnail_service,
|
||||
content_cache,
|
||||
retention_days,
|
||||
}
|
||||
}
|
||||
@@ -563,6 +569,10 @@ impl TrashUseCase for TrashService {
|
||||
match self.file_write_port.delete_file_permanently(&file_id).await {
|
||||
Ok(_) => {
|
||||
info!("Successfully deleted file permanently: {}", file_id);
|
||||
// Invalidate content cache for the deleted file.
|
||||
if let Some(cc) = &self.content_cache {
|
||||
cc.invalidate(&file_id).await;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// Check if the file is not found - in that case, we can continue
|
||||
@@ -715,6 +725,13 @@ impl TrashUseCase for TrashService {
|
||||
// Finally it clears the trash_items index for the user.
|
||||
self.trash_repository.clear_trash(&user_id).await?;
|
||||
|
||||
// Invalidate content cache for all permanently deleted files.
|
||||
if let Some(cc) = &self.content_cache {
|
||||
for file_id in &trashed_file_ids {
|
||||
cc.invalidate(file_id).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Best-effort thumbnail cleanup for all deleted files
|
||||
if let Some(thumb) = &self.thumbnail_service {
|
||||
for file_id in &trashed_file_ids {
|
||||
|
||||
+7
-2
@@ -250,10 +250,13 @@ impl AppServiceFactory {
|
||||
|
||||
// Refactored services with all infrastructure ports
|
||||
// In blob model, dedup is handled by the repository — no separate write-behind needed
|
||||
let file_upload_service = Arc::new(FileUploadService::new_with_read(
|
||||
let file_upload_service = Arc::new(
|
||||
FileUploadService::new_with_read(
|
||||
repos.file_write_repository.clone(),
|
||||
repos.file_read_repository.clone(),
|
||||
));
|
||||
)
|
||||
.with_content_cache(core.file_content_cache.clone()),
|
||||
);
|
||||
|
||||
let file_retrieval_service = Arc::new(FileRetrievalService::new_with_cache(
|
||||
repos.file_read_repository.clone(),
|
||||
@@ -268,6 +271,7 @@ impl AppServiceFactory {
|
||||
Some(repos.file_read_repository.clone()),
|
||||
Some(repos.folder_repository.clone()),
|
||||
Some(core.thumbnail_service.clone()),
|
||||
Some(core.file_content_cache.clone()),
|
||||
));
|
||||
|
||||
let file_use_case_factory = Arc::new(AppFileUseCaseFactory::new(
|
||||
@@ -343,6 +347,7 @@ impl AppServiceFactory {
|
||||
repos.folder_repository.clone(),
|
||||
self.config.storage.trash_retention_days,
|
||||
Some(core.thumbnail_service.clone()),
|
||||
Some(core.file_content_cache.clone()),
|
||||
));
|
||||
|
||||
// Initialize cleanup service (bulk-deletes expired items in 2 SQL queries)
|
||||
|
||||
@@ -2,6 +2,7 @@ use bytes::Bytes;
|
||||
use moka::future::Cache;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, info};
|
||||
|
||||
/// Configuration for the file content cache
|
||||
@@ -73,6 +74,8 @@ impl FileContentCache {
|
||||
// of weights exceeds max_capacity.
|
||||
value.content.len().min(u32::MAX as usize) as u32
|
||||
})
|
||||
.time_to_live(Duration::from_secs(3600))
|
||||
.time_to_idle(Duration::from_secs(300))
|
||||
.build();
|
||||
|
||||
Self {
|
||||
|
||||
Reference in New Issue
Block a user