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