v0.311.0: after a power cut, an app repairs the files the cut left empty; the household sees lost photos (R-1007)
gates / gates (push) Successful in 52s
gates / gates (push) Successful in 52s
New template block after_unclean_boot (decision 236): once per controller start that followed an UNCLEAN host boot (crashboot's own verdict, after the 15-minute crash grace; a normal or unknown boot never runs it), each app with the block rebuilds ONLY its zero-byte files (scan: regular files, no symlinks, its own filesystem; ids appended to the app's own command in batches; one retry, then reported) and its own lost-file check is read. The household sees lost files (name, date taken) on the app page until dismissed; the operator gets app_repaired_after_crash with counts only. Skips, with the reason, an unproven image, an unknown key, an app not running. MinAgent 0.155.0 unchanged. Tests red-proofed (14 guards). One new parity fixture (the new card only). Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
@@ -58,6 +58,7 @@ type Grace struct {
|
||||
mu sync.Mutex
|
||||
resolved bool
|
||||
crashBoot bool
|
||||
bootAt time.Time // the unclean boot this start followed (set only when crashBoot)
|
||||
}
|
||||
|
||||
// New builds the grace for a controller that started at start. probe may be nil (no agent): unknown.
|
||||
@@ -116,10 +117,24 @@ func (g *Grace) tryResolve(final bool) {
|
||||
g.resolve(false, "the host's last unclean boot ("+f.BootAt.UTC().Format(time.RFC3339)+") is not the one this start followed")
|
||||
default:
|
||||
g.crashBoot = true
|
||||
g.bootAt = f.BootAt
|
||||
g.resolve(true, "the host's last boot ("+f.BootAt.UTC().Format(time.RFC3339)+") followed an UNCLEAN stop")
|
||||
}
|
||||
}
|
||||
|
||||
// Decided reports the run's verdict once it is known (R-1007): resolved=false while the fact may still arrive;
|
||||
// then crash (and the unclean boot's time) or not. It asks the probe like Within does — unknown resolves to a normal
|
||||
// boot at the end of the normal grace, never to a crash. Pinned by TestDecided_* in crashboot_test.go.
|
||||
func (g *Grace) Decided(now time.Time) (resolved, crash bool, bootAt time.Time) {
|
||||
g.duration(now.Sub(g.start))
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
return g.resolved, g.crashBoot, g.bootAt
|
||||
}
|
||||
|
||||
// Start is the controller start this grace measures from.
|
||||
func (g *Grace) Start() time.Time { return g.start }
|
||||
|
||||
func (g *Grace) resolve(crash bool, why string) {
|
||||
g.resolved = true
|
||||
if g.logger == nil {
|
||||
|
||||
@@ -104,3 +104,20 @@ func TestR856_AgentProbeReadsTheCrashGuardState(t *testing.T) {
|
||||
t.Error("an agent that predates the route (404) is unknown — a normal boot")
|
||||
}
|
||||
}
|
||||
|
||||
// R-1007: Decided exposes the same verdict, with the unclean boot's time. Unknown stays unresolved inside the
|
||||
// normal grace and resolves to NOT a crash at its end.
|
||||
func TestDecided_CrashCarriesTheBootTimeUnknownResolvesNormal(t *testing.T) {
|
||||
boot := start.Add(-time.Minute)
|
||||
g := New(start, fixed(Fact{Known: true, Unclean: true, BootAt: boot}, nil), nil)
|
||||
if res, crash, at := g.Decided(start.Add(time.Second)); !res || !crash || !at.Equal(boot) {
|
||||
t.Fatalf("crash: resolved=%t crash=%t at=%s", res, crash, at)
|
||||
}
|
||||
u := New(start, fixed(Fact{}, nil), nil)
|
||||
if res, _, _ := u.Decided(start.Add(10 * time.Second)); res {
|
||||
t.Fatal("unknown must not resolve inside the normal grace (the agent may come up late)")
|
||||
}
|
||||
if res, crash, _ := u.Decided(start.Add(NormalGrace)); !res || crash {
|
||||
t.Fatalf("unknown at the end of the normal grace: resolved=%t crash=%t, want a normal boot", res, crash)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user