package release import "fmt" // HeldWakeInput is what the held wake check decides on (settings spec §4.3 // rule 3, "The wake checks too"; ruling R-HELD-PURE): the registry columns // and root's records the agent reads for the wake, read by the caller so the // decision stays pure. The host's heldTarget and the update simulation's // wake both call DecideHeldWake with it. type HeldWakeInput struct { Settings string // the route's update_settings column, as the registry holds it Build BuildStateCopy // the registry copy, refreshed for the wake Consumed *Consumed // root's file for the host; nil when it has none (or it does not read) Refusal *Refusal // root's refusal record for the host; nil when none Installed string // the build the live link names on this box; "" when none } // HeldWakeDecision is the held wake check's answer. The zero value is "not // held": today's wake. type HeldWakeDecision struct { Target string // the build the instance must run Held bool // Unknown is rule 3's "build unknown": refused, root installs nothing. Unknown bool // Failed: root reports Target failed on this box (its decision, or a // not-up refusal record). Failed bool // AskRoot: root has made no decision for the current settings, so the // wake cannot know whether the instance is held. It holds: Target (the // build state's build, "" when there is none) runs only if it is // already installed; otherwise the wake asks root and waits for its // decision. Never "not held" (Astra round 1 finding 4). AskRoot bool // Unread: the copy's latest refresh failed (ReadErr). Refused: a held // wake is never decided from a stale copy (Astra round 1 finding 1). Unread bool } // DecideHeldWake is the held wake check's decision, in root's rule order // (Target, §4.3) so root and the wake agree: // // - default settings: not held (today's wake), nothing else read; // - the copy's latest refresh failed: Unread, refused; // - a column that does not parse even leniently: held as unknown once // root has decided unknown for exactly this column (ruling // R-SETTINGS-UNREADABLE), otherwise AskRoot; // - a pin: held on it (rule 1); // - with the copy read, a pending request resolved to T, the build // state's resolution first (R-RESOLUTION-STATE-WINS): held on T (rule // 2; manual and major are ignored for it); // - otherwise rule 3's target as Target computes it: an unread copy is // unknown; else last_executed; else the installed build (Astra round 1 // finding 5); else, with a replica, unknown; else a new instance, which // root gives the head, so not held. Each named build is failure-checked // against root's word (Failed); // - whether rule 3 holds at all needs root when the setting is a major // hold (the agent does not read tags). Root's consumed Reason is read // only when it was decided for the current column (Consumed.Settings); // with no such decision the wake holds and asks root (AskRoot), carrying // rule 3's target as the one build it may run if installed. Manual // holds by rule 3; a major hold holds when root's decision for this // column is major, unknown, or failed on rule 3's target (ruling // R-HELD-FAILED); any other decision for this column is not held. func DecideHeldWake(in HeldWakeInput) HeldWakeDecision { c := in.Consumed decided := c != nil && c.Settings != "" && c.Settings == SettingsDigest(in.Settings) u, uerr := ParseUpdateSettingsLenient(in.Settings) if uerr == nil && u.Default() { return HeldWakeDecision{} } if in.Build.ReadErr != "" { return HeldWakeDecision{Held: true, Unread: true} } if uerr != nil { if decided && c.Reason == string(ReasonUnknown) { return HeldWakeDecision{Held: true, Unknown: true} } return HeldWakeDecision{Held: true, AskRoot: true} } rootFailed := func(v string) bool { return v != "" && ((c != nil && c.Reason == string(ReasonFailed) && c.Detail == v) || (in.Refusal != nil && in.Refusal.Kind == RefusalNotUp && in.Refusal.Version == v)) } held := func(v string) HeldWakeDecision { return HeldWakeDecision{Target: v, Held: true, Failed: rootFailed(v)} } if u.Pin != "" { return held(u.Pin) } state := in.Build.State if in.Build.Read() && u.InstallRequest > Watermark(c, state) { if res := resolvedFor(c, state, u.InstallRequest); res != nil { return held(res.Target) } } var r3 HeldWakeDecision switch { case !in.Build.Read(): r3 = HeldWakeDecision{Held: true, Unknown: true} case state != nil && state.LastExecuted != "": r3 = held(state.LastExecuted) case in.Installed != "": r3 = held(in.Installed) case in.Build.Replica: r3 = HeldWakeDecision{Held: true, Unknown: true} } if !decided { return HeldWakeDecision{Held: true, AskRoot: true, Target: r3.Target, Failed: r3.Failed} } if u.Updates == UpdatesManual { return r3 } if u.Major != nil { switch c.Reason { case string(ReasonMajor), string(ReasonUnknown): return r3 case string(ReasonFailed): if r3.Target != "" && c.Detail == r3.Target { return r3 } } } return HeldWakeDecision{} } // HeldExecStale is the pre-exec write's revalidation (Astra round 1 finding // 1): d is DecideHeldWake on the build state read under the lease. A held // target other than the build about to run means the wake decided on a // stale copy, and the record is refused, so nothing runs. The build the // state's last_executed names is never stale (the data last ran it: a // restart or recycle of it is not a move onto another build), and a target // root reports failed is left to the planner's recovery (ruling // R-HELD-FAILED), as an unknown target is to the wake's own refusal. nil // when the build may run. // // inPlaceOf is the build the zero-downtime planner chose build in place of, // for this exec only ("" when it chose nothing: ruling R-PLANNED-RECOVERY): // a planned fallback after B failed, the good build booted untried, or a // recovery boot onto the good build because the failure book blocks the // installed build. When the state read under the lease holds exactly that // build as the target, the planner's reason for leaving it is the reason the // held target is left (settings spec §6: the scheduler, swap, failure // records and fallback act on the settings' build), so build runs, before // root's consumed file names the target failed. A selection made in place of // any other build admits nothing: the good build is never exempt in general, // since it is often the stale build a wake decided on (Astra round 1 // finding 1). func HeldExecStale(d HeldWakeDecision, st *BuildState, build, inPlaceOf string) error { if !d.Held || d.Unknown || d.Unread || d.Failed || d.Target == "" || d.Target == build { return nil } if st != nil && st.LastExecuted == build { return nil } if inPlaceOf != "" && inPlaceOf == d.Target { return nil } return fmt.Errorf("the build state names %s as this instance's build, not %s: the wake decided on a stale copy", d.Target, build) }