perf: bulk-delete expired trash in 2 SQL queries instead of N+1 loop

This commit is contained in:
Dionisio
2026-02-26 00:42:41 +01:00
parent fb4eaf2cf9
commit 7e278a0b32
5 changed files with 66 additions and 74 deletions
@@ -3,25 +3,25 @@ use std::time::Duration;
use tokio::time;
use tracing::{debug, error, info, instrument};
use crate::application::ports::trash_ports::TrashUseCase;
use crate::common::errors::Result;
use crate::domain::repositories::trash_repository::TrashRepository;
/// Service for automatic cleanup of expired items in the trash
/// Service for automatic cleanup of expired items in the trash.
///
/// Uses `TrashRepository::delete_expired_bulk` to purge all expired items
/// in **2 SQL statements inside a single transaction**, instead of the
/// previous N+1 pattern that issued 3 queries per expired item.
pub struct TrashCleanupService {
trash_service: Arc<dyn TrashUseCase>,
trash_repository: Arc<dyn TrashRepository>,
cleanup_interval_hours: u64,
}
impl TrashCleanupService {
pub fn new(
trash_service: Arc<dyn TrashUseCase>,
trash_repository: Arc<dyn TrashRepository>,
cleanup_interval_hours: u64,
) -> Self {
Self {
trash_service,
trash_repository,
cleanup_interval_hours: cleanup_interval_hours.max(1), // Minimum 1 hour
}
@@ -31,7 +31,6 @@ impl TrashCleanupService {
#[instrument(skip(self))]
pub async fn start_cleanup_job(&self) {
let trash_repository = self.trash_repository.clone();
let trash_service = self.trash_service.clone();
let interval_hours = self.cleanup_interval_hours;
info!(
@@ -44,7 +43,7 @@ impl TrashCleanupService {
let mut interval = time::interval(interval_duration);
// First immediate execution
Self::cleanup_expired_items(trash_repository.clone(), trash_service.clone())
Self::cleanup_expired_items(trash_repository.clone())
.await
.unwrap_or_else(|e| error!("Error in initial trash cleanup: {:?}", e));
@@ -52,50 +51,31 @@ impl TrashCleanupService {
interval.tick().await;
debug!("Running scheduled trash cleanup task");
if let Err(e) =
Self::cleanup_expired_items(trash_repository.clone(), trash_service.clone())
.await
{
if let Err(e) = Self::cleanup_expired_items(trash_repository.clone()).await {
error!("Error in scheduled trash cleanup: {:?}", e);
}
}
});
}
/// Cleans up expired items in the trash
#[instrument(skip(trash_repository, trash_service))]
/// Bulk-delete all expired trash items in a single transaction.
#[instrument(skip(trash_repository))]
async fn cleanup_expired_items(
trash_repository: Arc<dyn TrashRepository>,
trash_service: Arc<dyn TrashUseCase>,
) -> Result<()> {
debug!("Starting cleanup of expired items in the trash");
debug!("Starting bulk cleanup of expired trash items");
// Get all expired items
let expired_items = trash_repository.get_expired_items().await?;
let (files, folders) = trash_repository.delete_expired_bulk().await?;
if expired_items.is_empty() {
if files == 0 && folders == 0 {
debug!("No expired items to clean up");
return Ok(());
} else {
info!(
"Trash cleanup completed: {} files + {} folders purged",
files, folders
);
}
info!("Found {} expired items to delete", expired_items.len());
// Delete each expired item
for item in expired_items {
let trash_id = item.id().to_string();
let user_id = item.user_id().to_string();
debug!("Deleting expired item: id={}, user={}", trash_id, user_id);
// If a deletion fails, continue with the rest
if let Err(e) = trash_service.delete_permanently(&trash_id, &user_id).await {
error!("Error deleting expired item {}: {:?}", trash_id, e);
} else {
debug!("Expired item deleted successfully: {}", trash_id);
}
}
info!("Trash cleanup completed");
Ok(())
}
}