R-1288: a restore of a set-aside unit sets the replaced install's unit aside at once, with no alarm. R-1282: the later media restart time is stored and resumed at start, once. Red-proved. Commit only.
gates / gates (push) Successful in 1m26s
gates / gates (push) Successful in 1m26s
This commit is contained in:
@@ -59,9 +59,17 @@ func (f *fakeRecoveryProvider) GetStackClassifiedBinds(string) ([]ClassifiedBind
|
||||
func (f *fakeRecoveryProvider) RecoverStackSecrets(string, []string) map[string]string {
|
||||
return f.secrets
|
||||
}
|
||||
func (f *fakeRecoveryProvider) RecreateStackDefinitionFromUnit(_, _ string, fullEnv map[string]string) error {
|
||||
func (f *fakeRecoveryProvider) RecreateStackDefinitionFromUnit(_, composeDir string, fullEnv map[string]string) error {
|
||||
f.gotEnv = fullEnv
|
||||
f.calls = append(f.calls, "recreate")
|
||||
// As the stack manager does (stacks/deploy.go, D47): the restored app takes the unit's install identity.
|
||||
if b, err := os.ReadFile(filepath.Join(composeDir, "app.yaml")); err == nil {
|
||||
for _, l := range strings.Split(string(b), "\n") {
|
||||
if v, ok := strings.CutPrefix(strings.TrimSpace(l), "install_id:"); ok && strings.TrimSpace(v) != "" {
|
||||
f.info.InstallID = strings.Trim(strings.TrimSpace(v), `"'`)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (f *fakeRecoveryProvider) StartStackServices(_ string, services []string) error {
|
||||
|
||||
@@ -323,5 +323,39 @@ func (m *Manager) RestoreSetAsideUnit(id string) (string, UnitRestoreResult, err
|
||||
}
|
||||
m.logger.Printf("[INFO] [backup] restore %s from the set-aside unit %s (an earlier install, D47)", k.Source, k.Path)
|
||||
res, err := m.RestoreFromRecoveryUnitAt(k.Source, k.Path)
|
||||
if err == nil || errors.Is(err, ErrRestoreNotStarted) {
|
||||
// The restored data stays in both cases (D49 = A), so the app is now the set-aside unit's install.
|
||||
m.retireReplacedUnit(k.Source)
|
||||
}
|
||||
return k.Source, res, err
|
||||
}
|
||||
|
||||
// retireReplacedUnit (R-1288): after the household restored an earlier install's set-aside unit, the app's primary
|
||||
// unit belongs to the install the restore REPLACED. It is set aside here, at the restore — moved whole, never deleted
|
||||
// (D47), one log line and NO operator alarm: the household chose this restore, so nothing unexpected happened. Before,
|
||||
// the next backup's writer found it and set it aside with a second alarm and a second kept copy that looked like a
|
||||
// reinstall. A unit that is already this install's (or none) is left alone; a move that fails leaves it for the next
|
||||
// writer, whose guard sets it aside as before.
|
||||
func (m *Manager) retireReplacedUnit(stack string) {
|
||||
nsRoot := m.namespaceRoot(m.GetAppDrivePath(stack))
|
||||
unitDir := RecoveryUnitPath(nsRoot, stack)
|
||||
exists, unitID, unitSecrets := unitOwnerOf(unitDir)
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
liveID, liveSecrets, err := m.liveInstall(stack)
|
||||
if err != nil {
|
||||
m.logger.Printf("[WARN] [backup] %s: after the restore of an earlier install, this install's identity cannot be read (%v) — the replaced unit is left for the next backup's check (D47)", stack, err)
|
||||
return
|
||||
}
|
||||
if v, _ := unitOwnerVerdict(unitID, liveID, unitSecrets, liveSecrets); v != ownerOther {
|
||||
return
|
||||
}
|
||||
dest, err := m.setAsideUnit(stack, unitDir, nsRoot)
|
||||
if err != nil {
|
||||
m.logger.Printf("[WARN] [backup] %s: the unit of the install the restore replaced (%s) could not be set aside: %v — the next backup's check sets it aside (D47)", stack, orUnknown(unitID), err)
|
||||
return
|
||||
}
|
||||
m.logger.Printf("[INFO] [backup] %s: the unit of the install the restore replaced (%s) is set aside whole → %s — replaced by the restored install %s; no alarm (R-1288, D47)",
|
||||
stack, orUnknown(unitID), dest, orUnknown(liveID))
|
||||
}
|
||||
|
||||
@@ -320,3 +320,56 @@ func TestD47_ASetAsideTellsTheOperatorOnce(t *testing.T) {
|
||||
t.Fatalf("the operator hears once, with both identities: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// R-1288, the Tester 1 order measured 2026-10-25 on 0.343.0: install A backed up → reinstall B → B's first backup sets
|
||||
// A's unit aside (one alarm) and writes B's own unit → the household restores A's set-aside copy. Before: the next
|
||||
// backup found B's unit and set it aside with a SECOND alarm. Now B's unit is set aside AT the restore, quietly, and
|
||||
// the next backup writes A's unit with nothing more set aside.
|
||||
// RED-PROOF: drop the retireReplacedUnit call → after the restore the primary place still holds B's unit, and the
|
||||
// next capture alarms a second time.
|
||||
func TestR1288_ARestoreOfASetAsideUnitRetiresTheReplacedUnitQuietly(t *testing.T) {
|
||||
r := d47Setup(t, "aaaa000000000001")
|
||||
var alarms []string
|
||||
r.m.SetUnitSetAsideNotify(func(stack, unitID, liveID, dest string) { alarms = append(alarms, unitID) })
|
||||
r.reinstall("bbbb000000000002")
|
||||
if err := r.m.captureRecoveryUnit("tandoor", false); err != nil { // B's first backup: A set aside, B's unit written
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(alarms) != 1 || len(r.setAside(t)) != 1 {
|
||||
t.Fatalf("precondition: one set-aside, one alarm; got alarms %v, set-aside %v", alarms, r.setAside(t))
|
||||
}
|
||||
r.m.discoverDBs = func(context.Context) ([]DiscoveredDB, error) {
|
||||
return []DiscoveredDB{{StackName: "tandoor", ContainerName: "tandoor-postgres", DBType: DBTypePostgres}}, nil
|
||||
}
|
||||
r.m.importDBDump = func(context.Context, DiscoveredDB, string) error { return nil }
|
||||
r.m.volumeReplayFrom = func(string, string) (int, error) { return 1, nil }
|
||||
r.m.undoVolumeProbeFn = func(string) (int64, bool, error) { return 10, true, nil }
|
||||
r.m.undoVolumeTarFn = func(vol, dir string) error {
|
||||
return os.WriteFile(filepath.Join(dir, vol+".tar"), []byte("live"), 0o600)
|
||||
}
|
||||
ks := r.sett.GetKeptCopies()
|
||||
if _, _, err := r.m.RestoreSetAsideUnit(ks[0].ID); err != nil {
|
||||
t.Fatalf("restore of A's set-aside unit: %v", err)
|
||||
}
|
||||
if r.prov.info.InstallID != "aaaa000000000001" {
|
||||
t.Fatalf("precondition: the restored app is install A, got %q", r.prov.info.InstallID)
|
||||
}
|
||||
if exists, id, _ := unitOwnerOf(r.unitDir()); exists {
|
||||
t.Fatalf("after the restore the primary place still holds install %q's unit — it must be set aside at the restore", id)
|
||||
}
|
||||
if got := len(r.setAside(t)); got != 2 {
|
||||
t.Fatalf("set-aside units after the restore: %d, want 2 (A's, kept as the household's copy, and B's)", got)
|
||||
}
|
||||
if len(alarms) != 1 {
|
||||
t.Fatalf("the restore the household chose must not alarm: alarms %v", alarms)
|
||||
}
|
||||
if !strings.Contains(r.logs.String(), "replaced by the restored install") {
|
||||
t.Fatal("the restore's set-aside must say why in one line")
|
||||
}
|
||||
if err := r.m.captureRecoveryUnit("tandoor", false); err != nil { // the next backup: A's own unit, nothing set aside
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(alarms) != 1 || len(r.setAside(t)) != 2 {
|
||||
t.Fatalf("the next backup set something aside again: alarms %v, set-aside %v", alarms, r.setAside(t))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user