carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Package hostlife is a deterministic, randomized model of the host
2// lifecycle claim (docs/superpowers/specs/2026-10-02-host-lifecycle-claim-design.md,
3// rev 6.5, d36adcfa) and Phase 1 of move-any-app
4// (docs/superpowers/specs/2026-10-02-move-any-app-design.md, rev 7.7).
5//
6// Since joint round 2 the model encodes what the specs say and nothing
7// stronger: restore reads the holder's location and the generation the
8// claim names, the start check compares markers, every multi-object step
9// is split at its real write boundaries, and the final oracle only reads.
10//
11// It is test-only. Nothing here is product code: every file is a _test.go
12// file, so `go build ./...` never sees it. It exists to find interleavings
13// that prose review rounds kept missing, and to prove each protocol rule is
14// load-bearing by mutation (see rules_test.go and README in the spec §14.2).
15//
16// The model is an abstraction. What it keeps, and what it drops, is listed
17// in the spec's §14.2; the short version:
18//
19// - one host, three regions, one box per region (plus replacement boxes);
20// - every bucket is versioned, with the real IAM split: a box has full
21// rights on its own data bucket, and only Put/Delete/CopyInto on another
22// region's moves/* (no Get, no ListVersions, no DeleteVersion);
23// - every S3 request may complete late, after the client gave up;
24// - boxes crash (agent restart: flat children die, memory lost), reboot,
25// freeze (whole-VM pause), pause (agent only), partition from the
26// control bucket, are destroyed, or boot-recover an old executable;
27// - the application writes and registers timers on its own, through
28// descendants that may outlive the main process.
29package hostlife
30
31import (
32 "fmt"
33 "sort"
34 "strings"
35)
36
37const (
38 nRegions = 3
39 app0 = "A"
40 app1 = "B"
41
42 // Time is in model steps.
43 // H, the late-completion horizon (claim §6.2a): a request may complete
44 // up to H steps after issue. It is the signature-age limit plus the
45 // request bound, request_limit_ns (claim rev 6.15 §2); the park scope
46 // persists the request bound and the horizon is computed from it (R14-3)
47 horizonH = sigAgeH + requestLimitH
48 sigAgeH = 45 // the signature-age allowance (the 15 minutes)
49 requestLimitH = 15 // request_limit_ns: the longest request, added to the signature age
50 execTTL = 40 // exec lease TTL
51 execRenew = 15 // renew when this close to expiry
52 lostMargin = 10 // declare-lost margin past expiry
53 cutBudget = 320 // absolute cut deadline after admission
54 stagingTTL = 400 // staging-work lease TTL: longer than the cut budget, as in production
55 probeTries = 3
56 // probeStartWindow is a probe attempt's readiness window in steps (the
57 // budget's 30 s, §7.3): the retry policy's app restarts within one
58 // admitted session share it, and when it passes the attempt fails and
59 // the operation counts one of its budgeted tries (rev 7.12 §7.7)
60 probeStartWindow = 30
61 readyWait = 800 // abort a move whose readiness never comes
62 maxSkew = 1 // a box clock is within this of true time
63 maxPause = 3 // a typical pause between a check and the act (a VM freeze is longer)
64 sigAge = 30 // S3's signature-age limit; H covers it plus the longest request
65 beyondH = 20 // one long-tail request in this many completes after H (R2-1)
66 retiredCap = 32 // retired_incarnations keeps this many (claim rev 6 §2)
67)
68
69// retire adds inc to retired_incarnations, dropping the oldest past the cap.
70func retire(xs []int, inc int) []int {
71 xs = addUniq(xs, inc)
72 if len(xs) > retiredCap {
73 xs = append([]int(nil), xs[len(xs)-retiredCap:]...)
74 }
75 return xs
76}
77
78// ---------- storage ----------
79
80type blob struct {
81 Kind string // "ltx", "base", "files", "snap", "lock", "manifest", "lease", "report", "gen"
82 Inc int // incarnation tag; 0 = untagged legacy
83 Gen string // lineage that wrote it
84 Seq int
85 SealKey int // the instance.key the data is sealed with
86 // an LTX file (ltx_test.go, R4-1, R5-2): its level (0 and 1, 9 for a
87 // snapshot), its range in replication numbering, the final image of
88 // every key it touches (0 is a delete), and, for the oracle only, the
89 // writes it covers. No database checksum: the pinned writers put none
90 // in a file (litestreamfacts, R5-2)
91 Level int
92 Min int
93 Max int
94 Pages map[string]int
95 TXs []int
96 // Full: the file carries every page (a snapshot, an anchor, litestream's
97 // first sync after a cold initialization, a compaction of one of those),
98 // so applying it replaces the state; a key absent from it is gone
99 Full bool
100 // a "gen" marker (R3-1, R4-1): the generation's snapshot by key, version
101 // id and TXID
102 Base int
103 SnapKey string
104 TX int
105 // a "closed" certificate (claim rev 6.8 §6.4a): the writer's stamp and
106 // session; Seq is its box and TX its max_txid
107 Stamp int
108 Session int
109 Files *filesDir
110 Timers []int
111 Refs []ref // a park manifest's archive set
112 Dels []ref // a park manifest's deletion set, delete markers included
113 Text string
114}
115
116type ref struct {
117 Region int // -1 = control bucket
118 Key string
119 Ver int
120}
121
122type version struct {
123 ID int
124 B *blob
125 Marker bool
126 Straggler bool // created by a request that completed after H
127 SweepOwed bool // a dead generation's files key its park kept while live: the at-rest sweep's once obsolete (claim §6.2a)
128}
129
130type bucket struct {
131 objs map[string][]*version
132}
133
134func newBucket() *bucket { return &bucket{objs: map[string][]*version{}} }
135
136func (b *bucket) keys(prefix string) []string {
137 var out []string
138 for k := range b.objs {
139 if strings.HasPrefix(k, prefix) {
140 out = append(out, k)
141 }
142 }
143 sort.Strings(out)
144 return out
145}
146
147func (b *bucket) current(key string) *version {
148 vs := b.objs[key]
149 if len(vs) == 0 {
150 return nil
151 }
152 v := vs[len(vs)-1]
153 if v.Marker {
154 return nil
155 }
156 return v
157}
158
159func (b *bucket) byID(key string, id int) *version {
160 for _, v := range b.objs[key] {
161 if v.ID == id {
162 return v
163 }
164 }
165 return nil
166}
167
168// dataVersions are every non-marker version under prefix.
169func (b *bucket) dataVersions(prefix string) []ref {
170 var out []ref
171 for _, k := range b.keys(prefix) {
172 for _, v := range b.objs[k] {
173 if !v.Marker {
174 out = append(out, ref{Key: k, Ver: v.ID})
175 }
176 }
177 }
178 return out
179}
180
181func (b *bucket) hasData(prefix string, skipLock bool) bool {
182 for _, k := range b.keys(prefix) {
183 if skipLock && strings.HasSuffix(k, "lock.json") {
184 continue
185 }
186 for _, v := range b.objs[k] {
187 if !v.Marker {
188 return true
189 }
190 }
191 }
192 return false
193}
194
195// ---------- locations and generations (claim C3a, moves rev 6 §5.1) ----------
196
197// locationFor is holder.location for an incarnation.
198func locationFor(inc int, legacy bool) string {
199 if legacy || !on("location-per-incarnation") {
200 return "instances/" + host + "/"
201 }
202 return fmt.Sprintf("instances/%s/%d/", host, inc)
203}
204
205// dataKey is a replica object of generation gen within location loc. The
206// origin generation keeps the bare layout; every later generation has its
207// own operation-scoped prefix, so a request from one generation that lands
208// late can only ever add an object to that generation.
209func dataKey(loc, gen, kind string) string {
210 if gen == "" {
211 return loc + kind
212 }
213 return loc + "g-" + gen + "/" + kind
214}
215
216func genMarkerKey(loc string) string { return loc + "generation.json" }
217
218func isDataKey(k string) bool {
219 return strings.HasPrefix(k, "instances/") && ltxKeyRe.MatchString(k)
220}
221
222// ---------- requests (late completion) ----------
223
224type reqKind int
225
226const (
227 rPut reqKind = iota
228 rDelVer
229 rDelKey // bare-key delete: forbidden on host prefixes (mutation only)
230 rCopy // copy srcRegion/srcKey?versionId=srcVer -> region/key
231)
232
233type request struct {
234 kind reqKind
235 region int
236 key string
237 b *blob
238 ver int
239 srcRegion int
240 srcKey string
241 srcVer int
242 doneAt int
243 beyond bool // completes after H
244 ifSet bool // a conditional PUT: If-Match ifVer, or If-None-Match when ifVer is 0
245 ifVer int
246 hold bool // the biased schedule holds it until its worst moment (rev 7.5: a generation marker)
247 by string
248 done bool // completed (or refused) on the server; the client may have given up long before
249 at int // when the client issued it
250 // result, for an immediate completion
251 resultVer int
252}
253
254// ---------- local box state ----------
255
256type filesDir struct {
257 Inc int
258 Key int // the app's instance.key identity
259 Gen string // .files-gen marker
260 Env int // admin token identity in <host>.env
261 Done bool // markers written last
262 Stamp int // the installation stamp that produced it (claim rev 6.5 §6.3)
263 // Obj is the files object that holds this content (region HA §5.6,
264 // rev 4): written by the generation that first held it, in ObjRegion's
265 // bucket; an unchanged file keeps naming it generations later
266 Obj string
267 ObjRegion int
268}
269
270func (f *filesDir) clone() *filesDir {
271 if f == nil {
272 return nil
273 }
274 c := *f
275 return &c
276}
277
278type localDB struct {
279 Inc int
280 Loc string // the holder location it was restored from (claim C3a)
281 Gen string // <dbPath>.gen
282 Key int // the key it was sealed with
283 State map[string]int
284 TX int // the lineage's transaction id: the oracle's order of acknowledged writes
285 Pos int // the replication position: litestream's TXID, the count of files shipped, which every LTX range is numbered in (R5-1)
286 Pending []int // writes since Pos (what the next LTX file ships)
287 // Epoch is the agent process that was admitted for this database: a
288 // session belongs to the agent's replicator, and a new agent process
289 // is a new session (claim rev 6.10), so a restarted agent neither
290 // replicates nor certifies for it (seed 8528 / 30,000: the restarted
291 // agent's first sync was skipped at the dead session's remote position)
292 Epoch int
293 // the local level-0 files not yet confirmed uploaded, oldest first:
294 // litestream writes each sync's file locally and the replica uploads
295 // them in TXID order, never past one that has not completed (rev 7.6)
296 Queue []*blob
297 InFlight *request // the head's upload, as the client sees it
298 InFlightAt int
299 // FirstFull: the next level-0 file carries every page (litestream's
300 // first sync after a cold initialization, fact R5-1c)
301 FirstFull bool
302 // Closing: the writer stopped cleanly and is draining its uploads
303 // before writing its certificate (claim rev 6.8 §6.4a); CloseReq is the
304 // marker PUT once issued
305 Closing bool
306 CloseReq *request
307 // Session is the writer session this database was admitted under; Issued
308 // is every request the writer issued that did not return at once (level
309 // 0, compaction, snapshot, retention), and Uncertain is sticky once any
310 // of them timed out and was retried (R8-5): no certificate then.
311 Session int
312 Issued []*request
313 Uncertain bool
314 // RemotePos is the remote position the pinned replica caches until an
315 // initialization or an error: the newest remote level-0 TXID it has
316 // seen or uploaded (R8-5)
317 RemotePos int
318 Log []int // every write applied here, for the oracle
319 Stamp int // the installation stamp that produced this artifact (claim rev 6.5 §6.3)
320 Seq int
321}
322
323func (d *localDB) clone() *localDB {
324 if d == nil {
325 return nil
326 }
327 c := *d
328 c.State = cloneState(d.State)
329 c.Pending = append([]int(nil), d.Pending...)
330 c.Queue = append([]*blob(nil), d.Queue...)
331 c.Log = append([]int(nil), d.Log...)
332 return &c
333}
334
335type proc struct {
336 Main bool
337 Strict bool // spawned into the host cgroup by strict placement
338 Inc int
339 Gen string
340 Session int // the writer session the process was started under: what its acknowledgements carry (rev 7.13, R12-1)
341 Box int
342}
343
344type row struct {
345 Inc int
346 App string
347 Status string // ready, moving, landing, restoring, releasing
348 HoldOp string
349 Repl string // carried column
350 Next int // carried column
351}
352
353type box struct {
354 ID int
355 Region int
356 Destroyed bool
357 Frozen bool // whole-VM pause: nothing on the box runs
358 AgentPaused bool // the agent alone is paused
359 AgentDown bool // the agent crashed and has not restarted
360 Partitioned bool // cannot reach the control bucket
361 Old bool // running a pre-floor executable
362 HasKey bool
363 Skew int
364 FailApp bool // the app fails its readiness check on this box
365 FailArchive bool // the final archive fails (too large) on this box
366 FailSettle bool // the app passes one readiness check, works, then fails
367 Nested bool // nested layout: children survive an agent crash
368 // region HA (two boxes per region, L18)
369 Terminated bool // the cloud terminated the instance (H1): it never runs again and never returns; a Destroyed box that is not Terminated is stopped and may start again with its disk
370 TerminatedAt int
371 // a relocation controller's durable record (hostexec, 1b-next Task 7):
372 // the operation it belongs to, whether its scoped cleanup is still owed
373 // (ground truth for INV-29), and the fault that makes the cleanup fail
374 execRecord string
375 cleanupPending bool
376 cleanupFails bool
377 relRecord *releaseRecord // (rev 9) ReleasePositions: the flushed positions of one release, recorded on disk against its stop; an agent restart keeps it
378 fencePending bool // (rev 8) this worker step found a relocation participant's fence it could not consume
379 FenceObservedAt int // the box-fences record's observed_at: when a worker first saw it terminated; 0 no record (rev 4)
380 Enrolled bool // the roster holds an active entry with its proven cloud identity (H11); a replacement box is enrolled automatically enrollDelay steps after it launches (H16, H17)
381 EnrollAt int
382 DeleteOnTermination bool // every volume the box had attached carried the flag while it ran (§5.9): decides only the automatic discharge of its park-local chores (H19), never a fence
383 installEpoch int // the agent process that made this box's local install for a relocation (B2-5): a later process never remakes it once an admission named P
384 // receipt is the box's install journal for a relocation (1b-next Task 8c,
385 // VerifyReceipt): durable, it survives an agent restart; a park of the
386 // installation removes it, and so does a lost volume (a witness)
387 receipt *receiptEntry
388 // the files publisher (§5.6, Task 4): the generation it publishes in,
389 // its sequence there, the state its last manifest carried (in memory),
390 // and the release's final manifest while its PUT is outstanding
391 pubGen string
392 pubSeq int
393 published string
394 releaseSeq int
395 // attemptTag is the probe attempt this agent process has counted and
396 // not yet ended (memory: a restart forgets it, and the next attempt is
397 // counted again, ruling 19)
398 attemptTag string
399 // L18 joint round findings 2 and 3 (regionha_completion_test.go):
400 // actFence is the activator's fence on the host (its owner; "" none),
401 // in the agent's memory, so a restart empties it; probeFence is the
402 // persisted owner a probe stop installs under (probe-fence.json),
403 // kept until the completion is marked; relFence is root's admission
404 // fence of a release (the op it closed admission for), written with
405 // Collect after the flush and the positions (ruling 28), never a row
406 // hold; owedDischarge
407 // is ground truth, the completed relocation whose participation this
408 // box has not yet discharged (C10). foreignFence is a witness's
409 // stand-in for another operation's fence that its owner never lifted
410 // (INV-32's ground truth). dischargeFails, markFails and crashAfter
411 // are witness knobs on the completion pass
412 actFence string
413 probeFence string
414 relFence string
415 // launchClosedOp and launchClosedSession: the session whose launch
416 // journal a release's StopWriters closed (durable; L18 joint round 2
417 // finding 3): it is never relaunched, an admitted start included
418 launchClosedOp string
419 launchClosedSession int
420 // probeRecord is the probe stop's recorded positions (ruling 87),
421 // durable: a restart after it stops the session without a new flush
422 probeRecord *probeRecordEntry
423 owedDischarge string
424 foreignFence string
425 dischargeFails bool
426 markFails bool
427 crashAfter string
428
429 Row *row
430 DB *localDB
431 Files *filesDir
432 Config int // incarnation the config.env was rendered for; 0 none
433 CfgVer int // the bundle version it was rendered from
434 Live string // app the live symlink points at
435 LiveInc int
436 Timers []int // active once-timers (registration ids)
437 History []int // delivered history (disposable)
438 Attempt int // runner bookkeeping
439 Procs []*proc
440 // Stamps is the local record per host incarnation (claim rev 6.5 §6.4,
441 // R4-2): {active_stamp | none, retired_stamps}. A stamp is the claim
442 // gen captured at the CAS that made an installation current here. A
443 // writer is admitted only if its stamp equals the active one.
444 Stamps map[int]*stamps
445 Parked int // count of locally parked artifacts
446 // open is an admitted session's start in progress (rev 7.11 §7.14):
447 // the claim CAS is done; the purge, the restore, the app start and
448 // readiness, the first upload, the marker and the anchor follow, each
449 // retried by the same agent process; the session is the agent's, and a
450 // new agent process is a new session (claim rev 6.10)
451 open *openState
452 // lastProbe is the last probe attempt's timing on this box (INV-19);
453 // envUnreadyFor is a witness knob holding the launch environment
454 // unready for that many steps after an attempt's app stage first runs
455 lastProbe probeTiming
456 envUnreadyFor int
457 // the retry policy for failed app starts within a session (rev 7.11
458 // §7.14): consecutive failures, the earliest next attempt, and the
459 // host marked failed once the limit is reached (no new sessions until
460 // an operator clears it)
461 startFails int
462 nextStart int
463 hostFailed bool
464 failCounted bool // the operation's probe counted this host failure as one failed try
465 failedAt int // when the host was marked failed; the operator looks after a while
466 // PurgeFails is the fault: the purge of the local database an admitted
467 // writer must make before it starts returns an error (claim rev 6.9)
468 PurgeFails bool
469 // anchors are the PUTs this box's activator must see acknowledged before
470 // it goes on (an anchor, a recovery's snapshot, a closed.json), by key
471 // (rev 7.7, R6-1): a request the client has waited uploadTimeout steps
472 // for is retried with the same bytes.
473 anchors map[string][]*request
474
475 ExecUntil int // local exec-lease file (watchdog input); 0 none
476 // in-memory, lost on crash
477 snap *claim
478 stale *claim // a stalled worker's old read
479 deferred []*localWrite
480 Epoch int
481 plans []*plan
482 stopped map[string]bool
483 StopEvidence map[string]bool // published stop evidence, by operation (survives a restart)
484 mem map[string]bool
485 evidAt map[string]int
486 pendingTimers []int
487}
488
489// plan is a host-specific write by a separate OS process
490// (carlos-config-sync, carlos-adopt), decided now and acted on later: the
491// shape of every stale-writer race (J-N8, J2-R2-3). It survives an agent
492// crash, because it is not the agent; a reboot ends it. It may pause
493// before it takes the host guard.
494type plan struct {
495 Kind string // "config", "adopt"
496 Inc int
497 App string
498 Ver int // the config bundle version it selected
499 At int // it takes the guard no earlier than this
500 // Stamp is the installation stamp the pass read when it selected the
501 // row (claim rev 6.5 §6.4, R2-3): the host guard admits the write only
502 // if it is still the active stamp.
503 Stamp int
504}
505
506// stamps is a box's local record for one incarnation of the host (claim
507// rev 6.3 §6.4): the active installation's stamp (0 is none) and the
508// stamps of the installations retired here.
509type stamps struct {
510 Active int
511 Retired []int
512}
513
514func (b *box) stampOf(inc int) int {
515 if s := b.Stamps[inc]; s != nil {
516 return s.Active
517 }
518 return 0
519}
520
521// stampRetired: st is in the record's retired_stamps for inc.
522func (b *box) stampRetired(inc, st int) bool {
523 s := b.Stamps[inc]
524 return s != nil && contains(s.Retired, st)
525}
526
527// install is activation (claim rev 6.5 §6.4): the box's record for inc
528// takes holder.stamp as the installing CAS wrote it (a create's serving
529// CAS, a landing's flip, an adoption, a reinstall, a dirty rollback's
530// probe CAS, a push's commit), or as initRecord read it fresh under the
531// guard. Activation is one of the two moves of a stamp: the old active
532// stamp, if any, goes into retired_stamps first, then the new one is set.
533// Under the guard.
534func (b *box) install(inc, gen int) {
535 if b.Stamps == nil {
536 b.Stamps = map[int]*stamps{}
537 }
538 if b.Stamps[inc] == nil {
539 b.Stamps[inc] = &stamps{}
540 }
541 s := b.Stamps[inc]
542 if s.Active != 0 && s.Active != gen && !contains(s.Retired, s.Active) {
543 s.Retired = append(s.Retired, s.Active)
544 }
545 s.Active = gen
546}
547
548// retireArtifacts is claim rev 6.5's retire-artifacts(S): the local
549// artifacts tagged S (the database, the files directory) are parked, and
550// the record is untouched: neither active_stamp nor retired_stamps moves.
551// Only activation and retire-installation move stamps. A landing uses it
552// on its predecessor's leftovers; a dirty rollback parks its pre-move
553// database with S still active, and S retires when the probe CAS's new
554// stamp activates. It parks by tag, as the real inventory does (R6-4): an
555// artifact of incarnation inc that carries another stamp stays, and the
556// callers assert what they expect the tags to be (INV-6).
557func (w *world) retireArtifacts(b *box, inc, stamp int) {
558 w.retireDatabase(b, inc, stamp)
559 if b.Files != nil && b.Files.Inc == inc && b.Files.Stamp == stamp {
560 b.Files = nil
561 b.Parked++
562 }
563}
564
565// retireDatabase is retire-artifacts(S) for the database alone: the files
566// directory is carried into the new generation (a push, F7 on §5.1; a
567// landing's restore phase after its files were installed; the activator's
568// park of a warm database from an earlier stay before a cold restore).
569func (w *world) retireDatabase(b *box, inc, stamp int) {
570 if b.DB != nil && b.DB.Inc == inc && b.DB.Stamp == stamp {
571 b.DB = nil
572 b.Parked++
573 }
574}
575
576// retireStamp is claim rev 6.5's retire-installation: the active stamp
577// moves into retired_stamps and becomes none; the caller parks every
578// artifact. Used at a delete, a move away, and the quarantine.
579func (b *box) retireStamp(inc int) {
580 if b.Stamps == nil {
581 b.Stamps = map[int]*stamps{}
582 }
583 if b.Stamps[inc] == nil {
584 b.Stamps[inc] = &stamps{}
585 }
586 if a := b.Stamps[inc].Active; a != 0 {
587 b.Stamps[inc].Retired = append(b.Stamps[inc].Retired, a)
588 }
589 b.Stamps[inc].Active = 0
590}
591
592// admitsStamp is the local record's admission (claim rev 6.5 §6.4): the
593// writer's stamp must equal the active stamp; none active refuses all,
594// and so does no record at all (R5-4: a writer is never admitted before
595// initialization). The mutation of fence-stamp admits any writer for the
596// incarnation while an installation is active (rev 7.4's behaviour); the
597// mutation of fence-by-installation is rev 5.2's marker: once retired,
598// never active again.
599func (b *box) admitsStamp(inc, stamp int) bool {
600 s := b.Stamps[inc]
601 if s == nil {
602 return false
603 }
604 if !on("fence-by-installation") && len(s.Retired) > 0 {
605 return false
606 }
607 if s.Active == 0 {
608 return false
609 }
610 if !on("fence-stamp") {
611 return true
612 }
613 return stamp == s.Active
614}
615
616func (b *box) forget() {
617 b.Epoch++ // every goroutine of the old agent is gone
618 // a new agent process is a new session (claim rev 6.10): an admitted
619 // start in progress is gone with its restored database and its
620 // outstanding requests (R10-2), and the start retry count with it
621 b.open = nil
622 b.startFails, b.nextStart = 0, 0
623 b.deferred = nil
624 b.snap, b.stopped, b.stale = nil, nil, nil
625 b.mem = map[string]bool{}
626 b.evidAt = map[string]int{}
627 b.pendingTimers = nil
628 b.attemptTag = ""
629 b.actFence = "" // the activator's fences die with the agent; probe-fence.json survives (ruling 75)
630 if b.DB != nil {
631 b.DB.InFlight = nil // the upload's client is gone; the restarted replica uploads the same file again
632 // a writer that was draining before its clean close is gone with
633 // its uploads unaccounted for: no closed.json (claim rev 6.6 §6.4a)
634 b.DB.Closing, b.DB.CloseReq = false, nil
635 }
636 b.anchors = nil // the activator issues the anchor again, the same bytes
637}
638
639// ackedPut is a PUT the activator must see acknowledged before it goes on
640// (R6-1): it issues the request once, retries the same bytes after
641// uploadTimeout steps while none has returned, and reports whether one has.
642func (w *world) ackedPut(b *box, key string, bl *blob, ifNone bool) bool {
643 acked, inflight := false, false
644 for _, r := range b.anchors[key] {
645 if r.done {
646 acked = true
647 } else if w.now-r.at < uploadTimeout {
648 inflight = true
649 }
650 }
651 if acked || inflight {
652 return acked
653 }
654 r := &request{kind: rPut, region: b.Region, key: key, b: bl, ifSet: ifNone}
655 if v := w.issue(b.ID, r); v > 0 {
656 return true
657 }
658 if r.done {
659 return true // refused (412): the object is already there, written once
660 }
661 if b.anchors == nil {
662 b.anchors = map[string][]*request{}
663 }
664 b.anchors[key] = append(b.anchors[key], r)
665 return false
666}
667
668// ---------- control plane ----------
669
670type holder struct {
671 Inc int
672 App string
673 Serial int // ground-truth creation event (model only)
674 Legacy bool
675 // Location is holder.location (claim C3a): instances/<host>/<inc>/ for a
676 // new incarnation, instances/<host>/ for an adopted legacy host. It never
677 // changes while this holder holds the host.
678 Location string
679 // Gen is the generation within the location that the claim names as
680 // current (moves rev 6 §5.1): "" is origin; a move, a push and a dirty
681 // rollback each install a generation named by their op id.
682 Gen string
683 // WriterBox is holder.writer_box (claim rev 6.9): the box every
684 // admission names as the current writer; it survives operation
685 // replacement, so a later cold admission's writes_lost names it (R9-2).
686 WriterBox int
687 // Admissions is holder.admissions (claim rev 6.12): the record of writer
688 // admissions whose chain is not yet finished or whose predecessors are
689 // not yet queued for retirement, plus the last few covered ones; it
690 // lives in the holder, so it survives every operation transition,
691 // release and new operation. An admission's predecessor is always the
692 // last entry here (session, box, generation), never the generation an
693 // operation chose for a restore (R11-4)
694 Admissions []admissionRecord
695 // Session is holder.session (claim rev 6.8 §6.4a, R8-1): the gen of the
696 // AdmitWriter CAS that admitted the current writer session. Every path
697 // that opens replication goes through that CAS, which moves the
698 // session forward, so a clean-close certificate (written per session)
699 // is one-use by construction.
700 Session int
701 // Stamp is holder.stamp (claim rev 6.5 §2, R5-4): the gen produced by
702 // the CAS that made the current installation current, written by that
703 // same CAS: a create's serving CAS, adoption, a flip, a reinstall, a
704 // dirty rollback's install, a push's commit. The claim is where a box
705 // recovers its local record from.
706 Stamp int
707}
708
709type obligation struct {
710 ID string
711 Op string
712 Kind string // park-replica, park-local, park-superseded, park-staging
713 Region int
714 Box int
715 Prefix string
716 Ret string
717 Quiet int // not before
718 Scope *parkScope // the persisted park scope of a remote chore (claim rev 6.15 §2), bound before its first step runs
719 Owner string // a regional chore's owner: "" is region:<r> (any worker of the region, P6-D5); box:<b> is the legacy form a fence passes to the region (1b-next rev 6 finding 2)
720}
721
722// parkScope is claim rev 6.23's park scope {location, incarnation,
723// evidence_at, request_limit_ns} (§2), persisted in the chore when it is
724// queued or bound before its first step, so every retry, an agent
725// restart's included, uses the same location, archive incarnation and
726// original evidence time: a retry's own clock is never evidence (R13-4,
727// R15-1). Location is exactly one of instances/<host>,
728// instances/<host>/<incarnation>, moves/<host>/<op id> (a move's staging
729// or a rollback's return lane) or moves/<host> (the staging sweep), with
730// no trailing "/". EvidenceAt is always the original evidence or fencing
731// bound: the stop evidence a whole-location park cites, the seal bound of
732// a staging lane (the time after which no staging writer, its lease
733// holder included, can still write), the sweeping box's listing, the
734// publication that supersedes a generation; never cleanup's start time.
735// Every scope has the one deadline: acknowledge only after a listing
736// later than EvidenceAt + H, H = sigAgeH + RequestLimit.
737type parkScope struct {
738 Location string
739 Inc int
740 EvidenceAt int
741 RequestLimit int // request_limit_ns: the longest request, which H adds to the signature age
742}
743
744// deadline is the scope's acknowledgement bound: a listing later than
745// evidence_at + H (claim rev 6.23 §2, §6.2a).
746func (s *parkScope) deadline() int { return s.EvidenceAt + sigAgeH + s.RequestLimit }
747
748// admissionRecord is one AdmitWriter (claim rev 6.8 §6.4a): the session it
749// produced, the box and generation it admitted, the stamp, the mode (warm
750// or cold), whether the writer has opened, and the predecessor it observed.
751type admissionRecord struct {
752 Session, Box int
753 Gen string
754 Stamp int
755 Mode string // warm or cold (claim rev 6.9)
756 Predecessor pred
757 Lost *writesLost // a cold admission whose observed session had no certificate (claim rev 6.9, R9-2)
758 // Coverage is the admission's coverage of its predecessor's writes
759 // (claim rev 6.12): "" (uncovered: writes_lost unless certified),
760 // "sealed" (a from_db seal), "clean-return" (a clean rollback with
761 // src_covered). The entry always names it
762 Coverage string
763 Covered bool // this cold admission's predecessor has had its park-generation chore queued by a PublishGeneration (claim rev 6.12)
764 // Region is where this writer's generation keeps its objects;
765 // PredRegion where the predecessor's are, -1 when it holds none
766 // anywhere (claim rev 6.14 §6.4a item 1: region, predecessor_region)
767 Region int
768 PredRegion int
769 // AdditionalWritesLost (claim rev 6.22 item 2, additional_writes_lost):
770 // behind a declared-lost target, every other session the target was
771 // admitted into since the cut, certified or not (no return lane carries
772 // any of their writes, seed 33373 / 100,000), and the source's cut
773 // session when src_covered is false (the seal in the lost region was
774 // the one holder of its writes beyond the replica, seeds 16395 and
775 // 27191 / 30,000; rev 7.11's LostSrc). With Lost, unique by (box,
776 // session) (R14-2)
777 AdditionalWritesLost []*writesLost
778}
779
780// pred is the predecessor an admission journals: the session, writer box,
781// generation and stamp it observed.
782type pred struct {
783 Session, Box int
784 Gen string
785 Stamp int
786}
787
788// openState is an admitted writer between its AdmitWriter CAS and its
789// process start (rev 7.10 §7.14): the generation and session it was
790// admitted to, the database it restored, and the stage whose PUT it waits
791// for: "first" (a fresh generation's full first upload), "marker"
792// (generation.json), "anchor" (a warm restore above the level-0 maximum).
793type openState struct {
794 Gen, Want string
795 Session int
796 Mode string
797 Prepared bool
798 Probe bool
799 Source string // the generation the restore reads
800 SourcePin int // how: -1 the marker's generation (restoreCalc); 0 a generation the run walk selected, rooted at [1, 1] (R14-1)
801 RemoteMax int // the certificate's max_txid a warm restore must reach
802 DB *localDB
803 stage string // restore, app, first, marker, anchor, open
804 req *request
805 blob *blob // the first upload's or the anchor's bytes, captured once: a retry carries the same bytes (R10-7; seed 7574 / 30,000)
806 pendAt int // the pending writes the captured blob covers; later acknowledgements stay pending
807 startedAt int // when the app stage first ran: a probe attempt's readiness window runs from here (rev 7.12 §7.7)
808}
809
810// probeTiming is what INV-19 reads: when a probe attempt's app stage first
811// ran and when its app answered readiness (rev 7.13, R12-2)
812type probeTiming struct {
813 startedAt, readyAt int
814}
815
816// writesLost is claim rev 6.9's writes_lost record: the writer whose
817// acknowledged, unuploaded writes a cold admission leaves behind, named by
818// holder.writer_box, never inferred from op.exec (R9-2). The box alerts.
819type writesLost struct {
820 Box int
821 Gen string
822 Session int
823 RemoteMaxTXID int
824}
825
826type opState struct {
827 Kind string // serving, creating, deleting, parking, purging, moving, landing, reverting, restoring, pushing
828 ID string
829 Phase string
830 Exec int // -1 none
831 Parts []int
832 // ParkSweep is intent.park_sweep (claim rev 6.15 §6.2a): a free host's
833 // at-rest sweep runs as a holderless parking op, {kind: parking,
834 // intent: {park_sweep: "v1"}}, with no exec and no holder, carrying
835 // only scoped remote chores; its last acknowledgement frees the claim
836 // again in the same CAS (FreeAfterPark)
837 ParkSweep string
838
839 // FenceBox is the journal's fence.<box> reference (claim rev 6.28,
840 // moves rev 7.20 §7.10): the terminated source whose termination fence
841 // converted a pre-flip move to relocating. Alerted: the operator was
842 // told once that the relocation waits for a box in the region.
843 FenceBox int
844 Alerted bool
845
846 // intent
847 Src, Tgt int
848 SrcRegion, TgtRegion int
849 NewInc int
850 NewApp string
851 NewRegion int
852 NewSerial int
853 NewBox int // the executor a create named once (region HA §5.2, H12): never substituted
854 // region HA relocation (spec 1 §5.8; claim rev 6.30 §4 L1). The
855 // journal entries below are what the claim's op.journal carries
856 RelocReason string // drain or failover
857 FenceAt int // fence.<box>.observed_at of the latest fence the operation cites; 0 none
858 FailedAt int // when the target entered probe-failed
859 Release *releaseEntry // the release sequence's journal entry (§5.8): the source's in releasing, a failed target's in probe-failed
860 Pin *pinEntry // the pinned recovery set
861 Prepared string // P, the prepared generation this installation builds; "" before installing
862 InstallStamp int // the stamp of the CAS that handed op.exec to the current target (op.journal.install_stamp)
863 Receipt *receiptEntry // the executor's local install receipt (local_install)
864 ProbingSince int
865 Replacements int // targets replaced in this operation; two, then stuck
866 Excluded []int // replaced targets, never named again for this host
867 InstalledOn []int // every box this operation handed op.exec for an installation: each owes a park-local at serving unless discharged
868 Discharged []int // fenced boxes whose park-local chores the fence CAS discharged (L3, H19)
869 Stuck string
870 // 1b-next rev 16's journal additions
871 ReleasingSince int // releasing_since: the release deadline's start (ruling 14)
872 Attempts int // probe_attempts.<op.exec>: counted before each attempt, at most probeTries (ruling 19)
873 PreparedOpened string // prepared_opened: journaled by the admission that opened P (rev 12 S1)
874 PinFailover bool // (rev 6) the initial failover's starting, 1b-next's StartRelocation with a fence: set by startFailover and by a drain's source lost in starting, cleared by the PinFailover CAS once the target is named and every participant has acknowledged
875 Resolved bool // an operator's resolve (model ruling 13): the source is eligible as a target again, and an empty listing is accepted
876 Observed []int // every fence.<box>.observed_at the operation consumed, kept through replacements (rev 4)
877 Acks []int // exec_ack.<box>: the participants that acknowledged, durable (rev 4)
878 Abandoned []string // abandoned_prepared: every unopened P the operation replaced or dropped, parked at serving (1b-next ruling 27)
879 // L18 joint round finding 2: the built completion protocol
880 // (regionha_completion_test.go). ProbePassedAt is the probe's
881 // journaled time (Probe.At, ruling 67's bound); ProbeStop the target's
882 // journaled stop of the passed probe's session (ruling 49); Completed
883 // the serving op's completed_relocation record (ruling 56), which the
884 // completion pass marks discharged last (ruling 80)
885 ProbePassedAt int
886 ProbeStop *probeStopEntry
887 Completed *completedEntry
888
889 // journal
890 ReportAt int
891 ReportOK bool
892 CutAt int
893 CutDeadline int
894 StopAt int
895 StopDigest digest
896 StopFiles *filesDir
897 SealDB ref
898 SealFiles ref
899 SealQuiet int
900 LandPhase string
901 Installed digest
902 ProbeTries int
903 ProbedAt int
904 RevPhase string
905 SrcPhase string
906 Dirty bool
907 DirtyKnown bool
908 ReturnDB ref
909 ReturnFiles ref
910 ReturnQuiet int
911 ReturnParked bool // the return lane's park-staging chore is owed (rev 7.10)
912 // SrcCovered (rev 7.11): at the seal, the source's own replica of its
913 // generation reached the stopped database's position, so a clean
914 // rollback may restore the source from it; otherwise the seal holds
915 // writes the replica does not, and the return carries the seal
916 SrcCovered bool
917 // CutSession (rev 7.12) is holder.session at the cut's stop: the
918 // source's session whose writes src_covered speaks for; a clean return's
919 // coverage applies to it and to the target's probe session only
920 CutSession int
921 // SealFromDB (rev 7.11): the seal was built from the stopped database
922 // itself, every page of it, so it covers every write the predecessor
923 // acknowledged; false when the disk was gone and the seal is the
924 // remote as it stood, which stops at the remote's position. The
925 // prepared generation's admission declares writes_lost in that case
926 // (seed 34260 / 100,000). ReturnFromDB is the same for a dirty
927 // return's seal of the target
928 SealFromDB bool
929 ReturnFromDB bool
930 LostBox int
931 LostAt int
932 RollbackID string
933 PushSnap *blob // a push's installed snapshot (the final checkpoint plus the push's mark)
934 BaseKey string
935 BaseTX int
936 RecVer int // a delete's or purge's intent: the record version it will DeleteIfMatch (claim §5.2 step 1, §5.3 step 4)
937 FlipGen int // the holder.stamp the flip CAS wrote: the landing's installation stamp (claim rev 6.5 §6.4)
938 SrcStamp int // the source installation's stamp before the flip, for the return to source (claim rev 6.5)
939 // SrcGen is the source's generation at the cut (rev 7.10): a clean
940 // rollback's return CAS names it again for the restore. Rev 7.11's
941 // op.journal.writers moved to holder.admissions (claim rev 6.12)
942 SrcGen string
943 Committed bool
944 Opened bool
945 ParkLocalNow bool
946 PinVer int // move.inputs: the bundle version pinned at admission
947 Auto bool
948 ReqAt int
949 Repl string
950 Next int
951 FlipAt int
952 TgtStopAt int
953 Reason string
954 App string
955 // FencedAbsent: a purge fence placed where no claim existed (claim rev
956 // 5.2 §5.3). Its release writes a free claim; the mutation of
957 // claim-never-deleted deletes the object instead
958 FencedAbsent bool
959 // BaseVer is the version id of the generation's installed snapshot,
960 // recorded by the install manifest and named by generation.json: the
961 // immutable snapshot identity the restore starts from (rev 7.4 §5.1,
962 // R3-1)
963 BaseVer int
964}
965
966// releaseEntry is §5.8's release journal entry: the box, generation and
967// session that gave the host up, its stopped database's final TXID (the
968// listing shows it before pinning may cite it), and, until Task 4's files
969// manifest, the files directory and wake state the box published. Flushed
970// is false when the session's replicator was gone (an agent restart) and
971// the entry names what the remote held: released coverage never applies
972// to it, and the next admission declares the rest.
973// releaseRecord is 1b-next Task 7's ReleasePositions record: the
974// release a box computed once its writers stopped and its database and
975// files flushed, bound to the stop (the operation, the phase and the
976// writer session), durable before the producers close (rev 9).
977type releaseRecord struct {
978 Op, Phase string
979 E releaseEntry
980 // Ctl is the flushing controller's identity: the agent process
981 // (box.Epoch) that flushed and recorded (rev 11). Receipt is root's
982 // durable stop receipt for this stop, persisted after the producers
983 // drained (step 4), tracked apart from the recorded positions: only
984 // the same controller may finish from the positions alone, and after a
985 // restart the release needs the receipt too (1b-next Task 7 Release,
986 // RecoveryReleaseDrain; Astra round 8)
987 Ctl int
988 Receipt bool
989}
990
991type releaseEntry struct {
992 Box, Session int
993 Gen string
994 FinalTX int
995 ManifestSeq int
996 Flushed bool
997 Files *filesDir
998 Timers []int
999 At int
1000}
1001
1002// pinEntry is §5.8's pinned recovery set: the database position N the
1003// build restores to (exact versions in the real pin; the model's build
1004// never restores past N), the files manifest version (Task 4), and when
1005// the pin was taken (after a release's final_txid is visible, or after a
1006// fence's observed_at plus failoverPinDelay).
1007type pinEntry struct {
1008 Gen string // the generation the pin reads (genselect over the admitted chain)
1009 TX int
1010 ManKey string
1011 ManVer, ManSeq int
1012 At int
1013 ClaimVer int // the claim version it was selected against (1b-next ruling 16: pin-<gen>.json)
1014 // Files and Timers are the files set the pin was taken with, until
1015 // Task 4's manifest: the release's, or a failover's stand-in
1016 Files *filesDir
1017 Timers []int
1018 ManGen string // the generation whose manifest the files pin names (Task 4)
1019}
1020
1021// receiptEntry is the local install receipt (§5.8 local_install): the
1022// box and the install_stamp of the CAS that handed it op.exec. A
1023// replacement executor never inherits its predecessor's.
1024// probeStopEntry is hostclaim.ProbeStop: the target's stop of the passed
1025// probe's session, with root's receipt (ruling 49).
1026type probeStopEntry struct {
1027 Box, Session int
1028}
1029
1030// completedEntry is hostclaim.CompletedRelocation on the serving op: the
1031// relocation, the box and stopped probe session it completed with, and
1032// Discharged, the mark (MarkCompletedDischarged).
1033type completedEntry struct {
1034 Op string
1035 Box, Session int
1036 Discharged bool
1037}
1038
1039type receiptEntry struct {
1040 Op string
1041 Box, Stamp int
1042}
1043
1044type digest struct {
1045 Writes string
1046 Files string
1047 Timers string
1048 Valid bool
1049}
1050
1051type claim struct {
1052 Ver int
1053 Holder *holder
1054 Op opState
1055 Regions []int
1056 Boxes []int
1057 Complete bool
1058 Obl []obligation
1059 Lost []int
1060 // Retired is retired_incarnations: anti-reuse only, capped at
1061 // retiredCap entries (claim rev 6 §2, §6.2a). The sweep does not read
1062 // it; it discovers locations from the namespace (R3-4).
1063 Retired []int
1064 Quiet [nRegions]int
1065 PurgeRes bool
1066 // Cancelled is cancelled_creates (claim rev 4 §5.1, J2-R2-7): creates
1067 // recovery cancelled, so a late record PUT can be removed. It survives free.
1068 Cancelled []cancelledCreate
1069 // readStamp is model bookkeeping on a box's read of the claim: the
1070 // installation stamp active on that box at the read, which a worker
1071 // carries with the claim it validated under (claim rev 6.5 §6.4)
1072 readStamp int
1073}
1074
1075type cancelledCreate struct {
1076 Inc int
1077 Until int
1078}
1079
1080// liteHost is a second hostname of the same apps, modelled for the
1081// control plane only: create, app admission and purge (J2-R2-2).
1082type liteHost struct {
1083 Inc int
1084 App string
1085 Box int // the box its create named (region HA §5.2); control-plane only
1086 Kind string // "", "creating", "serving", "purging", "fenced"
1087 OpID string
1088 Rec bool // its record is published
1089 At int
1090 // FencedAbsent: the fence found no claim (epoch 0). Its release writes
1091 // free; the mutation of claim-never-deleted deletes the object, so the
1092 // epoch returns to 0 and a creator's If-None-Match: * passes again
1093 FencedAbsent bool
1094}
1095
1096// admission is apps/<acct>/<app>/admission.json (claim rev 4 §5.1 step 0).
1097type admission struct {
1098 Closed bool
1099 Reg map[string]regEntry // create id -> registration
1100}
1101
1102type regEntry struct {
1103 Host string
1104 Until int // a registration is a lease: a create claims before it ends (CC16)
1105}
1106
1107type pendingRec struct {
1108 rec *record
1109 at int
1110}
1111
1112type record struct {
1113 Ver int
1114 Inc int
1115 App string
1116 Region int
1117 MoveID string
1118 // MoveState is the record's move journal state for the four transitions
1119 // the moves spec makes record-first (§7.5, §7.8, §7.9): "landing" after
1120 // the flip, "committed", "rolling_back". The claim follows each.
1121 MoveState string
1122 RollbackID string
1123 Reason string
1124 // LastMove names the move whose close cleared MoveID: the close is record
1125 // first (moves rev 7.23 §7.5, Paul's ruling M4), so a record can say a
1126 // move is over while the claim still holds its op, until the release.
1127 LastMove string
1128}
1129
1130// ---------- the world ----------
1131
1132type ack struct {
1133 W int
1134 Inc int
1135 Box int
1136 Session int // the writer session that acknowledged it (rev 7.13, R12-1): a writes_lost record exempts the writes of the session it names, no other
1137 Gen string // the generation that session wrote
1138 At int
1139 Kind string // "write" or "timer"
1140 Exempt bool // lost under a declared loss (M16) or past the last checkpoint of a lost disk
1141}
1142
1143// intner is what the model draws from: the seed's *rand.Rand in the random
1144// sweep, a quiet source in a witness scenario (witness_test.go), so the
1145// witnesses never touch the seeds' streams.
1146type intner interface{ Intn(n int) int }
1147
1148type world struct {
1149 perRegion int // boxes per region at the start (region HA L18): 1, today's model, or 2
1150 issued int // files manifests issued, in order: INV-28's ground truth (blob.Base)
1151 // pinWatch is INV-30's memory: per region that could still serve, the
1152 // manifest its next install would pin, as the oracle saw it last step
1153 pinWatch map[int]pinWatch
1154 // noPinWatch turns INV-30 off: TestFilesWitnessesStuckWithoutINV30's
1155 // liveness runs only
1156 noPinWatch bool
1157 // oneBoxRegion: a witness's region kept at one box by its group in the
1158 // two-box shape (H5's minimum is per region)
1159 oneBoxRegion [nRegions]bool
1160 // L18 joint round findings 2 and 4 (regionha_completion_test.go):
1161 // haOff is CARLOS_REGION_HA off with claim mode on, where only the
1162 // completion pass runs (ruling 82); claimC1 and claimC11 are claim
1163 // asks C1 (a failed target's release obligation) and C11 (root
1164 // launches a later probe attempt after P's opening). Both are open
1165 // today, so the model runs the built fallbacks; a witness or test of
1166 // the post-C1 or post-C11 behaviour sets them
1167 haOff, claimC1, claimC11 bool
1168 t testingT
1169 seed int64
1170 rng intner
1171 // hold, when set, is a witness's hand on S3: a request it answers true
1172 // for is accepted and completes only when the witness says so
1173 // (witness_test.go). The random sweep never sets it.
1174 hold func(r *request) bool
1175 // alerts raised to the operator (writes_lost, a marker disagreement, a
1176 // failed host, an uncertified close): counted for the oracle that every
1177 // writes_lost record was alerted (R10-6)
1178 alerts int
1179 lostAlert int
1180 markerAlerted map[string]bool
1181 // startHook runs right after the app starts, before replication opens:
1182 // a witness's background worker acknowledging work (R10-1)
1183 startHook func(b *box) bool // true: the hook crashed the box
1184 // openYield (rev 5, Astra round 2 on claim §11's open boundaries): a
1185 // witness's yield after generation.json names the new generation and
1186 // before replication opens; true: the start returns there and resumes
1187 // at "open" on the next call. Nil outside the witnesses
1188 openYield func(b *box) bool
1189 // lastIssued is the request issue saw last, for a caller that needs
1190 // the request object behind a helper's return value (resumeOpen)
1191 lastIssued *request
1192 // stampBox records, for every installation stamp the claim issued, the
1193 // box whose installation it names (claim rev 6.5: holder.stamp always
1194 // names the installation op.exec should run). The oracle refuses an
1195 // activation of another box's stamp (INV-6, R6-4).
1196 stampBox map[int]int
1197 now int
1198 ver int
1199 verbose bool
1200
1201 claim *claim // nil = absent
1202 rec *record
1203 stores map[string]bool
1204 cfgVer map[string]int // each app's current config bundle version
1205 purgeRes map[string]bool
1206 purgeStep map[string]int // the reservation's durable phase (purge journal)
1207 admit map[string]*admission
1208 h2 liteHost
1209 recPend *pendingRec // a record PUT whose response was lost
1210 lastRegion int // where the host served when it was last deleted
1211 legacyIncs map[int]bool // incarnations that were adopted legacy hosts
1212 purgeOp map[string]string // the purge id, kept in the reservation (the purge journal)
1213 h2Epoch int // the second host claim's ETag, for its fence and release; 0 is absent
1214 floor bool
1215 buckets [nRegions]*bucket
1216 control *bucket
1217 boxes []*box
1218 pending []*request
1219 acks []ack
1220 writeInc map[int]int // which incarnation acknowledged each write
1221 txs map[int]tx // every write as a transaction (ltx_test.go)
1222 durable map[string]map[int]bool // per generation prefix: writes a completed replica object holds
1223 genParent map[string]string // generation prefix -> the one it superseded
1224 nextW int
1225 nextInc int
1226 serial int
1227 nextID int
1228 delivered map[int]bool
1229 doorReads []*doorPlan
1230 execLease map[int]execLease
1231 staging [nRegions]stagingLease
1232 console []*consolePlan
1233 fail map[string]bool // which faults this seed exercises
1234 stats map[string]int
1235 stalled []*stalledSign
1236 ccache *claim // the console's short-lived claim cache
1237
1238 violations []string
1239 opsDone int
1240 calm bool
1241 trace []string
1242}
1243
1244type testingT interface {
1245 Fatalf(format string, args ...any)
1246 Logf(format string, args ...any)
1247}
1248
1249func (w *world) logf(format string, args ...any) {
1250 if w.verbose {
1251 w.trace = append(w.trace, fmt.Sprintf("%5d ", w.now)+fmt.Sprintf(format, args...))
1252 }
1253}
1254
1255func (w *world) violate(format string, args ...any) {
1256 msg := fmt.Sprintf("t=%d: ", w.now) + fmt.Sprintf(format, args...)
1257 if len(w.violations) >= 3 {
1258 return
1259 }
1260 w.violations = append(w.violations, msg)
1261 w.logf("VIOLATION %s", msg)
1262}
1263
1264func (w *world) newID(prefix string) string {
1265 w.nextID++
1266 return fmt.Sprintf("%s%d", prefix, w.nextID)
1267}
1268
1269func (w *world) newVer() int {
1270 w.ver++
1271 return w.ver
1272}
1273
1274// nextGen is the gen the claim CAS in progress will carry: what a writer
1275// captures as its installation stamp (claim rev 6.5 §6.4).
1276func (w *world) nextGen() int {
1277 if w.claim == nil {
1278 return 1
1279 }
1280 return w.claim.Ver + 1
1281}
1282
1283func (w *world) boxByID(id int) *box {
1284 for _, b := range w.boxes {
1285 if b.ID == id {
1286 return b
1287 }
1288 }
1289 return nil
1290}
1291
1292// regionBox is the live (not destroyed) box in a region.
1293func (w *world) regionBox(r int) *box {
1294 for _, b := range w.boxes {
1295 if b.Region == r && !b.Destroyed {
1296 return b
1297 }
1298 }
1299 return nil
1300}
1301
1302func contains(xs []int, x int) bool {
1303 for _, y := range xs {
1304 if y == x {
1305 return true
1306 }
1307 }
1308 return false
1309}
1310
1311func addUniq(xs []int, x int) []int {
1312 if contains(xs, x) {
1313 return xs
1314 }
1315 return append(xs, x)
1316}
1317
1318func intsKey(xs []int) string {
1319 c := append([]int(nil), xs...)
1320 sort.Ints(c)
1321 return fmt.Sprint(c)
1322}
1323
1324func (c *claim) clone() *claim {
1325 if c == nil {
1326 return nil
1327 }
1328 n := *c
1329 if c.Holder != nil {
1330 h := *c.Holder
1331 n.Holder = &h
1332 }
1333 n.Op.Parts = append([]int(nil), c.Op.Parts...)
1334 n.Op.StopFiles = c.Op.StopFiles.clone()
1335 n.Op.Excluded = append([]int(nil), c.Op.Excluded...)
1336 n.Op.InstalledOn = append([]int(nil), c.Op.InstalledOn...)
1337 n.Op.Discharged = append([]int(nil), c.Op.Discharged...)
1338 n.Op.Abandoned = append([]string(nil), c.Op.Abandoned...)
1339 n.Op.Acks = append([]int(nil), c.Op.Acks...)
1340 n.Op.Observed = append([]int(nil), c.Op.Observed...)
1341 if r := c.Op.Release; r != nil {
1342 cp := *r
1343 cp.Files = r.Files.clone()
1344 cp.Timers = append([]int(nil), r.Timers...)
1345 n.Op.Release = &cp
1346 }
1347 if p := c.Op.Pin; p != nil {
1348 cp := *p
1349 cp.Files = p.Files.clone()
1350 cp.Timers = append([]int(nil), p.Timers...)
1351 n.Op.Pin = &cp
1352 }
1353 if r := c.Op.Receipt; r != nil {
1354 cp := *r
1355 n.Op.Receipt = &cp
1356 }
1357 if c.Holder != nil {
1358 n.Holder.Admissions = append([]admissionRecord(nil), c.Holder.Admissions...)
1359 for i := range n.Holder.Admissions {
1360 if l := n.Holder.Admissions[i].Lost; l != nil {
1361 cp := *l
1362 n.Holder.Admissions[i].Lost = &cp
1363 }
1364 }
1365 }
1366 n.Regions = append([]int(nil), c.Regions...)
1367 n.Boxes = append([]int(nil), c.Boxes...)
1368 n.Obl = append([]obligation(nil), c.Obl...)
1369 n.Lost = append([]int(nil), c.Lost...)
1370 n.Retired = append([]int(nil), c.Retired...)
1371 n.Cancelled = append([]cancelledCreate(nil), c.Cancelled...)
1372 return &n
1373}
1374
1375// ---------- bucket access with IAM ----------
1376
1377// actor names: "box:N" or "console" or "operator".
1378
1379func (w *world) iamCheck(actorBox int, r *request) bool {
1380 if actorBox < 0 { // console: control bucket only, plus GetObjectVersion on data (door)
1381 return false
1382 }
1383 b := w.boxByID(actorBox)
1384 if r.region == b.Region {
1385 if r.kind == rCopy && r.srcRegion != b.Region {
1386 return false // no Get on a sibling
1387 }
1388 return true
1389 }
1390 // sibling bucket: Put/Delete on moves/* only, and CopyObject INTO moves/*
1391 // from the box's own bucket.
1392 if !strings.HasPrefix(r.key, "moves/") {
1393 return false
1394 }
1395 switch r.kind {
1396 case rPut, rDelKey:
1397 return true
1398 case rCopy:
1399 return r.srcRegion == b.Region
1400 }
1401 return false // DeleteObjectVersion is not granted on a sibling
1402}
1403
1404// issue sends a request. It returns the new version id when it completed
1405// immediately and the client saw the answer; 0 when the client timed out
1406// (the request may still complete later).
1407func (w *world) issue(actorBox int, r *request) int {
1408 r.at = w.now
1409 w.lastIssued = r
1410 if r.kind == rPut && r.b != nil && isDataKey(r.key) && (r.b.Min == 0 || r.b.Max == 0) {
1411 // INV-12 (R8-3): the pinned LTX encoder refuses a zero TXID, so no
1412 // such file can exist; a position-0 snapshot [1,0] or anchor [0,0]
1413 // is a builder bug, never an object
1414 w.violate("INV-12: box %d wrote an LTX file with a zero TXID, %s, which the pinned encoder refuses (R8-3)", actorBox, r.key)
1415 }
1416 if !w.iamCheck(actorBox, r) {
1417 w.violate("IAM: box %d may not %v %s in region %d", actorBox, r.kind, r.key, r.region)
1418 return 0
1419 }
1420 if w.hold != nil && w.hold(r) {
1421 r.doneAt, r.by = 1<<30, fmt.Sprintf("box:%d", actorBox)
1422 w.pending = append(w.pending, r)
1423 w.logf("box %d issued %v %s r%d (held by the witness)", actorBox, r.kind, r.key, r.region)
1424 return 0
1425 }
1426 if r.hold {
1427 // the biased schedule: accepted, completing at its worst moment or
1428 // long after everything else
1429 r.doneAt, r.by, r.beyond = w.now+40*horizonH, fmt.Sprintf("box:%d", actorBox), true
1430 w.pending = append(w.pending, r)
1431 w.logf("box %d issued %v %s r%d (held for its worst moment)", actorBox, r.kind, r.key, r.region)
1432 return 0
1433 }
1434 if w.rng.Intn(10) < 7 {
1435 return w.complete(r)
1436 }
1437 r.doneAt = w.now + 1 + w.rng.Intn(6)
1438 tail := 8
1439 if strings.HasPrefix(r.key, "moves/") {
1440 tail = 3 // staging rounds stall more often: the cross-region path is the slow one
1441 }
1442 if w.rng.Intn(tail) == 0 {
1443 r.doneAt = w.now + 1 + w.rng.Intn(horizonH) // the long tail: signed, cancelled, completed late
1444 if w.rng.Intn(4) == 0 {
1445 r.doneAt = w.now + horizonH // as late as the signature-age limit allows
1446 }
1447 if w.rng.Intn(beyondH) == 0 {
1448 // accepted before the signature limit, completed after it: S3
1449 // bounds acceptance, not completion (claim C3a, J2-R2-1)
1450 r.doneAt = w.now + horizonH + 1 + w.rng.Intn(10*horizonH)
1451 r.beyond = true
1452 if w.fail["recreate"] && !w.calm && w.rng.Intn(2) == 0 {
1453 // the biased schedule: no bound at all, so it can outlive a
1454 // delete and a recreate; it lands at its worst moment. Chaos
1455 // only: in the calm phase a chain of such holds, one per park
1456 // of a dead generation, can outlast the whole convergence
1457 // window (seed 20077 / 30,000), which says nothing about the
1458 // protocol
1459 r.doneAt = w.now + 40*horizonH
1460 }
1461 }
1462 }
1463 r.by = fmt.Sprintf("box:%d", actorBox)
1464 w.pending = append(w.pending, r)
1465 w.logf("box %d issued %v %s r%d (late until %d)", actorBox, r.kind, r.key, r.region, r.doneAt)
1466 return 0
1467}
1468
1469func (w *world) complete(r *request) int {
1470 r.done = true
1471 bk := w.buckets[r.region]
1472 switch r.kind {
1473 case rPut:
1474 if r.ifSet {
1475 cur := 0
1476 if v := bk.current(r.key); v != nil {
1477 cur = v.ID
1478 }
1479 if cur != r.ifVer {
1480 return 0 // 412 Precondition Failed
1481 }
1482 }
1483 v := &version{ID: w.newVer(), B: r.b, Straggler: r.beyond}
1484 bk.objs[r.key] = append(bk.objs[r.key], v)
1485 w.markDurable(r.key, r.b)
1486 return v.ID
1487 case rDelVer:
1488 vs := bk.objs[r.key]
1489 for i, v := range vs {
1490 if v.ID == r.ver {
1491 bk.objs[r.key] = append(vs[:i:i], vs[i+1:]...)
1492 break
1493 }
1494 }
1495 return -1
1496 case rDelKey:
1497 v := &version{ID: w.newVer(), Marker: true}
1498 bk.objs[r.key] = append(bk.objs[r.key], v)
1499 return v.ID
1500 case rCopy:
1501 src := w.buckets[r.srcRegion].byID(r.srcKey, r.srcVer)
1502 if src == nil || src.Marker {
1503 return 0
1504 }
1505 v := &version{ID: w.newVer(), B: src.B, Straggler: r.beyond}
1506 bk.objs[r.key] = append(bk.objs[r.key], v)
1507 w.markDurable(r.key, src.B)
1508 return v.ID
1509 }
1510 return 0
1511}
1512
1513// markDurable: a write is durable in a generation once a completed replica
1514// object of that generation holds it. The cold-restore checks (§14.2.3)
1515// require every write durable in the claim's generation or in any
1516// generation it superseded.
1517func (w *world) markDurable(key string, b *blob) {
1518 if b == nil || !isDataKey(key) {
1519 return
1520 }
1521 p := genPrefixOf(key)
1522 if w.durable[p] == nil {
1523 w.durable[p] = map[int]bool{}
1524 }
1525 for _, x := range b.TXs {
1526 w.durable[p][x] = true
1527 }
1528}
1529
1530// isDurable: W is durable anywhere (the disk-loss fault's RPO line).
1531func (w *world) isDurable(x int) bool {
1532 for _, m := range w.durable {
1533 if m[x] {
1534 return true
1535 }
1536 }
1537 return false
1538}
1539
1540// newGen records that the claim's generation moved from old to gen within
1541// loc, so the durability promise carries over.
1542func (w *world) newGen(loc, old, gen string) {
1543 if old != gen {
1544 w.genParent[dataKey(loc, gen, "")] = dataKey(loc, old, "")
1545 }
1546}
1547
1548// worstMoment: the biased schedule completes a pending write of an earlier
1549// incarnation's replica data the moment another incarnation holds the host
1550// with an executor in that region, inside its own window or past H, which
1551// is the interleaving C3a exists for (a recreate while the previous
1552// incarnation's requests are still in flight).
1553func (w *world) worstMoment(r *request) bool {
1554 if r.hold && r.b != nil && r.b.Kind == "gen" {
1555 // a held marker publication lands once a newer marker is current
1556 cur := w.buckets[r.region].current(r.key)
1557 return cur != nil && cur.B.Gen != r.b.Gen
1558 }
1559 if !w.fail["recreate"] || r.kind != rPut || r.b == nil || r.b.Inc == 0 || !isDataKey(r.key) {
1560 return false
1561 }
1562 c := w.claim
1563 if c == nil || c.Holder == nil || c.Holder.Inc == r.b.Inc {
1564 return false
1565 }
1566 if c.Op.Kind == "creating" && c.Op.NewRegion == r.region {
1567 return w.rng.Intn(2) == 0
1568 }
1569 e := w.execBox(c)
1570 return e != nil && e.Region == r.region && w.rng.Intn(2) == 0
1571}
1572
1573func (w *world) completeDue(force bool) {
1574 var keep []*request
1575 for _, r := range w.pending {
1576 if force || r.doneAt <= w.now || w.worstMoment(r) {
1577 w.complete(r)
1578 if r.beyond {
1579 w.stats["completed beyond H"]++
1580 }
1581 w.logf("late completion %v %s r%d (%s)", r.kind, r.key, r.region, r.by)
1582 } else {
1583 keep = append(keep, r)
1584 }
1585 }
1586 w.pending = keep
1587}
1588