| 1 | package release |
| 2 | |
| 3 | import "fmt" |
| 4 | |
| 5 | // HeldWakeInput is what the held wake check decides on (settings spec §4.3 |
| 6 | // rule 3, "The wake checks too"; ruling R-HELD-PURE): the registry columns |
| 7 | // and root's records the agent reads for the wake, read by the caller so the |
| 8 | // decision stays pure. The host's heldTarget and the update simulation's |
| 9 | // wake both call DecideHeldWake with it. |
| 10 | type HeldWakeInput struct { |
| 11 | Settings string // the route's update_settings column, as the registry holds it |
| 12 | Build BuildStateCopy // the registry copy, refreshed for the wake |
| 13 | Consumed *Consumed // root's file for the host; nil when it has none (or it does not read) |
| 14 | Refusal *Refusal // root's refusal record for the host; nil when none |
| 15 | Installed string // the build the live link names on this box; "" when none |
| 16 | } |
| 17 | |
| 18 | // HeldWakeDecision is the held wake check's answer. The zero value is "not |
| 19 | // held": today's wake. |
| 20 | type HeldWakeDecision struct { |
| 21 | Target string // the build the instance must run |
| 22 | Held bool |
| 23 | // Unknown is rule 3's "build unknown": refused, root installs nothing. |
| 24 | Unknown bool |
| 25 | // Failed: root reports Target failed on this box (its decision, or a |
| 26 | // not-up refusal record). |
| 27 | Failed bool |
| 28 | // AskRoot: root has made no decision for the current settings, so the |
| 29 | // wake cannot know whether the instance is held. It holds: Target (the |
| 30 | // build state's build, "" when there is none) runs only if it is |
| 31 | // already installed; otherwise the wake asks root and waits for its |
| 32 | // decision. Never "not held" (Astra round 1 finding 4). |
| 33 | AskRoot bool |
| 34 | // Unread: the copy's latest refresh failed (ReadErr). Refused: a held |
| 35 | // wake is never decided from a stale copy (Astra round 1 finding 1). |
| 36 | Unread bool |
| 37 | } |
| 38 | |
| 39 | // DecideHeldWake is the held wake check's decision, in root's rule order |
| 40 | // (Target, §4.3) so root and the wake agree: |
| 41 | // |
| 42 | // - default settings: not held (today's wake), nothing else read; |
| 43 | // - the copy's latest refresh failed: Unread, refused; |
| 44 | // - a column that does not parse even leniently: held as unknown once |
| 45 | // root has decided unknown for exactly this column (ruling |
| 46 | // R-SETTINGS-UNREADABLE), otherwise AskRoot; |
| 47 | // - a pin: held on it (rule 1); |
| 48 | // - with the copy read, a pending request resolved to T, the build |
| 49 | // state's resolution first (R-RESOLUTION-STATE-WINS): held on T (rule |
| 50 | // 2; manual and major are ignored for it); |
| 51 | // - otherwise rule 3's target as Target computes it: an unread copy is |
| 52 | // unknown; else last_executed; else the installed build (Astra round 1 |
| 53 | // finding 5); else, with a replica, unknown; else a new instance, which |
| 54 | // root gives the head, so not held. Each named build is failure-checked |
| 55 | // against root's word (Failed); |
| 56 | // - whether rule 3 holds at all needs root when the setting is a major |
| 57 | // hold (the agent does not read tags). Root's consumed Reason is read |
| 58 | // only when it was decided for the current column (Consumed.Settings); |
| 59 | // with no such decision the wake holds and asks root (AskRoot), carrying |
| 60 | // rule 3's target as the one build it may run if installed. Manual |
| 61 | // holds by rule 3; a major hold holds when root's decision for this |
| 62 | // column is major, unknown, or failed on rule 3's target (ruling |
| 63 | // R-HELD-FAILED); any other decision for this column is not held. |
| 64 | func DecideHeldWake(in HeldWakeInput) HeldWakeDecision { |
| 65 | c := in.Consumed |
| 66 | decided := c != nil && c.Settings != "" && c.Settings == SettingsDigest(in.Settings) |
| 67 | u, uerr := ParseUpdateSettingsLenient(in.Settings) |
| 68 | if uerr == nil && u.Default() { |
| 69 | return HeldWakeDecision{} |
| 70 | } |
| 71 | if in.Build.ReadErr != "" { |
| 72 | return HeldWakeDecision{Held: true, Unread: true} |
| 73 | } |
| 74 | if uerr != nil { |
| 75 | if decided && c.Reason == string(ReasonUnknown) { |
| 76 | return HeldWakeDecision{Held: true, Unknown: true} |
| 77 | } |
| 78 | return HeldWakeDecision{Held: true, AskRoot: true} |
| 79 | } |
| 80 | rootFailed := func(v string) bool { |
| 81 | return v != "" && ((c != nil && c.Reason == string(ReasonFailed) && c.Detail == v) || |
| 82 | (in.Refusal != nil && in.Refusal.Kind == RefusalNotUp && in.Refusal.Version == v)) |
| 83 | } |
| 84 | held := func(v string) HeldWakeDecision { |
| 85 | return HeldWakeDecision{Target: v, Held: true, Failed: rootFailed(v)} |
| 86 | } |
| 87 | if u.Pin != "" { |
| 88 | return held(u.Pin) |
| 89 | } |
| 90 | state := in.Build.State |
| 91 | if in.Build.Read() && u.InstallRequest > Watermark(c, state) { |
| 92 | if res := resolvedFor(c, state, u.InstallRequest); res != nil { |
| 93 | return held(res.Target) |
| 94 | } |
| 95 | } |
| 96 | var r3 HeldWakeDecision |
| 97 | switch { |
| 98 | case !in.Build.Read(): |
| 99 | r3 = HeldWakeDecision{Held: true, Unknown: true} |
| 100 | case state != nil && state.LastExecuted != "": |
| 101 | r3 = held(state.LastExecuted) |
| 102 | case in.Installed != "": |
| 103 | r3 = held(in.Installed) |
| 104 | case in.Build.Replica: |
| 105 | r3 = HeldWakeDecision{Held: true, Unknown: true} |
| 106 | } |
| 107 | if !decided { |
| 108 | return HeldWakeDecision{Held: true, AskRoot: true, Target: r3.Target, Failed: r3.Failed} |
| 109 | } |
| 110 | if u.Updates == UpdatesManual { |
| 111 | return r3 |
| 112 | } |
| 113 | if u.Major != nil { |
| 114 | switch c.Reason { |
| 115 | case string(ReasonMajor), string(ReasonUnknown): |
| 116 | return r3 |
| 117 | case string(ReasonFailed): |
| 118 | if r3.Target != "" && c.Detail == r3.Target { |
| 119 | return r3 |
| 120 | } |
| 121 | } |
| 122 | } |
| 123 | return HeldWakeDecision{} |
| 124 | } |
| 125 | |
| 126 | // HeldExecStale is the pre-exec write's revalidation (Astra round 1 finding |
| 127 | // 1): d is DecideHeldWake on the build state read under the lease. A held |
| 128 | // target other than the build about to run means the wake decided on a |
| 129 | // stale copy, and the record is refused, so nothing runs. The build the |
| 130 | // state's last_executed names is never stale (the data last ran it: a |
| 131 | // restart or recycle of it is not a move onto another build), and a target |
| 132 | // root reports failed is left to the planner's recovery (ruling |
| 133 | // R-HELD-FAILED), as an unknown target is to the wake's own refusal. nil |
| 134 | // when the build may run. |
| 135 | // |
| 136 | // inPlaceOf is the build the zero-downtime planner chose build in place of, |
| 137 | // for this exec only ("" when it chose nothing: ruling R-PLANNED-RECOVERY): |
| 138 | // a planned fallback after B failed, the good build booted untried, or a |
| 139 | // recovery boot onto the good build because the failure book blocks the |
| 140 | // installed build. When the state read under the lease holds exactly that |
| 141 | // build as the target, the planner's reason for leaving it is the reason the |
| 142 | // held target is left (settings spec §6: the scheduler, swap, failure |
| 143 | // records and fallback act on the settings' build), so build runs, before |
| 144 | // root's consumed file names the target failed. A selection made in place of |
| 145 | // any other build admits nothing: the good build is never exempt in general, |
| 146 | // since it is often the stale build a wake decided on (Astra round 1 |
| 147 | // finding 1). |
| 148 | func HeldExecStale(d HeldWakeDecision, st *BuildState, build, inPlaceOf string) error { |
| 149 | if !d.Held || d.Unknown || d.Unread || d.Failed || d.Target == "" || d.Target == build { |
| 150 | return nil |
| 151 | } |
| 152 | if st != nil && st.LastExecuted == build { |
| 153 | return nil |
| 154 | } |
| 155 | if inPlaceOf != "" && inPlaceOf == d.Target { |
| 156 | return nil |
| 157 | } |
| 158 | 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) |
| 159 | } |
| 160 | |