gunnar_server/gc_queue.rs
1//! **One gc at a time, for the whole box.**
2//!
3//! `Repos.Gc` exists so a console button can maintain a repository. The moment
4//! it exists, ten repository owners can press ten buttons inside one second,
5//! and a gc is not cheap: it seals the live logs, compacts the archive, and
6//! unlinks the entire lazily-exploded read cache — hundreds of thousands of
7//! files on a large repository, all of it I/O. Ten of those at once is not ten
8//! times the work, it is a box that has stopped answering.
9//!
10//! # Why the server and not the caller
11//!
12//! A console can serialise **itself**. It cannot serialise the CLI, a second
13//! console, a script, or the next client anyone writes — and every one of those
14//! holds a credential that reaches the same disk. A rule that only one
15//! participant obeys is not an invariant; it is a convention that holds until
16//! the first person who did not read it. The server is the only place that sees
17//! every request, so it is the only place the rule can actually live.
18//!
19//! # The two properties, and why they are tested rather than argued
20//!
21//! - **Never two at once.** A single jera lane takes jobs off one queue, one at a time.
22//! The queue counts the runs that were *in flight together* and remembers the
23//! peak, so `tests/gc_queue.rs` asserts on an observed maximum rather than on
24//! a sleep that happened not to overlap. Seen red against a
25//! thread-per-request queue, which reported a peak of 10 against ten requests.
26//! - **A double press is one job.** A request naming a repository that is
27//! already queued or running returns the job that is already there. Impatient
28//! double-clicking is the normal case for a button, not the exception, and a
29//! queue that took it literally would run the expensive thing twice for no
30//! reason. Asserted on the number of times the runner was actually CALLED,
31//! which a queue that merely deduplicated its bookkeeping could not fake.
32//!
33//! Both properties now live in [`crate::job_queue`] and are asserted here, on
34//! `GcQueue`, unchanged: a property proved about the runner is a property of
35//! every kind on it, but `tests/gc_queue.rs` is what says it holds for **this**
36//! door, and that file did not move.
37//!
38//! # What a finished job is for
39//!
40//! The worker does not drop a job when it finishes it. A console polls, and a
41//! poller that was between requests at the instant the gc completed would
42//! otherwise get "no such job" — indistinguishable from "you never asked". So a
43//! completed job is kept for [`FINISHED_RETENTION`], and beyond that a bounded
44//! number of the most recent are kept anyway, so a busy server forgets by age
45//! and a quiet one does not forget at all.
46//!
47//! # What this module is, and is not
48//!
49//! It is not the maintenance. That is [`crate::admin::gc_one`], which the CLI
50//! verb also calls, and this module reaches it through a runner handed in at
51//! construction (one implementation: one writer, and a queue that could be tested without a
52//! disk under it). The runner is the only thing here that knows what a gc is.
53//!
54//! It is not the queue either, any more. The jera lane, the FIFO, the
55//! dedup, the retention and the instruments are [`crate::job_queue`], shared
56//! with the purge queue and with anything added later; what is left here is
57//! **the gc's answers** to that runner's questions — what a gc's subject is (a
58//! repository path), what a gc produces (bytes), and what a gc is (the runner).
59//! One thing a gc gained by moving: the worker now catches a panicking gc and
60//! reports it as a FAILED job instead of dying with it, which the purge queue
61//! had paid for separately and the gc queue never had.
62
63use std::sync::Arc;
64use std::time::Duration;
65
66use crate::job_queue::{Job, JobKind, JobQueue, JobState, Reporter};
67
68/// How long a finished job stays readable. Ten minutes is far longer than any
69/// console poll interval and far shorter than a session, so a button that
70/// completed is still reporting its bytes when whoever pressed it looks back,
71/// and a server that has run thousands is not holding all of them.
72pub const FINISHED_RETENTION: Duration = <Gc as JobKind>::FINISHED_RETENTION;
73
74/// Finished jobs kept regardless of age, most recent first.
75///
76/// The floor under [`FINISHED_RETENTION`]: a server that has done nothing for
77/// an hour should still be able to say what its last gc freed. Bounded, because
78/// "keep everything" is how a long-lived process grows a leak with a good
79/// excuse.
80pub const FINISHED_KEPT: usize = <Gc as JobKind>::FINISHED_KEPT;
81
82/// Where a job is. Mirrors `gunnar.v1.GcState`; the mapping to the wire lives
83/// in `gunnar-grpc` so this crate does not depend on the protos.
84///
85/// The four words are [`JobState`]'s, shared with every other kind of job, so
86/// `GcStatus` and `PurgeStatus` cannot drift apart about what `Done` means.
87pub type GcState = JobState;
88
89/// One maintenance run, queued or completed.
90///
91/// A [`Job`] of kind [`Gc`]: the queue's own fields (`job_id`, `state`,
92/// `queue_position`, `error`, the timestamps, `progress`) plus a gc's
93/// [`GcReport`], which dereferences flat — `job.bytes_reclaimed` reads the
94/// report and `job.state` the queue, and a caller does not have to know which
95/// half a field is in. The repository is `job.key`, exactly as the queue keys
96/// on it.
97pub type GcJob = Job<Gc>;
98
99/// What a gc produced.
100#[derive(Debug, Clone, Default)]
101pub struct GcReport {
102 /// Bytes that left the filesystem. Only meaningful when
103 /// [`JobState::Done`]; 0 on a gc that failed, which reclaimed nothing.
104 pub bytes_reclaimed: u64,
105}
106
107/// What a gc actually is, handed in so this module does not have to know.
108///
109/// Takes `<namespace>/<name>` and returns the bytes that left the filesystem.
110/// The real one resolves the catalog row and calls [`crate::admin::gc_one`] —
111/// the same function `gunnar gc` runs.
112pub type GcRunner = Arc<dyn Fn(&str) -> Result<u64, String> + Send + Sync>;
113
114/// **The gc kind.** Everything the shared runner needs to know about a gc, and
115/// nothing else.
116pub struct Gc {
117 run: GcRunner,
118}
119
120impl JobKind for Gc {
121 const NAME: &'static str = "gc";
122
123 /// The repository, `<namespace>/<name>`. A gc has nothing else to be told:
124 /// there are no options, so the order and the subject are the same string.
125 type Order = String;
126 type Report = GcReport;
127
128 fn key(order: &String) -> &str {
129 order
130 }
131
132 /// Call the runner and record what it freed.
133 ///
134 /// A gc reports nothing on the way through — no [`Reporter::progress`] —
135 /// because as far as a caller is concerned it is one indivisible act, and a
136 /// fabricated `0/1` would be a progress bar that lies. The bytes are known
137 /// only at the end, which is when they are written.
138 fn run(&self, path: &String, report: &Reporter<Gc>) -> Result<(), String> {
139 let bytes = (self.run)(path)?;
140 report.report(|gc| gc.bytes_reclaimed = bytes);
141 Ok(())
142 }
143}
144
145/// The queue: one worker, one job at a time, FIFO, deduplicated by repository.
146///
147/// A named type over [`JobQueue<Gc>`] rather than an alias, so `Repos.Gc`'s
148/// door keeps speaking in repositories — `enqueue("acct/repo")` — instead of
149/// making every caller build an order to say one string.
150#[derive(Clone)]
151pub struct GcQueue(JobQueue<Gc>);
152
153impl GcQueue {
154 /// A queue over `run`. No thread is started until something is enqueued.
155 pub fn new(run: GcRunner) -> Self {
156 GcQueue(JobQueue::new(Gc { run }))
157 }
158
159 /// **Ask for `path` to be maintained, and return without maintaining it.**
160 ///
161 /// Returns the job that is now responsible for the repository: a new one,
162 /// or the one that was already queued or running. Never enqueues a second
163 /// job for a repository that has one live.
164 pub fn enqueue(&self, path: &str) -> GcJob {
165 self.0.enqueue(path.to_owned())
166 }
167
168 /// The job the queue is holding for `path`, if it still has one.
169 pub fn status(&self, path: &str) -> Option<GcJob> {
170 self.0.status(path)
171 }
172
173 /// The highest number of gcs that have ever been running together.
174 ///
175 /// **Must be 1 after any amount of traffic** — that is the invariant this
176 /// module exists for, and this is how a test sees it rather than infers it.
177 /// 0 before anything has run.
178 pub fn peak_in_flight(&self) -> usize {
179 self.0.peak_in_flight()
180 }
181
182 /// How many times the runner has been called — i.e. how many gcs actually
183 /// happened. The applied output the dedup gate asserts on.
184 pub fn runs(&self) -> u64 {
185 self.0.runs()
186 }
187
188 /// Stop the worker after the job it is on. For tests and shutdown; a queue
189 /// that is dropped without this simply leaves its thread parked on the
190 /// condvar until the process ends.
191 pub fn stop(&self) {
192 self.0.stop()
193 }
194}