An apply leaves a container a maintenance window holds still (hq issue 224)

A while-stopped step stops its module's containers, and an apply arriving
mid-window read the stopped server as broken and recreated it running under
the step - for the store's collector, a registry taking an upload the sweep
then deletes. The scheduler now records each window under the node's state
directory before its first stop and erases it after its last start, so every
apply on the machine (daemon, reconcile by hand, installer) reports a held
container held-still and leaves it for the first apply after the window.
A window whose host died, or past six hours, holds nothing; the report says
which windows are open.
This commit is contained in:
jochen
2026-10-05 18:27:04 +02:00
parent 238252e152
commit f08681f225
7 changed files with 648 additions and 13 deletions
+17 -5
View File
@@ -598,12 +598,15 @@ func runApply(ctx context.Context, opts options, d *declaration.Declaration, raw
}
fmt.Fprintf(out, "\napplying:\n")
report, updated, applyErr := apply.ApplyKeeping(ctx, sys, d, known, origin,
// Minding the windows the daemon's scheduler has open: this runs as its own process, and a
// `reconcile` by hand at 03:31 is exactly the apply that must not restart a server a collector
// is holding still (novox/hq issue 224).
report, updated, applyErr := apply.ApplyMindingWindows(ctx, sys, d, known, origin,
apply.ExecRunner, func(line string) {
if !opts.json {
fmt.Fprintln(out, line)
}
}, sealOpener(opts.state), apply.KeepIn(filepath.Dir(opts.state)))
}, sealOpener(opts.state), apply.KeepIn(filepath.Dir(opts.state)), apply.WindowsIn(filepath.Dir(opts.state)))
// What this apply settles about the node, whichever way it went. The mode is what the
// declaration said and the check above agreed with; the bundle, once applied, is consumed.
@@ -1041,6 +1044,9 @@ func runLink(ctx context.Context, opts options) error {
// across the reconcile loop; a host restart rebuilds it from the declaration the node kept, the
// first time either path applies. Its own loop is the thing on the clock — no system timer.
sched := apply.NewScheduler(apply.SystemClock(), apply.ExecRunner, say)
// A window it opens is written beside the node's state, where every apply on this machine —
// this process's, a `reconcile` run by hand, the installer's — reads it (novox/hq issue 224).
sched.RecordWindowsIn(apply.WindowsIn(filepath.Dir(opts.state)))
go sched.Run(ctx)
// **Standing aside for a successor happens between reconciles and nowhere else** (novox/hq ADR
@@ -1300,7 +1306,7 @@ func worthSaying(report link.Report) bool {
return false
}
return len(report.Held) > 0 || report.Firewall != "" || len(report.Outward) > 0 ||
len(report.Filters) > 0 || report.FoundFirewall != nil
len(report.Filters) > 0 || report.FoundFirewall != nil || len(report.Windows) > 0
}
// applyDeclared applies a declaration that has already been proved to come from the mesh.
@@ -1399,8 +1405,9 @@ func applyAndKeep(ctx context.Context, opts options, raw []byte, signed *store.D
//
// `say` already reaches stdout, and the launcher's unit sends that to the journal, so this needs
// no new mechanism — only for the argument to be passed.
outcome, updated, applyErr := apply.ApplyKeeping(ctx, built, declared, known, store.OriginDeclared,
apply.ExecRunner, announceOr(say), sealOpener(opts.state), apply.KeepIn(filepath.Dir(opts.state)))
outcome, updated, applyErr := apply.ApplyMindingWindows(ctx, built, declared, known, store.OriginDeclared,
apply.ExecRunner, announceOr(say), sealOpener(opts.state), apply.KeepIn(filepath.Dir(opts.state)),
apply.WindowsIn(filepath.Dir(opts.state)))
// The mode the mesh said, recorded whichever way the apply went: the declaration is kept
// either way, and the node is held to it from the next reconcile (novox/hq ADR 0100).
@@ -1443,6 +1450,11 @@ func applyAndKeep(ctx context.Context, opts options, raw []byte, signed *store.D
report.Held = append(report.Held, link.Held{ID: h.ID, Module: h.Module, Kind: h.Kind,
Target: h.Target, Since: h.Since, Changed: h.Changed, Kept: h.Kept, Facts: factsAsReported(h.Facts)})
}
// And a maintenance window open as the apply ended (novox/hq issue 224): a server stopped because
// its collector is running reads as working, not broken.
for _, w := range outcome.Windows {
report.Windows = append(report.Windows, link.Window{Step: w.Step, Holds: w.Holds, Since: w.Opened, Until: w.Until})
}
// And what runs here that nobody asked for (novox/hq ADR 0163).
if strays, err := apply.Strays(ctx, apply.ExecRunner, updated); err != nil {
fmt.Fprintf(os.Stderr, "mesh-host: applied, and could not list what else runs here: %v\n", err)