carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package activator
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "time"
8
9 "github.com/carlosframework/platform/internal/registry"
10 "github.com/carlosframework/platform/internal/release"
11)
12
13// The held wake check's refusals (settings spec §4.3 rule 3). None of them
14// ran anything of a build, so none is a failure of one. Members read them
15// through memberSafeWakeReason (ruling R-WAKEWORDS), never as this text.
16var (
17 errBuildUnknown = errors.New("build unknown: use Update now or set a pin")
18 errStaleLink = errors.New("the installed build is not this instance's build, and root has not installed it yet")
19 errTargetFailed = errors.New("this instance's build failed on this box, so it is not started again")
20)
21
22// targetFailedError is errTargetFailed naming the build, so the member's
23// sentence can name it too.
24type targetFailedError struct{ host, build string }
25
26func (e *targetFailedError) Error() string {
27 return fmt.Sprintf("%v: %s on %s", errTargetFailed, e.build, e.host)
28}
29
30func (e *targetFailedError) Unwrap() error { return errTargetFailed }
31
32// HeldWake is the agent's answer for a held instance's wake (settings spec
33// §4.3 rule 3).
34type HeldWake struct {
35 Target string // the build the instance must run
36 Held bool // false: root follows the channel for it; today's wake
37 Unknown bool // rule 3's "build unknown"
38 Failed bool // root reports Target failed on this box (its decision, or a refusal record)
39 // AskRoot: root has made no decision for the instance's current
40 // settings (ruling R-HELD-PURE), so whether it is held is not known
41 // here. The wake holds: it runs Target only if the live link already
42 // names it, and otherwise asks root for a pass and waits for its
43 // decision. Target is the build state's build ("" when there is none).
44 AskRoot bool
45 // Unread: the build state could not be read for this wake (its required
46 // refresh failed). Refused: never decided from a stale copy (Astra round
47 // 1 finding 1).
48 Unread bool
49 // Staged is a retained copy of Target on this box whose bytes match its
50 // staged manifest; nil when there is none.
51 Staged *release.StagedIdentity
52}
53
54// heldPoll is how often a held wake re-reads the live link while it waits
55// for root to install its target. A var so tests can shrink it.
56var heldPoll = 250 * time.Millisecond
57
58// wakeCheck is settings spec §4.3 rule 3's wake half, with its own admission
59// rule so it holds with CARLOS_SWAP off and no planner (Astra I3 #2, #5):
60//
61// - not held: "" and nothing to do, today's wake;
62// - unread (the build state could not be read for this wake):
63// errBuildStateUnwritten, nothing asked or run (Astra round 1 finding 1);
64// - root has not decided for the current settings (AskRoot, ruling
65// R-HELD-PURE): the build state's build runs if the live link already
66// names it; otherwise root is asked for a pass (RequestStage naming no
67// build) and the wake waits for its decision, within the same bound,
68// then goes on below with that answer. It never runs a live link it
69// does not know it may run;
70// - unknown: errBuildUnknown, and root is not asked (it installs nothing);
71// - a target root reports failed: refused (errTargetFailed) with no
72// planner, even while it is still the live link; with a planner the
73// wake is not held, and zdd-5's failure-aware plan chooses (recovery
74// onto the good build, or a refusal);
75// - the target installed: the target, and the caller checks the identity
76// it captures against it (heldIdentityOK);
77// - with no planner and direct set (the caller can exec a named file), a
78// retained verified copy of the target, which the caller runs as its
79// identity, recorded first; root is asked to install the target too,
80// without waiting (R-RETAINED-NUDGE);
81// - otherwise root is asked to install the target (RequestStage, the
82// stage relay) and the wake waits for the live link to name it, within
83// its own deadline and never past wakeWaitBound, the bound its waiters
84// have. A wait that ends first is errStaleLink. The answer is asked
85// again every heldReask polls, and a changed answer ends the wait at
86// once with its own refusal (unknown, failed) or errStaleLink.
87//
88// It never sets e.pin (Astra I2 #4): with a planner, what boots after root's
89// install is the plan's. The string is the target ("" when the wake is not
90// held).
91func (a *Service) wakeCheck(ctx context.Context, host string, direct bool) (string, *release.StagedIdentity, error) {
92 if a.HeldTarget == nil {
93 return "", nil, nil
94 }
95 h := a.HeldTarget(host)
96 if !h.Held {
97 return "", nil, nil
98 }
99 if h.Unread {
100 return "", nil, fmt.Errorf("%w: the build state of %s could not be read for its wake", errBuildStateUnwritten, host)
101 }
102 wait, cancel := context.WithTimeout(ctx, wakeWaitBound)
103 defer cancel()
104 if h.AskRoot {
105 var err error
106 if h, err = a.awaitRootDecision(wait, host, h); err != nil {
107 return "", nil, err
108 }
109 switch {
110 case !h.Held:
111 return "", nil, nil
112 case h.Unread:
113 return "", nil, fmt.Errorf("%w: the build state of %s could not be read for its wake", errBuildStateUnwritten, host)
114 }
115 }
116 if h.Unknown || h.Target == "" {
117 return "", nil, errBuildUnknown
118 }
119 if h.Failed {
120 if a.Planner != nil {
121 return "", nil, nil // zdd-5's decideWake: recovery onto the good build, or a refusal
122 }
123 return "", nil, &targetFailedError{host: host, build: h.Target}
124 }
125 installed := a.liveVersion(host)
126 if installed == h.Target {
127 return h.Target, nil, nil
128 }
129 if direct && a.Planner == nil && h.Staged != nil && h.Staged.Version == h.Target {
130 // Run the retained copy; step 2b records it first. Root is still
131 // asked to install the target, without waiting (ruling
132 // R-RETAINED-NUDGE): until it repoints, the live link names another
133 // build than the one running, and the sweep's convergence would
134 // cycle the instance after ConvergeGrace, waking it onto the same
135 // copy each time, until root's own pass.
136 if a.RequestStage != nil {
137 a.RequestStage(host, h.Target)
138 }
139 return h.Target, h.Staged, nil
140 }
141 if a.RequestStage != nil {
142 a.RequestStage(host, h.Target)
143 }
144 tick := time.NewTicker(heldPoll)
145 defer tick.Stop()
146 for n := 1; ; n++ {
147 if a.liveVersion(host) == h.Target {
148 return h.Target, nil, nil
149 }
150 if n%heldReask == 0 && wait.Err() == nil {
151 // The answer is asked again while the wake waits: a member's
152 // change (a pin, Update now, auto) or root's verdict (failed,
153 // unknown) ends this wait now, rather than at its bound onto a
154 // target that is no longer the instance's. The next wake checks
155 // afresh. The re-ask is bounded by the wait (Task 12a): it may
156 // read the bucket for up to its own budget, and an answer that
157 // comes back after the wait has ended is not waited for.
158 next, ok := a.reaskHeld(wait, host)
159 if !ok {
160 return "", nil, fmt.Errorf("%w: %s is on %q, wants %s", errStaleLink, host, installed, h.Target)
161 }
162 if !sameHeld(next, h) {
163 switch {
164 case next.Held && (next.Unknown || next.Target == ""):
165 return "", nil, errBuildUnknown
166 case next.Held && next.Failed && a.Planner == nil:
167 return "", nil, &targetFailedError{host: host, build: next.Target}
168 }
169 return "", nil, fmt.Errorf("%w: %s's target changed from %s while it waited", errStaleLink, host, h.Target)
170 }
171 }
172 select {
173 case <-wait.Done():
174 return "", nil, fmt.Errorf("%w: %s is on %q, wants %s", errStaleLink, host, installed, h.Target)
175 case <-tick.C:
176 }
177 }
178}
179
180// heldReask is how many polls a held wake's wait makes between asking
181// HeldTarget again (which may read the bucket).
182const heldReask = 8
183
184// awaitRootDecision is wakeCheck's wait for root when root has made no
185// decision for the instance's current settings (h.AskRoot; ruling
186// R-HELD-PURE). It returns h itself when the wake may go on now: its target
187// failed (the failure rule decides, below) or the live link already names
188// it, the build the data last ran. Otherwise it asks root for a pass,
189// naming no build (it does not know which root will install), and asks
190// HeldTarget again every heldReask polls until root's answer is no longer
191// AskRoot, which it returns; or the live link names the target (root decided
192// to hold it and installed it). A wait that ends first is errStaleLink.
193func (a *Service) awaitRootDecision(wait context.Context, host string, h HeldWake) (HeldWake, error) {
194 if h.Failed || (h.Target != "" && a.liveVersion(host) == h.Target) {
195 return h, nil
196 }
197 if a.RequestStage != nil {
198 a.RequestStage(host, "")
199 }
200 tick := time.NewTicker(heldPoll)
201 defer tick.Stop()
202 for n := 1; ; n++ {
203 if h.Target != "" && a.liveVersion(host) == h.Target {
204 return h, nil
205 }
206 if n%heldReask == 0 && wait.Err() == nil {
207 next, ok := a.reaskHeld(wait, host)
208 if !ok {
209 break
210 }
211 if !next.AskRoot {
212 return next, nil
213 }
214 h = next
215 }
216 select {
217 case <-wait.Done():
218 return HeldWake{}, fmt.Errorf("%w: root has not decided for %s's current update settings", errStaleLink, host)
219 case <-tick.C:
220 }
221 }
222 return HeldWake{}, fmt.Errorf("%w: root has not decided for %s's current update settings", errStaleLink, host)
223}
224
225// reaskHeld is HeldTarget(host) bounded by wait: false when wait ends first.
226// The call itself is not cancelled (HeldTarget takes no context); it finishes
227// within its own budget into a buffered channel nobody reads.
228func (a *Service) reaskHeld(wait context.Context, host string) (HeldWake, bool) {
229 got := make(chan HeldWake, 1)
230 go func() { got <- a.HeldTarget(host) }()
231 select {
232 case h := <-got:
233 return h, true
234 case <-wait.Done():
235 return HeldWake{}, false
236 }
237}
238
239// sameHeld reports whether two answers send the wake to the same place.
240func sameHeld(x, y HeldWake) bool {
241 return x.Held == y.Held && x.Target == y.Target && x.Unknown == y.Unknown && x.Failed == y.Failed &&
242 x.AskRoot == y.AskRoot && x.Unread == y.Unread
243}
244
245// liveVersion is what live/<host> names now, "" when it cannot be read.
246func (a *Service) liveVersion(host string) string {
247 if a.liveVer == nil {
248 return ""
249 }
250 v, _ := a.liveVer(host)
251 return v
252}
253
254// heldIdentityOK reports whether a held wake may run id: its target, or the
255// good build the planner chose as recovery because the failure book blocks
256// the target (zero-downtime failure records outrank every rule, §4.3). An
257// identity with no version (a live link outside the version store) is never
258// the target (ruling R-EXEC-NOVERSION-2).
259func heldIdentityOK(target string, id release.StagedIdentity, plan WakePlan, rt registry.Route) bool {
260 if target == "" || id.Version == target {
261 return true
262 }
263 return plan.planned() && !plan.FirstWake && id.Version != "" && id.Version == rt.GoodVersion
264}
265