D47 follow-up: a set-aside tells the operator once (backup_run_failures, leg unit-set-aside, both identities, own run id). Red-proved. Commit only.
gates / gates (push) Successful in 1m33s

This commit is contained in:
admin committed 2026-10-25 17:19:08 +01:00
1 parent 3a1f09d29f
commit 935e624f10
6 files changed
+43

No files matched your search

+3
View File
@@ -44,6 +44,9 @@ says „not started yet", alarms the operator; a late start is a success; never
started" (a healthy Vikunja restore was reported not started). It reads the inspect JSON now started" (a healthy Vikunja restore was reported not started). It reads the inspect JSON now
(`parseContainerSamples`, `TestFixA_ParseContainerSamples`); the sampler's error keeps Docker's stderr, and a DEBUG (`parseContainerSamples`, `TestFixA_ParseContainerSamples`); the sampler's error keeps Docker's stderr, and a DEBUG
line says why a sample was not ok. line says why a sample was not ok.
- **D47 follow-up (brief Part 5.2):** a set-aside now tells the operator once (`backup_run_failures`, leg
`unit-set-aside`, both identities, its own run id so the hub's cooldown never swallows a second one) — a household's
set-aside after the floor is visible (`TestD47_ASetAsideTellsTheOperatorOnce`, seen failing without the call).
- **Fix B (R-1279, `appexport/restore.go`):** the import asks the restores' own D48 helper (`PrepareRestoreImages`, - **Fix B (R-1279, `appexport/restore.go`):** the import asks the restores' own D48 helper (`PrepareRestoreImages`,
passed in by main.go — the package may not import `backup`) about the bundle's definition BEFORE step 1 removes the passed in by main.go — the package may not import `backup`) about the bundle's definition BEFORE step 1 removes the
running app's volumes; a version that cannot be downloaded refuses with the two D48 sentences and nothing changed. running app's volumes; a version that cannot be downloaded refuses with the two D48 sentences and nothing changed.
+8
View File
@@ -1543,6 +1543,14 @@ func main() {
}) })
// R-893 option A (D20 = A): a failed off-site restore that was PUT BACK — the live volumes and definition poured // R-893 option A (D20 = A): a failed off-site restore that was PUT BACK — the live volumes and definition poured
// back and the app started. The same operator-tier event as the hold above (no new event type), its own leg. // back and the app started. The same operator-tier event as the hold above (no new event type), its own leg.
// D47 follow-up: a unit of an earlier install was set aside whole — the operator sees it (its own leg).
backupMgr.SetUnitSetAsideNotify(func(stack, unitID, liveID, dest string) {
d := notify.BackupRunFailuresDetails{RunID: "unit-set-aside-" + stack + "-" + filepath.Base(dest), RunKind: "backup", Failed: 1, Attempted: 1}
d.Apps = append(d.Apps, notify.RunFailureDetail{App: stack, Leg: "unit-set-aside", Reason: "the backup of an earlier install was set aside whole (D47)"})
msg := fmt.Sprintf("App %q: its backup belonged to an earlier install (%s; this install %s), so it was SET ASIDE whole at %s (D47) "+
"and listed on the restore page. Expected after a reinstall; not expected on an upgrade — look if no one reinstalled the app.", stack, unitID, liveID, dest)
notifier.NotifyBackupRunFailures(msg, d)
})
// D48: a restore refused BEFORE anything was touched because an image of the backup's version could not be // D48: a restore refused BEFORE anything was touched because an image of the backup's version could not be
// downloaded — the operator line that tells a deleted version (R-698, R-933) from a busy registry. // downloaded — the operator line that tells a deleted version (R-698, R-933) from a busy registry.
backupMgr.SetRestorePullRefusedNotify(func(stack, ref string, cause error) { backupMgr.SetRestorePullRefusedNotify(func(stack, ref string, cause error) {
@@ -16,6 +16,9 @@ func TestR1279_MainWiresTheImportsDownloadFirst(t *testing.T) {
if !strings.Contains(string(b), "appExporter.SetImagePreparer(backupMgr.PrepareRestoreImages)") { if !strings.Contains(string(b), "appExporter.SetImagePreparer(backupMgr.PrepareRestoreImages)") {
t.Fatal("main.go must wire the import's download-first helper (SetImagePreparer)") t.Fatal("main.go must wire the import's download-first helper (SetImagePreparer)")
} }
if !strings.Contains(string(b), "backupMgr.SetUnitSetAsideNotify(") {
t.Fatal("main.go must wire the D47 set-aside operator line")
}
if !strings.Contains(string(b), "func (a *stackAdapter) ContainerSamples(") { if !strings.Contains(string(b), "func (a *stackAdapter) ContainerSamples(") {
t.Fatal("main.go's stack adapter must read container samples for the stable-start rule (fix A)") t.Fatal("main.go's stack adapter must read container samples for the stable-start rule (fix A)")
} }
+3
View File
@@ -248,6 +248,9 @@ type Manager struct {
imagePresentFn func(ref string) bool imagePresentFn func(ref string) bool
imagePullFn func(ctx context.Context, ref string) (string, error) imagePullFn func(ctx context.Context, ref string) (string, error)
restorePullRefusedNotify func(stack, ref string, cause error) restorePullRefusedNotify func(stack, ref string, cause error)
// unitSetAsideNotify (D47 follow-up, brief 2026-10-26 Part 5.2): one operator line when a box sets an earlier
// install's unit aside — so a household's set-aside after the floor is visible to the operator.
unitSetAsideNotify func(stack, unitID, liveID, dest string)
// R-403 unit-REHYDRATE seam — the copy that refills the primary unit from the Tier-2 mirror after // R-403 unit-REHYDRATE seam — the copy that refills the primary unit from the Tier-2 mirror after
// a restore, overridable so the "only when hollow" condition and the byte-identical non-effect are // a restore, overridable so the "only when hollow" condition and the byte-identical non-effect are
+8
View File
@@ -181,9 +181,17 @@ func (m *Manager) guardUnitDir(stack, unitDir, setAsideNs, what string) error {
} }
m.logger.Printf("[INFO] [backup] %s: the recovery unit of an earlier install (%s; %s) is SET ASIDE whole → %s; this install (%s) gets its own unit from its first backup (D47)", m.logger.Printf("[INFO] [backup] %s: the recovery unit of an earlier install (%s; %s) is SET ASIDE whole → %s; this install (%s) gets its own unit from its first backup (D47)",
stack, orUnknown(unitID), why, dest, orUnknown(liveID)) stack, orUnknown(unitID), why, dest, orUnknown(liveID))
if m.unitSetAsideNotify != nil {
m.unitSetAsideNotify(stack, orUnknown(unitID), orUnknown(liveID), dest)
}
return nil return nil
} }
// SetUnitSetAsideNotify wires the operator line for a set-aside unit (main.go).
func (m *Manager) SetUnitSetAsideNotify(fn func(stack, unitID, liveID, dest string)) {
m.unitSetAsideNotify = fn
}
func orUnknown(id string) string { func orUnknown(id string) string {
if id == "" { if id == "" {
return "unrecorded" return "unrecorded"
@@ -302,3 +302,21 @@ func TestD47_OwnerVerdict(t *testing.T) {
} }
} }
} }
// D47 follow-up (brief 2026-10-26 Part 5.2): a set-aside tells the operator once — the box's own households must not
// set units aside silently after the floor. COMPANION RED-PROOF: drop the notify call → this fails.
func TestD47_ASetAsideTellsTheOperatorOnce(t *testing.T) {
r := d47Setup(t, "aaaa000000000001")
var got []string
r.m.SetUnitSetAsideNotify(func(stack, unitID, liveID, dest string) { got = append(got, stack+" "+unitID+" "+liveID) })
r.reinstall("bbbb000000000002")
if err := r.m.captureRecoveryUnit("tandoor", false); err != nil {
t.Fatal(err)
}
if err := r.m.captureRecoveryUnit("tandoor", false); err != nil { // the next refresh: nothing more to set aside
t.Fatal(err)
}
if len(got) != 1 || got[0] != "tandoor aaaa000000000001 bbbb000000000002" {
t.Fatalf("the operator hears once, with both identities: %v", got)
}
}