R-1343 fallback: the box command's own settings.json edit (no controller running) goes through the same clear door, so a waiting put-back is dropped from the marker there too — test red first
gates / gates (push) Successful in 30s
gates / gates (push) Successful in 30s
This commit is contained in:
1 parent
5b3b541602
commit
6077ac0083
3 files changed
+61
-9
No files matched your search
+4
-2
@@ -22,11 +22,13 @@
|
|||||||
a unix socket `<data dir>/ctl/hold.sock`, 0600 in a 0700 dir, peer uid checked (SO_PEERCRED), nothing on TCP — not
|
a unix socket `<data dir>/ctl/hold.sock`, 0600 in a 0700 dir, peer uid checked (SO_PEERCRED), nothing on TCP — not
|
||||||
reachable through traefik, the LAN or an app container. Output: „Cleared by the RUNNING controller … no restart is
|
reachable through traefik, the LAN or an app container. Output: „Cleared by the RUNNING controller … no restart is
|
||||||
needed". Only when no controller answers (no socket, or a stale one) does it edit `settings.json`, and it says so,
|
needed". Only when no controller answers (no socket, or a stale one) does it edit `settings.json`, and it says so,
|
||||||
naming the restart INSIDE the guest (`pct exec <vmid> -- systemctl restart felhom-controller-bootstrap.service`). The
|
naming the restart INSIDE the guest (`pct exec <vmid> -- systemctl restart felhom-controller-bootstrap.service`); that
|
||||||
|
edit goes through the same door, so a waiting put-back is dropped from the marker there too. The
|
||||||
flag's help names the exact guest command. `--restore-holds` (the list) is unchanged.
|
flag's help names the exact guest command. `--restore-holds` (the list) is unchanged.
|
||||||
Tests: `TestR1343_TheBoxCommandClearsThroughTheRunningController` (red with the socket skipped: „the hold came back
|
Tests: `TestR1343_TheBoxCommandClearsThroughTheRunningController` (red with the socket skipped: „the hold came back
|
||||||
after a later save of the running controller" — R-1343 reproduced), `TestR1343_TheBoxCommandFallsBackWhenNoControllerRuns`,
|
after a later save of the running controller" — R-1343 reproduced), `TestR1343_TheBoxCommandFallsBackWhenNoControllerRuns`,
|
||||||
`TestHoldCtl_NotReachableFromOutside`, `TestHoldCtl_UnknownRequestIsRefused`, `TestHoldCtl_ANonSocketFileIsNotReplaced`.
|
`TestR1343_TheFallbackDropsAWaitingPutBackToo`, `TestHoldCtl_NotReachableFromOutside`,
|
||||||
|
`TestHoldCtl_UnknownRequestIsRefused`, `TestHoldCtl_ANonSocketFileIsNotReplaced`.
|
||||||
- **R-1340 — the operator's „HELD STOPPED" mail names the real cause** (`cmd/controller/holdmail.go`
|
- **R-1340 — the operator's „HELD STOPPED" mail names the real cause** (`cmd/controller/holdmail.go`
|
||||||
`heldStoppedOperatorText`, built from the hold record). Hold writers now record `RestoreKind` (off-site / local),
|
`heldStoppedOperatorText`, built from the hold record). Hold writers now record `RestoreKind` (off-site / local),
|
||||||
`MixedCause` (`definition_write` / `put_back` / `interrupted_put_back`), `FailedStep` (the put-back step that failed,
|
`MixedCause` (`definition_write` / `put_back` / `interrupted_put_back`), `FailedStep` (the put-back step that failed,
|
||||||
|
|||||||
@@ -238,18 +238,25 @@ func clearRestoreHoldCLI(dataDir, stack string, stdout, stderr io.Writer) int {
|
|||||||
fmt.Fprintf(stderr, "restore-hold: loading settings: %v\n", lerr)
|
fmt.Fprintf(stderr, "restore-hold: loading settings: %v\n", lerr)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
cleared, cerr := sett.ClearRestoreHold(stack)
|
// The same door as the running controller's (a waiting put-back is dropped from the app-stop marker too), over
|
||||||
if cerr != nil {
|
// this process's copy — the only copy while no controller runs.
|
||||||
fmt.Fprintf(stderr, "restore-hold: clearing %s: %v\n", stack, cerr)
|
g := backup.NewAppStopGuard(filepath.Join(dataDir, "appstop-state.json"), lg)
|
||||||
return 1
|
g.SetHolds(sett)
|
||||||
}
|
h, cerr := g.ClearHoldByOperator(stack)
|
||||||
if !cleared {
|
switch {
|
||||||
|
case errors.Is(cerr, backup.ErrNotHeld):
|
||||||
// "there was nothing to clear" is not success — a silent 0 here would let an operator believe they had
|
// "there was nothing to clear" is not success — a silent 0 here would let an operator believe they had
|
||||||
// unblocked an app whose name they mistyped.
|
// unblocked an app whose name they mistyped.
|
||||||
fmt.Fprintf(stderr, "no restore hold in force for %q — nothing was changed\n", stack)
|
fmt.Fprintf(stderr, "no restore hold in force for %q — nothing was changed\n", stack)
|
||||||
return 2
|
return 2
|
||||||
|
case cerr != nil:
|
||||||
|
fmt.Fprintf(stderr, "restore-hold: clearing %s: %v\n", stack, cerr)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
fmt.Fprintf(stdout, "restore hold cleared for %s in settings.json (no controller was running to ask; it was: %s).\n", stack, h.Why())
|
||||||
|
if h.Reason == settings.HoldReasonPutBackPending {
|
||||||
|
fmt.Fprintf(stdout, " Its waiting put-back is dropped; the pre-restore copy stays on disk at %s.\n", orNone(h.SafetyDump))
|
||||||
}
|
}
|
||||||
fmt.Fprintf(stdout, "restore hold cleared for %s in settings.json (no controller was running to ask).\n", stack)
|
|
||||||
fmt.Fprintf(stdout, " The controller reads it when it starts. If a controller IS running in this guest after all, restart it NOW,\n")
|
fmt.Fprintf(stdout, " The controller reads it when it starts. If a controller IS running in this guest after all, restart it NOW,\n")
|
||||||
fmt.Fprintf(stdout, " INSIDE THE GUEST — from the Proxmox host: pct exec <vmid> -- systemctl restart felhom-controller-bootstrap.service\n")
|
fmt.Fprintf(stdout, " INSIDE THE GUEST — from the Proxmox host: pct exec <vmid> -- systemctl restart felhom-controller-bootstrap.service\n")
|
||||||
fmt.Fprintf(stdout, " — or its next settings save writes the hold back (R-1343).\n")
|
fmt.Fprintf(stdout, " — or its next settings save writes the hold back (R-1343).\n")
|
||||||
|
|||||||
@@ -514,3 +514,46 @@ func TestD64_MainWiresTheNewPieces(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func timeAt(sec int) time.Time { return time.Date(2026, 10, 29, 12, 0, sec, 0, time.UTC) }
|
func timeAt(sec int) time.Time { return time.Date(2026, 10, 29, 12, 0, sec, 0, time.UTC) }
|
||||||
|
|
||||||
|
// No controller runs: the box command's own clear drops a waiting put-back from the app-stop marker too (the same
|
||||||
|
// door), so the next start with the drive bound pours nothing back.
|
||||||
|
// RED-PROOF (2026-10-29): with the fallback clearing through settings.ClearRestoreHold alone (its first shape in
|
||||||
|
// 5b3b541), this fails on „a later controller start poured the old copy back".
|
||||||
|
func TestR1343_TheFallbackDropsAWaitingPutBackToo(t *testing.T) {
|
||||||
|
r := newD64Rig(t)
|
||||||
|
undo := filepath.Join(r.dir, "drive", "felhom-data", "backups", "restore-undo", "immich", "20261029T080000Z")
|
||||||
|
if err := os.MkdirAll(undo, 0o700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := r.guard.Begin("unit-restore:immich", backup.ReasonUnitRestore, []string{"immich"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := r.guard.SetUndo("immich", undo); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
r.hold(t, settings.RestoreHold{Stack: "immich", Reason: settings.HoldReasonPutBackPending, SafetyDump: undo})
|
||||||
|
var out, errb bytes.Buffer
|
||||||
|
if code := clearRestoreHoldCLI(r.dir, "immich", &out, &errb); code != 0 {
|
||||||
|
t.Fatalf("exit %d, stderr %q", code, errb.String())
|
||||||
|
}
|
||||||
|
if !strings.Contains(out.String(), "waiting put-back is dropped") {
|
||||||
|
t.Errorf("output must say the put-back is dropped:\n%s", out.String())
|
||||||
|
}
|
||||||
|
sett2, err := settings.Load(filepath.Join(r.dir, "settings.json"), log.New(io.Discard, "", 0))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
poured := &recordingPutBacker{}
|
||||||
|
g2 := backup.NewAppStopGuard(filepath.Join(r.dir, "appstop-state.json"), r.logger)
|
||||||
|
g2.SetHolds(sett2)
|
||||||
|
g2.SetPutBacker(poured)
|
||||||
|
g2.SetStarter(nopStarter{})
|
||||||
|
g2.SetUndoReachable(func(string, string) bool { return true })
|
||||||
|
g2.Recover()
|
||||||
|
if len(poured.stacks) != 0 {
|
||||||
|
t.Fatalf("a later controller start poured the old copy back over the app: %v", poured.stacks)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(undo); err != nil {
|
||||||
|
t.Fatal("the copy must stay on disk")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user