Skip to main content

didbot_serve/
blob_collect.rs

1//! The timer that runs `didbot_pds::Provisioner::collect_blobs`.
2//!
3//! An upload starts unreferenced, and a blob whose reference count has
4//! fallen to zero stays on disk until something unlinks it. `collect_blobs`
5//! is that something, and this is its clock — the same shape as
6//! [`crate::stale_sweep`], for the same reason: a startup-only pass would
7//! leave a long-lived server accumulating bytes nothing names.
8
9use std::panic::AssertUnwindSafe;
10use std::sync::Arc;
11use std::time::Duration;
12
13use didbot_pds::BlobCollect;
14use tokio::sync::Notify;
15use tokio::task::JoinHandle;
16use tokio::time::MissedTickBehavior;
17use tracing::{info, warn};
18
19/// How often the collector runs, absent configuration.
20///
21/// An hour: the floor under a deployment's grace window is
22/// `didbot_pds::blobs::MIN_BLOB_COLLECTION_GRACE`, an hour, so a blob past
23/// its window waits at most another window's worth before a pass takes it.
24pub const DEFAULT_COLLECT_INTERVAL: Duration = Duration::from_secs(60 * 60);
25
26/// A running collector, stopped by [`BlobCollector::stop`] or by dropping it.
27pub struct BlobCollector {
28    handle: JoinHandle<()>,
29    stop: Arc<Notify>,
30}
31
32impl BlobCollector {
33    /// Starts collecting through `registry` every `interval`.
34    ///
35    /// The first tick fires immediately, so a restart takes what the
36    /// previous run left past its grace without waiting an interval. That
37    /// pass runs alongside serving, like every later one.
38    ///
39    /// Each pass runs on a blocking thread, as [`crate::stale_sweep`]'s does:
40    /// it takes the write lock every write path takes and unlinks files.
41    pub fn spawn(registry: Arc<dyn BlobCollect>, interval: Duration) -> Self {
42        let stop = Arc::new(Notify::new());
43        let notified = stop.clone();
44        let handle = tokio::spawn(async move {
45            let mut ticker = tokio::time::interval(interval);
46            ticker.set_missed_tick_behavior(MissedTickBehavior::Delay);
47            loop {
48                tokio::select! {
49                    biased;
50                    () = notified.notified() => break,
51                    _ = ticker.tick() => {}
52                }
53                collect_once(registry.clone()).await;
54            }
55        });
56        Self { handle, stop }
57    }
58
59    /// Stops the collector and waits for the task to finish.
60    pub async fn stop(mut self) {
61        self.stop.notify_one();
62        let _ = (&mut self.handle).await;
63    }
64}
65
66impl Drop for BlobCollector {
67    fn drop(&mut self) {
68        self.handle.abort();
69    }
70}
71
72/// One tick: collect, say what it took.
73///
74/// Public and separate from the timer for the reason
75/// [`crate::stale_sweep::sweep_once`] is: a caller can run exactly one pass
76/// without waiting on a clock.
77pub async fn collect_once(registry: Arc<dyn BlobCollect>) {
78    let collected = tokio::task::spawn_blocking(move || {
79        std::panic::catch_unwind(AssertUnwindSafe(|| registry.collect_blobs()))
80    })
81    .await;
82
83    match collected {
84        Ok(Ok(collected)) if collected.is_empty() => {}
85        Ok(Ok(collected)) => info!(
86            blobs = collected.len(),
87            bytes = collected.iter().map(|blob| blob.bytes).sum::<u64>(),
88            "collected blobs nothing referenced, past their grace window"
89        ),
90        // Both arms leave the timer running: what this pass did not take
91        // is still there for the next one.
92        Ok(Err(_)) => warn!("the blob collector panicked; the next tick tries again"),
93        Err(error) => {
94            warn!(%error, "the blob collector did not finish; the next tick tries again")
95        }
96    }
97}