v0.218.0: attribute a DB container by its compose project, and replay volumes on the off-site restore
gates / gates (push) Successful in 11s
gates / gates (push) Successful in 11s
R-355 (first, because it is the only one where data can be lost for good). paperless-ngx's PostgreSQL was dumped into backups/primary/paperless/db-dumps/ — a directory for a stack that does not exist, on the system drive — while the app's own unit recorded db_dumps: null. The same misattribution reached writeSafetyDump, so a destructive restore of that app took NO undo copy and the fail-closed refusal was never reached. Fixed by reading the compose project label, which is the stack name by construction (compose runs with cmd.Dir set to the stack dir and no -p). The old derivation stays as the fallback and an unresolvable attribution is now loud. Catalogue sweep, proven able to convict: one affected app of 53. The fix is in the controller, not the catalogue. R-354. ReconstituteFromOffsite skipped every unit placement and the volume archives live inside the unit, so the off-site restore had no volume leg at all — proven live with planted files: calibre-web's 1,422,848-byte config archive was in the unit, the snapshot and the checking folder, and the restore reported success without it. For the 40 of 53 apps that declare no data drive that archive is the whole dataset. restoreDockerVolumesFrom is the local path's own replay with an explicit directory: ONE implementation, two callers. Volumes replay before the database and inside the stopped window. VolumesReplayed reaches the message. The comment beside the skip was half false and is corrected; the half that still holds — the live unit is the local path's source — is named, and scenario D fingerprints the whole live unit across the operation. Seven red-proofs, each asserted applied and reverted. Two found defects in the tests, not the code: scenario D passed with the unit guard removed because the fingerprint had been narrowed and was blind to the unit root.
This commit is contained in:
@@ -1,3 +1,75 @@
|
||||
## v0.218.0 — the database nobody backed up, and the restore that returned most apps nothing (2026-08-22, R-354/R-355)
|
||||
**MinAgent: 0.129.0** (unchanged — no new agent coupling)
|
||||
|
||||
Both fixes were found by watching a real machine on the night of 2026-08-21, and both are confirmed
|
||||
the same way. Neither is a refactor: each removes a case where the product told a customer something
|
||||
that was not true about their own data.
|
||||
|
||||
**The database that was dumped and then abandoned (R-355).** `paperless-ngx` runs its PostgreSQL in a
|
||||
container called `paperless-postgres`. `deriveStackName` (`internal/appbackup/dbdump.go:770`) strips the
|
||||
role suffix to `paperless`, finds that is not a deployed stack, finds no known stack is a prefix of the
|
||||
container name — **and then returns the unresolved candidate anyway**. The nightly dump therefore landed
|
||||
in `backups/primary/paperless/db-dumps/` — a directory for an app that does not exist, on the SYSTEM
|
||||
drive — while the app's own recovery unit, on the data drive, recorded `db_dumps: null`. Nothing
|
||||
collected it, nothing off-sited it, nothing restored it. Measured live: **284 617 bytes, 72 tables,
|
||||
valid, unreachable**, and reproduced unattended by the box's own 02:30 cycle.
|
||||
|
||||
**And it compounded, which is why it went first.** `writeSafetyDump` filters the discovered databases
|
||||
with the same value, so a destructive restore of that app found no database, **took no undo copy**, and
|
||||
the fail-closed refusal that protects every other app could not fire — it was never reached. A customer
|
||||
could press restore, lose the live database and have no copy anywhere. Verified live on 2026-08-21:
|
||||
`find /mnt -name "pre-restore-*"` was empty both before and after a full restore over a live 72-table
|
||||
PostgreSQL.
|
||||
|
||||
**The fix is to stop guessing.** Every container the controller starts carries
|
||||
`com.docker.compose.project`, and that label IS the stack name by construction: compose is run with
|
||||
`cmd.Dir` set to `/opt/docker/stacks/<stack>` and no `-p` (`internal/stacks/manager.go:1218`). The label
|
||||
is now read and preferred whenever it names a deployed stack; the old derivation stays as the fallback
|
||||
for containers not started by compose, and an attribution that resolves to no known stack is now **loud**
|
||||
instead of silent. **A catalogue-wide sweep — proven able to convict by planting a second mismatch,
|
||||
watching it caught, removing it and watching it clear — reports exactly one affected app of 53.** The
|
||||
fix is in the controller, not the catalogue: renaming the container would have fixed this one app and
|
||||
left the guessing in place for the next.
|
||||
|
||||
**The restore that returned most apps nothing (R-354).** `ReconstituteFromOffsite` skipped every
|
||||
placement flagged as the unit, and the named-volume archives live INSIDE the unit — so the off-site
|
||||
restore had a files leg and a database leg and **no volume leg at all**. Proven live with planted,
|
||||
hash-recorded files: calibre-web's `calibre_web_config.tar` was in the unit, in the off-site snapshot
|
||||
and in the verification folder, and the restore returned the five declared files, reported success, and
|
||||
did not replay it. **For the 40 of 53 catalogue apps that declare no data drive, that archive is
|
||||
everything the customer owns.**
|
||||
|
||||
**The comment beside the skip was half false and is corrected rather than left.** It justified the skip
|
||||
by saying the snapshot's dump is replayed from the scratch unit so nothing is lost — true of the
|
||||
database, false of the volumes, and the reason a reader would not look. **The half that still holds is
|
||||
named:** the live unit is the LOCAL restore path's own source and must never be clobbered. Scenario D
|
||||
now fingerprints the whole live unit across the operation and compares.
|
||||
|
||||
`restoreDockerVolumesFrom` is the local path's own replay with an explicit directory — the same shape
|
||||
`reimportDBDumpsFrom` already had, and deliberately ONE implementation with two callers, because a
|
||||
second copy of that loop is what produced the divergence. Volumes replay **before** the database, so a
|
||||
logical dump still wins over a volume-tar copy of the same database, and inside the stopped window,
|
||||
because Docker will not replace a volume a container holds.
|
||||
|
||||
**And it reaches the sentence.** `VolumesReplayed` is on the result and in the message: „5 fájl **és 1
|
||||
adatkötet** visszaállítva". A restore that replayed an app's entire dataset and mentioned only its file
|
||||
count is how a silent loss reads as a success. A snapshot with no volumes produces the byte-identical
|
||||
sentence it produced before.
|
||||
|
||||
**„Ennek az alkalmazásnak nincs adatbázisa" is no longer inferred from a counter.** `DBsReplayed == 0`
|
||||
has two causes — the app has none, or it has one and the snapshot carried no dump — and both printed the
|
||||
same confident sentence over a live 72-table database. The undo copy is the honest discriminator, and
|
||||
the second case now says so and names the undo.
|
||||
|
||||
**Seven red-proofs, each mutation asserted applied and reverted.** The name fix reverted returned the
|
||||
empty database record with both divergent paths printed; the message predicate reverted returned the
|
||||
false „nincs adatbázisa" sentence verbatim; the refusal removed was seen letting a restore proceed with
|
||||
no undo; the volume leg removed returned the silent loss; the volume count dropped returned the
|
||||
true-but-incomplete sentence verbatim. **Two of them found defects in the tests rather than the code:**
|
||||
scenario D PASSED with the unit guard removed, because the fingerprint had been narrowed to the volume
|
||||
directory and was blind to a placement writing into the unit root — the R-181 class, in my own test, and
|
||||
the reason the red-proof is mandatory.
|
||||
|
||||
## v0.217.0 — the restore knows where the data lived (2026-08-21, R-351/R-352/R-353)
|
||||
**MinAgent: 0.129.0** (unchanged — no new agent coupling)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user