a / api /src /cleanup.rs
FTA7700
Add cache restart-persistence and 7-day cleanup job
1ad3b12
Raw History Blame Contribute Delete
4.78 kB
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, ActiveValue::Set};
use crate::entity::{available_ratings, cleanup_state, image_meta};
use crate::AppState;
const CLEANUP_INTERVAL_SECS: i64 = 7 * 24 * 3600;
fn now_secs() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64
}
/// Checks whether a cleanup run is overdue (last run more than 7 days ago,
/// or never run) and if so, runs it. Called periodically from a background
/// task. Reading a persisted timestamp (rather than an in-memory one) means
/// a run that was missed while the container was asleep/restarted still
/// happens promptly on the next check after waking, instead of being
/// silently skipped.
pub async fn run_if_due(state: &AppState) {
let now = now_secs();
let last_run = match cleanup_state::Entity::find_by_id(true).one(&state.db).await {
Ok(Some(row)) => row.last_run,
Ok(None) => None,
Err(e) => {
tracing::warn!(error = %e, "failed to read cleanup_state, skipping this check");
return;
}
};
let due = match last_run {
Some(t) => now - t >= CLEANUP_INTERVAL_SECS,
None => true,
};
if !due {
return;
}
run_cleanup(state, false).await;
let model = cleanup_state::ActiveModel {
id: Set(true),
last_run: Set(Some(now)),
};
if let Err(e) = cleanup_state::Entity::insert(model)
.on_conflict(
sea_orm::sea_query::OnConflict::column(cleanup_state::Column::Id)
.update_columns([cleanup_state::Column::LastRun])
.to_owned(),
)
.exec(&state.db)
.await
{
tracing::warn!(error = %e, "failed to update cleanup_state.last_run");
}
}
/// Deletes image_meta and available_ratings rows older than 7 days, and
/// purges the corresponding Cloudflare Cache-Tags. Lazy: does not
/// re-render anything, the next real request for a purged title is simply
/// treated as a cache miss. In dry_run mode, only logs what would be
/// deleted.
pub async fn run_cleanup(state: &AppState, dry_run: bool) {
let cutoff = now_secs() - CLEANUP_INTERVAL_SECS;
let expired_images = match image_meta::Entity::find()
.filter(image_meta::Column::UpdatedAt.lt(cutoff))
.all(&state.db)
.await
{
Ok(rows) => rows,
Err(e) => {
tracing::warn!(error = %e, "cleanup: failed to query expired image_meta rows");
return;
}
};
if expired_images.is_empty() {
tracing::info!("cleanup: no expired image_meta rows found");
return;
}
tracing::info!(count = expired_images.len(), dry_run, "cleanup: expired image_meta rows found");
if dry_run {
for row in &expired_images {
tracing::info!(cache_key = %row.cache_key, "cleanup (dry run): would delete");
}
return;
}
// Cloudflare tags are keyed "id:{id_type}:{id_value}" derived from the
// cache_key prefix (before the settings suffix), matching the format
// purge_cloudflare_tags() already expects.
let prefixes: Vec<String> = expired_images
.iter()
.filter_map(|row| {
let parts: Vec<&str> = row.cache_key.splitn(2, '/').collect();
if parts.len() == 2 {
let id_part = parts[1].split(|c| c == '@' || c == '.').next().unwrap_or(parts[1]);
Some(format!("{}/{}", parts[0], id_part))
} else {
None
}
})
.collect();
match crate::services::stremio_account::purge_cloudflare_tags(&state.http, &prefixes).await {
Ok(n) => tracing::info!(purged = n, "cleanup: Cloudflare tags purged"),
Err(e) => tracing::warn!(error = %e, "cleanup: Cloudflare purge failed, continuing with Postgres delete anyway"),
}
let cache_keys: Vec<String> = expired_images.iter().map(|r| r.cache_key.clone()).collect();
match image_meta::Entity::delete_many()
.filter(image_meta::Column::CacheKey.is_in(cache_keys))
.exec(&state.db)
.await
{
Ok(res) => tracing::info!(deleted = res.rows_affected, "cleanup: image_meta rows deleted"),
Err(e) => tracing::warn!(error = %e, "cleanup: failed to delete expired image_meta rows"),
}
match available_ratings::Entity::delete_many()
.filter(available_ratings::Column::UpdatedAt.lt(cutoff))
.exec(&state.db)
.await
{
Ok(res) => tracing::info!(deleted = res.rows_affected, "cleanup: available_ratings rows deleted"),
Err(e) => tracing::warn!(error = %e, "cleanup: failed to delete expired available_ratings rows"),
}
}