v0.283.1: a Stop holds during the volume dump in production and at the crash recovery (R-721, found live on v0.283.0)
gates / gates (push) Successful in 28s

Red-proofs RP43, RP44. MinAgent 0.131.0 (unchanged).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-30 10:52:19 +02:00
parent 6be6c53e29
commit 29d3c533aa
5 changed files with 104 additions and 0 deletions
+14
View File
@@ -2297,6 +2297,14 @@ type gatedAppStopStarter struct {
logger *log.Logger
}
// WantsStopped (R-721, v0.283.1) forwards the household's intent to the crash recovery (see Recover).
func (s gatedAppStopStarter) WantsStopped(name string) bool {
if w, ok := s.inner.(interface{ WantsStopped(string) bool }); ok {
return w.WantsStopped(name)
}
return false
}
func (s gatedAppStopStarter) StartStack(name string) error {
if ok, why := s.gate.MayStart(name); !ok {
s.logger.Printf("[WARN] [appstop] refusing to restart %q after an interrupted operation: %s", name, why)
@@ -2735,6 +2743,12 @@ func (a *stackAdapter) IsDeploying(name string) bool {
return ok && s.Deploying
}
// WantsStopped (R-721, v0.283.1): the backup package asks this before restarting an app it stopped for a
// volume dump. Measured live on 9202 2026-09-30 with v0.283.0: the method existed on the stacks manager but
// NOT on this adapter, so the dump's type assertion failed and a household Stop was undone 8 s later.
// Pinned by TestR721_TheBackupsStackProviderAnswersWantsStopped.
func (a *stackAdapter) WantsStopped(name string) bool { return a.mgr.WantsStopped(name) }
func (a *stackAdapter) StartStack(name string) error {
return a.mgr.StartStack(name)
}
@@ -0,0 +1,31 @@
package main
import (
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/quiesce"
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
)
// R-721 (v0.283.1) — the seam, not the rule. The rule is pinned in internal/backup and internal/quiesce with
// fakes that DO answer WantsStopped; production hands the backup manager a *stackAdapter and the quiesce loop
// a *stacks.Manager, and a type that does not answer makes the check silently skip. Measured live on 9202
// 2026-09-30 with v0.283.0: the adapter lacked the method and the dump restarted a stopped app.
// COMPANION RED-PROOF: delete stackAdapter.WantsStopped → this test fails.
func TestR721_TheBackupsStackProviderAnswersWantsStopped(t *testing.T) {
var prov interface{} = &stackAdapter{}
if _, ok := prov.(interface{ WantsStopped(string) bool }); !ok {
t.Fatal("the backup manager's production stack provider does not answer WantsStopped — a household Stop is undone by the volume dump (R-721)")
}
// The quiesce loop is handed the stacks manager itself (main.go quiesce.Options{Stacks: stackMgr}).
// The startup crash recovery is handed gatedAppStopStarter{inner: stackMgr} (main.go SetStarter).
var gs interface{} = gatedAppStopStarter{inner: (*stacks.Manager)(nil)}
if _, ok := gs.(interface{ WantsStopped(string) bool }); !ok {
t.Fatal("the crash recovery's starter does not answer WantsStopped (R-721)")
}
var mgr *stacks.Manager
var _ quiesce.Stacks = mgr
if _, ok := interface{}(mgr).(interface{ WantsStopped(string) bool }); !ok {
t.Fatal("the quiesce loop's production Stacks does not answer WantsStopped (R-721)")
}
}