feat(D53): the hub learns which apps are held — app_held / app_hold_cleared, daily repeat
gates / gates (push) Successful in 54s
gates / gates (push) Successful in 54s
internal/holdreport: one observer of the hold store, ticked every minute; app_held for a new or changed hold (stack_name, reason, since, why), app_hold_cleared when gone (also while the controller was down: settings.reported_holds), every hold again daily and at start. Severity info. RestoreHold.Why/WireReason (holdWhy calls it). 8 tests, red-proved (inert Tick; unwired scheduler line). Commit only (felhom.eu 08 §10).
This commit is contained in:
@@ -232,6 +232,10 @@ type Settings struct {
|
||||
// out-of-memory storm (RFC3339). Survives the hold being lifted by Start, so a second stop within
|
||||
// 24 h is known to be a second one.
|
||||
UnhealthyStops map[string]string `json:"unhealthy_stops,omitempty"`
|
||||
// ReportedHolds (D53, `08` §10) — the holds last told to the hub as app_held: stack → „reason|at". The hold
|
||||
// reporter (internal/holdreport) compares it with RestoreHolds to send app_held for a new or changed hold and
|
||||
// app_hold_cleared for one that is gone — also one cleared while the controller was down, because it is kept here.
|
||||
ReportedHolds map[string]string `json:"reported_holds,omitempty"`
|
||||
|
||||
// Cross-drive restic repo password (auto-generated on first use)
|
||||
CrossDriveResticPassword string `json:"cross_drive_restic_password,omitempty"`
|
||||
@@ -1998,6 +2002,35 @@ const (
|
||||
HoldReasonPutBackPending = "putback_pending"
|
||||
)
|
||||
|
||||
// WireReason is the hold's reason as the hub reads it (D53): the empty legacy value is named restore_failed.
|
||||
func (h RestoreHold) WireReason() string {
|
||||
if h.Reason == HoldReasonRestoreFailed {
|
||||
return "restore_failed"
|
||||
}
|
||||
return h.Reason
|
||||
}
|
||||
|
||||
// Why says which operation put a hold in place and what lifts it — one sentence per reason (R-1286). English,
|
||||
// operator-facing: the controller's own -list-restore-holds output and the hub's „Held apps" list (D53) both use it.
|
||||
func (h RestoreHold) Why() string {
|
||||
switch h.Reason {
|
||||
case HoldReasonUpdateFailed:
|
||||
return "held after a failed update (" + h.At + ") — restore it from its backup to start it"
|
||||
case HoldReasonUnhealthyStop:
|
||||
kind := h.UnhealthyKind
|
||||
if kind == "" {
|
||||
kind = "unhealthy"
|
||||
}
|
||||
return "stopped by the box for a " + strings.ReplaceAll(kind, "_", " ") + " (" + h.At + ") — the household's Start gives it one more try"
|
||||
case HoldReasonRestoreMixed:
|
||||
return "held after an off-site restore failed its database replay (" + h.At + ") — clear the hold to start it"
|
||||
case HoldReasonPutBackPending:
|
||||
return "held until the pre-restore copy is poured back (" + h.At + ") — the put-back starts it"
|
||||
default:
|
||||
return "held after a failed restore whose rollback also failed (" + h.At + ") — clear the hold to start it"
|
||||
}
|
||||
}
|
||||
|
||||
// SetRestoreHold records a hold. Modelled on SetDisconnected: a condition, plus what it is holding.
|
||||
func (s *Settings) SetRestoreHold(h RestoreHold) error {
|
||||
s.mu.Lock()
|
||||
@@ -2031,6 +2064,32 @@ func (s *Settings) ListRestoreHolds() []RestoreHold {
|
||||
return out
|
||||
}
|
||||
|
||||
// GetReportedHolds returns a copy of the holds last reported to the hub (D53): stack → „reason|at".
|
||||
func (s *Settings) GetReportedHolds() map[string]string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
out := make(map[string]string, len(s.ReportedHolds))
|
||||
for k, v := range s.ReportedHolds {
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// SetReportedHolds replaces the record of the holds reported to the hub (D53). An empty map removes the field.
|
||||
func (s *Settings) SetReportedHolds(m map[string]string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if len(m) == 0 {
|
||||
s.ReportedHolds = nil
|
||||
} else {
|
||||
s.ReportedHolds = make(map[string]string, len(m))
|
||||
for k, v := range m {
|
||||
s.ReportedHolds[k] = v
|
||||
}
|
||||
}
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// ClearRestoreHold removes a hold. Returns false when there was none, so a caller can tell "cleared"
|
||||
// from "there was nothing to clear" rather than reporting success either way.
|
||||
func (s *Settings) ClearRestoreHold(stack string) (bool, error) {
|
||||
|
||||
Reference in New Issue
Block a user