v0.190.0 — the boot settle window, both gates on intent, and R-171
gates / gates (push) Successful in 8s
gates / gates (push) Successful in 8s
R-171 (a regression v0.189.0 introduced, CONFIRMED on hardware before any fix was written). Replacing isBootOrphan's container-count term with recorded intent made a drive-gate-stopped app read as a boot orphan: the gate stops apps with `compose down` (zero containers) and never touches desired_state, because it is not the customer. Observed on 9201 with the drive held unmounted — the sweep found and started it, burned both attempts, and handed it to the dead-app alarm. The write hazard did not materialise (the unbound mountpoint is host-root-owned and the guest is unprivileged) but that protection is accidental and untested. New consumer-side seam bootrecon.StartGate, fail-safe (cannot determine ⇒ do not start), wired in main.go. The rule is not new: the API's startGatedByMissingDrive already refuses this; the sweep bypassed it. R-157 mechanism A. The sweep looked once at T+5s, deriving candidates from a fleet docker was still restoring — three of six hard resets. Now a settle-then- sweep window: sample every 5s, settled after 3 identical samples, sweep ONCE at the end; ends on settled or a 50s budget, and the log says which. The budget is 50s because settle+budget+one retry must stay under the 90s dead-app grace — a test rejected 60s at 95s. A window that overruns emits a LATE RECOVERY warn rather than the grace being widened to hide it. Widening the window made two more holders reachable, so the one gate covers all three: an absent drive, a quiesce, and an in-flight app-data operation — reusing quiesce.SuppressedStacks() and a new read-only AppStopGuard.HeldStacks(). R-170. shouldRecreateOnBoot now reads desired_state with the identical three-way table; absent keeps the old hasContainers behaviour exactly. Its comment argued for the container count and was rewritten. presentStable is untouched. The two gates' agreement is pinned from both sides against one fixture table. 27/27 packages green; 6 red-proofs observed FAIL then restored.
This commit is contained in:
@@ -724,6 +724,23 @@ func (m *Manager) UpdateOptionalConfig(stackName string, values map[string]strin
|
||||
return m.RefreshStatus()
|
||||
}
|
||||
|
||||
// DriveLive reports whether an app's data drive is a live mountpoint right now.
|
||||
//
|
||||
// It is the SAME signal the userdata belt uses (manager.go, the `isMountPoint` seam) rather than a
|
||||
// second implementation, so the two can never disagree about whether a drive is there — a drift that
|
||||
// would be invisible until one of them acted on it. The system/local path is legitimately not a
|
||||
// mountpoint and is never gated, exactly as the belt treats it.
|
||||
//
|
||||
// R-171: exported because the boot reconciler must ask this question and lives in another package.
|
||||
// Before v0.190.0 nothing asked it on that path, so the sweep started apps whose drive was absent —
|
||||
// observed live on 2026-08-02 (audits/DIAG-bootrecon-drive-absent-2026-08-02.md).
|
||||
func (m *Manager) DriveLive(hddPath string) bool {
|
||||
if hddPath == "" || hddPath == m.sysDataPath {
|
||||
return true // SSD-resident: no external drive to be absent
|
||||
}
|
||||
return m.isMountPoint(hddPath)
|
||||
}
|
||||
|
||||
// LoadAppConfigByName reads app.yaml for a named stack. Returns nil if not found.
|
||||
func (m *Manager) LoadAppConfigByName(stackName string) *AppConfig {
|
||||
stack, ok := m.GetStack(stackName)
|
||||
|
||||
Reference in New Issue
Block a user