R-361: the safety dump destroyed the app's own database backup
gates / gates (push) Successful in 11s
gates / gates (push) Successful in 11s
writeSafetyDump called DumpOne into the app's OWN unit dir and renamed the result to pre-restore-* afterwards. DumpOne writes <stack>-<dbtype>.sql - the app's canonical dump - so every safety dump overwrote the app's real backup and then moved it away, leaving the app with no database backup until the next nightly run. A local restore-from-unit in that window tells the customer the app never had a database. The comment beside it asserted the rename meant it 'can never overwrite the app's real dump'. False as written, and believed for four months. Measured live before the fix: docmost and bookstack each held only pre-restore-* files and no canonical dump. DumpOneTo takes the final path and derives its own .tmp from it. DumpOne keeps its signature and calls it with the canonical name. writeSafetyDump asks for its own name directly; the rename is gone; the comment now states the invariant and how it is enforced. db_dumps no longer lists the undo copies. All three consumers of Manifest.DBDumps were grepped and named - all inside recovery_unit.go, none reads it for recovery. The files are neither deleted nor hidden. Tests 1485 -> 1493. FIVE red-proofs, TWO PASSED first time and both are reported: the behavioural tests inject the dump seam so a mutation inside DumpOneTo was invisible, and 1.3 had no test at all. Guards added at the layer each defect lives in; both mutations then convicted.
This commit is contained in:
@@ -128,7 +128,23 @@ func (m *Manager) CaptureRecoveryUnit(stackName string) error {
|
||||
checksums["app.yaml"] = sha256Hex(appYaml)
|
||||
configFiles = append(configFiles, "app.yaml")
|
||||
|
||||
dbDumps := listFileNames(AppDBDumpPath(nsRoot, stackName), ".sql")
|
||||
// R-361: `db_dumps` lists the app's OWN dumps and NOT the `pre-restore-*` undo copies.
|
||||
//
|
||||
// THE DECISION, and the reasoning, because a decision recorded only in a commit message is a
|
||||
// decision nobody finds: the undo copies are LOCAL material for a restore that went wrong, not
|
||||
// part of the app's recovery set. Nothing reads them from the manifest — the reconstitution skips
|
||||
// `pre-restore-*` at three sites, and a grep of every consumer of `Manifest.DBDumps` found only
|
||||
// this file (the declaration, this enumeration, and the change-detection compare below). Listing
|
||||
// them meant three copies per app were enumerated into the manifest and pushed off-site
|
||||
// permanently, for no recovery value.
|
||||
//
|
||||
// It also makes the compare below STABLE: the undo copies come and go with every restore and
|
||||
// every prune, so including them forced a manifest rewrite each time for a change that says
|
||||
// nothing about the app's backup.
|
||||
//
|
||||
// The files themselves are NOT deleted and NOT hidden — their visibility is a recorded design
|
||||
// (see preRestoreDumpPrefix). This is about the manifest only.
|
||||
dbDumps := filterOutUndoCopies(listFileNames(AppDBDumpPath(nsRoot, stackName), ".sql"))
|
||||
volDumps := listFileNames(AppVolumeDumpPath(nsRoot, stackName), ".tar")
|
||||
version := m.versionLocked()
|
||||
|
||||
@@ -526,3 +542,16 @@ func atomicWrite(path string, data []byte, perm os.FileMode) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// filterOutUndoCopies drops `pre-restore-*` names from a dump listing. R-361: the undo copies are not
|
||||
// part of the app's recovery set — see the reasoning at the call site.
|
||||
func filterOutUndoCopies(names []string) []string {
|
||||
out := make([]string, 0, len(names))
|
||||
for _, n := range names {
|
||||
if strings.HasPrefix(n, preRestoreDumpPrefix) {
|
||||
continue
|
||||
}
|
||||
out = append(out, n)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user