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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
//! Reclaiming records: what delete prunes, and what collection sweeps.
//!
//! Both operations answer the same question — which manifests is anything
//! still pointing at — and D10 requires them to answer it through **one
//! read-only query**. Sharing a primitive is not a dependency; reaching
//! through the other operation's entry point is. Before this module, delete
//! ran gc's whole mutating partition sweep, so delete's result depended on
//! gc's marking logic, and gc's marking logic pruned records delete owned.
//!
//! **Nothing here touches content.** Whether a blob is still referenced is a
//! question about every workspace at once, because content is deduplicated
//! and lineage has nothing to do with it (D10). Only
//! [`crate::collect_garbage`] can answer it, so only it may remove a blob.
//! Everything in this module is scoped to one partition, which is exactly
//! why none of it is allowed near the shared blob store: a partition-scoped
//! answer applied to a global space deletes other workspaces' content.
//!
//! The two entry points differ in what they are allowed to reach:
//!
//! | | reaches | removes |
//! |---|---|---|
//! | [`prune_sessions`] | the turn ids and manifests the deleted logs named | only those |
//! | [`collect_partition`] | every record in the partition | anything unreachable |
use BTreeSet;
use fs;
use Path;
use PathBuf;
use Duration;
use SystemTime;
use crateResult;
use crateManifestStore;
use crateGcStats;
use crateRefStore;
use crateTurnIndex;
/// How new a file has to be for a sweep to leave it alone regardless.
///
/// A capture publishes in three steps — blobs, then the manifest, then the
/// log entry — and none of it is atomic across processes. A sweep that read
/// the logs before that last step but listed the files after it would delete
/// a snapshot a live session believes it holds, which is worse than any
/// amount of retained garbage. Nothing coordinates the two: a workspace is
/// explicitly multi-session, and collection runs from whichever process asked
/// for it.
///
/// Git answers the same race the same way rather than by locking
/// (`gc.pruneExpire`): fresh objects are simply never pruned, and whatever
/// garbage is among them waits for the next sweep. Reclamation is delayed;
/// nothing is lost.
pub const GC_GRACE: Duration = from_secs;
/// Whether `path` is old enough that its absence from the live set can be
/// trusted.
///
/// Unreadable or undatable files count as **young**: a sweep declines to
/// delete anything it cannot age, because the cost of waiting is retained
/// bytes and the cost of guessing is a snapshot a live session believes it
/// holds.
pub
/// A temporary name for `path` that no other writer can be using.
///
/// **Not `path.with_extension("tmp")`.** Two writers producing the same final
/// path — which is the ordinary case for content-addressed records, and for a
/// turn entry two forks share — would then write the *same* temporary and race
/// on it: one renames it into place and the other's rename fails with ENOENT,
/// so a capture that had done all its work reports an I/O error on a file it
/// successfully wrote. Nothing wider than a session is locked (D18), so this
/// has to be safe without a lock rather than because of one.
///
/// The process id distinguishes writers across processes and the counter
/// distinguishes them within one. Both are only needed until the rename; after
/// it the name is gone.
pub
/// Mark `path` as referenced now, so the grace window measures last use
/// rather than creation.
///
/// Both stores dedup: a write whose content is already present writes
/// nothing. That makes an object's mtime the time it was *created*, and the
/// window is asking a different question — whether anyone might still be in
/// the middle of publishing something that names it. Freshening makes the
/// timestamp answer the question actually being asked.
///
/// Failure is ignored. A blob that cannot be freshened is one the sweep will
/// judge by an older timestamp; the cost is a race that was already there,
/// and failing a capture over it would be worse.
pub
/// Whether the live set below could be built from every root there is.
///
/// A record that cannot be read is not evidence that anything is dead — it is
/// evidence of nothing at all. So an unreadable root does not make a sweep
/// fail; it makes the sweep's answer **incomplete**, and an incomplete answer
/// may not be used to remove anything. The cost is retained bytes until the
/// damaged record is dealt with. The alternative is deleting the snapshots of
/// a session nobody touched, because the file naming them happened to be
/// corrupt.
///
/// This is also what keeps delete free of preconditions (D9): a corrupt log
/// belonging to some *other* session cannot make deleting this one fail. It
/// only defers the reclamation, which was never part of delete's success
/// criterion (VIII.3).
pub
/// What the surviving **logs** name: their manifests, and their turn ids in
/// on-disk form.
///
/// Logs are the primary root. The turn index and the undo records are roots
/// too, but they are read *after* the stale entries among them have been
/// pruned — otherwise a turn entry about to be removed vouches for the very
/// manifest it was the last thing pointing at, and the sweep converges one
/// pass later than it should.
///
/// **Read-only.** An earlier version pruned the turn index as a side effect,
/// which is how delete's own record cleanup ended up inside gc's marking
/// helper — one hole with two symptoms (D10, C14). What each operation prunes
/// is now explicit at its call site.
/// Remove the records the just-deleted sessions owned, and nothing else.
///
/// `doomed_turns` and `doomed_manifests` are what those sessions' logs named,
/// gathered **before** the logs were unlinked — after the unlink there is no
/// way to learn it, which is why delete reads first and removes second.
///
/// Scoped on purpose. The previous implementation reconciled the whole turn
/// index by global elimination, so deleting one conversation could unlink a
/// turn entry belonging to a live session whose log entry had landed but
/// whose turn file had not yet been written — a rewind lost permanently, in a
/// session nobody deleted (C12). Nothing here enumerates a directory: every
/// candidate was named by a log that is now gone.
pub
/// Sweep every record in this partition that nothing points at.
///
/// Unlike [`prune_sessions`] this enumerates, so it is the one that finds
/// what a crashed operation left behind — the orphans D8 makes collection's
/// job. It removes **records only**; see the module header.
pub
/// Unlink `*.tmp` residue under `dir` that is past the grace window.
///
/// Every atomic write in the store is write-to-`.tmp`-then-rename, so a
/// process killed between the two leaves one behind. D10 assigns this to
/// collection, and until now nothing removed one: all three enumerations
/// merely *skipped* `.tmp`, which made a stray file permanent — and, where a
/// record's name could collide with it, an uncollectable GC root (C4).
///
/// Errors on individual entries are ignored. Residue is a tidiness matter,
/// and failing a collection over a file nobody can read would trade a small
/// leak for a large one.
pub
/// Declared sets belonging to sessions that have no log any more.
///
/// The third record under a session's name, and the one nothing enumerated.
/// Delete removes it with the other two; this finds the ones a crash left.
pub