feat(job-registry): wire engine + wire trash cleaner
This commit is contained in:
@@ -6,7 +6,9 @@ use tracing::{debug, error, info, instrument};
|
||||
use crate::common::errors::Result;
|
||||
use crate::domain::repositories::trash_repository::TrashRepository;
|
||||
use crate::infrastructure::repositories::pg::trash_db_repository::TrashDbRepository;
|
||||
use crate::infrastructure::scheduler::{JobHandler, JobOutcome};
|
||||
use crate::infrastructure::services::dedup_service::DedupService;
|
||||
use async_trait::async_trait;
|
||||
|
||||
/// Service for automatic cleanup of expired items in the trash.
|
||||
///
|
||||
@@ -27,6 +29,8 @@ pub struct TrashCleanupService {
|
||||
}
|
||||
|
||||
impl TrashCleanupService {
|
||||
pub const JOB_NAME: &'static str = "trash_cleanup";
|
||||
|
||||
pub fn new(
|
||||
trash_repository: Arc<TrashDbRepository>,
|
||||
dedup_service: Arc<DedupService>,
|
||||
@@ -39,6 +43,12 @@ impl TrashCleanupService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Registered interval as a `Duration` — helper for DI wiring so
|
||||
/// the composition root doesn't reinvent the `hours × 3600` cast.
|
||||
pub fn interval(&self) -> Duration {
|
||||
Duration::from_secs(self.cleanup_interval_hours * 3600)
|
||||
}
|
||||
|
||||
/// Starts the periodic cleanup job
|
||||
#[instrument(skip(self))]
|
||||
pub async fn start_cleanup_job(&self) {
|
||||
@@ -107,4 +117,69 @@ impl TrashCleanupService {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// One-shot execution used by both the legacy `start_cleanup_job`
|
||||
/// timer AND the new `JobHandler::run` path. Returns the counts a
|
||||
/// caller can turn into either a log line (legacy) or a `JobOutcome`
|
||||
/// (scheduler).
|
||||
async fn run_once(&self) -> Result<TrashCleanupStats> {
|
||||
let (files, folders) = self.trash_repository.delete_expired_bulk().await?;
|
||||
// GC failure is non-fatal — the expiry itself succeeded. Report
|
||||
// reclaimed bytes when possible; log + swallow otherwise.
|
||||
let (gc_items, gc_bytes) = match self.dedup_service.garbage_collect().await {
|
||||
Ok(pair) => pair,
|
||||
Err(e) => {
|
||||
error!("Trash cleanup GC failed: {:?}", e);
|
||||
(0, 0)
|
||||
}
|
||||
};
|
||||
Ok(TrashCleanupStats {
|
||||
files_purged: files,
|
||||
folders_purged: folders,
|
||||
gc_items,
|
||||
gc_bytes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Structured counters for one trash-cleanup sweep. Consumed by the
|
||||
/// scheduler's `JobHandler::run` to shape `JobOutcome::Ok.extra`.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct TrashCleanupStats {
|
||||
files_purged: u64,
|
||||
folders_purged: u64,
|
||||
gc_items: u64,
|
||||
gc_bytes: u64,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl JobHandler for TrashCleanupService {
|
||||
fn name(&self) -> &str {
|
||||
Self::JOB_NAME
|
||||
}
|
||||
|
||||
/// Runs one bulk-delete-expired + GC sweep. `count` on the returned
|
||||
/// `JobOutcome::Ok` is the total number of rows this tick removed
|
||||
/// from the trash (files + folders); `extra` carries GC reclaim
|
||||
/// counts so operators can see "how much did this actually free."
|
||||
///
|
||||
/// Failure of the trash sweep itself → `Err`. GC failure alone is
|
||||
/// non-fatal and stays logged only.
|
||||
async fn run(&self) -> JobOutcome {
|
||||
match self.run_once().await {
|
||||
Ok(stats) => {
|
||||
let removed = stats.files_purged + stats.folders_purged;
|
||||
JobOutcome::ok_with(
|
||||
removed,
|
||||
serde_json::json!({
|
||||
"files_purged": stats.files_purged,
|
||||
"folders_purged": stats.folders_purged,
|
||||
"gc_items": stats.gc_items,
|
||||
"gc_bytes": stats.gc_bytes,
|
||||
}),
|
||||
)
|
||||
}
|
||||
Err(e) => JobOutcome::Err(format!("trash cleanup failed: {e}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user