OXYGENHUB
# 62cdf34daec1 oxygenhub / crates / hub / src / maintenance.rs
4.3 KiB · Rust · last changed Reports retention: files beyond the newest 50 runs off the default bookmark expire 24 days ago source blame history raw
1
//! Housekeeping that runs on its own (hub spec §17.2): the nightly
2
//! garbage collection of every repository, one at a time under its
3
//! write lock, so a push never races a repack. `oxygenhub gc` runs the
4
//! same pass by hand.
5
6
use crate::db;
7
use crate::repos::RepoName;
8
use crate::Shared;
9
10
/// What one repository's gc did, for the log and `oxygenhub gc`.
11
#[derive(Clone, Debug)]
12
pub struct Done {
13
    pub repo: String,
14
    pub failed: bool,
15
    pub summary: String,
16
}
17
18
impl std::fmt::Display for Done {
19
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20
        write!(f, "{}: {}", self.repo, self.summary)
21
    }
22
}
23
24
/// Garbage-collect every repository, in turn. A repository that fails is
25
/// reported and the rest still run.
26
pub async fn gc_all(app: &Shared) -> Vec<Done> {
27
    // Report files past retention first (#127), so this run prunes them.
28
    match crate::reports::expire(&app.db).await {
29
        Ok(0) => {}
30
        Ok(n) => tracing::info!("gc: {n} reports' files expired under retention"),
31
        Err(e) => tracing::warn!("gc: expiring report files: {e}"),
32
    }
33
    let rows = match db::repos::list(&app.db).await {
34
        Ok(rows) => rows,
35
        Err(e) => {
36
            tracing::warn!("gc: listing repositories: {e}");
37
            return Vec::new();
38
        }
39
    };
40
    let mut done = Vec::with_capacity(rows.len());
41
    for row in rows {
42
        let slug = format!("{}/{}", row.owner, row.name);
43
        let Some(name) = RepoName::new(&row.owner, &row.name) else { continue };
44
        if !app.repos.exists(&name) {
45
            continue;
46
        }
47
        let label = slug.clone();
48
        // Flow's artifacts and reports' files are named only in the
49
        // database: kept as roots, or gc would prune them after the grace
50
        // period (#126, #127). A listing that fails skips this repository
51
        // rather than risk them.
52
        let listed = match db::flow::artifact_blobs(&app.db, row.id).await {
53
            Ok(mut blobs) => crate::reports::file_blobs(&app.db, row.id).await.map(|files| {
54
                blobs.extend(files);
55
                blobs
56
            }),
57
            Err(e) => Err(e),
58
        };
59
        let keep: Vec<oxy_core::hash::ObjectRef> = match listed {
60
            Ok(blobs) => blobs.iter().filter_map(|b| oxy_core::hash::ObjectRef::from_hex(b)).collect(),
61
            Err(e) => {
62
                tracing::warn!("gc: {slug}: listing its artifacts: {e}; skipped");
63
                continue;
64
            }
65
        };
66
        let started = std::time::Instant::now();
67
        let result = app
68
            .repos
69
            .write(&name, move |repo, _| Ok(repo.gc_keeping(false, &keep, &mut |phase| tracing::debug!("gc {label}: {phase}"))?))
70
            .await;
71
        let d = match result {
72
            Ok(r) => Done {
73
                summary: format!(
74
                    "{} reachable; pruned {} ({} bytes){}; packed {} into one pack in {:.1} s",
75
                    r.reachable,
76
                    r.pruned.objects,
77
                    r.pruned.bytes,
78
                    if r.pruned.deferred > 0 { format!(", {} deferred by the grace period", r.pruned.deferred) } else { String::new() },
79
                    r.packed.objects,
80
                    started.elapsed().as_secs_f64()
81
                ),
82
                repo: slug,
83
                failed: false,
84
            },
85
            Err(e) => Done { summary: format!("failed: {e}"), repo: slug, failed: true },
86
        };
87
        if d.failed {
88
            tracing::warn!("gc {}", d);
89
        } else {
90
            tracing::info!("gc {}", d);
91
        }
92
        done.push(d);
93
    }
94
    done
95
}
96
97
/// Run [`gc_all`] once a day at `retention.gc_hour` UTC.
98
pub async fn nightly(app: Shared) {
99
    let hour = app.config.retention.gc_hour;
100
    if hour >= 24 {
101
        tracing::info!("gc: the nightly run is off (retention.gc_hour = {hour})");
102
        return;
103
    }
104
    let mut last_day = None;
105
    loop {
106
        tokio::time::sleep(std::time::Duration::from_secs(60)).await;
107
        let now = time::OffsetDateTime::now_utc();
108
        if u32::from(now.hour()) != hour {
109
            continue;
110
        }
111
        let day = now.date();
112
        if last_day == Some(day) {
113
            continue;
114
        }
115
        last_day = Some(day);
116
        let done = gc_all(&app).await;
117
        tracing::info!("gc: nightly run over {} repositories, {} failed", done.len(), done.iter().filter(|d| d.failed).count());
118
    }
119
}