D22: every restore puts the data back exactly on failure and after a power cut (R-1177, R-1195); whole restore asks the unit first (R-1178); no mirror during a restore (R-1180); W22 restore refusals in Hungarian; unreleased
gates / gates (push) Successful in 1m40s
gates / gates (push) Successful in 1m40s
This commit is contained in:
@@ -46,6 +46,8 @@ const (
|
||||
ReasonOffboxReconstitute AppStopReason = "offbox_reconstitute"
|
||||
// ReasonAppExport — a .fab export taken with "stop the app first".
|
||||
ReasonAppExport AppStopReason = "app_export"
|
||||
// ReasonUnitRestore — a local restore from a recovery unit (the app's own, the second drive's, a kept one).
|
||||
ReasonUnitRestore AppStopReason = "unit_restore"
|
||||
)
|
||||
|
||||
// humanReason is the operator-facing phrasing for each reason.
|
||||
@@ -57,6 +59,8 @@ func (r AppStopReason) humanReason() string {
|
||||
return "an off-site restore"
|
||||
case ReasonAppExport:
|
||||
return "an app export"
|
||||
case ReasonUnitRestore:
|
||||
return "a local restore"
|
||||
default:
|
||||
return string(r)
|
||||
}
|
||||
@@ -74,6 +78,8 @@ func (r AppStopReason) MessageKey() string {
|
||||
return "appstop.reason.offbox_reconstitute"
|
||||
case ReasonAppExport:
|
||||
return "appstop.reason.app_export"
|
||||
case ReasonUnitRestore:
|
||||
return "appstop.reason.unit_restore"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
@@ -87,6 +93,9 @@ type AppStopMarker struct {
|
||||
Reason AppStopReason `json:"reason"`
|
||||
Stacks []string `json:"stacks"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
// Undo (D22, R-1195) names, per app, the copy of its live volumes and definition a restore took right after the
|
||||
// stop (restore_crash_putback.go). Set only once the copy is whole; Recover pours it back before the restart.
|
||||
Undo map[string]string `json:"undo,omitempty"`
|
||||
}
|
||||
|
||||
// AppStopStarter is the one thing recovery needs: the ability to start a stack. StartStack must be
|
||||
@@ -121,6 +130,9 @@ type AppStopGuard struct {
|
||||
now func() time.Time
|
||||
// starter is only needed by Recover; Begin/End work without one.
|
||||
starter AppStopStarter
|
||||
// putBacker (D22, R-1195) pours a restore's undo copy back before Recover restarts the app. nil = no put-back
|
||||
// (the pre-D22 restart, unchanged).
|
||||
putBacker InterruptedRestorePutBacker
|
||||
|
||||
// suppressed (R-330) is the in-memory "the alarm must not fire for these, we are holding them"
|
||||
// set. It is deliberately NOT persisted: after a restart the guard no longer holds anything —
|
||||
@@ -147,6 +159,9 @@ type AppStopRecovery struct {
|
||||
// (R-174). The marker is kept for these too, but they are NOT a fault and MUST NOT alarm: the
|
||||
// holder owns the restart. Separate from Failed for the reason recorded on ErrStartRefused.
|
||||
Refused []string
|
||||
// PutBack are apps whose interrupted restore was undone from its copy before the restart (D22, R-1195): their
|
||||
// data is as it was before the restore.
|
||||
PutBack []string
|
||||
}
|
||||
|
||||
// Alarming reports whether this recovery is worth paging an operator about. A recovery that only
|
||||
@@ -197,6 +212,9 @@ func (r *AppStopRecovery) Detail() string {
|
||||
if len(r.Refused) > 0 {
|
||||
d += fmt.Sprintf(" held_by_drive=%v", r.Refused)
|
||||
}
|
||||
if len(r.PutBack) > 0 {
|
||||
d += fmt.Sprintf(" put_back=%v", r.PutBack)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
@@ -218,6 +236,43 @@ func (g *AppStopGuard) SetStarter(s AppStopStarter) {
|
||||
g.starter = s
|
||||
}
|
||||
|
||||
// InterruptedRestorePutBacker pours an interrupted restore's undo copy back (restore_crash_putback.go). It must leave
|
||||
// the app stopped; Recover starts it.
|
||||
type InterruptedRestorePutBacker interface {
|
||||
PutBackInterruptedRestore(stack, undoDir string) error
|
||||
}
|
||||
|
||||
// SetPutBacker wires the put-back Recover runs for an app whose restore was cut after its copy was taken.
|
||||
func (g *AppStopGuard) SetPutBacker(p InterruptedRestorePutBacker) {
|
||||
if g != nil {
|
||||
g.putBacker = p
|
||||
}
|
||||
}
|
||||
|
||||
// SetUndo records, in the active marker, where stack's undo copy is (D22, R-1195). Called once the copy is whole;
|
||||
// a marker that is not active (or not for this app) is left alone.
|
||||
func (g *AppStopGuard) SetUndo(stack, dir string) error {
|
||||
if g == nil || g.path == "" {
|
||||
return nil
|
||||
}
|
||||
m, ok := g.read()
|
||||
if !ok || !m.Active {
|
||||
return fmt.Errorf("no active app-stop marker to record the undo copy in")
|
||||
}
|
||||
found := false
|
||||
for _, s := range m.Stacks {
|
||||
found = found || s == stack
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("the active app-stop marker does not hold %s", stack)
|
||||
}
|
||||
if m.Undo == nil {
|
||||
m.Undo = map[string]string{}
|
||||
}
|
||||
m.Undo[stack] = dir
|
||||
return g.write(m)
|
||||
}
|
||||
|
||||
// Begin records that `stacks` are about to be stopped by `reason`. It MUST be called BEFORE the
|
||||
// first stop — an error here means the marker could not be written, and the caller must not proceed
|
||||
// to stop an app it cannot promise to restart.
|
||||
@@ -304,6 +359,21 @@ func (g *AppStopGuard) Recover() *AppStopRecovery {
|
||||
g.logger.Printf("[INFO] [appstop] crash recovery: NOT restarting %s — the household stopped it", name)
|
||||
continue
|
||||
}
|
||||
if dir := m.Undo[name]; dir != "" && g.putBacker != nil {
|
||||
// D22 (R-1195): the restore was cut after its copy of the live data was taken — the app may be on the
|
||||
// backup's data, or on a mix. Pour the copy back first; a put-back that fails is a FAILED recovery
|
||||
// (the marker is kept, so the next startup tries again) and the app is not started on unknown data.
|
||||
if _, statErr := os.Stat(dir); statErr != nil {
|
||||
g.logger.Printf("[WARN] [appstop] crash recovery: %s's restore copy %s is gone (%v) — restarting as before", name, dir, statErr)
|
||||
} else if err := g.putBacker.PutBackInterruptedRestore(name, dir); err != nil {
|
||||
g.logger.Printf("[ERROR] [appstop] crash recovery: putting %s back from %s FAILED: %v — NOT starting it", name, dir, err)
|
||||
res.Failed = append(res.Failed, name)
|
||||
continue
|
||||
} else {
|
||||
g.logger.Printf("[WARN] [appstop] crash recovery: %s's interrupted restore is undone — its data is as before the restore (R-1195)", name)
|
||||
res.PutBack = append(res.PutBack, name)
|
||||
}
|
||||
}
|
||||
if err := g.starter.StartStack(name); err != nil {
|
||||
// R-174: a REFUSAL is not a failure. The starter's gate has said this app must not be
|
||||
// started (an absent data drive), so the app is left down deliberately and the holder
|
||||
@@ -324,6 +394,7 @@ func (g *AppStopGuard) Recover() *AppStopRecovery {
|
||||
sort.Strings(res.Failed)
|
||||
sort.Strings(res.Refused)
|
||||
sort.Strings(res.Restarted)
|
||||
sort.Strings(res.PutBack)
|
||||
|
||||
// The marker is kept for BOTH unfinished outcomes, for the same reason and with different
|
||||
// urgency: a failed restart is retried next startup, and a refused one is genuinely unfinished
|
||||
|
||||
Reference in New Issue
Block a user