use std::time::Duration;
use mockforge_registry_core::models::Snapshot;
use sqlx::PgPool;
use tracing::{debug, error, info};
use crate::storage::PluginStorage;
const TICK_INTERVAL: Duration = Duration::from_secs(15 * 60);
const BATCH_LIMIT: i64 = 100;
pub fn start_snapshot_retention_worker(pool: PgPool, storage: PluginStorage) {
info!(
"snapshot retention worker started — ticking every {}s, batch={}",
TICK_INTERVAL.as_secs(),
BATCH_LIMIT
);
tokio::spawn(async move {
let mut interval = tokio::time::interval(TICK_INTERVAL);
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
interval.tick().await;
loop {
interval.tick().await;
if let Err(e) = run_tick(&pool, &storage).await {
error!(error = %e, "snapshot retention tick failed");
}
}
});
}
pub async fn run_tick(pool: &PgPool, storage: &PluginStorage) -> sqlx::Result<u32> {
let expired = Snapshot::mark_expired_batch(pool, BATCH_LIMIT).await?;
if expired.is_empty() {
debug!("snapshot retention tick: nothing to expire");
return Ok(0);
}
let count = expired.len();
info!(count, "snapshot retention: marked snapshots expired");
let mut reclaimed = 0u32;
for snapshot in &expired {
let url = snapshot.storage_url.as_deref().unwrap_or("");
if url.starts_with("inline-manifest://") {
continue;
}
match storage.delete_snapshot_blob(snapshot.workspace_id, snapshot.id).await {
Ok(_) => reclaimed += 1,
Err(e) => {
tracing::warn!(
snapshot_id = %snapshot.id,
error = %e,
"snapshot blob delete failed; row stayed expired",
);
}
}
}
if reclaimed > 0 {
info!(reclaimed, "snapshot retention: blobs reclaimed");
}
Ok(count as u32)
}
#[cfg(test)]
mod tests {
#[test]
fn smoke_module_links() {}
}