Collect orphaned snapshots by name: the sidecar lives in tmpfs
A reboot mid-backup orphaned the entire tree, permanently. The sidecar is the record of which snapshots a run pinned -- and /run is tmpfs. A reboot or crash between the recursive snapshot and its cleanup destroyed that record, leaving one snapshot per descendant dataset (250+ on a real pool) with nothing pointing at them. Nothing would ever have found them. gc_stale_snapshots() identifies leftovers by NAME, so it works when the record is gone. It runs after the sidecar reclaim -- the recorded path stays authoritative and the collector only mops up what the record lost. It deletes data on a name match, which is a weaker claim than a recorded fact, so the selection is a pure function with the harshest tests here. A snapshot is collected only if the name is exactly <dataset>@<task>-<YYYYMMDDHHMMSS>, it is not the current run's, NOTHING IS MOUNTED FROM IT (this, not the age guard, is what protects a concurrent backup), and it is over an hour old. Checked against the real pool: of 4728 snapshots including 2341 periodic ones, it selects exactly the orphans of the task being run and nothing else.
This commit is contained in:
+29
-1
@@ -207,7 +207,35 @@ worse than no alert, because one day it carries a security fix.
|
||||
warning, not a refusal: declining to install over a string we failed to read would
|
||||
be a worse failure than the one being prevented.
|
||||
|
||||
- **A stable release may not leave work stranded under `## Unreleased`.** Either it
|
||||
- **A stable release may not leave work stranded under `## Unreleased
|
||||
|
||||
### Fixed
|
||||
|
||||
- **A reboot mid-backup orphaned the entire snapshot tree, permanently.** The sidecar
|
||||
is the record of which snapshots a run pinned — and it lives in `/run`, which is
|
||||
**tmpfs**. A reboot (or a crash) between taking the recursive snapshot and cleaning
|
||||
it up destroyed that record, leaving one snapshot per descendant dataset — **250+ on
|
||||
a real pool** — with nothing left pointing at them. Nothing would ever have found
|
||||
them again.
|
||||
|
||||
`gc_stale_snapshots()` is the backstop: it identifies leftovers **by name**, so it
|
||||
works when the record is gone. It runs at the start of every backup, after the
|
||||
sidecar reclaim — the recorded path stays authoritative, and the collector only ever
|
||||
mops up what the record lost.
|
||||
|
||||
Because it deletes data on a *name match* — a weaker claim than a recorded fact — the
|
||||
selection is a **pure function** with the harshest tests in the suite. A snapshot is
|
||||
collected only if **all** of these hold:
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| name is exactly `<dataset>@<task>-<YYYYMMDDHHMMSS>` | so `cloud_backup-5` never matches `cloud_backup-50`, an `auto-*` periodic snapshot, or anything a human made |
|
||||
| it is not the current run's | parent *and* children are excluded |
|
||||
| **nothing is mounted from it** | an in-flight run pins its own snapshots — this, not the age guard, is what protects a concurrent backup |
|
||||
| it is **over an hour old** | covers the seconds-long window where a live run has snapshotted but not yet mounted |
|
||||
|
||||
Verified against the real pool: of **4,728** snapshots — including **2,341** periodic
|
||||
ones — it selects exactly the orphans of the task being run, and nothing else.`.** Either it
|
||||
is finished and belongs in the release, or the release is premature. Candidates
|
||||
are exempt: an rc may legitimately have work queued behind it.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user