carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package engines
2
3import (
4 "fmt"
5 "sort"
6)
7
8// The launch gate (spec §5.1 "The invariant", "Setting the mark", "Starts
9// systemd makes on its own").
10//
11// I1: a process of a build whose manifest declares doc_engine starts only
12// after the edge has fenced its group as internal. Every activator launch
13// path ends in the gate; systemd's own starts (Restart=always, socket
14// activation) execute the live link and pass no gate, so they are safe only
15// because adoption gates before it repoints and nothing else moves the link.
16
17// fenceWaitTicks is the gate's bound: 30 s of the edge's reloads, one per
18// tick.
19const fenceWaitTicks = 3
20
21// gateRule names the rule that gates a launch path; systemd and socket
22// starts are not activator launches and have none.
23var gateRule = map[string]string{
24 "adopt": "gate-adopt",
25 "swap": "gate-swap",
26 "fallback": "gate-fallback",
27 "recovery": "gate-recovery",
28 "seed": "gate-seed",
29 "cmd": "gate-cmd",
30 "systemd": "",
31 "socket": "",
32}
33
34// gateSeesDeclaring is what the gate reads from the build's manifest: its
35// declaration, or, when the manifest cannot be read, declaring.
36func (w *world) gateSeesDeclaring(ver int) bool {
37 bd := w.builds[ver]
38 if !bd.ManifestOK {
39 return on("unreadable-declares")
40 }
41 return bd.Declares
42}
43
44// gate is the launch gate for a build about to start in group g: a build it
45// sees as declaring writes 1 if 1 is not already written, then always waits
46// for the edge's fence to cover the group's mark, within the bound. False is
47// a refused launch (the wake fails).
48func (w *world) gate(b *box, g string, ver int, path string) bool {
49 if !w.gateSeesDeclaring(ver) {
50 return true
51 }
52 if !w.dedicatedLaunchCheck(b, g, ver, path) {
53 return false
54 }
55 wrote := false
56 if w.groupMark(b, g) != markInternal {
57 w.writeMark(b, g, markInternal)
58 wrote = true
59 }
60 if !wrote && !on("gate-waits-when-written") {
61 w.logf("box%d: gate(%s) %s v%d: 1 already written; not waiting", b.ID, path, g, ver)
62 return true
63 }
64 for i := 0; i < fenceWaitTicks && !w.fenced(b, g); i++ {
65 w.reload(b)
66 }
67 if !w.fenced(b, g) {
68 w.logf("box%d: gate(%s) %s v%d: no fence within the bound; refused", b.ID, path, g, ver)
69 return false
70 }
71 return true
72}
73
74// launch starts build ver in group g by path, through the gate where the
75// path has one. It returns false when the gate refuses.
76func (w *world) launch(b *box, g string, ver int, path string) bool {
77 rule, known := gateRule[path]
78 if !known {
79 panic(fmt.Sprintf("launch: unknown path %q", path))
80 }
81 // the gate holds the group's lock from its check through process
82 // creation; --public holds the same lock for its whole run, so a launch
83 // that finds it held waits (here: is refused this pass)
84 if rule != "" && b.Locked[g] && on("clear-launch-lock") {
85 w.logf("box%d: %s %s v%d: --public holds the group's lock; not this pass", b.ID, path, g, ver)
86 return false
87 }
88 if rule != "" && on(rule) && !w.gate(b, g, ver, path) {
89 return false
90 }
91 // a group keeps at most three processes: the oldest has exited
92 n := 0
93 for _, p := range b.Procs {
94 if p.Group == g {
95 n++
96 }
97 }
98 if n >= 3 {
99 for i, p := range b.Procs {
100 if p.Group == g {
101 b.Procs = append(b.Procs[:i], b.Procs[i+1:]...)
102 w.logf("box%d: process %d of %s exited", b.ID, p.ID, g)
103 break
104 }
105 }
106 }
107 w.stage(b, g, ver) // a build runs from /opt/carlos/versions: it is staged
108 w.nextID++
109 p := &proc{ID: w.nextID, Group: g, Ver: ver, Slice: "inst:" + g}
110 b.Procs = append(b.Procs, p)
111 w.logf("box%d: %s started %s v%d as process %d", b.ID, path, g, ver, p.ID)
112 w.applyTamper(b, p)
113 w.checkStart(b, p)
114 return true
115}
116
117// stopGroup stops every process of group g.
118func (w *world) stopGroup(b *box, g string) {
119 kept := b.Procs[:0]
120 for _, p := range b.Procs {
121 if p.Group != g {
122 kept = append(kept, p)
123 }
124 }
125 b.Procs = kept
126}
127
128func (w *world) running(b *box, g string) bool {
129 for _, p := range b.Procs {
130 if p.Group == g {
131 return true
132 }
133 }
134 return false
135}
136
137// adopt makes ver the live release of group g: set-before (the gate, reached
138// before the repoint; on a timeout the link does not move and the mark stays
139// 1), then the repoint, then the restart, itself a gated launch. between runs
140// after the repoint and before the restart: whatever systemd does there.
141func (w *world) adopt(b *box, g string, ver int, between func()) bool {
142 w.logf("box%d: adopt %s v%d (live v%d)", b.ID, g, ver, b.Live[g])
143 if !w.repoint(b, g, ver) {
144 return false
145 }
146 if between != nil {
147 between()
148 }
149 w.stopGroup(b, g)
150 return w.launch(b, g, ver, "adopt")
151}
152
153// repointAwake is adoption that repoints the live link and does not restart:
154// an awake hibernating route (release/adopt.go:1174), or a generation
155// rollout that only schedules the swap. What ran keeps running.
156func (w *world) repointAwake(b *box, g string, ver int) bool {
157 w.logf("box%d: adopt %s v%d without a restart (live v%d)", b.ID, g, ver, b.Live[g])
158 if !w.repoint(b, g, ver) {
159 return false
160 }
161 w.logf("box%d: %s keeps its running processes until their next start", b.ID, g)
162 return true
163}
164
165// repoint is adoption up to the repoint: the group's lock (public-holds-lock),
166// set-before (the gate, reached before the repoint; on a timeout the link
167// does not move and the mark stays 1), then the repoint.
168func (w *world) repoint(b *box, g string, ver int) bool {
169 if b.Locked[g] && on("public-holds-lock") {
170 w.logf("box%d: adopt %s v%d: --public holds the group's lock; not this pass", b.ID, g, ver)
171 return false
172 }
173 if on("set-before-repoint") && !w.gate(b, g, ver, "adopt") {
174 w.logf("box%d: adopt %s v%d: not repointed this pass", b.ID, g, ver)
175 return false
176 }
177 b.Live[g] = ver
178 w.stage(b, g, ver)
179 if b.Good[g] == 0 {
180 b.Good[g] = ver
181 }
182 w.logf("box%d: live link of %s -> v%d", b.ID, g, ver)
183 return true
184}
185
186// stage records that build ver of group g is staged on the box. A version is
187// staged once per app (/opt/carlos/versions holds the app's versions): if
188// another instance of the app already staged it, it stays recorded there
189// only, and this group runs it from that copy.
190func (w *world) stage(b *box, g string, ver int) {
191 for _, v := range w.stagedForApp(b, g) {
192 if v == ver {
193 return
194 }
195 }
196 b.Staged[g] = append(b.Staged[g], ver)
197}
198
199// appGroups is group g and every other instance group on the box whose own
200// row has the same non-empty account/app, sorted.
201func (w *world) appGroups(b *box, g string) []string {
202 own := b.Rows[g]
203 if own == nil || own.App == "" {
204 return []string{g}
205 }
206 out := []string{}
207 seen := false
208 for _, h := range w.groups(b) {
209 if r := b.Rows[h]; r != nil && r.App == own.App {
210 out = append(out, h)
211 seen = seen || h == g
212 }
213 }
214 if !seen {
215 out = append(out, g)
216 sort.Strings(out)
217 }
218 return out
219}
220
221// stagedForApp is every version of g's app staged on the box, whichever
222// instance of the app staged it.
223func (w *world) stagedForApp(b *box, g string) []int {
224 var out []int
225 for _, h := range w.appGroups(b, g) {
226 out = append(out, b.Staged[h]...)
227 }
228 return out
229}
230
231// wake is the activator waking group g by its wake plan: "recovery" starts
232// the recovery build, "fallback" first fails to start the live build and
233// falls back; both choose GoodVersion. The triggering request, if any, is
234// delivered afterwards (or finds the fence cancelled it). It reports whether
235// there was a build to launch.
236func (w *world) wake(b *box, g string, mode string, trigger *req) bool {
237 ver := b.Good[g]
238 if mode == "fallback" {
239 w.logf("box%d: wake %s: live v%d fails to start; falling back to good v%d", b.ID, g, b.Live[g], ver)
240 } else {
241 w.logf("box%d: wake %s: recovery build good v%d", b.ID, g, ver)
242 }
243 if ver != 0 {
244 w.launch(b, g, ver, mode)
245 }
246 if trigger != nil {
247 w.deliver(trigger)
248 }
249 return ver != 0
250}
251
252// moveLive is anything other than adoption moving group g's live link (a
253// rollout tool, a hand repair). Only adoption, through the gate, and
254// --public may move a live link.
255func (w *world) moveLive(b *box, g string, ver int, who string) bool {
256 if on("live-link-only-adoption") {
257 w.logf("box%d: %s may not move the live link of %s; refused", b.ID, who, g)
258 return false
259 }
260 b.Live[g] = ver
261 w.logf("box%d: %s moved the live link of %s -> v%d", b.ID, who, g, ver)
262 return true
263}
264
265// systemdRestart is Restart=always: the unit's main process exits and
266// systemd starts /opt/carlos/live/<host> again, with no activator. False
267// when the route has no live release.
268func (w *world) systemdRestart(b *box, g string) bool {
269 if b.Live[g] == 0 {
270 return false
271 }
272 w.stopGroup(b, g)
273 w.launch(b, g, b.Live[g], "systemd")
274 return true
275}
276
277// socketActivate is carlos-app@.socket starting a stopped unit from the live
278// link, with no activator. False when there is nothing to start.
279func (w *world) socketActivate(b *box, g string) bool {
280 if b.Live[g] == 0 || w.running(b, g) {
281 return false
282 }
283 w.launch(b, g, b.Live[g], "socket")
284 return true
285}
286
287// ---------- the random walk ----------
288
289func (w *world) pickBox() *box { return w.boxes[w.rng.Intn(len(w.boxes))] }
290
291// groups is every instance group of b's registry, sorted: the groups of the
292// instance rows that are not platform-minted (adoption skips minted rows,
293// release/adopt.go:1210; their processes are their source's).
294func (w *world) groups(b *box) []string {
295 seen := map[string]bool{}
296 var out []string
297 for h, r := range b.Rows {
298 if r.Kind != "instance" || r.Platform {
299 continue
300 }
301 if g := w.group(b, h); !seen[g] {
302 seen[g] = true
303 out = append(out, g)
304 }
305 }
306 sort.Strings(out)
307 return out
308}
309
310func (w *world) pickGroup(b *box) string {
311 gs := w.groups(b)
312 if len(gs) == 0 {
313 return ""
314 }
315 return gs[w.rng.Intn(len(gs))]
316}
317
318func (w *world) pickVer() int { return 1 + w.rng.Intn(nBuilds) }
319
320// randomPublic sends one public request to a host of b's edge map, by its
321// own name, an alias or a path map.
322func (w *world) randomPublic(b *box) *req {
323 hosts := make([]string, 0, len(b.EdgeRows))
324 for h := range b.EdgeRows {
325 hosts = append(hosts, h)
326 }
327 if len(hosts) == 0 {
328 return nil
329 }
330 sort.Strings(hosts)
331 h := hosts[w.rng.Intn(len(hosts))]
332 paths := []string{"/"}
333 for p := range b.EdgeRows[h].PathTo {
334 paths = append(paths, p)
335 }
336 sort.Strings(paths[1:])
337 return w.admitPublic(b, h, paths[w.rng.Intn(len(paths))])
338}
339
340// heldFor is the oldest held request on b whose proxy reaches group g.
341func (w *world) heldFor(b *box, g string) *req {
342 for _, r := range b.Admitted {
343 if r.Held && w.backendGroup(b, r.Backend) == g {
344 return r
345 }
346 }
347 return nil
348}
349
350func init() {
351 actions = append(actions,
352 action{name: "adopt", weight: 3, run: func(w *world) bool {
353 b := w.pickBox()
354 g := w.pickGroup(b)
355 if g == "" {
356 return false
357 }
358 ver := w.pickVer()
359 var between func()
360 switch w.rng.Intn(3) {
361 case 1:
362 between = func() { w.systemdRestart(b, g) }
363 case 2:
364 between = func() {
365 if r := w.randomPublic(b); r != nil {
366 w.socketActivate(b, w.backendGroup(b, r.Backend))
367 w.deliver(r)
368 }
369 }
370 }
371 w.adopt(b, g, ver, between)
372 return true
373 }},
374 action{name: "reload", weight: 4, run: func(w *world) bool { w.reload(w.pickBox()); return true }},
375 action{name: "reload-fails", weight: 1, run: func(w *world) bool {
376 b := w.pickBox()
377 b.ReloadFails = !b.ReloadFails && !w.calm()
378 w.logf("box%d: reload fails = %v", b.ID, b.ReloadFails)
379 return true
380 }},
381 // 9, not 6 (Task 6): fence-box-wide's sweep catch needs a request held
382 // through a service row that is deleted before a declaring adoption's
383 // fence; at 6 the 3000-seed sweep caught it 7 times, at 9 12 times
384 action{name: "public-request", weight: 9, run: func(w *world) bool {
385 b := w.pickBox()
386 r := w.randomPublic(b)
387 if r == nil {
388 return false
389 }
390 if len(b.Admitted) <= 8 && w.rng.Intn(2) == 0 {
391 r.Held = true
392 w.logf("box%d: request %d held in the activator's queue", b.ID, r.ID)
393 return true
394 }
395 w.deliver(r)
396 return true
397 }},
398 action{name: "resume", weight: 3, run: func(w *world) bool {
399 var held []*req
400 for _, b := range w.boxes {
401 for _, r := range b.Admitted {
402 if r.Held {
403 held = append(held, r)
404 }
405 }
406 }
407 if len(held) == 0 {
408 return false
409 }
410 sort.Slice(held, func(i, j int) bool { return held[i].ID < held[j].ID })
411 w.deliver(held[w.rng.Intn(len(held))])
412 return true
413 }},
414 action{name: "wake", weight: 3, run: func(w *world) bool {
415 b := w.pickBox()
416 g := w.pickGroup(b)
417 if g == "" {
418 return false
419 }
420 mode := "recovery"
421 if w.rng.Intn(2) == 0 {
422 mode = "fallback"
423 }
424 w.stopGroup(b, g) // the group had gone to sleep
425 return w.wake(b, g, mode, w.heldFor(b, g))
426 }},
427 action{name: "swap", weight: 2, run: func(w *world) bool {
428 b := w.pickBox()
429 g := w.pickGroup(b)
430 if g == "" {
431 return false
432 }
433 if w.launch(b, g, w.pickVer(), "swap") {
434 // the incoming generation serves; the outgoing ones stop
435 newest := b.Procs[len(b.Procs)-1]
436 kept := b.Procs[:0]
437 for _, p := range b.Procs {
438 if p.Group != g || p == newest {
439 kept = append(kept, p)
440 }
441 }
442 b.Procs = kept
443 }
444 return true
445 }},
446 action{name: "seed", weight: 1, run: func(w *world) bool {
447 b := w.pickBox()
448 g := w.pickGroup(b)
449 if g == "" {
450 return false
451 }
452 w.stopGroup(b, g)
453 w.launch(b, g, w.pickVer(), "seed")
454 return true
455 }},
456 action{name: "cmd", weight: 1, run: func(w *world) bool {
457 b := w.pickBox()
458 g := w.pickGroup(b)
459 if g == "" {
460 return false
461 }
462 w.launch(b, g, w.pickVer(), "cmd")
463 return true
464 }},
465 action{name: "systemd-restart", weight: 2, run: func(w *world) bool {
466 b := w.pickBox()
467 return w.systemdRestart(b, w.pickGroup(b))
468 }},
469 action{name: "socket-activation", weight: 2, run: func(w *world) bool {
470 b := w.pickBox()
471 g := w.pickGroup(b)
472 w.stopGroup(b, g) // the unit exited idle; a connection arrives
473 ok := w.socketActivate(b, g)
474 if r := w.heldFor(b, g); r != nil {
475 w.deliver(r)
476 }
477 return ok
478 }},
479 action{name: "repoint-live", weight: 1, run: func(w *world) bool {
480 b := w.pickBox()
481 g := w.pickGroup(b)
482 if g == "" {
483 return false
484 }
485 w.moveLive(b, g, w.pickVer(), "a rollout tool")
486 return true
487 }},
488 )
489}
490