R-1253 (P2): an export that stops its app refuses while another operation (a restore) holds the app-stop marker — its Undo is never replaced. Commit only.
gates / gates (push) Successful in 1m28s
gates / gates (push) Successful in 1m28s
This commit is contained in:
@@ -3601,9 +3601,12 @@ func (a *exportAdapter) GetStacksBaseDir() string {
|
||||
type exportStopGuard struct{ g *backup.AppStopGuard }
|
||||
|
||||
func (a exportStopGuard) Begin(opID string, stacks []string) error {
|
||||
return a.g.Begin(opID, backup.ReasonAppExport, stacks)
|
||||
// R-1253: never replace the marker another operation (a restore) holds — its Undo is the only crash record.
|
||||
return a.g.BeginExclusive(opID, backup.ReasonAppExport, stacks)
|
||||
}
|
||||
|
||||
func (a exportStopGuard) BusyWith() string { return a.g.BusyWith() }
|
||||
|
||||
func (a exportStopGuard) End() { a.g.End() }
|
||||
|
||||
func (a exportStopGuard) ReleaseFailed(stacks ...string) { a.g.ReleaseFailed(stacks...) }
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
||||
)
|
||||
|
||||
// R-1253 (review round 2, HUNT-1 H3): an export with „stop the app first" during another app's restore replaced the
|
||||
// ONE app-stop marker — the restore's crash record and its Undo were lost, and a power cut then left the restored app
|
||||
// on mixed data with no put-back. The test is the review's mechanism with two Begins on one guard, through the
|
||||
// exporter's real adapter. RED-PROOF (2026-10-24): with exportStopGuard.Begin calling Begin instead of BeginExclusive,
|
||||
// this fails on „the restore's Undo is gone from the marker".
|
||||
func TestR1253_AnExportNeverReplacesARestoresMarker(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
marker := filepath.Join(dir, "appstop-state.json")
|
||||
g := backup.NewAppStopGuard(marker, log.New(io.Discard, "", 0))
|
||||
if err := g.Begin("unit-restore:bookstack", backup.ReasonUnitRestore, []string{"bookstack"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
undo := filepath.Join(dir, "backups", "restore-undo", "bookstack", "20261024T200000Z")
|
||||
if err := g.SetUndo("bookstack", undo); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ex := exportStopGuard{g: g}
|
||||
if busy := ex.BusyWith(); busy != "unit-restore:bookstack" {
|
||||
t.Fatalf("the export must see the restore holding the marker, got %q", busy)
|
||||
}
|
||||
err := ex.Begin("app-export:immich", []string{"immich"})
|
||||
if !errors.Is(err, backup.ErrAppStopBusy) {
|
||||
t.Fatalf("the export's Begin must be refused while the restore holds the marker, got %v", err)
|
||||
}
|
||||
b, _ := os.ReadFile(marker)
|
||||
if !strings.Contains(string(b), undo) || strings.Contains(string(b), "immich") {
|
||||
t.Fatalf("the restore's Undo is gone from the marker (or the export replaced it): %s", b)
|
||||
}
|
||||
// the restore ends → the export may run
|
||||
g.End()
|
||||
if err := ex.Begin("app-export:immich", []string{"immich"}); err != nil {
|
||||
t.Fatalf("after the restore ended the export must start: %v", err)
|
||||
}
|
||||
ex.End()
|
||||
// a marker a CRASH left (a new process over the same file) does not block: nothing in this process will End it
|
||||
if err := g.Begin("volume-dump:paperless", backup.ReasonVolumeDump, []string{"paperless"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fresh := exportStopGuard{g: backup.NewAppStopGuard(marker, log.New(io.Discard, "", 0))}
|
||||
if busy := fresh.BusyWith(); busy != "" {
|
||||
t.Fatalf("a crash-left marker is not a live operation, got %q", busy)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user