0054d4bd69
gates / gates (push) Successful in 27s
R-634: a whole-box backup no longer stops/restarts a DEPLOYING app (the measured cause of containers running under 'not deployed'); StopStack and StartStack refuse a deploying stack for every caller. R-625: held badge 'Stopped - restore needed', no Update button. R-636: kernel oom_kill counter; 20+ in 30 min -> one app_oom_storm. R-647: held error per reader, copy_holds key, two log wordings. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
62 lines
2.4 KiB
Go
62 lines
2.4 KiB
Go
package backup
|
|
|
|
import (
|
|
"io"
|
|
"log"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
|
)
|
|
|
|
// R-634 (v0.265.0). Measured 2026-09-23 on 9202: a whole-box backup pressed 20 s into outline's
|
|
// deploy stopped it (`compose down`), dumped its half-made volumes and ran a SECOND `compose up -d`
|
|
// beside the deploy's own — both failed, and the deploy recorded „not deployed".
|
|
|
|
type deployingFake struct {
|
|
volDumpFakeProvider
|
|
deploying map[string]bool
|
|
}
|
|
|
|
func (f *deployingFake) IsDeploying(name string) bool { return f.deploying[name] }
|
|
|
|
// Through the dumpVolumesSafe SEAM, never the real dump: the real one runs `docker run … tar` against a
|
|
// named volume, which on a developer machine CREATES that volume (it did, once, on DooPlex while this
|
|
// test was written). The seam is where the stop happens, so a call to it IS the leak.
|
|
//
|
|
// COMPANION RED-PROOF (REPORT.md): delete the stackIsDeploying block in runVolumeDumps — the dump
|
|
// (and with it the stop) is called for outline and this fails with dumped=[outline].
|
|
func TestR634_VolumeLegNeverStopsADeployingApp(t *testing.T) {
|
|
cfg := &config.Config{}
|
|
cfg.Paths.SystemDataPath = filepath.Join(t.TempDir(), "sys")
|
|
fake := &deployingFake{
|
|
volDumpFakeProvider: volDumpFakeProvider{
|
|
stacks: []StackSummary{{Name: "outline"}},
|
|
volumes: map[string][]string{"outline": {"outline_outline_data"}},
|
|
},
|
|
deploying: map[string]bool{"outline": true},
|
|
}
|
|
m := &Manager{cfg: cfg, logger: log.New(io.Discard, "", 0), systemDataPath: cfg.Paths.SystemDataPath,
|
|
stackProvider: fake}
|
|
var calls []string
|
|
m.dumpVolumesSafe = func(name string) error { calls = append(calls, name); return nil }
|
|
|
|
summary, dumped, ok := m.runVolumeDumps()
|
|
if len(calls) != 0 {
|
|
t.Fatalf("a DEPLOYING app was stopped and dumped by the backup: dumped=%v (R-634's race)", calls)
|
|
}
|
|
if !ok || dumped != 0 {
|
|
t.Errorf("a deploy in flight is a SKIP, never a failure that pages the operator: ok=%v dumped=%d %v", ok, dumped, summary)
|
|
}
|
|
if !containsSummary(summary, "SKIP outline volumes (deploying)") {
|
|
t.Errorf("the skip must be visible in the run summary, got %v", summary)
|
|
}
|
|
|
|
// Control: the same app once its deploy has finished IS backed up (the skip is conditional).
|
|
fake.deploying["outline"] = false
|
|
m.runVolumeDumps()
|
|
if len(calls) != 1 || calls[0] != "outline" {
|
|
t.Errorf("control: a finished app must be dumped as before, dumped=%v", calls)
|
|
}
|
|
}
|