carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostlife
2
3import (
4 "encoding/json"
5 "fmt"
6 "math/rand"
7 "os"
8 "strconv"
9 "strings"
10 "testing"
11)
12
13const (
14 chaosSteps = 2000
15 calmSteps = 6000
16)
17
18func newWorld(seed int64, verbose bool) *world {
19 w := &world{seed: seed, rng: &counting{r: rand.New(rand.NewSource(seed))}, verbose: verbose, perRegion: boxesPerRegion,
20 stores: map[string]bool{app0: true, app1: true}, cfgVer: map[string]int{app0: 1, app1: 1}, purgeRes: map[string]bool{}, purgeStep: map[string]int{},
21 control: newBucket(), delivered: map[int]bool{}, durable: map[string]map[int]bool{}, writeInc: map[int]int{}, txs: map[int]tx{}, legacyIncs: map[int]bool{}, purgeOp: map[string]string{}, genParent: map[string]string{}, markerAlerted: map[string]bool{}, execLease: map[int]execLease{}, fail: map[string]bool{}, stats: map[string]int{}}
22 for r := 0; r < nRegions; r++ {
23 w.buckets[r] = newBucket()
24 }
25 w.admit = map[string]*admission{app0: {Reg: map[string]regEntry{}}, app1: {Reg: map[string]regEntry{}}}
26 for _, f := range []string{"partition", "freeze", "pause", "destroy", "disk", "recreate", "pausecreate", "lostreturn", "markerhold", "freezetgt", "retention"} {
27 w.fail[f] = w.rng.Intn(2) == 0
28 }
29 for r := 0; r < nRegions; r++ {
30 b := &box{ID: r, Region: r, HasKey: r == 0 || w.rng.Intn(4) != 0, Skew: w.rng.Intn(3) - 1, Nested: w.rng.Intn(2) == 0, Enrolled: true}
31 if r != 0 && w.rng.Intn(4) == 0 {
32 b.FailApp = true
33 }
34 if w.rng.Intn(10) == 0 {
35 b.FailArchive = true
36 }
37 if r != 0 && w.rng.Intn(2) == 0 {
38 b.FailSettle = true
39 }
40 b.forget()
41 w.boxes = append(w.boxes, b)
42 }
43 if w.rng.Intn(3) == 0 {
44 w.legacyStart()
45 } else if w.fail["retention"] {
46 w.retentionStart()
47 }
48 // the two-box shape (region HA L18): a second enrolled box in every
49 // region, drawn after every one-box draw of this world, so the one-box
50 // stream is untouched; id nRegions+r, as addBox gives a witness
51 for r := 0; r < nRegions && w.perRegion > 1; r++ {
52 b := &box{ID: nRegions + r, Region: r, HasKey: w.rng.Intn(4) != 0, Skew: w.rng.Intn(3) - 1, Nested: w.rng.Intn(2) == 0, Enrolled: true}
53 if w.rng.Intn(4) == 0 {
54 b.FailApp = true
55 }
56 if w.rng.Intn(10) == 0 {
57 b.FailArchive = true
58 }
59 if w.rng.Intn(2) == 0 {
60 b.FailSettle = true
61 }
62 b.forget()
63 w.boxes = append(w.boxes, b)
64 }
65 if w.perRegion > 1 {
66 w.fail["terminate"] = w.rng.Intn(2) == 0
67 for _, b := range w.boxes {
68 b.DeleteOnTermination = w.rng.Intn(8) != 0 // one box in eight has a volume without the flag (§5.9, H19)
69 }
70 }
71 return w
72}
73
74// retentionStart: the host has been recreated more times than
75// retired_incarnations remembers (claim rev 6.5 §2 caps it at 32), and a
76// replication object of an incarnation the list forgot is still in a
77// bucket (rev 7.5, R4 §3: the retention-boundary fixture). Only a sweep
78// that discovers locations from the namespace parks it.
79func (w *world) retentionStart() {
80 w.claim = &claim{Ver: w.newVer()}
81 old := 1 // an incarnation older than every one the list remembers
82 w.nextInc = old
83 for i := 0; i < retiredCap+1; i++ {
84 w.nextInc++
85 w.claim.Retired = retire(w.claim.Retired, w.nextInc) // 2 to 33: the cap drops nothing but 1 was never listed
86 }
87 if contains(w.claim.Retired, old) || len(w.claim.Retired) != retiredCap {
88 panic("retention fixture: the injected incarnation must be absent from a full list (R5-6)")
89 }
90 r := w.rng.Intn(nRegions)
91 k := ltxKey(locationFor(old, false), "", 0, 1, 1)
92 // a request that completed long after its incarnation was retired: a
93 // straggler, which only the sweep retires (INV-10 at an operation's end
94 // exempts it, as it exempts every completion after H)
95 w.buckets[r].objs[k] = []*version{{ID: w.newVer(), Straggler: true, B: &blob{Kind: "ltx", Level: 0, Min: 1, Max: 1, Inc: old, Pages: map[string]int{"k0": 1}}}}
96 w.logf("retention fixture: incarnation %d's replica in region %d, past the cap", old, r)
97}
98
99// legacyStart: a host that exists before the release. Its data is untagged,
100// a best-effort-placed descendant may be running, and an earlier
101// (pre-release) move left a replica in another region.
102func (w *world) legacyStart() {
103 r := w.rng.Intn(nRegions)
104 b := w.regionBox(r)
105 w.nextInc++
106 inc := w.nextInc
107 w.rec = &record{Ver: w.newVer(), Inc: inc, App: app0, Region: r}
108 w.legacyIncs[inc] = true
109 key := 7
110 b.Row = &row{Inc: inc, App: app0, Status: "ready"}
111 b.DB = &localDB{Inc: inc, Loc: locationFor(inc, true), Key: key, State: map[string]int{}, Epoch: b.Epoch}
112 for i := 0; i < 3; i++ {
113 w.nextW++
114 w.acks = append(w.acks, ack{W: w.nextW, Inc: inc, Box: b.ID, Session: 0, Gen: "", At: 0, Kind: "write"}) // the pre-release writer: the legacy identity, session 0 (claim rev 6.24)
115 w.writeInc[w.nextW] = inc
116 w.commit(b.DB, w.nextW)
117 }
118 b.Files = &filesDir{Inc: inc, Key: key, Env: 1, Done: true}
119 b.Config, b.CfgVer, b.Live, b.LiveInc = inc, 1, app0, inc
120 // the pre-release replica: one level-0 file of everything, untagged
121 full := w.deltaOf(b.DB, 0)
122 full.Inc = 0
123 b.DB.Pos, b.DB.Pending = 1, nil
124 k := ltxKey(b.DB.Loc, "", 0, 1, 1)
125 w.buckets[r].objs[k] = []*version{{ID: w.newVer(), B: full}}
126 w.markDurable(k, full)
127 // an earlier move's replica in another region: the first transaction only
128 first := w.txs[b.DB.Log[0]]
129 part := &blob{Kind: "ltx", Level: 0, Min: 1, Max: 1, Inc: 0, SealKey: key, Pages: map[string]int{first.Key: first.Val}, TXs: []int{first.W}}
130 other := (r + 1 + w.rng.Intn(nRegions-1)) % nRegions
131 w.buckets[other].objs[ltxKey(b.DB.Loc, "", 0, 1, 1)] = []*version{{ID: w.newVer(), B: part}}
132 if w.rng.Intn(2) == 0 {
133 // the pre-release processes: the legacy identity, session 0 in the origin generation (claim rev 6.24; rev 7.13's seed 44450 found them acknowledging under no identity at all)
134 b.Procs = []*proc{{Main: true, Strict: false, Inc: inc, Session: 0, Box: b.ID}, {Main: false, Strict: false, Inc: inc, Session: 0, Box: b.ID}}
135 }
136 if w.rng.Intn(2) == 0 {
137 // that earlier move also left the old box's local state behind:
138 // a database, a files directory and env files, and no row
139 ob := w.regionBox(other)
140 ob.DB = &localDB{Inc: inc, Loc: locationFor(inc, true), Key: key, State: cloneState(part.Pages), TX: 1, Pos: 1, Epoch: ob.Epoch}
141 ob.Files = &filesDir{Inc: inc, Key: key, Env: 1, Done: true}
142 }
143}
144
145// ---------- the scheduler ----------
146
147func (w *world) step() {
148 w.now++
149 w.completeDue(false)
150 if p := w.recPend; p != nil && p.at <= w.now {
151 w.recPend = nil
152 if w.rec == nil { // create-only: it lands only where no record is
153 w.rec = p.rec
154 w.logf("a late record PUT for inc %d landed", p.rec.Inc)
155 }
156 }
157 w.runStalled()
158 if w.perRegion > 1 {
159 w.twoBoxStep() // the two-box shape's own draws, after the one-box ones (ruling 1)
160 }
161 bs := w.liveBoxes()
162 b := bs[w.rng.Intn(len(bs))]
163 n := w.rng.Intn(100)
164 switch {
165 case !w.calm && n < 6:
166 switch w.rng.Intn(10) {
167 case 9:
168 // a config or secrets update publishes a new bundle version
169 app := []string{app0, app1}[w.rng.Intn(2)]
170 w.cfgVer[app]++
171 w.logf("config bundle %s v%d published", app, w.cfgVer[app])
172 case 0, 1:
173 w.userCreate()
174 case 2, 7, 8:
175 w.userMove()
176 case 3:
177 w.userDelete()
178 case 4:
179 w.userPush()
180 case 5:
181 if w.rng.Intn(3) == 0 {
182 w.userPurge()
183 }
184 default:
185 w.doorAuthorize()
186 }
187 case !w.calm && n < 12:
188 w.fault()
189 case n < 20:
190 w.consoleStep()
191 case n < 24:
192 w.recoveryPass()
193 case n < 25:
194 w.doorOpen()
195 case n < 26:
196 if !w.calm {
197 w.doorAuthorize()
198 }
199 case n < 28:
200 if w.calm || w.rng.Intn(4) == 0 {
201 w.operator() // operators act during the incident too, not only after
202 }
203 case n < 56:
204 w.worker(b)
205 case n < 62:
206 w.obligationRunner(b)
207 case n < 67:
208 w.reconcile(b)
209 case n < 73:
210 w.traffic(b)
211 w.wake(b)
212 case n < 77:
213 w.replicate(b)
214 case n < 78:
215 w.idleStop(b)
216 case n < 81:
217 w.passPlan(b, "config")
218 case n < 84:
219 w.passPlan(b, "adopt")
220 case n < 89:
221 w.passExec(b)
222 case n < 94:
223 w.runner(b)
224 default:
225 w.watchdog(b)
226 }
227 w.checkStep()
228}
229
230// heal ends the chaos. On the first call some faults linger, so the
231// operator meets a frozen, paused or partitioned participant, and destroyed
232// boxes stay down; on the final call everything heals, and each stopped box
233// either comes back (an instance stopped, then started) or is replaced.
234func (w *world) heal(final bool) {
235 w.calm = true
236 c := w.claim
237 for _, b := range w.boxes {
238 linger := !final && c != nil && (c.Op.Kind == "landing" || c.Op.Kind == "restoring") && b.ID == c.Op.Tgt && w.rng.Intn(2) == 0
239 if linger {
240 continue
241 }
242 b.Partitioned, b.Frozen, b.AgentPaused, b.AgentDown, b.PurgeFails = false, false, false, false, false
243 if b.hostFailed {
244 b.hostFailed, b.failCounted, b.startFails, b.nextStart = false, false, 0, 0 // the operator cleared the failed host
245 w.stats["failed hosts cleared"]++
246 }
247 }
248 if final {
249 for _, b := range w.boxes {
250 if b.Destroyed && !b.Terminated && w.regionBox(b.Region) == nil && w.rng.Intn(2) == 0 {
251 b.Destroyed = false // started again, with its old disk
252 b.forget()
253 w.logf("stopped box %d started again", b.ID)
254 }
255 }
256 w.replaceDestroyed()
257 }
258 // a box with no key gets one: ops distributes it (the approved fix)
259 for _, b := range w.boxes {
260 b.HasKey = true
261 b.FailArchive = false
262 }
263}
264
265func (w *world) quiescent() bool {
266 if len(w.pending) > 0 || len(w.console) > 0 || w.recPend != nil || !w.atRest() {
267 return false
268 }
269 return len(w.residue()) == 0
270}
271
272// whyNotQuiescent names, for a non-convergence report, what quiescent()
273// found unfinished.
274func (w *world) whyNotQuiescent() string {
275 var out []string
276 for _, r := range w.pending {
277 out = append(out, fmt.Sprintf("pending %v %s r%d by %s issued %d due %d beyond=%v", r.kind, r.key, r.region, r.by, r.at, r.doneAt, r.beyond))
278 }
279 if len(w.console) > 0 {
280 out = append(out, fmt.Sprintf("%d console plans", len(w.console)))
281 }
282 if w.recPend != nil {
283 out = append(out, "a record PUT pending")
284 }
285 if w.h2.Kind != "" {
286 out = append(out, "h2 "+w.h2.Kind)
287 }
288 for app, ad := range w.admit {
289 if ad.Closed {
290 out = append(out, "admission closed for "+app)
291 }
292 for id, e := range ad.Reg {
293 if w.now <= e.Until || !on("registration-recovery") {
294 out = append(out, fmt.Sprintf("registration %s of %s live until %d", id, app, e.Until))
295 }
296 }
297 }
298 if c := w.claim; c != nil && c.Holder != nil {
299 if c.Op.Kind != "serving" {
300 out = append(out, "op "+c.Op.Kind)
301 }
302 for _, o := range c.Obl {
303 out = append(out, fmt.Sprintf("obligation %s %s %s", o.ID, o.Kind, o.Prefix))
304 }
305 if e := w.execBox(c); e != nil && (e.Config != c.Holder.Inc || e.Live != c.Holder.App || e.LiveInc != c.Holder.Inc) {
306 out = append(out, "config or adoption owed")
307 }
308 for _, b := range w.liveBoxes() {
309 if e := w.execBox(c); b != e && (b.Row != nil || len(b.Procs) > 0) {
310 out = append(out, fmt.Sprintf("box %d has a row or processes", b.ID))
311 }
312 if b.open != nil {
313 out = append(out, fmt.Sprintf("box %d admitted into %q, start in progress at stage %s", b.ID, b.open.Gen, b.open.stage))
314 }
315 }
316 }
317 if r := w.residue(); len(r) > 0 {
318 out = append(out, fmt.Sprintf("residue %v", r))
319 }
320 if len(out) == 0 {
321 return "quiescent"
322 }
323 return strings.Join(out, "; ")
324}
325
326// atRest: the claim, record and boxes have converged.
327func (w *world) atRest() bool {
328 if w.h2.Kind == "creating" || w.h2.Kind == "purging" || w.h2.Kind == "fenced" {
329 return false
330 }
331 for _, ad := range w.admit {
332 if ad.Closed {
333 return false
334 }
335 for _, e := range ad.Reg {
336 if w.now <= e.Until || !on("registration-recovery") {
337 return false // a live registration: its create is still in flight
338 }
339 }
340 }
341 c := w.claim
342 if c == nil {
343 return w.rec == nil
344 }
345 if c.Holder == nil {
346 return c.Op.Kind == "" && w.rec == nil && len(c.Obl) == 0
347 }
348 if c.Op.Kind != "serving" || len(c.Obl) > 0 || w.rec == nil || w.rec.Inc != c.Holder.Inc || w.rec.MoveID != "" {
349 return false
350 }
351 e := w.execBox(c)
352 if e == nil || e.Row == nil || e.Row.Status != "ready" {
353 return false
354 }
355 if d := c.Op.Completed; d != nil && !d.Discharged || e.actFence != "" {
356 return false // a completion not yet marked, or a fence not yet lifted (L18 finding 2)
357 }
358 if e.Config != c.Holder.Inc || e.Live != c.Holder.App || e.LiveInc != c.Holder.Inc {
359 return false // config sync and adoption still owe this incarnation
360 }
361 for _, b := range w.liveBoxes() {
362 if b != e && (b.Row != nil || len(b.Procs) > 0) {
363 return false
364 }
365 if b.open != nil {
366 return false // an admitted writer still finishing its start
367 }
368 }
369 return true
370}
371
372// ---------- invariants ----------
373
374// checkStep runs after every step.
375func (w *world) checkStep() {
376 // one writer per host, across every region
377 var writers []int
378 for _, b := range w.boxes {
379 if !b.Destroyed && !b.Frozen && len(b.Procs) > 0 {
380 writers = append(writers, b.ID)
381 }
382 }
383 if len(writers) > 1 {
384 w.violate("INV-1: boxes %v run the host at once", writers)
385 }
386 // an app store exists while any instance of the app is held for serving
387 if w.h2.Kind == "serving" && !w.stores[w.h2.App] {
388 w.violate("purge: store of app %s destroyed while its second host serves", w.h2.App)
389 }
390 if c := w.claim; c != nil && c.Holder != nil && !w.stores[c.Holder.App] {
391 switch c.Op.Kind {
392 case "purging", "deleting", "parking":
393 default:
394 w.violate("purge: store of app %s destroyed while its host is %s", c.Holder.App, c.Op.Kind)
395 }
396 }
397 if c := w.claim; c != nil && c.Op.Kind == "restoring" && c.Op.SrcPhase == "stuck" {
398 w.violate("stuck: source could not recover %s", c.Op.ID)
399 }
400 if c := w.claim; c != nil && c.Op.Kind == "reverting" && c.Op.RevPhase == "stuck" {
401 w.violate("stuck: source could not revert %s", c.Op.ID)
402 }
403 // INV-27 (region HA HA1, HA7): while relocating, op.exec is none or an
404 // enrolled box that is not on lost; execution authority never names a
405 // fenced or unenrolled reference
406 if c := w.claim; c != nil && c.Op.Kind == "relocating" && c.Op.Exec >= 0 {
407 released := c.Op.Release != nil && c.Op.Release.Box == c.Op.Exec // a released executor fenced before PinAfterRelease (FenceSource after the release, rev 4)
408 if e := w.boxByID(c.Op.Exec); e == nil || !e.Enrolled || contains(c.Lost, e.ID) && !released {
409 w.violate("INV-27: relocation %s names box %d as op.exec, which is unenrolled or lost", c.Op.ID, c.Op.Exec)
410 }
411 }
412 w.checkCompletion() // INV-31 (L18 finding 2): only a relocation sets owedDischarge
413 if w.perRegion > 1 && !w.noPinWatch {
414 w.checkFilesPreserved() // INV-30 (joint round 2): only the two-box shape has files keys
415 }
416}
417
418func (w *world) boxesState() string {
419 s := ""
420 for _, b := range w.liveBoxes() {
421 s += fmt.Sprintf(" [box %d r%d row=%+v procs=%d db=%v files=%v cfg=%d live=%s/%d old=%v]", b.ID, b.Region, b.Row, len(b.Procs), b.DB != nil, b.Files != nil, b.Config, b.Live, b.LiveInc, b.Old)
422 }
423 return s
424}
425
426// residue lists live-namespace data that should not exist: everything
427// under the host's namespace in a region the host does not serve from, and
428// in the serving region anything but the claim's location and generation
429// (and the parkers' own locks). Nothing here writes.
430func (w *world) residue() []string { return w.residueOf(false) }
431
432// residueAtEnd is INV-10 as the spec states it, checked when an operation
433// ends (a release to serving, a free): no copy outside the live set, except
434// a straggler that completed after H, which only the sweep can retire.
435func (w *world) residueAtEnd() []string { return w.residueOf(true) }
436
437func (w *world) residueOf(exceptStragglers bool) []string {
438 var out []string
439 c := w.claim
440 live := map[string]bool{}
441 serving := -1
442 if c != nil && c.Holder != nil {
443 if e := w.execBox(c); e != nil {
444 serving = e.Region
445 loc, gen := c.Holder.Location, c.Holder.Gen
446 live[dataKey(loc, gen, "")] = true // the generation's prefix: every LTX file of the generation
447 live[genMarkerKey(loc)] = true
448 live[dataKey(loc, gen, "closed/")] = true // the live generation's certificates, every session's (claim rev 6.8 §6.4a)
449 // an unfinished admission's predecessor is its baseline until the
450 // marker names the new generation (claim rev 6.10, R10-2)
451 if bg, _ := w.bucketGen(e, loc); bg != gen {
452 if chain, ok := w.unfinishedChain(c, bg); ok {
453 for _, g := range append(chain, bg) {
454 live[dataKey(loc, g, "")] = true
455 live[dataKey(loc, g, "closed/")] = true
456 }
457 }
458 }
459 }
460 }
461 for r := 0; r < nRegions; r++ {
462 bk := w.buckets[r]
463 for _, p := range []string{"instances/" + host + "/", "moves/" + host + "/"} {
464 for _, k := range bk.keys(p) {
465 if strings.HasSuffix(k, "lock.json") || (r == serving && (live[k] || isDataKey(k) && live[genPrefixOf(k)] || isClosedKey(k) && live[k[:strings.LastIndex(k, "closed/")+7]] || w.filesLiveSet(c, r, k))) {
466 continue
467 }
468 if exceptStragglers && w.owed(r, k) {
469 continue // a pending chore of another operation covers it
470 }
471 for _, v := range bk.objs[k] {
472 if !v.Marker && !(exceptStragglers && (v.Straggler || v.SweepOwed)) {
473 out = append(out, fmt.Sprintf("r%d:%s", r, k))
474 break
475 }
476 }
477 }
478 }
479 }
480 return out
481}
482
483// owed: an open park obligation names this region and a prefix of k.
484func (w *world) owed(r int, k string) bool {
485 c := w.claim
486 if c == nil {
487 return false
488 }
489 for _, o := range c.Obl {
490 if o.Region == r && o.Prefix != "" && strings.HasPrefix(k, o.Prefix) {
491 return true
492 }
493 }
494 // a cold admission whose writer died after its marker landed and before
495 // it queued the retire chores (rev 7.11): the writers journal still
496 // shows its predecessor uncovered, and the next admission's marker
497 // retires it (§5.1 step 9); until then the predecessor is owed, as a
498 // pending chore's keys are (seeds 4159, 26817, 34163 / 100,000)
499 if h := c.Holder; h != nil && strings.HasPrefix(k, h.Location) {
500 g := genOf(h.Location, k)
501 for i := range c.Holder.Admissions {
502 if e := &c.Holder.Admissions[i]; e.Mode == "cold" && !e.Covered && e.Predecessor.Gen == g {
503 return true
504 }
505 }
506 }
507 return false
508}
509
510func hasNonMarker(vs []*version) bool {
511 for _, v := range vs {
512 if !v.Marker {
513 return true
514 }
515 }
516 return false
517}
518
519// canStart is the start check and admission, computed without starting
520// anything: the final oracle never writes.
521func (w *world) canStart(e *box, c *claim) string {
522 c = c.clone()
523 // the wake's first step is the record's initialization from this fresh
524 // read (claim rev 6.5 §6.4): a box with no record for the incarnation
525 // that the claim names as op.exec takes holder.stamp. On a copy of the
526 // box: the oracle never writes (R6-1)
527 eb := *e
528 eb.Stamps = map[int]*stamps{}
529 for k, v := range e.Stamps {
530 cp := *v
531 cp.Retired = append([]int(nil), v.Retired...)
532 eb.Stamps[k] = &cp
533 }
534 inc := c.Holder.Inc
535 if eb.stampOf(inc) == 0 && c.Holder.Stamp != 0 && c.Op.Exec == eb.ID && !(eb.Stamps[inc] != nil && contains(eb.Stamps[inc].Retired, c.Holder.Stamp)) {
536 eb.install(inc, c.Holder.Stamp)
537 }
538 c.readStamp = eb.stampOf(inc) // as a fresh read by e would carry it
539 if !w.admits(&eb, c, "") {
540 return "not admitted"
541 }
542 h := c.Holder
543 bg, binc := w.bucketGen(e, h.Location)
544 if bg != h.Gen || (bg != "" && binc != h.Inc) {
545 return fmt.Sprintf("generation.json %q, claim %q", bg, h.Gen)
546 }
547 if e.Files != nil && (e.Files.Gen != bg || e.Files.Inc != h.Inc) {
548 return fmt.Sprintf(".files-gen %q inc %d", e.Files.Gen, e.Files.Inc)
549 }
550 return ""
551}
552
553// finalFreed is the end state of a freed host: no record, and no box holds
554// local state for it.
555func (w *world) finalFreed() {
556 if w.rec != nil {
557 w.violate("claim/record: claim free but record inc %d exists", w.rec.Inc)
558 }
559 for _, b := range w.liveBoxes() {
560 if b.DB != nil || b.Files != nil {
561 w.violate("INV-10: box %d still holds local state for a freed host", b.ID)
562 }
563 }
564}
565
566func (w *world) final() {
567 if !w.quiescent() {
568 if len(w.pending) == 0 && len(w.console) == 0 && w.atRest() {
569 w.violate("INV-10: residue never retired: %v", w.residue())
570 return
571 }
572 w.violate("INV-3: no convergence after %d calm steps: %s; %s%s", calmSteps, w.whyNotQuiescent(), w.state(), w.boxesState())
573 return
574 }
575 c := w.claim
576 if c == nil || c.Holder == nil {
577 w.finalFreed()
578 return
579 }
580 e := w.execBox(c)
581 if !hasMain(e) && c.Op.Kind == "serving" && w.certificate(e, c) == nil {
582 // the host is at rest after a session that did not certify: its
583 // next wake is a cold admission that declares the acknowledged,
584 // unuploaded writes (R10-6). Rev 7.11's oracle exempted them as a
585 // prospective declaration, recording and alerting nothing; the
586 // recovery admission is run for real now, then everything it
587 // queued, and the writes are judged by what it declared
588 w.stats["final: recovery admission run"]++
589 w.drive(400, func() bool { return hasMain(e) && e.open == nil }, func() { w.wake(e) }, w.actAll(), w.runConsole, w.recoveryPass, w.operator)
590 if len(w.violations) > 0 {
591 return
592 }
593 if !(hasMain(e) && e.open == nil) {
594 w.stats["final: unverified prospective declaration"]++
595 w.logf("final: the recovery admission on box %d did not complete; its declaration is unverified", e.ID)
596 } else {
597 w.drive(calmSteps, w.quiescent, w.actAll(), w.runConsole, w.recoveryPass, w.operator)
598 if len(w.violations) > 0 {
599 return
600 }
601 if !w.quiescent() {
602 w.violate("INV-3: no convergence after the final recovery admission: %s; %s%s", w.whyNotQuiescent(), w.state(), w.boxesState())
603 return
604 }
605 }
606 c = w.claim
607 if c == nil || c.Holder == nil {
608 // the final drive ran the console too: a purge may have freed
609 // the host (seed 2547 / 3,000 under admission-registration)
610 w.finalFreed()
611 return
612 }
613 e = w.execBox(c)
614 }
615 if e == nil || w.rec == nil {
616 w.violate("claim/record: the claim's holder has no record or no executing box at the end: %s", w.state())
617 return
618 }
619 if w.rec.Region != e.Region {
620 w.violate("claim/record: record region %d, claim serves from box %d in region %d", w.rec.Region, e.ID, e.Region)
621 }
622 for _, b := range w.liveBoxes() {
623 if b != e && (b.DB != nil || b.Files != nil) {
624 w.violate("INV-10: box %d holds local state; the host serves from box %d", b.ID, e.ID)
625 }
626 }
627 if !hasMain(e) {
628 if why := w.canStart(e, c); why != "" {
629 w.violate("final: the serving box %d cannot start the host: %s (%s)", e.ID, why, w.state())
630 return
631 }
632 }
633 // the serving box's database as it stands, or, if it has none, the
634 // cold restore it would do: computed, never written
635 before := w.fingerprint()
636 defer w.pure("final oracle", before)
637 local := e.DB
638 if local == nil {
639 local = w.restoreAsNext(e, c)
640 }
641 if local == nil {
642 w.violate("final: the serving box %d has no database and nothing to restore", e.ID)
643 return
644 }
645 if w.stats["final: unverified prospective declaration"] == 0 || w.certificate(e, c) != nil {
646 w.checkLocal(c, e, local)
647 }
648 w.checkDurable(c, "final")
649 if w.lostAlert != w.stats["writes_lost declared"] {
650 w.violate("INV-14: %d writes_lost records, %d alerts delivered (R10-6)", w.stats["writes_lost declared"], w.lostAlert)
651 }
652}
653
654// checkDurable is the cold-restore promise, checked at every release to
655// serving and at the end: a restore of the claim's location and generation
656// in the serving region holds the effect of every acknowledged write that
657// a completed replica object ever covered, and equals the database the
658// acknowledged writes up to its TXID produce. It reads the bucket and
659// writes nothing.
660func (w *world) checkDurable(c *claim, when string) {
661 if c == nil || c.Holder == nil {
662 return
663 }
664 e := w.execBox(c)
665 if e == nil {
666 return
667 }
668 before := w.fingerprint()
669 defer w.pure("INV-2 oracle", before)
670 db := w.restoreAsNext(e, c)
671 need := map[int]bool{}
672 seen := map[string]bool{}
673 for p := dataKey(c.Holder.Location, c.Holder.Gen, ""); p != "" && !seen[p]; p = w.genParent[p] {
674 seen[p] = true
675 for x := range w.durable[p] {
676 need[x] = true
677 }
678 }
679 have := map[int]bool{}
680 if db != nil {
681 for _, x := range db.Log {
682 have[x] = true
683 }
684 }
685 for _, a := range w.acks {
686 if a.Inc != c.Holder.Inc || a.Exempt || a.Kind != "write" || !need[a.W] {
687 continue
688 }
689 // coverage by write identity (R7-2): a required write is present
690 // only if a file the restore applied carried it; a maximum TXID
691 // says nothing after a fork
692 if db == nil || !have[a.W] {
693 w.violate("INV-2 (cold restore, %s): durable write %d (box %d, t=%d) lost (%s)", when, a.W, a.Box, a.At, w.ltxShape(e.Region, dataKey(c.Holder.Location, c.Holder.Gen, "")))
694 return
695 }
696 }
697 if db == nil {
698 return
699 }
700 w.foreignWrites(db, c.Holder.Inc, "cold restore, "+when)
701 // the restored state must be the replay, in acknowledgement order, of
702 // exactly the writes its files carried (R7-2): a write declared lost
703 // with a disk that landed anyway is replayed because it is there, a
704 // required write that is missing was caught above, and no key is ever
705 // masked; a later required write re-establishes its key's value
706 if why, ok := statesEqual(w.expectFrom(c.Holder.Inc, have), db.State); !ok {
707 w.violate("INV-2 (cold restore, %s): the restored database differs from the replay of the writes it carries: %s (restored TXID %d; %s; %s; %s)", when, why, db.TX, w.keyHistory(c.Holder.Inc, why, have), w.ltxShape(e.Region, dataKey(c.Holder.Location, c.Holder.Gen, "")), w.fileDetail(e.Region, dataKey(c.Holder.Location, c.Holder.Gen, ""), why))
708 }
709}
710
711// fileDetail lists, for diagnosis, each current file of the generation
712// with its kind, range, TXs and the page it holds for the key named in a
713// statesEqual message.
714func (w *world) fileDetail(r int, prefix, why string) string {
715 key := ""
716 if i := strings.Index(why, "key "); i >= 0 {
717 rest := why[i+4:]
718 if j := strings.Index(rest, " "); j > 0 {
719 key = rest[:j]
720 }
721 }
722 bk := w.buckets[r]
723 var out []string
724 for _, k := range bk.keys(prefix) {
725 v := bk.current(k)
726 if v == nil || v.B == nil || !isDataKey(k) {
727 continue
728 }
729 val, ok := v.B.Pages[key]
730 out = append(out, fmt.Sprintf("%s{full=%v txs=%v %s=%d/%v}", strings.TrimPrefix(k, prefix), v.B.Full, v.B.TXs, key, val, ok))
731 }
732 return "detail " + strings.Join(out, " ")
733}
734
735// keyHistory lists, for diagnosis, the acknowledged writes of inc that
736// touched the key named in a statesEqual message: W, lineage TXID, value,
737// whether the restored files carried it, whether it is exempt.
738func (w *world) keyHistory(inc int, why string, have map[int]bool) string {
739 key := ""
740 if i := strings.Index(why, "key "); i >= 0 {
741 rest := why[i+4:]
742 if j := strings.Index(rest, " "); j > 0 {
743 key = rest[:j]
744 }
745 }
746 var out []string
747 for _, a := range w.acks {
748 if a.Inc != inc || a.Kind != "write" {
749 continue
750 }
751 t := w.txs[a.W]
752 k := t.Key
753 if k == "" {
754 k = fmt.Sprintf("k%d", a.W)
755 }
756 if k != key {
757 continue
758 }
759 out = append(out, fmt.Sprintf("W%d:TX%d=%d have=%v exempt=%v session=%d gen=%q", a.W, t.TX, t.Val, have[a.W], a.Exempt, a.Session, a.Gen))
760 }
761 return "history of " + key + ": " + strings.Join(out, " ")
762}
763
764// expectFrom replays, in acknowledgement order, the acknowledged writes of
765// inc that the restored files carried (have), into the state a correct
766// restore of those files must equal.
767func (w *world) expectFrom(inc int, have map[int]bool) map[string]int {
768 state := map[string]int{}
769 for _, a := range w.acks {
770 if a.Inc != inc || a.Kind != "write" || !have[a.W] {
771 continue
772 }
773 t := w.txs[a.W]
774 if t.Key == "" {
775 t.Key, t.Val = fmt.Sprintf("k%d", a.W), a.W
776 }
777 if t.Val == 0 {
778 delete(state, t.Key)
779 } else {
780 state[t.Key] = t.Val
781 }
782 }
783 return state
784}
785
786// counting is the sweep's random source, counting its draws so the oracle
787// purity check can show an oracle consumed none (R6-1).
788type counting struct {
789 r *rand.Rand
790 n int
791}
792
793func (c *counting) Intn(n int) int {
794 c.n++
795 return c.r.Intn(n)
796}
797
798// fingerprint is everything an oracle must leave alone: the buckets (the
799// version counter and every version), the pending requests, the boxes'
800// upload queues and anchors, the console's plans and the random source's
801// position (R6-1). An oracle that changes any of it is repairing or
802// disturbing the system it checks.
803func (w *world) fingerprint() string {
804 objs := 0
805 for _, bk := range w.buckets {
806 for _, vs := range bk.objs {
807 objs += len(vs)
808 }
809 }
810 queue, anchors := 0, 0
811 for _, b := range w.boxes {
812 if b.DB != nil {
813 queue += len(b.DB.Queue)
814 }
815 for _, rs := range b.anchors {
816 anchors += len(rs)
817 }
818 }
819 draws := -1
820 if c, ok := w.rng.(*counting); ok {
821 draws = c.n
822 }
823 return fmt.Sprintf("ver=%d objs=%d pending=%d queue=%d anchors=%d console=%d draws=%d", w.ver, objs, len(w.pending), queue, anchors, len(w.console), draws)
824}
825
826// pure records a violation if an oracle changed the system (R6-1): the
827// fingerprint taken before it must equal the one after.
828func (w *world) pure(what, before string) {
829 if after := w.fingerprint(); after != before {
830 w.violate("oracle purity: the %s changed the system: before %s, after %s", what, before, after)
831 }
832}
833
834// ltxShape describes the current LTX files under prefix, for diagnosis.
835func (w *world) ltxShape(r int, prefix string) string {
836 var parts []string
837 bk := w.buckets[r]
838 for _, k := range bk.keys(prefix) {
839 if !isDataKey(k) {
840 continue
841 }
842 var vs []string
843 for _, v := range bk.objs[k] {
844 if v.Marker {
845 vs = append(vs, "M")
846 } else {
847 vs = append(vs, fmt.Sprintf("v%d", v.ID))
848 }
849 }
850 parts = append(parts, strings.TrimPrefix(k, prefix)+":"+strings.Join(vs, ","))
851 }
852 return "files " + strings.Join(parts, " ")
853}
854
855// foreignWrites: a database restored or started for incarnation inc holds
856// no key another incarnation wrote (C1, INV-4, claim C3a).
857func (w *world) foreignWrites(db *localDB, inc int, how string) {
858 for k, v := range db.State {
859 if v == 0 {
860 continue
861 }
862 if i, ok := w.writeInc[keyID(k)]; ok && i != inc {
863 w.violate("C3a (%s): incarnation %d's database holds key %s of incarnation %d", how, inc, k, i)
864 return
865 }
866 }
867}
868
869// checkLocal: the serving box's database holds the effect of every
870// acknowledged, non-exempt write of the incarnation (INV-2), and every
871// acknowledged timer is delivered or still registered.
872func (w *world) checkLocal(c *claim, e *box, db *localDB) {
873 // the serving database is the replay of every acknowledged write that
874 // was never declared lost, and of every write declared lost that came
875 // back: an upload in flight at a lost disk that landed anyway, which a
876 // later cold admission's remote baseline carried (R7-2's identity rule,
877 // as checkDurable applies it; seed 5863 / 30,000)
878 have := map[int]bool{}
879 for _, x := range db.Log {
880 have[x] = true
881 }
882 want := map[int]bool{}
883 for _, a := range w.acks {
884 if a.Inc == c.Holder.Inc && a.Kind == "write" && (!a.Exempt || have[a.W]) {
885 want[a.W] = true
886 }
887 }
888 if why, ok := statesEqual(w.expectFrom(c.Holder.Inc, want), db.State); !ok {
889 w.violate("INV-2 (local): the serving database differs from the acknowledged writes: %s (%s)", why, w.keyHistory(c.Holder.Inc, why, have))
890 return
891 }
892 timers := map[int]bool{}
893 for _, x := range e.Timers {
894 timers[x] = true
895 }
896 for _, a := range w.acks {
897 if a.Inc != c.Holder.Inc || a.Exempt || a.Kind != "timer" {
898 continue
899 }
900 if !w.delivered[a.W] && !timers[a.W] {
901 w.violate("INV-2: acknowledged timer %d (box %d, t=%d) lost", a.W, a.Box, a.At)
902 return
903 }
904 }
905}
906
907func (w *world) state() string {
908 c := w.claim
909 if c == nil {
910 return fmt.Sprintf("claim absent, record %+v", w.rec)
911 }
912 obl := ""
913 for _, o := range c.Obl {
914 obl += fmt.Sprintf(" %s:%s/r%d/b%d/%s", o.ID, o.Kind, o.Region, o.Box, o.Prefix)
915 }
916 adm := ""
917 for app, ad := range w.admit {
918 adm += fmt.Sprintf(" %s:closed=%v/reg=%d", app, ad.Closed, len(ad.Reg))
919 }
920 return fmt.Sprintf("op=%s/%s phase=%s land=%s src=%s rev=%s exec=%d obl=%d holder=%+v rec=%+v pending=%d console=%d obls=[%s] quiet=%v h2=%+v admission=[%s] residue=%v",
921 c.Op.Kind, c.Op.ID, c.Op.Phase, c.Op.LandPhase, c.Op.SrcPhase, c.Op.RevPhase, c.Op.Exec, len(c.Obl), c.Holder, w.rec, len(w.pending), len(w.console), obl, c.Quiet, w.h2, adm, w.residue())
922}
923
924// ---------- running ----------
925
926func runSeed(seed int64, verbose bool) *world {
927 w := newWorld(seed, verbose)
928 for i := 0; i < chaosSteps; i++ {
929 w.step()
930 }
931 w.heal(false)
932 for i := 0; i < calmSteps && !w.quiescent(); i++ {
933 w.step()
934 if i == 400 || i%400 == 399 {
935 w.heal(true)
936 if w.verbose {
937 w.logf("calm step %d: %s", i, w.whyNotQuiescent())
938 }
939 }
940 }
941 w.completeDue(true)
942 w.final()
943 return w
944}
945
946func seedsFromEnv(def int) int {
947 if s := os.Getenv("HOSTLIFE_SEEDS"); s != "" {
948 if n, err := strconv.Atoi(s); err == nil {
949 return n
950 }
951 }
952 return def
953}
954
955// TestHostLifecycle runs the joint model. HOSTLIFE_SEEDS sets the seed
956// count (the gate's default is short); HOSTLIFE_SEED=n runs one seed and
957// prints its trace; HOSTLIFE_BOXES=2 runs the two-box shape (region HA
958// L18) in place of the one-box one.
959func TestHostLifecycle(t *testing.T) {
960 sweep(t)
961}
962
963// TestHostLifecycleTwoBox runs the joint model with two boxes per region
964// (region HA L18) beside the one-box sweep, so the gate covers both
965// shapes. With HOSTLIFE_BOXES set, TestHostLifecycle runs that shape and
966// this test stands aside.
967func TestHostLifecycleTwoBox(t *testing.T) {
968 if os.Getenv("HOSTLIFE_BOXES") != "" {
969 t.Skip("HOSTLIFE_BOXES is set: TestHostLifecycle runs that shape")
970 }
971 saved := boxesPerRegion
972 boxesPerRegion = 2
973 defer func() { boxesPerRegion = saved }()
974 sweep(t)
975}
976
977func sweep(t *testing.T) {
978 if s := os.Getenv("HOSTLIFE_SEED"); s != "" {
979 seed, _ := strconv.ParseInt(s, 10, 64)
980 w := runSeed(seed, true)
981 for _, line := range w.trace {
982 t.Log(line)
983 }
984 for _, v := range w.violations {
985 t.Errorf("seed %d: %s", seed, v)
986 }
987 return
988 }
989 n := seedsFromEnv(300)
990 failed := 0
991 kinds := map[string]int{}
992 firstSeed := map[string]int64{}
993 ops := 0
994 stats := map[string]int{}
995 for seed := int64(1); seed <= int64(n); seed++ {
996 w := runSeed(seed, false)
997 ops += w.opsDone
998 for k, v := range w.stats {
999 stats[k] += v
1000 }
1001 if len(w.violations) > 0 {
1002 failed++
1003 k := kind(w.violations[0])
1004 kinds[k]++
1005 if _, ok := firstSeed[k]; !ok {
1006 firstSeed[k] = seed
1007 t.Logf("seed %d: %s", seed, w.violations[0])
1008 }
1009 }
1010 }
1011 t.Logf("%d seeds, %d failed, %d completed operations; mutated=%q boxes=%d", n, failed, ops, mutated, boxesPerRegion)
1012 t.Logf("outcomes: %v", stats)
1013 if js, err := json.Marshal(stats); err == nil {
1014 t.Logf("COUNTERS %s", js) // structured, for the spec's generator (R10-9)
1015 }
1016 for k, c := range kinds {
1017 t.Logf(" %4d × %s (first seed %d)", c, k, firstSeed[k])
1018 }
1019 if failed > 0 {
1020 t.Fatalf("%d of %d seeds violated an invariant", failed, n)
1021 }
1022}
1023
1024func kind(v string) string {
1025 if i := strings.Index(v, ": "); i >= 0 {
1026 v = v[i+2:]
1027 }
1028 // strip ids and numbers so a family groups together
1029 var b strings.Builder
1030 skip := false
1031 for _, r := range v {
1032 if r >= '0' && r <= '9' {
1033 if !skip {
1034 b.WriteByte('#')
1035 }
1036 skip = true
1037 continue
1038 }
1039 skip = false
1040 b.WriteRune(r)
1041 }
1042 v = b.String()
1043 if i := strings.Index(v, " holder="); i >= 0 {
1044 v = v[:i]
1045 }
1046 if len(v) > 110 {
1047 v = v[:110]
1048 }
1049 return v
1050}
1051