carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package rollout
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "time"
8
9 "github.com/carlosframework/platform/internal/activator"
10 "github.com/carlosframework/platform/internal/registry"
11 "github.com/carlosframework/platform/internal/release"
12)
13
14// wakeBook is the failure book's read half the planner needs.
15type wakeBook interface {
16 Blocks(acct, app, build, host string, served bool) bool
17 HeldDown(acct, app, build, host string) bool
18 HoldRanOn(host string) (HoldRan, bool)
19}
20
21// holdRanFor is host's hold-ran marker when it is one of (acct, app)'s: a
22// marker left by another app's route under the same host name says nothing
23// about this one's database.
24func holdRanFor(b wakeBook, acct, app, host string) (HoldRan, bool) {
25 h, ok := b.HoldRanOn(host)
26 if !ok || h.Account != acct || h.App != app {
27 return HoldRan{}, false
28 }
29 return h, true
30}
31
32type wakeInputs struct {
33 Route registry.Route
34 OptDir string
35 Policy AppPolicy
36 Book wakeBook
37 // Demand is the canonical check for an adoption (spec §4.2), returning a
38 // digest always. An error is a staging refusal: the staged file is
39 // missing or is not the release's artifact.
40 Demand func(version, stagedPath string) (demand, digest string, err error)
41}
42
43// HeldDownRefusal is the wake refusal for an instance where build failed in
44// hold mode (spec §6.4, §8.3). It reaches the member's "wake failed" event,
45// so it names the build only. A box that declined CARLOS_SWAP=overlap but
46// honours the failure records refuses with the same words (internal/host).
47func HeldDownRefusal(build string) string {
48 return fmt.Sprintf("build %s failed in hold mode on this instance, so it stays down until you roll back or retry the build", build)
49}
50
51// BookUnreadableRefusal is the wake refusal when this box's failure records
52// cannot be read or written: an unreadable book at arming (internal/host's
53// refuseTexts), or a planned first wake whose intent could not be saved
54// (Task 5). Approved text, shared rather than copied.
55const BookUnreadableRefusal = "this box cannot start instances right now: its record of failed builds cannot be read"
56
57// decideWake is spec §8.2's "when the wake picks a build", as a pure
58// function. The installed build (root's stamp) boots unless a failure record
59// holds it for this instance, in which case the good build boots from its
60// staged path with its retained identity, and nothing is repointed. A build
61// this instance has never served is a first wake; in overlap mode it carries
62// the good build as its fallback. A build that failed in hold mode on this
63// instance refuses the wake: nothing falls back automatically once a hold
64// build has run against the database (spec §6.4, §8.3). An instance with no
65// good build is never refused by a record alone (spec §8.5). An error is a
66// staging refusal for the caller to report.
67func decideWake(in wakeInputs) (activator.WakePlan, error) {
68 rt := in.Route
69 acct, app, host := release.RouteAccount(rt), rt.App, rt.Host
70 installed := rt.Version
71 if installed == "" {
72 // Plan installs a cold host first; a plan for a host with nothing
73 // installed refuses rather than let the live link run unplanned.
74 return activator.WakePlan{Refuse: "no build is installed for this instance yet"}, nil
75 }
76 hasGood := rt.GoodVersion != "" && rt.GoodDigest != ""
77 good := release.StagedIdentity{Version: rt.GoodVersion, StagedPath: release.VersionPath(in.OptDir, acct, rt.GoodVersion, app),
78 Digest: rt.GoodDigest, Demand: rt.GoodDemand}
79 recovery := activator.WakePlan{Boot: good, Mode: RecoveryMode(in.Policy, good.Demand)}
80 if in.Book.HeldDown(acct, app, installed, host) {
81 return activator.WakePlan{Refuse: HeldDownRefusal(installed)}, nil
82 }
83 if mark, ok := holdRanFor(in.Book, acct, app, host); ok {
84 return decideHoldRan(in, mark, hasGood)
85 }
86 if hasGood && installed == rt.GoodVersion {
87 return recovery, nil
88 }
89 // A record holds the installed build for this instance: boot the good
90 // build instead. With no good build (every instance, until it first
91 // serves after the upgrade) there is nothing to fall back to, which is
92 // today's behaviour and not a regression (spec §8.5, §8.2): the wake
93 // plans the installed build as a first wake with no fallback, as it
94 // would had no record been written. Refusing it would turn one
95 // sibling's failure into an outage for every sleeping sibling. A
96 // hold-down was refused above and stays refused (spec §6.4).
97 if hasGood && in.Book.Blocks(acct, app, installed, host, false) { // rolloutsim:M5
98 return recovery, nil
99 }
100 path := release.VersionPath(in.OptDir, acct, installed, app)
101 demand, digest, err := in.Demand(installed, path)
102 if err != nil {
103 if hasGood {
104 return recovery, err
105 }
106 return activator.WakePlan{Refuse: fmt.Sprintf("build %s is not staged correctly on this box", installed)}, err
107 }
108 mode := AttemptMode(in.Policy, demand)
109 plan := activator.WakePlan{Boot: release.StagedIdentity{Version: installed, StagedPath: path, Digest: digest, Demand: demand},
110 Mode: mode, FirstWake: true}
111 if hasGood && mode == activator.ModeOverlap {
112 plan.Fallback = &good // rolloutsim:M6
113 }
114 return plan, nil
115}
116
117// decideHoldRan is decideWake for an instance with a hold-ran marker (final
118// review wave 2, N2; spec §6.4: once a hold build has executed against an
119// instance's database, nothing falls back automatically on that instance).
120// The good build never boots in place of the marked build, as a recovery
121// or as a fallback, and the marker never lifts a failure record.
122// - the installed build is the marked build: it boots, in hold mode with
123// no fallback, unless a failure record holds it for the instance (Astra
124// R3 N7: spec §8.4 exempts an instance that passed on the build, not one
125// where it merely ran). That holds even when the marked build is also
126// the good build (Task 3 fix round 1): the instance has served it, so a
127// sibling's record does not hold it (Blocks with served), and booting
128// it is the marked-build rule, not a fallback.
129//
130// With a good build, for any other installed build:
131// - the installed build is the good build: refused. An installed good
132// build is no proof of an explicit rollback, because root's automatic
133// sibling repoint installs it too (ruling 205, Astra R3). The proof is
134// a signed pointer naming the good build, newer than the marker, and the
135// pointer pass (applyPointers) clears the marker when it sees one; the
136// next wake then boots the good build as an ordinary wake. The planner
137// itself reads no pointer.
138// - the installed build is held for the instance by a failure record:
139// refused. With the good build barred as well, the instance stays down
140// until a retry or a newer build.
141//
142// Otherwise the installed build boots as a first wake with no fallback: the
143// marked build in hold mode, whose failure here is a hold-down, any other
144// build in its own mode. With no good build a record alone refuses nothing
145// (spec §8.5), so the marked build boots even when a record blocks it.
146//
147// The hold-down refusal above this call still comes first. The refusal
148// words are HeldDownRefusal's for the marked build: no new member text.
149func decideHoldRan(in wakeInputs, mark HoldRan, hasGood bool) (activator.WakePlan, error) {
150 rt := in.Route
151 acct, app, host := release.RouteAccount(rt), rt.App, rt.Host
152 installed := rt.Version
153 switch {
154 case installed == mark.Build:
155 if hasGood && in.Book.Blocks(acct, app, installed, host, installed == rt.GoodVersion) { // rolloutsim:M7
156 return activator.WakePlan{Refuse: HeldDownRefusal(mark.Build)}, nil
157 }
158 case hasGood && installed == rt.GoodVersion:
159 return activator.WakePlan{Refuse: HeldDownRefusal(mark.Build)}, nil
160 case hasGood && in.Book.Blocks(acct, app, installed, host, false):
161 return activator.WakePlan{Refuse: HeldDownRefusal(mark.Build)}, nil
162 }
163 path := release.VersionPath(in.OptDir, acct, installed, app)
164 demand, digest, err := in.Demand(installed, path)
165 if err != nil {
166 // A staging refusal cannot boot the good build instead here, as it
167 // does for an instance with no marker: that would be the fallback.
168 return activator.WakePlan{Refuse: fmt.Sprintf("build %s is not staged correctly on this box", installed)}, err
169 }
170 mode := AttemptMode(in.Policy, demand)
171 if installed == mark.Build {
172 mode = activator.ModeHold
173 }
174 return activator.WakePlan{Boot: release.StagedIdentity{Version: installed, StagedPath: path, Digest: digest, Demand: demand},
175 Mode: mode, FirstWake: true}, nil
176}
177
178// Plan is the activator's Planner (spec §8.2). It is called without
179// activator.Service.mu held, before the wake's lease. It also snapshots the
180// instance's delivered config revision into the plan (WakePlan.ConfigRev).
181// The wake's spawn reads the config file after this moment, so activating
182// that revision when the wake is healthy (OnGen, from the revision its
183// event carries) can only under-claim, which costs one more config attempt,
184// never a missed one.
185//
186// Every plan gets a token of its own (WakePlan.Token, final review F4),
187// taken before anything is registered for it. The activator carries it
188// onto every event of the wake, and OnGen acts on a host's wake state
189// (Manager.planned, observing, the config activation) only for the token
190// that state belongs to, so a late event of an older wake never touches a
191// newer one's.
192//
193// A Refuse text reaches the member's "wake failed" event (the activator's
194// wake error), so it names builds only, never an error's text.
195//
196// Every plan for a managed host, refused or not, replaces the host's entry
197// in Manager.planned, so prune keeps the failure record of a first wake's
198// build until that wake is over. The installed build is registered there
199// before the planner's slow reads (planRow), so a Pass that runs while they
200// do keeps a record they could no longer see.
201//
202// The row is read again after those reads. A Pass whose snapshot preceded
203// the registration could have pruned the build's record only if no route
204// installed the build when that Pass listed them, so this host's row had
205// already moved off it, and the read after the registration sees that. Such
206// a plan is made once more against the fresh row. A row that moves again
207// during the replan refuses the wake rather than chase the row: two installs
208// inside one plan are installs still landing, and the activator's next wake
209// plans against whatever settled.
210func (m *Manager) Plan(ctx context.Context, rt registry.Route) (plan activator.WakePlan) {
211 if !Managed(rt) {
212 return activator.WakePlan{}
213 }
214 // Joined by Close for its whole length (spec §8.6, Task 11 fix round 1):
215 // a plan under way may still write its wake's intent. Deferred first, so
216 // it is given back last, after beginWake below has counted the wake
217 // whose intent it wrote: the count never touches zero in between.
218 m.enter(false)
219 defer m.leave()
220 tok, planAt := m.newWakeToken(rt.Host)
221 defer func() {
222 plan.Token = tok
223 // Write-ahead (Task 5, Astra R1 #5): a first wake's intent is on disk
224 // before the activator takes the lease, with no activator lock held
225 // here. It ends on the wake's terminal event (OnGen). A plan whose
226 // intent could not be written boots nothing, with the approved
227 // refusal for a failure book this box cannot keep, and is
228 // registered as the refusal it now is.
229 plan = m.beginWake(rt, plan, tok, planAt)
230 m.notePlanned(rt, plan, tok)
231 // The book's snapshot is taken here, at the plan's end, after every
232 // book read the planner made (decideWake, the replan): the wake's
233 // failure is stale only after a retry the plan could not have seen
234 // (ruling 191, Task 4 fix round 1). A retry landing during the slow
235 // reads (Materialize, the policy load) let this plan boot the build,
236 // so the build's failure in it is a real one. Its good write clears
237 // only markers set before here (Astra R3 N6): the exec follows the
238 // plan, and the plan's check re-reads the marker there.
239 m.noteWakeStart(rt.Host, tok, planAt, m.d.Book.Snapshot(), plan)
240 }()
241 if m.attemptRunning(rt.Host) {
242 // The instance's attempt has not reported yet (spec §6.4, §8.1): a
243 // hold that went down, or a swap whose entry stopped under it, is
244 // recorded by done() before anything wakes the instance, so no wake
245 // can boot the good build ahead of a hold-down record.
246 return activator.WakePlan{Refuse: movingRefusal}
247 }
248 if rt.Version == "" {
249 // A cold managed host (spec §6.4, §8.4): its build is installed and
250 // stamped first, and the wake is planned against that installed
251 // identity, with its failure records and its first-wake canary.
252 // Nothing managed ever runs through an empty plan: the live-link
253 // wake would materialize the channel head and exec it unplanned.
254 if m.d.Materialize == nil {
255 return activator.WakePlan{Refuse: "no build is installed for this instance yet"}
256 }
257 if err := m.d.Materialize(ctx, rt.Host); err != nil {
258 m.d.Logf("rollout: %s: install before its wake: %v", rt.Host, err)
259 return activator.WakePlan{Refuse: "its build could not be installed"}
260 }
261 fresh, err := m.d.Routes.Get(rt.Host)
262 if err != nil || fresh == nil || fresh.Version == "" {
263 if err != nil {
264 m.d.Logf("rollout: %s: read the row after its install: %v", rt.Host, err)
265 }
266 return activator.WakePlan{Refuse: "no build is installed for this instance yet"}
267 }
268 rt = *fresh
269 }
270 for again := false; ; again = true {
271 plan = m.planRow(ctx, rt, tok)
272 fresh, err := m.d.Routes.Get(rt.Host)
273 if err != nil || fresh == nil {
274 // Nothing confirms the row the plan was made against, so nothing
275 // may boot from it: the same answer as an unreadable row after a
276 // cold install.
277 if err != nil {
278 m.d.Logf("rollout: %s: read the row after its plan: %v", rt.Host, err)
279 }
280 return activator.WakePlan{Refuse: "no build is installed for this instance yet"}
281 }
282 if fresh.Version == rt.Version {
283 return plan
284 }
285 if again {
286 m.d.Logf("rollout: %s: its installed build moved twice while its wake was planned (%s, now %s); refusing the wake", rt.Host, rt.Version, fresh.Version)
287 return activator.WakePlan{Refuse: "no build is installed for this instance yet"}
288 }
289 m.d.Logf("rollout: %s: its installed build moved from %s to %s while its wake was planned; planning again", rt.Host, rt.Version, fresh.Version)
290 rt = *fresh
291 }
292}
293
294// movingRefusal is a wake's refusal while the instance's attempt has not
295// reported (Plan's attemptRunning check, and beginWake meeting the
296// attempt's intent between its slot freeing and its End).
297const movingRefusal = "this instance is still moving to another build"
298
299// beginWake writes the intent of plan, a first wake planned with token tok
300// at planAt (Task 5 rule 1), and returns the plan to hand the activator:
301// plan itself, or a refusal when the intent could not be written. Any
302// other plan writes nothing. The intent's mode is the plan's: the activator
303// runs a planned wake in the mode it was planned in (a foreign-layout row
304// gets one generation at a time, so no override applies; its events carry
305// plan.Mode), and that is the mode a failure of the wake records.
306func (m *Manager) beginWake(rt registry.Route, plan activator.WakePlan, tok uint64, planAt time.Time) activator.WakePlan {
307 if !plan.FirstWake || plan.Boot.Version == "" || plan.Refuse != "" {
308 return plan
309 }
310 in := Intent{Account: release.RouteAccount(rt), App: rt.App, Build: plan.Boot.Version, Mode: string(plan.Mode),
311 ID: m.wakeExecID(tok), Start: planAt}
312 err := m.d.Book.Begin(rt.Host, in)
313 if err == nil {
314 // The intent is on disk: Close now waits for the terminal event
315 // that ends it (OnGen, leaveWake). A refused plan, and a Begin that
316 // failed, wrote nothing and is never counted.
317 m.enterWake(tok)
318 return plan
319 }
320 m.d.Logf("rollout: %s: write the intent of its first wake of %s: %v; refusing the wake", rt.Host, plan.Boot.Version, err)
321 if errors.Is(err, ErrIntentStands) {
322 // The instance's attempt finished and has not yet ended its intent:
323 // it is still, for a moment, moving. Not a book fault.
324 return activator.WakePlan{Refuse: movingRefusal, Token: tok}
325 }
326 return activator.WakePlan{Refuse: BookUnreadableRefusal, Token: tok}
327}
328
329// planRow is one plan against rt (Plan's body, after the cold install), for
330// the plan whose token is tok. It registers rt's installed build as the
331// host's in-flight planned build before any slow read (the policy, the
332// book's reading in decideWake, the canonical demand, the failing pointer);
333// Plan's deferred notePlanned replaces or clears that entry with what the
334// plan finally boots.
335func (m *Manager) planRow(ctx context.Context, rt registry.Route, tok uint64) activator.WakePlan {
336 m.notePlanning(rt, tok)
337 acct, app := release.RouteAccount(rt), rt.App
338 in := wakeInputs{Route: rt, OptDir: m.d.OptDir, Policy: m.policies.get(ctx, acct, app), Book: m.d.Book,
339 Demand: func(ver, path string) (string, string, error) {
340 // The same reading an adoption attempt gets (resolve): only a
341 // staging fault is an error, and anything unverifiable is hold.
342 return m.stagedDemand(ctx, rt, ver, path)
343 }}
344 // The delivered revision now, before the wake's spawn reads the file.
345 // It travels with the plan and comes back on the wake's events, so no
346 // later plan for the host can change what this wake activates.
347 m.mu.Lock()
348 rev := m.track(rt.Host).delivered
349 m.mu.Unlock()
350 plan, err := decideWake(in)
351 plan.ConfigRev = rev
352 if err != nil {
353 m.stagingRefused(rt, rt.Version, err)
354 }
355 if plan.FirstWake && plan.Boot.Version != "" {
356 // Read now, before the wake runs, so a failure of the build is
357 // published with no blocking read (buildFailed, spec §8.1).
358 plan.FailedUnder = m.failedUnder(ctx, rt, plan.Boot.Version)
359 }
360 if plan.Boot.StagedPath != "" {
361 plan.Check = m.planCheck(rt, plan)
362 }
363 return plan
364}
365
366// planCheck is a plan's selection gate (WakePlan.Check), asked by the
367// activator at each planned exec, after the lease and the restore (spec
368// §7.4, §8.1–8.4): a first wake's build that a failure record now holds for
369// this instance (a sibling's failure since the plan, or a hold-down) may not
370// boot. The good build always may: it is what a held build falls back to.
371// An instance with no good build is refused only by a hold-down: a record
372// alone would not move it onto anything, and it has no fallback by design
373// until it first serves (spec §8.5), so refusing it would be an outage.
374//
375// A hold-ran marker (final review wave 2, N2), set since the plan or
376// before it, applies decideHoldRan's rule at the exec:
377// - the marked build may not boot while it is held down there or, with a
378// good build, while a failure record holds it for the instance (Astra
379// R3 N7: the marker lifts no record). When it is also the good build it
380// is under this rule only as the installed build (Task 3 fix round 1),
381// and a record does not hold a build the instance served;
382// - otherwise the good build may not boot, whatever the route installs:
383// as a recovery, a fallback, an untried first wake's stand-in, or the
384// installed build itself (ruling 205: only the pointer pass's clearing
385// of the marker on a signed rollback lets it boot again, and the check
386// reads no pointer);
387// - any other build is checked as without a marker.
388//
389// The book is in memory; nothing here blocks.
390func (m *Manager) planCheck(rt registry.Route, plan activator.WakePlan) func(build string) error {
391 acct, app, host := release.RouteAccount(rt), rt.App, rt.Host
392 boot := plan.Boot.Version
393 hasGood := rt.GoodVersion != "" && rt.GoodDigest != ""
394 // The texts reach the member's "wake failed" event (memberSafeWakeReason
395 // passes a planned wake's error through).
396 held := func(build string) error {
397 return fmt.Errorf("build %s failed on this box while this instance was starting, so it was not started", build)
398 }
399 return func(build string) error {
400 if mark, ok := holdRanFor(m.d.Book, acct, app, host); ok {
401 isGood := hasGood && build == rt.GoodVersion
402 switch {
403 case build == mark.Build && (!isGood || rt.Version == mark.Build):
404 if m.d.Book.HeldDown(acct, app, build, host) || (hasGood && m.d.Book.Blocks(acct, app, build, host, isGood)) {
405 return held(build)
406 }
407 return nil
408 case isGood:
409 return errors.New(HeldDownRefusal(mark.Build))
410 }
411 }
412 if !plan.FirstWake || build != boot {
413 return nil
414 }
415 if m.d.Book.HeldDown(acct, app, build, host) || (hasGood && m.d.Book.Blocks(acct, app, build, host, false)) {
416 return held(build)
417 }
418 return nil
419 }
420}
421
422// attemptRunning reports whether host has an attempt in flight: admitted
423// and not yet finished by done().
424func (m *Manager) attemptRunning(host string) bool {
425 s := m.schedulerOf(host)
426 if s == nil {
427 return false
428 }
429 s.mu.Lock()
430 defer s.mu.Unlock()
431 h := s.q.hosts[host]
432 return h != nil && h.running != nil
433}
434
435// OnGen is the activator's OnGen: a planned wake's outcome (spec §6.4, §8.2,
436// §8.5). A healthy wake records the serving build and activates the config
437// it started with; a first wake becomes the good build only once it has
438// survived its 10 s; a failed one goes through the cohort-failure
439// transition, held down when it was a hold build. It is never called with
440// activator.Service.mu held (emitGen), which served and publishServing need:
441// they take goodMu and call back into Current.
442//
443// The host's wake state is the event's only when the token matches (final
444// review F4): the planned entry and the observation each end only by an
445// event carrying their own token, and only the host's newest wake (the
446// token of its last plan) opens an observation or activates a config
447// revision, the one its event carries. A late event of an older wake (an
448// observer's delayed GenCancelled, say) leaves a newer wake's registration,
449// observation and config alone. What an event says about its build is
450// still acted on whoever's it is: a failure is recorded, and a serving or
451// good build is published only while its generation is the one the
452// activator serves (acknowledged). A token of zero matches nothing.
453func (m *Manager) OnGen(ev activator.GenEvent) {
454 // Joined by Close (spec §8.6): a wake's terminal event writes its outcome
455 // and ends its intent, which must be on disk before the agent exits.
456 m.enter(false)
457 defer m.leave()
458 rt := ev.Route
459 // The wake's start, its plan: a hold-ran marker it leaves carries the
460 // plan time (ruling 205), and its good write and its failure are ordered
461 // against the book by the plan's snapshot (Astra R3 N6, ruling 191).
462 // Every kind but a first wake's healthy is the wake's last event, which
463 // lets the entry go: an ordinary wake (an untried first wake's good
464 // build included) reports healthy once and nothing after.
465 ended := ev.Kind != activator.GenHealthy || !ev.FirstWake
466 started := m.wakeStarted(rt.Host, ev.Token, ended)
467 m.mu.Lock()
468 newest := ev.Token != 0 && m.wakeTok[rt.Host] == ev.Token
469 // The wake has an outcome: its build is no longer only planned, so its
470 // failure record need not outlive the install for its sake.
471 if w, ok := m.planned[rt.Host]; ok && ev.Token != 0 && w.token == ev.Token {
472 delete(m.planned, rt.Host)
473 }
474 switch {
475 case ev.Kind == activator.GenHealthy && ev.FirstWake:
476 if newest {
477 m.observing[rt.Host] = ev.Token
478 }
479 case ev.Kind != activator.GenHealthy:
480 // GenSettled, GenFailed, GenFellBack and GenCancelled (an
481 // intentional stop ended the observation with no verdict) all close
482 // the observation their wake opened. The switch below has no
483 // recording arm for GenCancelled, so a cancelled observation records
484 // nothing (spec §6.4).
485 if tok, ok := m.observing[rt.Host]; ok && ev.Token != 0 && tok == ev.Token {
486 delete(m.observing, rt.Host)
487 }
488 }
489 m.mu.Unlock()
490 defer m.changed() // after the event's own work, with no lock held
491 switch ev.Kind {
492 case activator.GenHealthy:
493 // A wake that is not a first wake includes the good build booted
494 // untried under Task 9's verdict floor: an ordinary wake of the good
495 // build, recorded as served, with nothing against the build it skipped.
496 if !ev.FirstWake {
497 m.served(rt.Host, ev.Gen, started.snap)
498 }
499 // A first wake is published when it settles: until then the
500 // activator hides it (Current), so it is not yet acknowledged.
501 m.wakeConfigActivated(rt.Host, ev.Token, ev.ConfigRev)
502 case activator.GenSettled:
503 m.served(rt.Host, ev.Gen, started.snap)
504 case activator.GenFellBack:
505 // The failure hold first (spec §8.1): nothing that reads the
506 // serving build or the active config may see the fallback before the
507 // failed build is held.
508 m.buildFailed(rt, ev, started)
509 m.publishServing(rt.Host, ev.Gen)
510 m.wakeConfigActivated(rt.Host, ev.Token, ev.ConfigRev)
511 m.emit(rt, EvFallbackServed, "warn", textFallback(ev.Failed.Version, ev.Gen.Build))
512 case activator.GenFailed:
513 m.buildFailed(rt, ev, started)
514 case activator.GenCancelled:
515 // An intentional stop ended the first wake's observation, or the
516 // wake's lifecycle ended it after its build ran (spec §6.4): no
517 // verdict on the build, so nothing is recorded or published. A hold
518 // build that ran leaves the instance's hold-ran marker (final review
519 // wave 2, N2): its database may be the build's now, so the next wake
520 // must not boot the good build. A fact about the instance, like a
521 // failure, so it is not token-scoped.
522 if ev.Executed && ev.FirstWake && ev.Mode == activator.ModeHold && ev.Gen.Build != "" {
523 // At is the wake's plan time, its execution's start (ruling 205):
524 // a rollback signed while the wake ran is newer than the marker.
525 if started.at.IsZero() {
526 m.d.Logf("rollout: %s: the start of the wake that ran %s is not known; its hold-ran marker is stamped now", rt.Host, ev.Gen.Build)
527 }
528 mark := HoldRan{Account: release.RouteAccount(rt), App: rt.App, Build: ev.Gen.Build, Exec: m.wakeExecID(ev.Token), At: started.at}
529 if err := m.d.Book.MarkHoldRan(rt.Host, mark); err != nil {
530 m.d.Logf("rollout: %s: write hold-ran marker for %s: %v (held in memory)", rt.Host, mark.Build, err)
531 }
532 m.pointerReadDue(appKey{mark.Account, mark.App})
533 }
534 }
535 // The wake's intent ends with the first event that settles its planned
536 // build's fate (Task 5 rule 2, plan review #1), after that event's own
537 // book write above (buildFailed's record, the hold-ran marker): settled,
538 // failed, fell back, cancelled (with or without a run), and healthy when
539 // it is not a first wake's, which is the good build booted under the
540 // verdict floor with the planned build never run. A first wake's healthy
541 // event opens its observation and is not the end. An older wake's event
542 // carries its own token, so it never ends a newer wake's intent. Not
543 // under activator.Service.mu (emitGen's rule).
544 if ended {
545 if err := m.d.Book.End(rt.Host, m.wakeExecID(ev.Token)); err != nil {
546 m.d.Logf("rollout: %s: end the intent of its wake: %v (it stays on disk until a save lands)", rt.Host, err)
547 }
548 // The wake's count in Close's join goes with the End (beginWake
549 // took it). This call is itself counted until it returns, so the
550 // join cannot see zero before the End above is written. A wake
551 // replaced by a newer plan has its intent replaced on disk, but its
552 // own terminal event still ends its count here: the activator sends
553 // exactly one per planned wake (afterWake, cancelPlanned).
554 m.leaveWake(ev.Token)
555 }
556}
557
558// wakeConfigActivated is configActivated for a planned wake's event (final
559// review wave 2, N3): the revision activates only while tok is still the
560// host's newest wake token, and that check and the activation are one
561// critical section of m.mu. A check made earlier (OnGen's newest) can go
562// stale while the event does its other work (served's registry write): a
563// newer wake could plan and activate its own revision meanwhile, and this
564// older one would then put its revision back. The held ack is written, and
565// the queue's pending config attempt cleared, after the lock is released.
566func (m *Manager) wakeConfigActivated(host string, tok uint64, rev string) {
567 if rev == "" || tok == 0 {
568 return
569 }
570 m.mu.Lock()
571 if m.wakeTok[host] != tok {
572 m.mu.Unlock()
573 return
574 }
575 write, _ := m.track(host).activate(rev)
576 ack := m.ackLocked(write)
577 m.mu.Unlock()
578 m.activationDone(host, rev, ack)
579}
580
581// buildFailed is a failed first wake's cohort-failure transition (spec
582// §8.1, §8.2 step 4). started is the wake's start (wakeStarted): a wake
583// planned before a retry of its build fails stale (ruling 191), and runs
584// none of the transition.
585func (m *Manager) buildFailed(rt registry.Route, ev activator.GenEvent, started wakeStart) {
586 // No blocking read before the record (spec §8.1): the failing pointer's
587 // PromotedAt was read when the wake was planned (WakePlan.FailedUnder).
588 rec := FailureRecord{Account: release.RouteAccount(rt), App: rt.App, Build: ev.Failed.Version, Host: rt.Host,
589 Cause: string(activator.CauseRelease), Mode: string(ev.Mode), Reason: ev.Reason,
590 PromotedAt: ev.FailedUnder, At: m.d.Now(),
591 // The wake's start for a stale failure's marker: Record sets one in
592 // place of a hold-down when the failure is pre-retry (ruling 191).
593 Seq: started.snap, HoldRanAt: started.at, HoldRanExec: m.wakeExecID(ev.Token)}
594 if rec.PromotedAt.IsZero() {
595 // No pointer named the build when the wake was planned: the
596 // failure's own time, so only a pointer signed after it clears the
597 // record (as done() does for an attempt).
598 rec.PromotedAt = rec.At
599 }
600 if ev.Mode == activator.ModeHold {
601 // A hold build executed against this instance's database: nothing
602 // falls back on it automatically (spec §6.4, §8.3).
603 rec.HeldDown = []string{rt.Host}
604 }
605 s := m.schedulerOf(rt.Host)
606 if s == nil {
607 // No scheduler for the host's app: the record still halts the
608 // cohort, and the halt is still announced, on the same condition
609 // cohortFailed announces it (a newly recorded failure, never a
610 // pre-retry one or one merged into a record that stands). Without
611 // it a member whose first wake failed here saw the fallback event
612 // and never the halt (Astra R3 flag triage T11).
613 res, err := m.d.Book.Record(rec)
614 if err != nil {
615 m.d.Logf("rollout: write failure record %s: %v (held in memory)", rec.Build, err)
616 }
617 if res == Recorded {
618 m.d.Logf("rollout: %s/%s build %s failed on %s: %s", rec.Account, rec.App, rec.Build, rec.Host, rec.Reason)
619 m.emit(rt, EvCohortHalted, "warn", textCohortHalted(rec.Build, m.d.Box, reasonWord(rec.Reason)))
620 }
621 return
622 }
623 m.cohortFailed(s, rt, rec)
624}
625