//! Housekeeping that runs on its own (hub spec ยง17.2): the nightly //! garbage collection of every repository, one at a time under its //! write lock, so a push never races a repack. `oxygenhub gc` runs the //! same pass by hand. use crate::db; use crate::repos::RepoName; use crate::Shared; /// What one repository's gc did, for the log and `oxygenhub gc`. #[derive(Clone, Debug)] pub struct Done { pub repo: String, pub failed: bool, pub summary: String, } impl std::fmt::Display for Done { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}: {}", self.repo, self.summary) } } /// Garbage-collect every repository, in turn. A repository that fails is /// reported and the rest still run. pub async fn gc_all(app: &Shared) -> Vec { let rows = match db::repos::list(&app.db).await { Ok(rows) => rows, Err(e) => { tracing::warn!("gc: listing repositories: {e}"); return Vec::new(); } }; let mut done = Vec::with_capacity(rows.len()); for row in rows { let slug = format!("{}/{}", row.owner, row.name); let Some(name) = RepoName::new(&row.owner, &row.name) else { continue }; if !app.repos.exists(&name) { continue; } let label = slug.clone(); // Flow's artifacts and reports' files are named only in the // database: kept as roots, or gc would prune them after the grace // period (#126, #127). A listing that fails skips this repository // rather than risk them. let listed = match db::flow::artifact_blobs(&app.db, row.id).await { Ok(mut blobs) => crate::reports::file_blobs(&app.db, row.id).await.map(|files| { blobs.extend(files); blobs }), Err(e) => Err(e), }; let keep: Vec = match listed { Ok(blobs) => blobs.iter().filter_map(|b| oxy_core::hash::ObjectRef::from_hex(b)).collect(), Err(e) => { tracing::warn!("gc: {slug}: listing its artifacts: {e}; skipped"); continue; } }; let started = std::time::Instant::now(); let result = app .repos .write(&name, move |repo, _| Ok(repo.gc_keeping(false, &keep, &mut |phase| tracing::debug!("gc {label}: {phase}"))?)) .await; let d = match result { Ok(r) => Done { summary: format!( "{} reachable; pruned {} ({} bytes){}; packed {} into one pack in {:.1} s", r.reachable, r.pruned.objects, r.pruned.bytes, if r.pruned.deferred > 0 { format!(", {} deferred by the grace period", r.pruned.deferred) } else { String::new() }, r.packed.objects, started.elapsed().as_secs_f64() ), repo: slug, failed: false, }, Err(e) => Done { summary: format!("failed: {e}"), repo: slug, failed: true }, }; if d.failed { tracing::warn!("gc {}", d); } else { tracing::info!("gc {}", d); } done.push(d); } done } /// Run [`gc_all`] once a day at `retention.gc_hour` UTC. pub async fn nightly(app: Shared) { let hour = app.config.retention.gc_hour; if hour >= 24 { tracing::info!("gc: the nightly run is off (retention.gc_hour = {hour})"); return; } let mut last_day = None; loop { tokio::time::sleep(std::time::Duration::from_secs(60)).await; let now = time::OffsetDateTime::now_utc(); if u32::from(now.hour()) != hour { continue; } let day = now.date(); if last_day == Some(day) { continue; } last_day = Some(day); let done = gc_all(&app).await; tracing::info!("gc: nightly run over {} repositories, {} failed", done.len(), done.iter().filter(|d| d.failed).count()); } }