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 "fmt"
5 "sort"
6 "strings"
7)
8
9// ---------- relocation (region HA spec 1 §5.8; claim rev 6.30 §4, L1; plan 1b-next rev 16) ----------
10//
11// A same-region executor change. The model's steps are abstract time:
12// probeStartWindow (30) is a probe attempt's readiness window, so the
13// probe deadline holds three attempts; sigAgeH (45) is the 15-minute
14// signature age, so the failover pin delay (30 s) is two steps. Each
15// guard names the plan 1b-next rev 16 function it mirrors.
16
17const (
18 failoverPinDelay = 2 // §5.8: pin no earlier than the fence's observed_at plus 30 s (hostclaim.FailoverPinDelay)
19 probeDeadline = 3*probeStartWindow + 10 // §5.8: at most three attempts within the probe deadline (hostrelocate.ProbeDeadline)
20 releaseDeadline = probeDeadline // 1b-next ruling 14: a release not journaled this long after releasing or probe-failed began is stuck (hostrelocate.ReleaseDeadline, 10 minutes like the probe deadline)
21 maxReplacements = 2 // §5.8: after two replacements of the target, stuck (hostclaim.MaxTargetReplacements)
22 enrollDelay = 20 // H16, H17: a launched box is confirmed and enrolled by a worker some time later
23 replaceDelay = 30 // H5: the group launches a replacement for a terminated box
24)
25
26// userRelocate is a scale-in drain (§5.8, reason drain; spec 4 owns the
27// decision): the serving host's executor is asked to give the host to
28// another eligible box of its region. The start CAS (StartRelocation)
29// records the intent, op.exec stays on the source, and both participants
30// acknowledge by writing execution leases (role()). The two-box step
31// draws it. A host that never admitted a writer is not drained (1b-next
32// ruling 11, Worker.Start; rule reloc-drain-needs-writer): its release has
33// no writer session to stop, so root refuses its stop evidence and the
34// drain could only wait for its release deadline; a failover relocates it.
35func (w *world) userRelocate() {
36 c := w.claim
37 if c == nil || c.Holder == nil || c.Op.Kind != "serving" || w.haOff {
38 return
39 }
40 from := w.execBox(c)
41 if from == nil || from.Destroyed || from.Old || !from.Enrolled {
42 return
43 }
44 // Worker.Start's order (rev 7): the source unfenced, a writer to
45 // release, an eligible target, the admission registration; the CAS
46 // rechecks the admission and applies the start guard
47 if on("reloc-drain-needs-writer") && c.Holder.Session == 0 {
48 w.stats["drain refused: the host never had a writer"]++
49 return
50 }
51 to := w.chooseBoxExcept(from.Region, c.Holder.App, from.ID)
52 if to == nil || to.Old {
53 return
54 }
55 if ad := w.admit[c.Holder.App]; ad != nil && ad.Closed {
56 return // a box-started relocation needs the app's admission registration (1b-next ruling 4): a purge closed it
57 }
58 id := w.newID("rl")
59 if w.cas(c, func(n *claim) bool {
60 if n.Holder == nil || n.Op.Kind != "serving" || n.Op.Exec != from.ID || contains(n.Lost, to.ID) {
61 return false
62 }
63 if ad := w.admit[n.Holder.App]; ad != nil && ad.Closed {
64 return false // Admission.Recheck
65 }
66 if !w.relocationStartAllowed(n, from, to) {
67 return false
68 }
69 n.Op = opState{Kind: "relocating", ID: id, Phase: "starting", Exec: from.ID, Parts: []int{from.ID, to.ID},
70 Src: from.ID, Tgt: to.ID, SrcRegion: from.Region, RelocReason: "drain", ReqAt: w.now, App: n.Holder.App,
71 SrcGen: n.Holder.Gen, SrcStamp: n.Holder.Stamp}
72 return true
73 }) {
74 w.stats["relocation started: drain"]++
75 w.logf("relocation %s: drain of inc %d from box %d to box %d in region %d", id, c.Holder.Inc, from.ID, to.ID, from.Region)
76 }
77}
78
79// relocationStartAllowed is the claim's start guard as L11 amends it for
80// relocating (RelocationStartGuard): a park-replica or any other
81// whole-location chore in the region blocks (CC11), a park-local on the
82// target or on a live source blocks, and a park-generation in the region
83// does not: relocation writes only its own new prepared generation, never
84// one being retired. The mutation of reloc-start-guard starts over
85// anything.
86func (w *world) relocationStartAllowed(c *claim, from, to *box) bool {
87 return w.relocationStartGuard(c, from, to, false)
88}
89
90// relocationStartGuard is RelocationStartGuard(c, region, source, target,
91// sourceFenced): a failover's fenced source's park-local does not block.
92func (w *world) relocationStartGuard(c *claim, from, to *box, sourceFenced bool) bool {
93 fenced := -1
94 if sourceFenced {
95 fenced = from.ID
96 }
97 if w.completionBarrier(c, fenced) {
98 return false // ruling 77, before the chores (RelocationStartGuard)
99 }
100 if !on("reloc-start-guard") {
101 return true
102 }
103 for _, o := range c.Obl {
104 switch {
105 case o.Kind == "park-generation":
106 continue // L11
107 case o.Kind == "park-local":
108 if o.Box == to.ID || o.Box == from.ID && !sourceFenced && !contains(c.Lost, from.ID) {
109 return false
110 }
111 case o.Region == from.Region:
112 return false
113 }
114 }
115 return true
116}
117
118// relocExcept is every box the operation may not name as its target
119// (replaceTargetSlot, Worker.next): the source, unless an operator's
120// resolve made it eligible again (model ruling 13), every replaced target,
121// and the extra boxes given.
122func relocExcept(op opState, extra ...int) []int {
123 var out []int
124 if !op.Resolved {
125 out = append(out, op.Src)
126 }
127 out = append(out, op.Excluded...)
128 return append(out, extra...)
129}
130
131// relocRelease is the release sequence (§5.8, A3-2; 1b-next Task 7's
132// Release) a live box runs to give the host up: the source in releasing,
133// a failed target in probe-failed. One step per call; it returns the
134// journal entry once every step is done, nil while one is owed.
135//
136// 1. stop every writer: admission closed (the row held), the process
137// tree stopped and empty; an escaped descendant fails the step (L10);
138// 2. flush: the final checkpoint and SyncAndWait, passing only once the
139// listing shows the stopped database's position N (moves §5.1 step
140// 1). It needs the session's own replicator and the box's own files
141// directory, or the positions this box recorded for this stop (rule
142// reloc-release-needs-flush, rev 9). When the replicator died with
143// its agent (claim rev 6.10) and nothing was recorded there is no
144// release, and the release deadline makes the operation stuck; the
145// mutation journals what the remote holds, Flushed false;
146// 3. publish every pending file and wake-state mutation as one final
147// manifest of the generation (ReleaseFlushFiles, Task 4);
148// 4. record the positions, then the producers' drain and root's
149// receipt; recorded positions finish a release only for the
150// controller that recorded them or with that receipt (rule
151// reloc-release-needs-receipt, rev 11).
152func (w *world) relocRelease(b *box, c *claim) *releaseEntry {
153 h := c.Holder
154 // StopWriters (L18 joint round 2 finding 3): the current attempt's and
155 // session's launch journal closes first, so the stopped session is never
156 // relaunched; root's durable admission fence (relFence) comes only with
157 // Collect, after the flush and the recorded positions (ruling 28). In
158 // between, no wake admits a writer: AdmitWriter's relocatingAdmission
159 // admits nobody in starting or releasing (rule reloc-admits-probe-only).
160 // The row hold is reconcile's alone (reloc-row-cases, the catch-up hold)
161 w.closeLaunches(b, c.Op.ID, h.Session)
162 if !w.evidence(b, c.Op.ID) {
163 w.stats["release: a process survives the stop"]++
164 return nil
165 }
166 if r := b.relRecord; r != nil && on("reloc-release-needs-flush") && r.Op == c.Op.ID && r.Phase == c.Op.Phase && r.E.Session == h.Session && r.E.Gen == h.Gen {
167 // a flush this box recorded for this stop (ReleasePositions.Recorded):
168 // never flushed or guessed again (rev 9). Rev 11 (Astra round 8):
169 // the positions are not proof of the stop. The controller that
170 // recorded them resumes at the producers' drain, and root persists
171 // the receipt (step 4); after a restart only root's durable receipt
172 // for the recorded identity finishes the release, and with none
173 // RecoveryReleaseDrain refuses: no release, and the deadline
174 // makes the operation stuck. The mutation of
175 // reloc-release-needs-receipt replays the positions alone, as rev
176 // 10 did
177 switch {
178 case r.Receipt || !on("reloc-release-needs-receipt"):
179 if r.Ctl != b.Epoch {
180 w.stats["release: replayed from the recorded positions"]++
181 }
182 case r.Ctl == b.Epoch:
183 r.Receipt = true // the producers drain; root persists the receipt
184 w.stats["release: receipt persisted"]++
185 b.relFence = c.Op.ID // Collect: root's durable admission fence, after the flush and the positions
186 default:
187 w.stats["release: recorded positions without a receipt, refused"]++
188 return nil
189 }
190 b.relFence = c.Op.ID // root's fence stands with the receipt (Collect)
191 e := r.E
192 e.Files, e.Timers = r.E.Files.clone(), append([]int(nil), r.E.Timers...)
193 return &e
194 }
195 prefix := dataKey(h.Location, h.Gen, "")
196 remote := w.remoteMax(w.buckets[b.Region], prefix)
197 n, flushed := remote, false
198 if on("reloc-release-needs-flush") && !(b.DB != nil && b.DB.Inc == h.Inc && b.DB.Gen == h.Gen && b.DB.Epoch == b.Epoch && b.DB.Session == h.Session) {
199 // relocationFlushDB needs the session's own replicator, and
200 // nothing recorded a flush for this stop: no release (1b-next Task
201 // 7; rev 9, Astra round 6). The release deadline makes the
202 // operation stuck; the mutation journals the remote position with
203 // Flushed false, as rev 8 did
204 w.stats["release: no replicator and no recorded flush, refused"]++
205 return nil
206 }
207 if db := b.DB; db != nil && db.Inc == h.Inc && db.Gen == h.Gen {
208 if db.Epoch == b.Epoch && db.Session == h.Session {
209 if len(db.Pending) > 0 {
210 bl := w.deltaOf(db, db.Inc)
211 db.FirstFull = false
212 db.Queue = append(db.Queue, bl)
213 db.Pos, db.Pending = bl.Max, nil
214 }
215 w.ship(b)
216 if len(db.Queue) > 0 || db.InFlight != nil || remote < db.Pos {
217 w.stats["release: waits for the flush"]++
218 return nil
219 }
220 n, flushed = db.Pos, true
221 } else {
222 w.stats["release: the session's replicator is gone, unflushed"]++
223 }
224 }
225 // the files the box holds, or, when it holds none of this incarnation's
226 // (a failed target whose unopened install a scoped cleanup retired),
227 // the set its own installation applied: a release never invents a
228 // files identity
229 files := b.Files.clone()
230 if files == nil || files.Inc != h.Inc {
231 if on("reloc-release-needs-flush") {
232 // ReleaseFlushFiles publishes this box's own files directory;
233 // there is none to publish (rev 9)
234 w.stats["release: no files directory to flush, refused"]++
235 return nil
236 }
237 files = w.relocFiles(c).clone()
238 }
239 // 3. (Task 4) publish every pending file and wake-state mutation,
240 // whatever the app's files_durability, as one final manifest of the
241 // generation (ReleaseFlushFiles): create-only, acknowledged before the
242 // entry is journaled, its sequence journaled as manifest_seq
243 seq := 0
244 if w.perRegion > 1 {
245 if b.pubGen != h.Gen {
246 b.pubGen, b.pubSeq, b.releaseSeq = h.Gen, 0, 0
247 }
248 if !w.ensureObject(b, h, files) {
249 w.stats["release: waits for its files object"]++
250 return nil
251 }
252 if b.releaseSeq == 0 {
253 b.pubSeq++
254 b.releaseSeq = b.pubSeq
255 }
256 w.issued++
257 bl := &blob{Kind: "files", Inc: h.Inc, Gen: h.Gen, Seq: b.releaseSeq, Files: files.clone(), Timers: append([]int(nil), b.Timers...), Base: w.issued}
258 if !w.ackedPut(b, manifestKey(h.Location, h.Gen, b.releaseSeq), bl, true) {
259 w.stats["release: waits for its manifest"]++
260 return nil
261 }
262 seq, b.releaseSeq = b.releaseSeq, 0
263 b.published = filesState(b.Timers, files)
264 }
265 e := &releaseEntry{Box: b.ID, Session: h.Session, Gen: h.Gen, FinalTX: n, ManifestSeq: seq, Flushed: flushed,
266 Files: files, Timers: append([]int(nil), b.Timers...), At: w.now}
267 if on("reloc-release-needs-flush") {
268 // ReleasePositions.Record: durable before the producers close
269 b.relRecord = &releaseRecord{Op: c.Op.ID, Phase: c.Op.Phase, E: *e, Ctl: b.Epoch}
270 b.relRecord.E.Files, b.relRecord.E.Timers = files.clone(), append([]int(nil), b.Timers...)
271 if on("reloc-release-needs-receipt") {
272 // the producers' drain and root's receipt are the next step:
273 // a crash here leaves positions and no receipt (rev 11)
274 return nil
275 }
276 }
277 if b.relFence != c.Op.ID {
278 b.relFence = c.Op.ID // Collect, on the mutations' paths that skip the record or the receipt
279 }
280 return e
281}
282
283// pinSource is the generation the pin reads (StorePinner's genselect.Select
284// over the admitted chain): holder.generation when it holds an object or
285// the marker names it; else the baseline the recovery walk selects for an
286// unfinished run of cold admissions (claim rev 6.22 §6.2a), as a cold
287// admission would restore it. An unexplained empty generation refuses:
288// absence is never evidence (§5.9).
289func (w *world) pinSource(b *box, c *claim) (string, bool) {
290 h := c.Holder
291 loc := h.Location
292 if w.hasFiles(b, loc, h.Gen) {
293 return h.Gen, true
294 }
295 bg, _ := w.bucketGen(b, loc)
296 if bg == h.Gen {
297 return h.Gen, true
298 }
299 run, ok := w.unfinishedRun(c, bg)
300 if !ok {
301 return "", false
302 }
303 return w.runBaseline(b, c, run, bg)
304}
305
306// selectPin is StorePinner.Pin against the observation c: the source
307// generation and its listing's newest position N, after a release only
308// once N reaches the release's final_txid (rule reloc-pin-reaches-release),
309// and an empty listing only with evidence (ruling 15, rule
310// reloc-pin-evidenced-empty): a journaled release that flushed at 0, or a
311// holder that never admitted a writer; any other empty listing marks the
312// operation stuck for an operator. The pin carries the claim version it
313// was selected against (ruling 16, RecoveryPinKey). Task 4 adds the files
314// pin. It returns nil while the pin must wait.
315func (w *world) selectPin(b *box, c *claim) *pinEntry {
316 op, h := c.Op, c.Holder
317 src, ok := w.pinSource(b, c)
318 if !ok {
319 w.stats["pin: the source generation cannot be selected yet"]++
320 return nil
321 }
322 n := w.remoteMax(w.buckets[b.Region], dataKey(h.Location, src, ""))
323 r := op.Release
324 if r != nil && on("reloc-pin-reaches-release") && n < r.FinalTX {
325 w.stats["pin: waits for the release's final txid"]++
326 return nil
327 }
328 if n == 0 && on("reloc-pin-evidenced-empty") && !op.Resolved && h.Session != 0 && !(r != nil && r.Flushed && r.FinalTX == 0) {
329 w.markStuck(c, fmt.Sprintf("an empty listing of %q is no evidence of an empty database (session %d)", src, h.Session))
330 return nil
331 }
332 pin := &pinEntry{Gen: src, TX: n, ClaimVer: c.Ver, At: w.now, Files: w.relocFiles(c).clone(), Timers: append([]int(nil), w.relocTimers(c)...)}
333 // (Task 4) the files pin: the newest landed manifest of the admitted
334 // chain, by exact version, independently of the database baseline
335 // (B2-4); after a release it must reach the release's manifest_seq
336 if mk, mv, ms, mg, _ := w.newestManifest(b.Region, h.Location, h.Gen); mk != "" {
337 if r != nil && r.ManifestSeq > 0 && on("reloc-pin-reaches-release") && (mg != r.Gen || ms < r.ManifestSeq) {
338 w.stats["pin: waits for the release's manifest"]++
339 return nil
340 }
341 pin.ManKey, pin.ManVer, pin.ManSeq, pin.ManGen = mk, mv, ms, mg
342 }
343 // 1b-next ruling 25 as rev 18 amended it (finding F2; rule
344 // files-pin-needs-key): when a writer was admitted and the pinned
345 // database is nonempty, the pinned manifest must carry a live
346 // instance.key, or the operation is stuck; no operator replay accepts
347 // it, since a database is never installed without its key. The L21
348 // window (the app mints the key after it starts, and its first
349 // manifest with the key has not landed) ends here. The mutation pins
350 // what it found, rev 16's behaviour. Only the two-box shape has a
351 // files publisher (ruling 11)
352 if w.perRegion > 1 && on("files-pin-needs-key") && h.Session != 0 && n > 0 && !manifestHasKey(w.pinnedManifestOf(b.Region, pin)) {
353 w.stats["pin: files unevidenced, no instance.key"]++
354 w.markStuck(c, fmt.Sprintf("the pinned files carry no instance.key for a nonempty database (TXID %d)", n))
355 return nil
356 }
357 if m := w.pinnedManifestOf(b.Region, pin); m != nil && m.Files != nil && m.Files.Obj != "" && !w.originAuthorized(b.Region, c, m.Files.Obj) {
358 // filesPin's referencedOrigins (1b-next rev 12 finding 1, R11-1):
359 // the object the pinned manifest names must be current in this
360 // region's bucket, under this holder's location, at an origin the
361 // pin may cite; otherwise the pin refuses (ErrOrigin) and the
362 // worker retries on a later pass
363 w.stats["pin: files object refused"]++
364 w.logf("DEBUG pin refused obj %s cur=%v", m.Files.Obj, w.buckets[b.Region].current(m.Files.Obj) != nil)
365 return nil
366 }
367 return pin
368}
369
370// ---------- files objects (rev 4, Astra R1-8; spec §5.6 "Objects are immutable") ----------
371
372// objectKey is a files object: <holder.location>/files/<generation>/objects/<content>,
373// written create-only by the generation that first held the content.
374func objectKey(loc, gen string, f *filesDir) string {
375 return dataKey(loc, gen, fmt.Sprintf("files/objects/%d-%d", f.Key, f.Env))
376}
377
378// isFilesKey: a manifest or an object of the files tree.
379func isFilesKey(k string) bool {
380 return strings.Contains(k, "files/m-") || strings.Contains(k, "files/objects/")
381}
382
383// objectCurrent: f names an object holding its content, in b's region
384// (the region binding; the mutation of files-objects-region-bound accepts
385// another region's object, as a moved host's carried reference).
386func (w *world) objectCurrent(b *box, f *filesDir) bool {
387 if f == nil || f.Obj == "" || !strings.HasSuffix(f.Obj, fmt.Sprintf("/%d-%d", f.Key, f.Env)) {
388 return false
389 }
390 return f.ObjRegion == b.Region || !on("files-objects-region-bound")
391}
392
393// ensureObject uploads f's content as an object of the current
394// generation when f names none for it in this region (a changed file, or
395// a host that came from another region), and reports once one is
396// acknowledged; an unchanged file keeps naming the object an older
397// generation wrote, which is what keeps that generation's objects live.
398func (w *world) ensureObject(b *box, h *holder, f *filesDir) bool {
399 if f == nil || w.objectCurrent(b, f) {
400 return true
401 }
402 k := objectKey(h.Location, h.Gen, f)
403 if !w.ackedPut(b, k, &blob{Kind: "files-object", Inc: h.Inc, Gen: h.Gen, Files: f.clone()}, true) {
404 return false
405 }
406 f.Obj, f.ObjRegion = k, b.Region
407 w.stats["files objects written"]++
408 return true
409}
410
411// originAuthorized is the pin's check of one referenced object: current
412// in region r's bucket under the holder's location, and at an authorized
413// origin. With the rule (files-origins-referenced) the origins are the
414// generations the pinned manifest references, so any such object is
415// authorized; the mutation derives them from holder.generation and the
416// retained admissions, as rev 11 did, which the trim to eight shortens.
417func (w *world) originAuthorized(r int, c *claim, obj string) bool {
418 h := c.Holder
419 if !strings.HasPrefix(obj, h.Location) || w.buckets[r].current(obj) == nil {
420 return false
421 }
422 if on("files-origins-referenced") {
423 return true
424 }
425 g := genOf(h.Location, obj)
426 if g == h.Gen || c.Op.Prepared != "" && g == c.Op.Prepared {
427 return true
428 }
429 for _, e := range h.Admissions {
430 if e.Gen == g || e.Predecessor.Gen == g {
431 return true
432 }
433 }
434 return false
435}
436
437// pinnedManifestOf is the manifest pin names, read by version.
438func (w *world) pinnedManifestOf(r int, pin *pinEntry) *blob {
439 if pin.ManKey == "" {
440 return nil
441 }
442 if v := w.buckets[r].byID(pin.ManKey, pin.ManVer); v != nil && !v.Marker {
443 return v.B
444 }
445 return nil
446}
447
448// manifestHasKey: the manifest carries a live instance.key (InstanceKeyPath,
449// liveEntry): its files directory names one.
450func manifestHasKey(m *blob) bool { return m != nil && m.Files != nil && m.Files.Key != 0 }
451
452// restoreUpTo restores generation gen of region r from its current
453// listing, capped at TXID upto: the newest current snapshot at or below
454// upto as the root, then the planner's rule (ltx.IsContiguous, highest
455// max, lowest min, higher level) over the current level-0 and level-1
456// files whose range ends at or below upto. It is §5.8's build from the
457// pinned recovery set: nothing past the pin is restored, and a listing
458// that does not reach upto refuses (nil), as a pin short of its release
459// would. It reads the bucket and writes nothing.
460func (w *world) restoreUpTo(r, inc int, loc, gen string, upto int) *localDB {
461 bk := w.buckets[r]
462 prefix := dataKey(loc, gen, "")
463 db := &localDB{Inc: inc, Loc: loc, Gen: gen, State: map[string]int{}}
464 var root *blob
465 for _, s := range w.currentLTX(bk, prefix, 9) {
466 if s.B.Max <= upto && (root == nil || s.B.Max > root.Max) {
467 root = s.B
468 }
469 }
470 cur := 0
471 var applied []*blob
472 if root != nil {
473 apply(db.State, root)
474 cur = root.Max
475 applied = append(applied, root)
476 db.Key = root.SealKey
477 if root.Inc != 0 {
478 db.Inc = root.Inc
479 }
480 }
481 var cands []*blob
482 for _, level := range []int{0, 1} {
483 for _, f := range w.currentLTX(bk, prefix, level) {
484 if f.B.Max <= upto {
485 cands = append(cands, f.B)
486 }
487 }
488 }
489 for cur < upto {
490 var best *blob
491 for _, f := range cands {
492 if f.Min <= cur+1 && f.Max > cur && (best == nil || f.Max > best.Max || f.Max == best.Max && (f.Min < best.Min || f.Min == best.Min && f.Level > best.Level)) {
493 best = f
494 }
495 }
496 if best == nil {
497 return nil // the listing does not reach upto: a gap, or objects still landing
498 }
499 apply(db.State, best)
500 cur = best.Max
501 applied = append(applied, best)
502 if db.Key == 0 {
503 db.Key = best.SealKey
504 }
505 if best.Inc != 0 {
506 db.Inc = best.Inc
507 }
508 }
509 db.Log = txsOf(applied...)
510 db.TX = w.lineageTX(applied...)
511 db.Pos = cur
512 return db
513}
514
515// relocFiles is the files directory the local install applies: the
516// pinned set's (the pin carries the files and wake state it was taken
517// with), else the release's (a drain, a nested relocation), else the
518// converted move's or the failover's stand-in for the files publisher
519// (moves §14.2.6). Task 4 puts the pinned manifest first.
520func (w *world) relocFiles(c *claim) *filesDir {
521 if m := w.pinnedManifest(c); m != nil && m.Files != nil {
522 return m.Files
523 }
524 if p := c.Op.Pin; p != nil && p.Files != nil {
525 return p.Files
526 }
527 if r := c.Op.Release; r != nil && r.Files != nil {
528 return r.Files
529 }
530 return c.Op.StopFiles
531}
532
533// relocTimers is the wake state the local install replaces timers with.
534func (w *world) relocTimers(c *claim) []int {
535 if m := w.pinnedManifest(c); m != nil {
536 return m.Timers
537 }
538 if p := c.Op.Pin; p != nil && p.Files != nil {
539 return p.Timers
540 }
541 if r := c.Op.Release; r != nil {
542 return r.Timers
543 }
544 return nil
545}
546
547// nameReplacement names next as the operation's target in n, counting a
548// replacement when it replaces another box (replaceTargetSlot): the
549// replaced box is excluded for this host, and past maxReplacements the
550// operation is marked stuck at the current phase instead (rule
551// reloc-replacement-cap). Inside the CAS it refuses a box that owes a
552// park-local for the host (rule reloc-replace-not-owing, Task 3). It
553// reports whether next was named.
554func (w *world) nameReplacement(n *claim, next int, phase string) bool {
555 if on("reloc-replace-not-owing") {
556 if b := w.boxByID(next); b != nil && w.owesLocal(n, b) {
557 w.stats["replacement refused: the box owes a park-local"]++
558 return false
559 }
560 }
561 old := n.Op.Tgt
562 if old >= 0 && old != next {
563 if on("reloc-replacement-cap") && n.Op.Replacements >= maxReplacements {
564 n.Op.Stuck = fmt.Sprintf("two replacements of the target used; a third was needed at %s", n.Op.Phase)
565 w.stats["relocation stuck: replacement cap"]++
566 return false
567 }
568 n.Op.Replacements++
569 n.Op.Excluded = addUniq(n.Op.Excluded, old)
570 var parts []int
571 for _, p := range n.Op.Parts {
572 if p != old {
573 parts = append(parts, p)
574 }
575 }
576 n.Op.Parts = parts
577 }
578 n.Op.Tgt = next
579 n.Op.Parts = addUniq(n.Op.Parts, next)
580 n.Op.Phase = phase
581 return true
582}
583
584// markStuck is MarkStuck: a journal entry, never a phase; every relocation
585// step refuses afterwards until the operator resolves it. It never stops
586// an operation between installed and a probe while P is unopened: that
587// live target's safe point is its probe.
588func (w *world) markStuck(c *claim, why string) {
589 id := c.Op.ID
590 if w.cas(c, func(n *claim) bool {
591 if !sameOp(n.Op.ID, id) || n.Op.Kind != "relocating" || n.Op.Stuck != "" {
592 return false
593 }
594 if n.Op.Phase == "installed" && n.Op.Receipt != nil && !preparedOpened(n) {
595 return false
596 }
597 n.Op.Stuck = why
598 return true
599 }) {
600 w.alerts++
601 w.stats["relocation stuck"]++
602 w.logf("relocation %s stuck: %s", id, why)
603 }
604}
605
606// relocationLeftovers queues, inside the serving CAS (CompleteRelocation),
607// the park-local chore every abandoned installation owes (claim §4: "every
608// abandoned local installation receives an exact park-local"): the
609// source, when it is alive and its fence did not discharge it, and every
610// other box this operation handed op.exec to.
611func (w *world) relocationLeftovers(n *claim, op opState) {
612 // every prepared generation the operation abandoned unopened
613 // (abandoned_prepared) is a dead generation of holder.location: its
614 // remote build is retired by a park-generation queued here, beside the
615 // park-local chores (1b-next ruling 27, CompleteRelocation; plan ruling
616 // 16). An opened P is part of the writer lineage and never listed
617 if h := n.Holder; h != nil {
618 for _, g := range op.Abandoned {
619 if g == h.Gen || w.ancestorGen(h.Location, h.Gen, g) {
620 continue
621 }
622 n.Obl = append(n.Obl, w.scoped(obligation{ID: w.newID("ob"), Op: op.ID, Kind: "park-generation", Region: op.SrcRegion, Box: -1, Prefix: h.Location, Ret: g, Quiet: w.now + horizonH}, w.now, h.Inc))
623 w.stats["relocation: abandoned prepared generation parked"]++
624 }
625 }
626 // the pinned manifest of a superseded generation (rev 5): the files
627 // live set kept it while the operation ran, so a park-generation of
628 // that generation that ran meanwhile left it; once the operation
629 // completes it is a dead generation's key, parked here. This is the
630 // model's abstraction, not a CompleteRelocation guarantee (rev 6,
631 // Astra round 3 finding 2): in 1b-next rev 19 completion queues only
632 // the local chores and the abandoned prepared generations; an obsolete
633 // files manifest is the at-rest sweep's (claim §6.2a), and the
634 // pinner's retained control object is the storage follow-up's (1b-next
635 // ruling 21). The model parks it at once so INV-10 can check the
636 // residue at completion
637 if h := n.Holder; h != nil && op.Pin != nil && op.Pin.ManKey != "" && op.Pin.ManGen != h.Gen {
638 ret := op.Pin.ManGen
639 if ret == "" {
640 ret = "@root"
641 }
642 queued := false
643 for _, o := range n.Obl {
644 if o.Kind == "park-generation" && o.Region == op.SrcRegion && o.Ret == ret && o.Prefix == h.Location {
645 queued = true
646 }
647 }
648 if !queued {
649 n.Obl = append(n.Obl, w.scoped(obligation{ID: w.newID("ob"), Op: op.ID, Kind: "park-generation", Region: op.SrcRegion, Box: -1, Prefix: h.Location, Ret: ret, Quiet: w.now + horizonH}, w.now, h.Inc))
650 w.stats["relocation: the pinned manifest's generation parked"]++
651 }
652 }
653 owed := append([]int{op.Src}, op.InstalledOn...)
654 for _, id := range owed {
655 if id < 0 || id == op.Exec || contains(op.Discharged, id) {
656 continue
657 }
658 if b := w.boxByID(id); b == nil || w.owesLocal(n, b) {
659 continue
660 }
661 n.Obl = append(n.Obl, obligation{ID: w.newID("ob"), Op: op.ID, Kind: "park-local", Box: id, Region: -1, Ret: op.ID})
662 }
663}
664
665// relocStarting: the barrier (claim D6, requireAcks in ToReleasing). A
666// drain leaves starting for releasing once both participants' execution
667// leases are current (role() writes them on every read of the claim), and
668// the CAS records releasing_since for the release deadline; a failover
669// never enters starting in this model (it starts at pinning, Task 3, and
670// PinFailover's barrier is checked there before the pin is selected:
671// rule reloc-failover-ack-before-pin, rev 5 and 6). The mutation of
672// reloc-exec-ack-barrier moves on with no acknowledgement.
673func (w *world) relocStarting(b *box, c *claim) {
674 op := c.Op
675 if on("reloc-exec-ack-barrier") && !acked(c) {
676 return
677 }
678 w.cas(c, func(n *claim) bool {
679 if !sameOp(n.Op.ID, op.ID) || n.Op.Kind != "relocating" || n.Op.Phase != "starting" {
680 return false
681 }
682 n.Op.Phase, n.Op.ReleasingSince = "releasing", w.now
683 return true
684 })
685}
686
687// relocReleasing: the source runs the release sequence and journals it
688// (JournalRelease); then a worker's CAS (PinAfterRelease) cites the entry
689// and leaves for pinning with op.exec none and the stamp kept (claim
690// R6-4), once every participant has acknowledged (rule
691// reloc-ack-before-pinning: a target replaced in releasing acknowledges
692// before the operation leaves the phase; 1b-next rev 4 N6). A source that
693// has not journaled its release releaseDeadline after releasing began is
694// stuck (rule reloc-release-deadline, Worker.releaseOverdue): nothing
695// moves away from a live box without its stop evidence. The mutation of
696// reloc-release-first pins without the entry: the source is never
697// stopped.
698func (w *world) relocReleasing(b *box, c *claim) {
699 op := c.Op
700 if op.Release == nil {
701 if on("reloc-release-deadline") && w.now > op.ReleasingSince+releaseDeadline {
702 w.markStuck(c, fmt.Sprintf("box %d has not released the host %d steps after releasing", op.Exec, releaseDeadline))
703 return
704 }
705 if b.ID != op.Exec {
706 if !on("reloc-release-first") {
707 w.cas(c, func(n *claim) bool {
708 ok := sameOp(n.Op.ID, op.ID) && n.Op.Kind == "relocating" && n.Op.Phase == "releasing"
709 if ok {
710 n.Op.Phase, n.Op.Exec = "pinning", -1
711 }
712 return ok
713 })
714 }
715 return
716 }
717 w.relocJournalRelease(b, c, false)
718 return
719 }
720 if on("reloc-ack-before-pinning") && !acked(c) {
721 w.stats["pin after release: waits for the acknowledgements"]++
722 return
723 }
724 w.cas(c, func(n *claim) bool {
725 ok := sameOp(n.Op.ID, op.ID) && n.Op.Kind == "relocating" && n.Op.Phase == "releasing" && n.Op.Release != nil && n.Op.Release.Box == n.Op.Exec
726 if ok {
727 n.Op.Phase, n.Op.Exec = "pinning", -1
728 }
729 return ok
730 })
731}
732
733// relocPinning: a worker selects the pin against its observation and
734// journals it (JournalPin) in a CAS bound to that observation's claim
735// version (rule reloc-pin-claim-gen, ruling 16: a pin selected against an
736// older claim is abandoned, and the next pass selects afresh); a failover
737// pins no earlier than the fence's observed_at plus failoverPinDelay (rule
738// reloc-failover-pin-delay, a loss bound). Then it names a target when the
739// operation has none (NameTarget: a converted move, a resolved stuck
740// operation, a target lost with no spare); then, with every participant's
741// lease current (rule reloc-exec-ack-barrier), hands op.exec to the
742// target for a new installation (ToInstalling): a new stamp, journaled as
743// install_stamp, and a fresh prepared generation id (claim R6-4). The
744// target must owe no park-local for the host.
745//
746// An initial failover (op.PinFailover: 1b-next's starting after
747// StartRelocation with a fence) first passes PinFailover's barrier (rev
748// 5, Astra round 2 finding 1; narrowed in rev 6, round 3 finding 1): the
749// target is named, every participant has acknowledged, and the PinFailover
750// CAS clears the flag, all before any pin is selected; so a manifest that
751// lands while the target's controller is unavailable is in the pin (rule
752// reloc-failover-ack-before-pin). The flag is the model's starting
753// (ruling 4). A converted move, a nested failover after a replacement and
754// an operator's replay enter pinning directly, as in 1b-next, where
755// Worker.pinning selects and journals the pin first and checks the
756// acknowledgements only for ToInstalling. The mutation selects the pin
757// before the barrier, as rev 4 did.
758func (w *world) relocPinning(b *box, c *claim) {
759 op := c.Op
760 if op.PinFailover && on("reloc-failover-ack-before-pin") {
761 if op.Tgt < 0 {
762 w.relocNameTarget(c)
763 return
764 }
765 if !acked(c) {
766 w.stats["failover pin: waits for the acknowledgements"]++
767 return
768 }
769 if w.cas(c, func(n *claim) bool {
770 ok := sameOp(n.Op.ID, op.ID) && n.Op.Kind == "relocating" && n.Op.Phase == "pinning" && n.Op.PinFailover && n.Op.Tgt == op.Tgt && n.Op.Stuck == ""
771 if ok {
772 n.Op.PinFailover = false
773 }
774 return ok
775 }) {
776 w.logf("relocation %s: PinFailover, box %d acknowledged", op.ID, op.Tgt)
777 }
778 return
779 }
780 if op.Tgt < 0 && on("reloc-name-before-pin") {
781 // Worker.pinning names a missing target before it selects any pin
782 // (1b-next rev 19; rev 7, Astra round 4 finding 1), on every path
783 // that enters pinning without one: a converted move, an operator's
784 // replay. With no eligible box it waits, and a manifest that lands
785 // meanwhile is in the later pin. The mutation pins first, as rev 6
786 // did for these paths
787 w.relocNameTarget(c)
788 return
789 }
790 if op.Pin == nil {
791 if on("reloc-failover-pin-delay") && (op.Release == nil || on("reloc-pin-delay-every-fence")) && w.now < latestObservation(op)+failoverPinDelay {
792 return
793 }
794 pin := w.selectPin(b, c)
795 if pin == nil {
796 return
797 }
798 if w.cas(c, func(n *claim) bool {
799 ok := sameOp(n.Op.ID, op.ID) && n.Op.Kind == "relocating" && n.Op.Phase == "pinning" && n.Op.Pin == nil && n.Op.Stuck == ""
800 if ok && on("reloc-pin-claim-gen") && n.Ver != pin.ClaimVer {
801 w.stats["pin: selected against an older claim, abandoned"]++
802 ok = false
803 }
804 if ok {
805 n.Op.Pin = pin
806 }
807 return ok
808 }) {
809 w.logf("relocation %s: pinned %q at TXID %d (selected against claim version %d)", op.ID, pin.Gen, pin.TX, pin.ClaimVer)
810 }
811 return
812 }
813 if op.Tgt < 0 {
814 w.relocNameTarget(c)
815 return
816 }
817 if on("reloc-exec-ack-barrier") && !acked(c) {
818 return
819 }
820 to := op.Tgt
821 if w.cas(c, func(n *claim) bool {
822 if !sameOp(n.Op.ID, op.ID) || n.Op.Kind != "relocating" || n.Op.Phase != "pinning" || n.Op.Tgt != to || n.Op.Pin == nil || n.Op.Stuck != "" || contains(n.Lost, to) {
823 return false
824 }
825 if t := w.boxByID(to); t == nil || w.owesLocal(n, t) {
826 return false
827 }
828 w.handExec(n, to, "installing", w.newID("P"))
829 n.Op.BaseKey, n.Op.BaseVer, n.Op.BaseTX = "", 0, 0
830 return true
831 }) {
832 w.logf("relocation %s: box %d installs with stamp %d", op.ID, to, w.claim.Holder.Stamp)
833 }
834}
835
836// relocNameTarget is NameTarget at pinning: choose() in the source region
837// over the eligible boxes, the excluded ones left out. A converted move's
838// own target (1b-next's move_target_box, in the region the move was
839// going to) leaves the participants in the same CAS (rule
840// reloc-name-drops-move-target, 1b-next 151261fe): readiness binds to the
841// box named here (moves §7.10) and a failover's participant is its
842// target alone (§5.8); the move's target never held op.exec on the
843// converted operation. A relocation's participants are otherwise all in
844// its region, so the model drops every participant outside op.SrcRegion,
845// which only a converted move leaves there. The mutation keeps it, and
846// the barrier waits for a box that may never acknowledge: a stopped move
847// target holds the relocation at pinning with no deadline and no stuck
848// (a liveness guard: TestConvertedMoveDropsTheMoveTargetFromTheBarrier).
849func (w *world) relocNameTarget(c *claim) {
850 op := c.Op
851 to := w.chooseBoxExcept(op.SrcRegion, c.Holder.App, relocExcept(op)...)
852 if to == nil {
853 return // no eligible box: the recovery pass alerts once (moves §7.10)
854 }
855 dropped := false
856 if w.cas(c, func(n *claim) bool {
857 ok := sameOp(n.Op.ID, op.ID) && n.Op.Kind == "relocating" && n.Op.Phase == "pinning" && n.Op.Tgt < 0
858 if ok && on("reloc-name-drops-move-target") {
859 var parts []int
860 for _, p := range n.Op.Parts {
861 if x := w.boxByID(p); x == nil || x.Region == n.Op.SrcRegion {
862 parts = append(parts, p)
863 }
864 }
865 dropped = len(parts) < len(n.Op.Parts)
866 n.Op.Parts = parts
867 }
868 if ok {
869 ok = w.nameReplacement(n, to.ID, "pinning")
870 }
871 return ok
872 }) && dropped {
873 w.stats["name target: the move's target left the participants"]++
874 }
875}
876
877// abandonPrepared is abandonPrepared(c) in hostclaim (1b-next ruling
878// 27, rev 18 finding 3): before an unopened P is replaced or dropped, it
879// is journaled in abandoned_prepared, so the serving CAS parks it. Every
880// path that replaces or drops P calls it: install, the replacement
881// table's nested branch, the nested relocation after probe-failed and the
882// operator's replay. The mutation of reloc-abandoned-prepared records
883// nothing, which reopens finding F3.
884func (w *world) abandonPrepared(n *claim) {
885 p := n.Op.Prepared
886 if p == "" || preparedOpened(n) || hasStr(n.Op.Abandoned, p) || !on("reloc-abandoned-prepared") {
887 return
888 }
889 n.Op.Abandoned = append(n.Op.Abandoned, p)
890}
891
892// handExec is install() in hostclaim: op.exec to box at phase with
893// prepared generation P, a new stamp (this CAS's), journaled as
894// install_stamp, no receipt, the probe attempts reset for the new
895// executor (BeginProbeAttempt counts per box), and the box noted as owing
896// a park-local should it be abandoned.
897func (w *world) handExec(n *claim, box int, phase, prepared string) {
898 if prepared != n.Op.Prepared {
899 w.abandonPrepared(n) // install replaces an unopened P (1b-next ruling 27)
900 }
901 n.Op.Exec, n.Op.Phase, n.Op.Prepared = box, phase, prepared
902 n.Holder.Stamp = w.issueStamp(n, box)
903 n.Op.InstallStamp = n.Holder.Stamp
904 if on("reloc-install-receipt") {
905 n.Op.Receipt = nil
906 }
907 n.Op.Attempts = 0
908 n.Op.ProbeStop, n.Op.ProbePassedAt = nil, 0
909 n.Op.InstalledOn = addUniq(n.Op.InstalledOn, box)
910 n.Boxes = addUniq(n.Boxes, box)
911}
912
913// relocInstalling: the target builds P, the remote prepared generation,
914// exactly as a landing builds its generation (moves §7.6, L16): the
915// snapshot [1, N] of the pinned generation, create-only and acknowledged,
916// then the level-0 anchor [N, N], acknowledged, then the installed CAS
917// (ToInstalled, R7-4). A pin at N = 0 builds nothing: the probe is an
918// empty bootstrap (claim P6-D3). Only op.exec installs (rule
919// reloc-exec-only-installs, HA2: being in the region is never enough).
920func (w *world) relocInstalling(b *box, c *claim) {
921 op, h := c.Op, c.Holder
922 if on("reloc-exec-only-installs") && op.Exec != b.ID {
923 return
924 }
925 loc, g := h.Location, op.Prepared
926 bk := w.buckets[b.Region]
927 installed := func(n *claim) bool {
928 ok := sameOp(n.Op.ID, op.ID) && n.Op.Kind == "relocating" && n.Op.Phase == "installing" && n.Op.Exec == op.Exec && n.Op.Prepared == g
929 if ok && on("reloc-exec-ack-barrier") && !acked(n) {
930 ok = false // ToInstalled's requireAcks (rev 4, Astra R1-4)
931 }
932 if ok {
933 n.Op.Phase = "installed"
934 }
935 return ok
936 }
937 if !w.buildPreparedFiles(b, c) {
938 return // Task 4: P's prepared files manifest first
939 }
940 if op.Pin.TX == 0 {
941 w.cas(c, installed)
942 return
943 }
944 if op.BaseKey == "" {
945 db := w.restoreUpTo(b.Region, h.Inc, loc, op.Pin.Gen, op.Pin.TX)
946 if db == nil || db.Pos != op.Pin.TX {
947 w.stats["build: the pinned set cannot be restored yet"]++
948 return
949 }
950 key := snapKey(loc, g, op.Pin.TX)
951 if !w.ackedPut(b, key, snapshotOf(db, h.Inc), true) {
952 return
953 }
954 v := bk.current(key)
955 if v == nil {
956 return
957 }
958 w.cas(c, func(n *claim) bool {
959 ok := sameOp(n.Op.ID, op.ID) && n.Op.Kind == "relocating" && n.Op.Phase == "installing" && n.Op.Exec == op.Exec && n.Op.Prepared == g && n.Op.BaseKey == ""
960 if ok {
961 n.Op.BaseKey, n.Op.BaseVer, n.Op.BaseTX = key, v.ID, op.Pin.TX
962 }
963 return ok
964 })
965 return
966 }
967 if !w.anchorAcked(b, loc, g, op.BaseKey, op.BaseVer, op.BaseTX) {
968 return
969 }
970 if w.cas(c, installed) {
971 w.logf("relocation %s: P %q built on box %d at TXID %d", op.ID, g, b.ID, op.BaseTX)
972 }
973}
974
975// buildPreparedFiles writes P's prepared files manifest before anything
976// of the database (1b-next Task 8b; CopyPinnedManifest; claim rev 6.30
977// R18-1, moves rev 7.22 §7.6): create-only at sequence 0, bound to
978// {location, P, op id, install_stamp}, with the pinned manifest's files
979// and wake state; the installed CAS waits for its acknowledgement
980// (ToInstalled requires files_seq 0 and the pin's source sequence as
981// provenance). The first admission consumes it as the session
982// publisher's baseline, and the publisher starts at sequence 1 (rule
983// files-prepared-seq-zero); the mutation keeps the source manifest's
984// sequence, as rev 14 did, so the session's first manifests sort below
985// it. In the one-box shape there is no publisher (ruling 11) and nothing
986// to write.
987func (w *world) buildPreparedFiles(b *box, c *claim) bool {
988 if w.perRegion <= 1 {
989 return true
990 }
991 op, h := c.Op, c.Holder
992 seq := 0
993 if !on("files-prepared-seq-zero") && op.Pin != nil {
994 seq = op.Pin.ManSeq
995 }
996 bl := &blob{Kind: "files", Inc: h.Inc, Gen: op.Prepared, Seq: seq, Files: w.relocFiles(c).clone(), Timers: append([]int(nil), w.relocTimers(c)...), Text: fmt.Sprintf("%s/%d", op.ID, op.InstallStamp), Base: w.issued}
997 return w.ackedPut(b, manifestKey(h.Location, op.Prepared, seq), bl, true)
998}
999
1000// localInstall advances the executor's local install by one step (§5.8,
1001// B1-2; 1b-next Task 8c's Install) and reports whether it is whole: park
1002// what an earlier stay left as retire-artifacts of its stamp (claim §6.4),
1003// extract the files, replace the timers with the pinned wake state (the
1004// files Done marker last), render and materialize with current inputs,
1005// and write the box's install journal (its receipt) under install_stamp.
1006// EnsureLocal reuses it to restore a retired install under the same
1007// stamp.
1008func (w *world) localInstall(b *box, c *claim) bool {
1009 op, h := c.Op, c.Holder
1010 g := op.Prepared
1011 if b.Files == nil || b.Files.Gen != g {
1012 for _, st := range []int{dbStamp(b.DB), filesStamp(b.Files)} {
1013 if st != 0 && st != b.stampOf(h.Inc) {
1014 w.retireArtifacts(b, h.Inc, st)
1015 }
1016 }
1017 if b.DB != nil {
1018 b.DB = nil // never reused (claim R6); the probe's admission restores
1019 b.Parked++
1020 }
1021 src := w.relocFiles(c)
1022 if src == nil {
1023 w.nextID++
1024 src = &filesDir{Inc: h.Inc, Key: w.nextID, Env: w.nextID}
1025 }
1026 f := src.clone()
1027 f.Inc, f.Gen, f.Done, f.Stamp = h.Inc, g, false, b.stampOf(h.Inc)
1028 if !on("markers-last") {
1029 f.Done = true
1030 }
1031 b.Files = f
1032 b.pendingTimers = append([]int(nil), w.relocTimers(c)...)
1033 return false
1034 }
1035 if !b.Files.Done {
1036 b.Timers = append([]int(nil), b.pendingTimers...) // ReplaceTimers: the pinned wake state
1037 b.Attempt, b.History = 0, nil
1038 b.Files.Done = true
1039 b.installEpoch = b.Epoch
1040 return false
1041 }
1042 if b.HasKey {
1043 b.Config, b.CfgVer = h.Inc, w.cfgVer[h.App]
1044 }
1045 b.Live, b.LiveInc = h.App, h.Inc
1046 b.receipt = &receiptEntry{Op: op.ID, Box: b.ID, Stamp: op.InstallStamp}
1047 return true
1048}
1049
1050// relocInstalled: the executor's local install, then its receipt by CAS
1051// (RecordLocalInstall), bound to this box and install_stamp; probing
1052// needs that exact receipt (ToProbing; rule reloc-install-receipt), and
1053// the install is verified, and restored if a lost lease's scoped cleanup
1054// retired it, before the probing CAS (EnsureLocal; rule
1055// reloc-ensure-local). The mutation of reloc-install-receipt lets a
1056// replacement inherit its predecessor's receipt and skip its install.
1057func (w *world) relocInstalled(b *box, c *claim) {
1058 op := c.Op
1059 if on("reloc-exec-only-installs") && op.Exec != b.ID {
1060 return
1061 }
1062 probing := func(n *claim) bool {
1063 ok := sameOp(n.Op.ID, op.ID) && n.Op.Kind == "relocating" && n.Op.Phase == "installed" && n.Op.Exec == op.Exec && n.Op.Receipt != nil
1064 if ok && on("reloc-exec-ack-barrier") && !acked(n) {
1065 ok = false // ToProbing's requireAcks (rev 4, Astra R1-4)
1066 }
1067 if ok {
1068 n.Op.Phase, n.Op.ProbingSince = "probing", w.now
1069 }
1070 return ok
1071 }
1072 if r := op.Receipt; r != nil {
1073 mine := r.Box == b.ID && r.Stamp == op.InstallStamp
1074 if !mine && !on("reloc-install-receipt") {
1075 w.cas(c, probing) // the mutation: any receipt will do, the install is skipped
1076 return
1077 }
1078 if mine {
1079 if w.ensureLocal(b, c) {
1080 w.cas(c, probing)
1081 }
1082 return
1083 }
1084 }
1085 if op.Exec != b.ID || !w.localInstall(b, c) {
1086 return
1087 }
1088 w.cas(c, func(n *claim) bool {
1089 ok := sameOp(n.Op.ID, op.ID) && n.Op.Kind == "relocating" && n.Op.Phase == "installed" && n.Op.Exec == b.ID && n.Op.InstallStamp == op.InstallStamp && !preparedOpened(n)
1090 if ok {
1091 n.Op.Receipt = &receiptEntry{Op: op.ID, Box: b.ID, Stamp: op.InstallStamp}
1092 }
1093 return ok
1094 })
1095}
1096
1097// ensureLocal is EnsureLocal (1b-next rev 15 finding 2): before P's first
1098// admission the executor's local install must be whole, so it is
1099// verified before the probing CAS and before every attempt, and a
1100// retired one (the scoped cleanup of a lost controller lease retires an
1101// unopened P's local install, role()) is restored under the same
1102// install_stamp. It never touches an opened P. It reports whether the
1103// install is whole now. The mutation goes on without verifying, so P's
1104// first admission can meet a missing install.
1105func (w *world) ensureLocal(b *box, c *claim) bool {
1106 op := c.Op
1107 if preparedOpened(c) || !on("reloc-ensure-local") {
1108 return true
1109 }
1110 if b.Files != nil && b.Files.Gen == op.Prepared && b.Files.Done && b.receipt != nil && b.receipt.Op == op.ID && b.receipt.Stamp == op.InstallStamp {
1111 return true
1112 }
1113 if w.localInstall(b, c) {
1114 w.stats["local install restored before the first admission"]++
1115 w.logf("relocation %s: box %d restored its retired local install under stamp %d", op.ID, b.ID, op.InstallStamp)
1116 return true
1117 }
1118 return false
1119}
1120
1121// verifyReceipt is VerifyReceipt (1b-next rev 16 finding 1): once P is
1122// opened the local install is never remade, and before every attempt the
1123// box's durable receipt (its install journal) must still be the one the
1124// claim journaled; a missing or different one marks the operation stuck
1125// and nothing is admitted. Files, env and timers the admitted writers
1126// changed are never compared. The mutation goes on to the next attempt.
1127func (w *world) verifyReceipt(b *box, c *claim) bool {
1128 op := c.Op
1129 if r := b.receipt; r != nil && r.Op == op.ID && r.Box == b.ID && r.Stamp == op.InstallStamp {
1130 return true
1131 }
1132 if !on("reloc-verify-receipt") {
1133 w.stats["receipt missing after P opened, attempt made anyway (mutation)"]++
1134 return true
1135 }
1136 w.markStuck(c, fmt.Sprintf("box %d's local install receipt is missing after P was admitted", b.ID))
1137 return false
1138}
1139
1140// preparedOpened is PreparedOpened: an admission has named P (B2-5). The
1141// admission that opens P journals prepared_opened in the same CAS
1142// (admitCommit), so the fact outlives holder.admissions' trim to eight
1143// (1b-next rev 12 S1; rule reloc-prepared-opened-journal); the mutation
1144// reads the retained admissions alone, as rev 11 did.
1145func preparedOpened(c *claim) bool {
1146 if c.Op.Prepared == "" || c.Holder == nil {
1147 return false
1148 }
1149 if on("reloc-prepared-opened-journal") && c.Op.PreparedOpened == c.Op.Prepared {
1150 return true
1151 }
1152 for _, e := range c.Holder.Admissions {
1153 if e.Gen == c.Op.Prepared {
1154 return true
1155 }
1156 }
1157 return false
1158}
1159
1160// relocProbing: each attempt is counted in the claim before it is made
1161// (BeginProbeAttempt, ruling 19; rule reloc-probe-attempts: a crash, a
1162// lost response or a restart never buys a fourth), then one cold
1163// AdmitWriter by op.exec into P (the first) or a fresh generation
1164// restored from the previous attempt's (claim §6.4a; spawn and admitWriter
1165// do both), then the probe. The fourth is refused and the target enters
1166// probe-failed, as it does past the probe deadline. Before P's first
1167// admission the local install is verified and restored (ensureLocal);
1168// after it, the receipt alone is verified (verifyReceipt) and the install
1169// is never remade (rule reloc-no-reinstall; its mutation reapplies the
1170// pinned wake state on every agent restart, as rev 4.2's install marker
1171// did, over the timers the admitted session registered).
1172func (w *world) relocProbing(b *box, c *claim) {
1173 op, h := c.Op, c.Holder
1174 failed := func(n *claim) bool {
1175 ok := sameOp(n.Op.ID, op.ID) && n.Op.Kind == "relocating" && n.Op.Phase == "probing" && n.Op.Exec == op.Exec
1176 if ok {
1177 n.Op.Phase, n.Op.FailedAt, n.Op.Release = "probe-failed", w.now, nil
1178 }
1179 return ok
1180 }
1181 if op.Exec != b.ID {
1182 if w.now > op.ProbingSince+probeDeadline && op.Exec >= 0 {
1183 w.stats["probe-failed: deadline"]++
1184 w.cas(c, failed) // the worker's: ToProbeFailed past the probe deadline
1185 }
1186 return
1187 }
1188 if b.Row == nil || b.Row.Status == "ready" {
1189 w.holdRow(b, "relocating", op.ID, c)
1190 }
1191 opened := preparedOpened(c)
1192 if !opened {
1193 if !w.ensureLocal(b, c) {
1194 return
1195 }
1196 } else if !w.verifyReceipt(b, c) {
1197 return
1198 }
1199 if !on("reloc-no-reinstall") && opened && b.installEpoch != b.Epoch {
1200 b.Timers = append([]int(nil), w.relocTimers(c)...) // the mutation: the install marker re-applied after a restart
1201 b.installEpoch = b.Epoch
1202 w.stats["local install remade after admission (mutation)"]++
1203 }
1204 if !w.admits(b, c, op.ID) {
1205 return
1206 }
1207 if b.HasKey {
1208 b.Config, b.CfgVer, b.Live, b.LiveInc = h.Inc, w.cfgVer[h.App], h.App, h.Inc
1209 }
1210 probed := func(n *claim) bool {
1211 ok := sameOp(n.Op.ID, op.ID) && n.Op.Phase == "probing" && n.Op.Exec == b.ID
1212 if ok {
1213 n.Op.Phase, n.Op.ProbePassedAt = "probed", w.now // RecordProbe's At (ruling 67's bound)
1214 }
1215 return ok
1216 }
1217 if hasMain(b) {
1218 w.probeAccepted(b, c)
1219 w.cas(c, probed)
1220 return
1221 }
1222 if w.now > op.ProbingSince+probeDeadline {
1223 w.stats["probe-failed: deadline"]++
1224 w.cas(c, failed)
1225 return
1226 }
1227 tag := fmt.Sprintf("%s/%d", op.ID, op.Attempts)
1228 if b.open == nil {
1229 // a new attempt: counted before it is made. attemptTag is the
1230 // attempt this agent counted (tag) or made (tag+"+"); one made and
1231 // ended, or none counted, is counted now
1232 if on("reloc-probe-attempts") && b.attemptTag != tag {
1233 if preparedOpened(c) && w.c11Fallback() {
1234 // Preflight's ErrLaterAttemptUnlaunchable (ruling 58, until
1235 // claim ask C11): root pins every relocating launch to
1236 // install_stamp, so a cold attempt after P's opening is not
1237 // made; probe-failed now, before any attempt is counted
1238 w.stats["probe-failed: a later attempt root cannot launch (C11 open)"]++
1239 w.cas(c, failed)
1240 return
1241 }
1242 if op.Attempts >= probeTries {
1243 w.stats["probe-failed: attempts"]++
1244 w.cas(c, failed)
1245 return
1246 }
1247 if w.cas(c, func(n *claim) bool {
1248 ok := sameOp(n.Op.ID, op.ID) && n.Op.Phase == "probing" && n.Op.Exec == b.ID && n.Op.Attempts == op.Attempts
1249 if ok {
1250 n.Op.Attempts++
1251 }
1252 return ok
1253 }) {
1254 b.attemptTag = fmt.Sprintf("%s/%d", op.ID, op.Attempts+1)
1255 }
1256 return
1257 }
1258 if !on("reloc-probe-attempts") && op.ProbeTries >= probeTries {
1259 w.stats["probe-failed: attempts"]++
1260 w.cas(c, failed) // the mutation: failures counted after the fact, as rev 1 did
1261 return
1262 }
1263 }
1264 if b.open == nil {
1265 b.attemptTag = tag + "+" // made now
1266 }
1267 if w.spawn(b, c, op.Prepared, true) {
1268 w.probeAccepted(b, c)
1269 w.cas(c, probed)
1270 }
1271}
1272
1273// relocProbeFailed: the live target runs its own release sequence (§5.8
1274// probe-failed; JournalRelease), then a worker starts the nested
1275// relocation from pinning (PinAfterRelease with a next target), pinned to
1276// at least the target's final TXID (the release entry, which
1277// relocPinning reads), the failed target excluded and counted as a
1278// replacement. Exhaustion is checked before a replacement is looked for
1279// (1b-next R1-16): with the replacements used the operation is stuck. A
1280// target that cannot finish its release within releaseDeadline goes to
1281// stuck (ruling 14): nothing moves away from a live box without its stop
1282// evidence. With no eligible box for releaseDeadline the operation is
1283// stuck too (model ruling 14: 1b-next waits for a spare with no bound,
1284// and in a two-box region the source is never a spare).
1285func (w *world) relocProbeFailed(b *box, c *claim) {
1286 op, h := c.Op, c.Holder
1287 if op.Release == nil || op.Release.Box != op.Exec {
1288 if on("reloc-release-deadline") && w.now > op.FailedAt+releaseDeadline {
1289 w.markStuck(c, fmt.Sprintf("box %d cannot finish its release sequence", op.Exec))
1290 return
1291 }
1292 if b.ID != op.Exec {
1293 return
1294 }
1295 w.relocJournalRelease(b, c, false)
1296 return
1297 }
1298 if on("reloc-replacement-cap") && op.Replacements >= maxReplacements {
1299 w.markStuck(c, fmt.Sprintf("two replacements of the target used; a third was needed at %s", op.Phase))
1300 return
1301 }
1302 next := w.chooseBoxExcept(op.SrcRegion, h.App, relocExcept(op, op.Exec)...)
1303 if next == nil {
1304 // 1b-next ruling 26 (rev 17, finding F1; rule reloc-no-spare-bound):
1305 // with no spare ReleaseDeadline after probe_failed_since, stuck;
1306 // never the source, which the operator's resolve alone may name.
1307 // The mutation waits for a spare for ever, rev 16's behaviour
1308 if on("reloc-no-spare-bound") && w.now > op.FailedAt+releaseDeadline {
1309 w.markStuck(c, "no eligible box for the nested relocation")
1310 }
1311 return
1312 }
1313 w.cas(c, func(n *claim) bool {
1314 if !sameOp(n.Op.ID, op.ID) || n.Op.Kind != "relocating" || n.Op.Phase != "probe-failed" || n.Op.Release == nil || n.Op.Release.Box != n.Op.Exec {
1315 return false
1316 }
1317 if !w.nameReplacement(n, next.ID, "pinning") {
1318 return n.Op.Stuck != "" // stuck: the CAS records it
1319 }
1320 w.abandonPrepared(n)
1321 n.Op.Exec = -1
1322 n.Op.Pin, n.Op.Prepared, n.Op.Receipt = nil, "", nil
1323 n.Op.BaseKey, n.Op.BaseVer, n.Op.BaseTX = "", 0, 0
1324 n.Op.ProbeTries, n.Op.Attempts = 0, 0
1325 w.stats["nested relocation after probe-failed"]++
1326 return true
1327 })
1328}
1329
1330// relocSettle is the end of a relocation as 1b-next builds it (L18 joint
1331// round finding 2; regionha_completion_test.go): at probed (1b-next's
1332// probing with the probe journaled) op.exec stops the probe's session with
1333// root's receipt (relocStopProbe, ruling 49), bounded by ruling 67 (no
1334// stop ReleaseDeadline after the probe passed is stuck for an operator);
1335// then any worker of the region makes the serving CAS (relocComplete,
1336// CompleteRelocation), which records the completion. The row stays held:
1337// the completion pass releases it after the discharge, and the first
1338// serving wake is a separate admission (N6). Rev 7.20's settle window and
1339// open are gone: a probe that dies after it passed is stopped all the same.
1340func (w *world) relocSettle(b *box, c *claim) {
1341 op := c.Op
1342 if op.Phase != "probed" {
1343 return
1344 }
1345 if op.ProbeStop == nil && on("reloc-probe-stop") {
1346 if w.now > op.ProbePassedAt+releaseDeadline {
1347 w.markStuck(c, fmt.Sprintf("box %d passed its probe but has no root stop receipt for session %d", op.Exec, c.Holder.Session))
1348 return
1349 }
1350 if b.ID == op.Exec {
1351 w.relocStopProbe(b, c)
1352 }
1353 return
1354 }
1355 w.relocComplete(c)
1356}
1357
1358// scopedCleanup is the relocation controller's scoped cleanup when its
1359// lease was lost (1b-next rev 14 finding 1, Task 7's live): an unopened
1360// P's local install is retired and its receipt (the install journal)
1361// kept, so EnsureLocal restores it before the first admission. role()
1362// calls it when the executor's execution lease expired before it renewed
1363// it (a freeze, a pause, a partition longer than the TTL).
1364func (w *world) scopedCleanup(b *box, c *claim) {
1365 if c == nil || c.Holder == nil || c.Op.Kind != "relocating" || c.Op.Exec != b.ID || preparedOpened(c) {
1366 return
1367 }
1368 if f := b.Files; f != nil && f.Gen == c.Op.Prepared && f.Gen != "" {
1369 w.retireArtifacts(b, c.Holder.Inc, f.Stamp)
1370 w.stats["relocation controller lost: unopened local install retired"]++
1371 w.logf("box %d: relocation %s's controller lease was lost; the scoped cleanup retired its unopened local install", b.ID, c.Op.ID)
1372 }
1373}
1374
1375// ---------- termination, enrollment and the fence (region HA §5.9; H1, H10, H11, H16, H17, H19) ----------
1376
1377// terminate is the cloud terminating a box (H1): nothing on it runs
1378// again, it never returns (a stopped box may; heal skips a terminated
1379// one), and the requests it issued still complete at their own time,
1380// which is the late-PUT-after-termination case. The two-box step draws
1381// it; it never leaves the world without an enrolled live box, since H17's
1382// cross-region confirmer must exist for a replacement to enroll.
1383func (w *world) terminate(b *box) {
1384 if b.Destroyed {
1385 return
1386 }
1387 enrolled := 0
1388 for _, x := range w.liveBoxes() {
1389 if x.Enrolled && x != b {
1390 enrolled++
1391 }
1392 }
1393 if enrolled == 0 {
1394 return
1395 }
1396 b.Destroyed, b.Terminated, b.TerminatedAt = true, true, w.now
1397 b.Procs, b.plans = nil, nil
1398 w.stats["terminated"]++
1399 w.logf("box %d terminated by the cloud (region %d)", b.ID, b.Region)
1400}
1401
1402// launchReplacements: the group keeps the region at its minimum (H5): a
1403// replacement launches replaceDelay steps after the termination that left
1404// the region short, unenrolled (H16), with its volume flag drawn (two-box
1405// sweeps only: a witness world's quiet source draws nothing).
1406func (w *world) launchReplacements() {
1407 for r := 0; r < nRegions; r++ {
1408 if len(w.regionBoxes(r)) >= w.regionMin(r) {
1409 continue
1410 }
1411 last := 0
1412 for _, b := range w.boxes {
1413 if b.Region == r && b.Terminated && b.TerminatedAt > last {
1414 last = b.TerminatedAt
1415 }
1416 }
1417 if last == 0 || w.now < last+replaceDelay {
1418 continue
1419 }
1420 w.nextID++
1421 nb := &box{ID: 100 + w.nextID, Region: r, HasKey: true, EnrollAt: w.now, DeleteOnTermination: w.rng.Intn(8) != 0}
1422 nb.forget()
1423 w.boxes = append(w.boxes, nb)
1424 w.stats["replacements launched"]++
1425 w.logf("box %d launched in region %d, unenrolled", nb.ID, r)
1426 }
1427}
1428
1429// enrollStep is automatic enrollment (H16, L19): a launched box's boxid
1430// evidence verifies and an enrolled box confirms its group membership
1431// enrollDelay steps after launch. Any enrolled box of any region may
1432// confirm (H17: the first box of an emptied region is confirmed from
1433// another region); with none, nothing enrolls.
1434func (w *world) enrollStep() {
1435 confirmer := false
1436 for _, b := range w.liveBoxes() {
1437 if b.Enrolled {
1438 confirmer = true
1439 }
1440 }
1441 if !confirmer {
1442 return
1443 }
1444 for _, b := range w.liveBoxes() {
1445 if !b.Enrolled && w.now >= b.EnrollAt+enrollDelay {
1446 b.Enrolled = true
1447 w.stats["boxes enrolled automatically"]++
1448 w.logf("box %d enrolled (region %d)", b.ID, b.Region)
1449 }
1450 }
1451}
1452
1453// twoBoxStep is the two-box shape's extra step (ruling 1): replacements
1454// launch and enroll; during the chaos a box is terminated now and then,
1455// and a drain is asked for now and then.
1456func (w *world) twoBoxStep() {
1457 w.launchReplacements()
1458 w.enrollStep()
1459 for _, b := range w.liveBoxes() {
1460 w.renewController(b)
1461 w.publishFiles(b)
1462 }
1463 if !w.calm && w.fail["terminate"] && w.rng.Intn(200) == 0 {
1464 bs := w.liveBoxes()
1465 if b := bs[w.rng.Intn(len(bs))]; w.regionHAOwnsLoss(b) {
1466 w.terminate(b)
1467 }
1468 }
1469 if !w.calm && w.rng.Intn(60) == 0 {
1470 w.userRelocate()
1471 }
1472 if !w.calm && w.rng.Intn(400) == 0 {
1473 bs := w.liveBoxes()
1474 w.updateExecutable(bs[w.rng.Intn(len(bs))]) // claim §5.10: refused while a controller record exists (rev 4)
1475 }
1476}
1477
1478// regionHAOwnsLoss: the loss of b is region HA's to recover (model ruling
1479// 15): the claim holds the host and names b, if at all, only as a serving
1480// executor, a relocation's participant or a chore's owner; a box holding
1481// a legacy host's state before its adoption is never terminated. A participant of a move,
1482// a landing, a rollback, a revert, a push, a create or a delete is
1483// recovered from a lost box by moves' and the claim's declare-lost paths
1484// (L14), which the model walks for a destroyed box that is stopped and
1485// may return; the sweep never terminates one.
1486func (w *world) regionHAOwnsLoss(b *box) bool {
1487 c := w.claim
1488 if c == nil || c.Holder == nil {
1489 return b.Row == nil && b.DB == nil && b.Files == nil // a legacy host not yet adopted is not region HA's
1490 }
1491 switch c.Op.Kind {
1492 case "serving", "relocating":
1493 if c.Op.Exec == b.ID && w.perRegion > 1 {
1494 // the L21 window (plan ruling 17): an executor whose newest
1495 // landed manifest carries no instance.key (none landed yet, or
1496 // only the pre-start copy) is unrecoverable by any relocation:
1497 // its failover is stuck by design (1b-next ruling 25) and never
1498 // converges, so the sweep leaves it alone; the witness for
1499 // files-pin-needs-key and TestFailoverBeforeTheFirstManifest
1500 // show it fail safe
1501 _, _, _, _, m := w.newestManifest(b.Region, c.Holder.Location, c.Holder.Gen)
1502 return manifestHasKey(m)
1503 }
1504 return true
1505 }
1506 return !(c.Op.Exec == b.ID || contains(c.Op.Parts, b.ID) || c.Op.Src == b.ID || c.Op.Tgt == b.ID || c.Op.NewBox == b.ID)
1507}
1508
1509// fenceStep is the regional worker's termination fence (§5.9; claim L2
1510// to L5; Worker.Step's "evidence first"): for a terminated, enrolled box
1511// of its own region that the claim still names, it writes the fence in
1512// one CAS (fence). A stopped box is never fenced (rule
1513// fence-terminated-only: "stopped and stopping do not count, because a
1514// stopped box can start again with its disk"); the mutation fences any
1515// destroyed box. An unenrolled box has no roster entry to bind the fence
1516// to (HA7) and is never fenced. The source's fence is consumed before the
1517// target's, as Worker.Step orders them.
1518func (w *world) fenceStep(b *box, c *claim) (changed, pending bool) {
1519 if c == nil || !b.Enrolled || contains(c.Lost, b.ID) || w.haOff {
1520 return false, false
1521 }
1522 var cands []*box
1523 for _, x := range w.boxes {
1524 if x.Region != b.Region || x == b || !x.Destroyed || !x.Enrolled || contains(c.Lost, x.ID) {
1525 continue
1526 }
1527 if !x.Terminated && on("fence-terminated-only") {
1528 continue
1529 }
1530 if !w.claimNames(c, x) {
1531 continue
1532 }
1533 cands = append(cands, x)
1534 }
1535 if len(cands) == 0 {
1536 return false, false
1537 }
1538 // Worker.Step's order (rev 8): the source's fence, then the target's,
1539 // then any other box the claim names (a chore's owner)
1540 rank := func(x *box) int {
1541 switch {
1542 case c.Op.Kind == "relocating" && x.ID == c.Op.Src:
1543 return 0
1544 case c.Op.Kind == "relocating" && x.ID == c.Op.Tgt:
1545 return 1
1546 }
1547 return 2
1548 }
1549 sort.SliceStable(cands, func(i, j int) bool { return rank(cands[i]) < rank(cands[j]) })
1550 if x := cands[0]; x.FenceObservedAt == 0 {
1551 // hostfence.TerminationWorker: the first worker to observe the
1552 // terminated state writes the box-fences record with that
1553 // observed_at; every deadline and delay runs from it, never from
1554 // the termination itself, which no worker saw (rev 4, Astra R1-5)
1555 x.FenceObservedAt = w.now
1556 w.logf("box %d observed box %d terminated: fence record, observed_at %d", b.ID, x.ID, w.now)
1557 }
1558 before := w.claim.Ver
1559 w.fence(b, c, cands[0])
1560 if w.claim.Ver != before {
1561 return true, false
1562 }
1563 // nothing written: for a relocation's source or target the fence is
1564 // pending (rule reloc-fence-pending-blocks)
1565 return false, c.Op.Kind == "relocating" && rank(cands[0]) < 2
1566}
1567
1568// claimNames: the claim gives x a role, so a fence changes it.
1569func (w *world) claimNames(c *claim, x *box) bool {
1570 if c.Op.Exec == x.ID || contains(c.Op.Parts, x.ID) || c.Op.Kind == "creating" && c.Op.NewBox == x.ID || c.Op.Kind == "moving" && c.Op.Src == x.ID {
1571 return true
1572 }
1573 if c.Op.Kind == "relocating" && (c.Op.Src == x.ID || c.Op.Tgt == x.ID) {
1574 return true
1575 }
1576 for _, o := range c.Obl {
1577 if o.Owner == boxOwner(x.ID) {
1578 return true
1579 }
1580 }
1581 return w.owesLocal(c, x)
1582}
1583
1584// boxOwner is an obligation's owner box:<b> (claim §2): the legacy form a
1585// regional chore may carry; "" is region:<r>, any eligible worker of the
1586// region (P6-D5).
1587func boxOwner(id int) string { return fmt.Sprintf("box:%d", id) }
1588
1589// fence is the fence CAS for x, observed terminated by worker b. Moves'
1590// cases keep their owners: a pre-flip move's source converts (moves
1591// §7.10, the existing CAS); a move's target before the flip, a landing's
1592// or a rollback's target are moves' declare-lost path, which this model
1593// gives the operator (L14); a create's box is cancelled by the recovery
1594// pass (Task 1). Everything else is one CAS here: the box goes on lost
1595// with the fence reference and observed_at, its chores are discharged if
1596// its volume evidence qualifies, its regional work passes to its region,
1597// and the operation's rule applies (StartRelocation's failover,
1598// FenceSource, ReplaceTarget, FenceExhaustedTarget). A target with no
1599// spare to name is fenced on a later pass (Worker.next: "no eligible box
1600// yet").
1601func (w *world) fence(b *box, c *claim, x *box) {
1602 op := c.Op
1603 switch {
1604 case op.Kind == "moving" && op.Src == x.ID:
1605 if on("fence-converts-moving") && !op.Committed && !(w.rec != nil && w.rec.MoveID == op.ID && w.rec.MoveState == "landing") {
1606 w.convertToRelocating(c, x)
1607 }
1608 return
1609 case op.Kind == "landing" && op.Committed && op.Tgt == x.ID && on("fence-converts-committed-landing"):
1610 // finding F5 (rev 4): the record committed the landing, so no
1611 // rollback is possible, and its dead target can never finish it.
1612 // The fence makes it a failover in the target's region, as a
1613 // serving executor's fence does (plan ruling 20); the mutation
1614 // leaves it to the operator's path, which has nothing to do. The
1615 // failover keeps the landing's op id, as hostclaim
1616 // ConvertCommittedLanding does (and as convertToRelocating does
1617 // before the flip): the move's admission registration is the move
1618 // id, and judgeRegistration drains it 90 seconds in unless the
1619 // claim's operation carries that id (hostops RegisterMove)
1620 id := op.ID
1621 if w.cas(c, func(n *claim) bool {
1622 if !sameOp(n.Op.ID, id) || n.Op.Kind != "landing" || !n.Op.Committed || contains(n.Lost, x.ID) {
1623 return false
1624 }
1625 w.startFailover(n, x, nil, id) // the model's conversion (plan ruling 20): its target is named at pinning
1626 w.fenceBox(n, x)
1627 return true
1628 }) {
1629 w.stats["fences written"]++
1630 w.stats["relocation started: failover of a committed landing"]++
1631 w.logf("box %d fenced box %d, the committed landing %s's dead target: failover in region %d", b.ID, x.ID, id, x.Region)
1632 w.declareTimers(x, w.claim.Holder)
1633 }
1634 return
1635 case (op.Kind == "landing" || op.Kind == "restoring") && op.Tgt == x.ID:
1636 // claim D2's automatic termination for a move's target (rev 4,
1637 // claim §11 "automatic target loss racing the record commit CAS"):
1638 // the same record-first declared loss the operator makes
1639 w.declareTargetLost(c)
1640 return
1641 case op.Kind == "moving" && op.Tgt == x.ID, op.Kind == "creating":
1642 return
1643 case op.Kind == "relocating" && op.Stuck != "":
1644 return // Worker.Step consumes no fence of a stuck operation: the operator's resolve does
1645 }
1646 id := op.ID
1647 var to *box
1648 if c.Holder != nil && op.Kind == "serving" && op.Exec == x.ID {
1649 var ok bool
1650 if to, ok = w.failoverStart(c, x); !ok {
1651 return // no start yet: the claim, and the fence, wait for a later pass
1652 }
1653 }
1654 if w.cas(c, func(n *claim) bool {
1655 if !sameOp(n.Op.ID, id) || n.Op.Kind != op.Kind || contains(n.Lost, x.ID) {
1656 return false
1657 }
1658 switch {
1659 case n.Holder != nil && n.Op.Kind == "serving" && n.Op.Exec == x.ID:
1660 if !w.failoverStartAllowed(n, x, to) {
1661 w.stats["failover start: refused by the start CAS"]++
1662 return false
1663 }
1664 w.startFailover(n, x, to, w.newID("rl"))
1665 case n.Op.Kind == "relocating" && n.Op.Stuck != "":
1666 return false
1667 case n.Op.Kind == "relocating" && n.Op.Src == x.ID:
1668 w.fenceSource(n, x)
1669 if n.Op.Tgt == x.ID && !w.replaceTarget(n, x) {
1670 // an operator's replay made the source eligible and named it
1671 // the target (model ruling 13): its fence is a target loss
1672 // too, or pinning waits on a lost target for ever
1673 return false // no spare yet
1674 }
1675 case n.Op.Kind == "relocating" && n.Op.Tgt == x.ID:
1676 if !w.replaceTarget(n, x) {
1677 return false // no spare yet
1678 }
1679 }
1680 w.fenceBox(n, x)
1681 return true
1682 }) {
1683 w.stats["fences written"]++
1684 w.logf("box %d fenced box %d (terminated at %d) under %s/%s", b.ID, x.ID, x.TerminatedAt, w.claim.Op.Kind, w.claim.Op.ID)
1685 w.declareTimers(x, w.claim.Holder)
1686 }
1687}
1688
1689// fenceBox is plan 1b-next's fenceBox: lost with the fence reference
1690// (FenceBox, FenceAt); the box's regional work (a park-generation or
1691// park-replica owned by box:<x>) passed to region:<r> keeping its
1692// identity (rule fence-reassigns-regional, 1b-next rev 6 finding 2, as
1693// DeclareLost does for a declared loss); and the box's park-local chores
1694// discharged only when its volume evidence qualifies (L3, H19: every
1695// attached volume DeleteOnTermination while it ran); otherwise they stay
1696// open for the operator path, and nothing about recovery waits on them.
1697// The mutation of fence-discharge-qualified discharges regardless (INV-26
1698// is the oracle); the mutation of fence-reassigns-regional leaves the
1699// regional work with the dead box, where nothing runs it.
1700func (w *world) fenceBox(n *claim, x *box) {
1701 n.Lost = addUniq(n.Lost, x.ID)
1702 at := x.FenceObservedAt
1703 if at == 0 {
1704 at = w.now
1705 }
1706 n.Op.FenceBox, n.Op.FenceAt = x.ID, at
1707 n.Op.Observed = append(n.Op.Observed, at) // fence.<box>.observed_at, retained for every later pin (JournalPin)
1708 var parts []int
1709 for _, p := range n.Op.Parts {
1710 if p != x.ID {
1711 parts = append(parts, p)
1712 }
1713 }
1714 n.Op.Parts = parts
1715 if on("fence-reassigns-regional") {
1716 for i := range n.Obl {
1717 if o := &n.Obl[i]; o.Kind != "park-local" && o.Owner == boxOwner(x.ID) {
1718 o.Owner = "" // region:<r>, the chore's id, operation, retirement and scope kept
1719 w.stats["fence: regional work passed to the region"]++
1720 }
1721 }
1722 }
1723 // a fence exists only for a terminated box (TerminationWorker verifies
1724 // terminated), and only a terminated box's volume evidence says
1725 // anything: a stopped box keeps its disk, so its park-local chores
1726 // stay owed whatever DeleteOnTermination says (claim §7.3 D5, H19;
1727 // rev 10, Astra round 7). The operator's declared loss of a stopped
1728 // box reaches this function too, and discharges nothing
1729 qualified := x.Terminated && x.DeleteOnTermination
1730 if !on("fence-discharge-qualified") {
1731 qualified = true
1732 }
1733 if !qualified {
1734 w.stats["fence: park-local chores left open (volume evidence)"]++
1735 return
1736 }
1737 var keep []obligation
1738 for _, o := range n.Obl {
1739 if o.Kind == "park-local" && o.Box == x.ID {
1740 if !x.Terminated || !x.DeleteOnTermination {
1741 w.violate("INV-26: box %d's park-local chore %s discharged without qualifying volume evidence (§5.9, H19; terminated %v)", x.ID, o.ID, x.Terminated)
1742 }
1743 w.stats["fence: park-local chores discharged"]++
1744 continue
1745 }
1746 keep = append(keep, o)
1747 }
1748 n.Obl = keep
1749 if !x.Terminated || !x.DeleteOnTermination {
1750 // INV-26 (rev 10): a discharge recorded for the operation's
1751 // leftovers is a discharge too, though no chore exists yet
1752 w.violate("INV-26: box %d's local retirement discharged without qualifying volume evidence (§5.9, H19, claim §7.3 D5; terminated %v)", x.ID, x.Terminated)
1753 }
1754 n.Op.Discharged = addUniq(n.Op.Discharged, x.ID)
1755}
1756
1757// declareTimers: the timers acknowledged on a fenced box and not yet
1758// delivered are declared lost with it and alerted (moves §7.10: no return
1759// lane carries them; §5.9: the loss record's timer knowledge is partial).
1760// Task 4 narrows it to the timers its last published manifest does not
1761// carry. The mutation of fence-declares-timers drops them silently.
1762func (w *world) declareTimers(x *box, h *holder) {
1763 if h == nil || !on("fence-declares-timers") {
1764 return
1765 }
1766 n := 0
1767 for i := range w.acks {
1768 a := &w.acks[i]
1769 if a.Box == x.ID && a.Inc == h.Inc && a.Kind == "timer" && !a.Exempt && !w.delivered[a.W] && !w.timerCarried(x, h, a.W) {
1770 a.Exempt = true
1771 n++
1772 }
1773 }
1774 if n > 0 {
1775 w.stats["timers declared lost"] += n
1776 w.alert("box %d fenced: %d acknowledged timers registered on it are lost with it", x.ID, n)
1777 }
1778}
1779
1780// timerCarried: the newest landed manifest of the admitted chain carries
1781// the timer (Task 4), so the install that follows restores it and the
1782// fence does not declare it. INV-28 is the oracle beside it: a timer the
1783// manifest published last of the generation carries is never declared,
1784// whatever the sequences say.
1785func (w *world) timerCarried(x *box, h *holder, id int) bool {
1786 _, _, _, _, m := w.newestManifest(x.Region, h.Location, h.Gen)
1787 if m != nil && contains(m.Timers, id) {
1788 return true
1789 }
1790 if last := w.lastLandedManifest(x.Region, h.Location, h.Gen); last != nil && contains(last.Timers, id) {
1791 w.violate("INV-28: box %d's timer %d is declared lost, but the manifest published last in generation %q carries it (seq %d)", x.ID, id, last.Gen, last.Seq)
1792 }
1793 return false
1794}
1795
1796// deadBoxFiles is what a fenced box's files directory contributes to the
1797// recovery: in the one-box shape, which has no files publisher (ruling
1798// 11), its local directory stands in for what it published (L17), as
1799// convertToRelocating's does; in the two-box shape nothing, because a
1800// dead box's disk is never read: the files come from the pinned manifest
1801// alone.
1802func (w *world) deadBoxFiles(x *box) *filesDir {
1803 if w.perRegion > 1 {
1804 return nil
1805 }
1806 return x.Files.clone()
1807}
1808
1809// startFailover is the fence's start of a relocation for a serving
1810// executor (§5.8 reason failover, StartRelocation with a fence; spec 3's
1811// placer supplies the decision): op.exec none, the stamp kept, at pinning
1812// with op.PinFailover standing for 1b-next's starting (ruling 4). The
1813// start names the target to and makes it the only participant (rev 7,
1814// Astra round 4 finding 3); only the mutation of
1815// reloc-failover-start-guard starts with none, and pinning names one.
1816// The target acknowledges before the pin is selected (PinFailover's
1817// barrier, rule reloc-failover-ack-before-pin, rev 5 and 6), and the pin
1818// waits out failoverPinDelay from the fence. id is the operation's id: a
1819// fresh one for a serving executor's failover, the landing's own for a
1820// committed landing's conversion (ConvertCommittedLanding keeps it).
1821func (w *world) startFailover(n *claim, x *box, to *box, id string) {
1822 n.Op = opState{Kind: "relocating", ID: id, Phase: "pinning", Exec: -1, Src: x.ID, Tgt: -1, SrcRegion: x.Region,
1823 RelocReason: "failover", ReqAt: w.now, App: n.Holder.App, SrcGen: n.Holder.Gen, SrcStamp: n.Holder.Stamp, StopFiles: w.deadBoxFiles(x), PinFailover: true}
1824 if to != nil {
1825 // StartRelocation names the target and makes it the only
1826 // participant (rev 7, Astra round 4 finding 3)
1827 n.Op.Tgt, n.Op.Parts = to.ID, []int{to.ID}
1828 }
1829 w.stats["relocation started: failover"]++
1830}
1831
1832// failoverStart is Worker.Start for a failover (1b-next rev 19; rev 7,
1833// Astra round 4 finding 3), the checks before the start CAS in its order:
1834// the claim serves on the fenced source, an eligible target exists
1835// (Eligible), and the app admits the registration (Admission.Register).
1836// It returns the target, or nil when the start must wait for a later
1837// pass; the CAS rechecks the admission and applies RelocationStartGuard
1838// with the source fenced (failoverStartAllowed). The mutation of
1839// reloc-failover-start-guard starts at once with no target, as rev 6 did.
1840func (w *world) failoverStart(c *claim, x *box) (*box, bool) {
1841 if !on("reloc-failover-start-guard") {
1842 return nil, true
1843 }
1844 to := w.chooseBoxExcept(x.Region, c.Holder.App, x.ID)
1845 if to == nil {
1846 w.stats["failover start: no eligible target yet"]++
1847 return nil, false
1848 }
1849 if ad := w.admit[c.Holder.App]; ad != nil && ad.Closed {
1850 w.stats["failover start: app admission closed"]++
1851 return nil, false
1852 }
1853 return to, true
1854}
1855
1856// failoverStartAllowed is the start CAS's own checks for a failover:
1857// the admission recheck (Admission.Recheck), the target not on lost, and
1858// RelocationStartGuard with the source fenced, so the source's own
1859// park-local does not block (L11) and a park-replica in the region does.
1860func (w *world) failoverStartAllowed(n *claim, x, to *box) bool {
1861 if !on("reloc-failover-start-guard") {
1862 return true
1863 }
1864 if ad := w.admit[n.Holder.App]; ad != nil && ad.Closed {
1865 return false
1866 }
1867 return to != nil && !contains(n.Lost, to.ID) && w.relocationStartGuard(n, x, to, true)
1868}
1869
1870// fenceSource is §5.8's source loss (B1-6, FenceSource): a drain whose
1871// source is terminated before its release is journaled becomes a
1872// failover in the same CAS, op.exec none, the stamp kept, the source off
1873// the barrier, its files directory the publisher's stand-in; after its
1874// release only its liveness changes. (1b-next keeps a source lost in
1875// starting at starting until the remaining participant acknowledges,
1876// then PinFailover; the model goes to pinning at once with
1877// op.PinFailover set for a source lost in starting, so the target
1878// acknowledges before the pin is selected, rule
1879// reloc-failover-ack-before-pin, and the pin waits out the delay.) The
1880// mutation of reloc-source-loss takes a source lost in releasing as
1881// released at what the remote holds, so its unuploaded writes go
1882// undeclared.
1883func (w *world) fenceSource(n *claim, x *box) {
1884 switch {
1885 case n.Op.Phase == "starting", n.Op.Phase == "releasing" && n.Op.Release == nil:
1886 if !on("reloc-source-loss") && n.Op.Phase == "releasing" {
1887 h := n.Holder
1888 n.Op.Release = &releaseEntry{Box: x.ID, Session: h.Session, Gen: h.Gen, FinalTX: w.remoteMax(w.buckets[x.Region], dataKey(h.Location, h.Gen, "")), Flushed: true, Files: w.deadBoxFiles(x), Timers: append([]int(nil), x.Timers...), At: w.now}
1889 n.Op.Exec, n.Op.Phase = -1, "pinning"
1890 return
1891 }
1892 n.Op.PinFailover = n.Op.Phase == "starting" // 1b-next keeps it in starting until PinFailover (rev 6)
1893 n.Op.RelocReason, n.Op.Exec, n.Op.Phase = "failover", -1, "pinning"
1894 n.Op.Release, n.Op.StopFiles = nil, w.deadBoxFiles(x)
1895 w.stats["relocation: source lost before its release, converted to failover"]++
1896 default:
1897 w.stats["relocation: source lost after its release"]++
1898 }
1899}
1900
1901// replaceTarget is §5.8's replacement table (ReplaceTarget) for a target
1902// terminated by its fence, within the fence's CAS:
1903//
1904// starting, releasing, pinning -> the same phase, to' named
1905// installing -> installing: to' is op.exec, a new stamp, a fresh P'
1906// installed, or probing before P -> installed: to' is op.exec, a new stamp, the same P, no receipt
1907// after P's first admission, or -> pinning: a nested failover from holder.generation as observed
1908// probe-failed
1909//
1910// Exhaustion is checked first (Worker.replace, 1b-next R1-16): with the
1911// replacements used the fence goes to fenceExhaustedTarget and no spare
1912// is looked for. With no eligible spare it reports false and the fence
1913// waits for a later pass (Worker.next). The mutation of
1914// reloc-replacement-table resumes every late loss at installed with the
1915// same P.
1916func (w *world) replaceTarget(n *claim, x *box) bool {
1917 phase := n.Op.Phase
1918 if on("reloc-replacement-cap") && n.Op.Replacements >= maxReplacements {
1919 w.fenceExhaustedTarget(n, x)
1920 return true
1921 }
1922 next := w.chooseBoxExcept(n.Op.SrcRegion, n.Holder.App, relocExcept(n.Op, x.ID)...)
1923 if next == nil {
1924 if on("reloc-dead-target-bound") && x.FenceObservedAt > 0 && w.now > observedAt(x)+releaseDeadline {
1925 // FenceTargetWithoutSpare (1b-next rev 18 finding 2): the fence
1926 // is consumed ReleaseDeadline after its observed_at and the
1927 // operation is stuck; the mutation waits for a spare for ever
1928 w.stats["relocation: dead target with no spare fenced"]++
1929 w.fenceTargetStuck(n, x, fmt.Sprintf("box %d terminated and no spare in %d steps", x.ID, releaseDeadline))
1930 return true
1931 }
1932 w.stats["relocation: target lost, no spare yet"]++
1933 return false
1934 }
1935 if !w.nameReplacement(n, next.ID, phase) {
1936 return n.Op.Stuck != ""
1937 }
1938 w.stats["relocation: target replaced at "+phase]++
1939 late := preparedOpened(n) || phase == "probe-failed" || phase == "probed" || phase == "opened"
1940 switch {
1941 case phase == "starting" || phase == "releasing" || phase == "pinning":
1942 case phase == "installing":
1943 n.Op.BaseKey, n.Op.BaseVer, n.Op.BaseTX = "", 0, 0
1944 w.handExec(n, next.ID, "installing", w.newID("P"))
1945 case !late || !on("reloc-replacement-table"):
1946 w.handExec(n, next.ID, "installed", n.Op.Prepared) // P was never opened: to' makes its own local install and opens it
1947 default:
1948 w.abandonPrepared(n)
1949 n.Op.Exec, n.Op.Phase = -1, "pinning"
1950 n.Op.Pin, n.Op.Prepared, n.Op.Receipt, n.Op.Release = nil, "", nil, nil
1951 n.Op.BaseKey, n.Op.BaseVer, n.Op.BaseTX = "", 0, 0
1952 n.Op.ProbeTries, n.Op.Attempts = 0, 0
1953 w.stats["nested failover after P was admitted"]++
1954 }
1955 return true
1956}
1957
1958// fenceExhaustedTarget is FenceExhaustedTarget (1b-next ruling 23 as rev
1959// 9 amended it; rule reloc-exhausted-fence): a target terminated after
1960// the operation used its replacements is fenced (the caller's fenceBox)
1961// and the operation is stuck, with no third target chosen. While another
1962// box is op.exec (the live source in starting or releasing) only stuck is
1963// new: the target's fence is no evidence against the source, whose
1964// execution identity, phase and release stay, so its own stop evidence
1965// still decides. Otherwise (op.exec the dead target, or none) the dead
1966// installation's authority is revoked: op.exec none at pinning, its
1967// receipt and release dropped, and P's pin and remote build kept when P
1968// was never opened, dropped for a nested pinning when it was. The
1969// mutation is rev 8's: always op.exec none at pinning, which revokes a
1970// live source without its stop evidence.
1971func (w *world) fenceExhaustedTarget(n *claim, x *box) {
1972 w.stats["relocation: exhausted target fenced"]++
1973 w.fenceTargetStuck(n, x, fmt.Sprintf("box %d terminated after %d target replacements", x.ID, maxReplacements))
1974}
1975
1976// fenceTargetStuck is 1b-next's fenceTargetStuck, the body
1977// FenceExhaustedTarget and FenceTargetWithoutSpare share (rev 18): phase
1978// sensitive as above; an unopened P is kept for the operator.
1979func (w *world) fenceTargetStuck(n *claim, x *box, why string) {
1980 if on("reloc-exhausted-fence") && n.Op.Exec >= 0 && n.Op.Exec != x.ID {
1981 n.Op.Stuck = why
1982 return
1983 }
1984 opened := preparedOpened(n)
1985 n.Op.Exec, n.Op.Phase = -1, "pinning"
1986 n.Op.Receipt, n.Op.Release = nil, nil
1987 n.Op.ProbeTries, n.Op.Attempts = 0, 0
1988 if opened {
1989 n.Op.Pin, n.Op.Prepared = nil, ""
1990 n.Op.BaseKey, n.Op.BaseVer, n.Op.BaseTX = "", 0, 0
1991 }
1992 n.Op.Stuck = why
1993}
1994
1995// ---------- the files publisher and the files pin (region HA §5.6, L13, L17) ----------
1996
1997// manifestKey is a files manifest of generation gen within loc, by
1998// sequence; isManifestKey recognizes one. genOf reads files/ under the
1999// origin generation as "".
2000func manifestKey(loc, gen string, seq int) string {
2001 return dataKey(loc, gen, fmt.Sprintf("files/m-%06d", seq))
2002}
2003
2004func isManifestKey(k string) bool { return strings.Contains(k, "files/m-") }
2005
2006// filesState is what a manifest carries, as the publisher compares it.
2007func filesState(timers []int, f *filesDir) string {
2008 if f == nil {
2009 return fmt.Sprint(timers)
2010 }
2011 return fmt.Sprint(timers, f.Key, f.Env)
2012}
2013
2014// publishFiles is the files publisher (§5.6, H6: platform-managed files
2015// and wake state sync continuously on every plan): the executor
2016// publishes a manifest, files and timers as they stand, whenever that
2017// state changed since its last one, at the generation's next sequence
2018// (the publisher of a generation starts at 1; a relocation's P has its
2019// prepared manifest at 0). The PUT goes through issue, so it may complete
2020// late, after the box is terminated: the late-file-upload case. A
2021// relocation's target publishes its probe session's state under P
2022// (B2-4). Only the two-box shape runs it (ruling 11).
2023func (w *world) publishFiles(b *box) {
2024 c := w.claim
2025 if c == nil || c.Holder == nil || !w.agentUp(b) || b.Row == nil || b.Files == nil || b.Files.Inc != c.Holder.Inc {
2026 return
2027 }
2028 if c.Op.Exec != b.ID || !(c.Op.Kind == "serving" || c.Op.Kind == "relocating" && (b.ID == c.Op.Src || preparedOpened(c))) {
2029 return // a relocation's target publishes only once P is opened, into its own generations
2030 }
2031 h := c.Holder
2032 if b.pubGen != h.Gen {
2033 b.pubGen, b.pubSeq, b.releaseSeq = h.Gen, 0, 0
2034 }
2035 state := filesState(b.Timers, b.Files)
2036 if state == b.published && w.objectCurrent(b, b.Files) {
2037 return
2038 }
2039 if !w.ensureObject(b, h, b.Files) {
2040 return // the object first: a manifest names only durable objects (§5.6)
2041 }
2042 b.pubSeq++
2043 w.issued++
2044 bl := &blob{Kind: "files", Inc: h.Inc, Gen: h.Gen, Seq: b.pubSeq, Files: b.Files.clone(), Timers: append([]int(nil), b.Timers...), Base: w.issued}
2045 w.issue(b.ID, &request{kind: rPut, region: b.Region, key: manifestKey(h.Location, h.Gen, b.pubSeq), b: bl})
2046 b.published = state
2047 w.stats["files manifests published"]++
2048}
2049
2050// newestManifest is the files pin's choice (§5.8, B2-4; StorePinner): the
2051// newest landed manifest in the admitted chain, holder.generation first,
2052// then each generation it superseded (genParent: the files live set's
2053// chain, which no trim of holder.admissions shortens; 1b-next rev 12
2054// finding 1), by sequence; not the database baseline's generation.
2055// Nothing landed: "" and nil.
2056func (w *world) newestManifest(r int, loc, gen string) (key string, ver, seq int, mgen string, m *blob) {
2057 bk := w.buckets[r]
2058 seen := map[string]bool{}
2059 for p := dataKey(loc, gen, ""); !seen[p]; {
2060 seen[p] = true
2061 var best *blob
2062 for _, k := range bk.keys(p + "files/") {
2063 v := bk.current(k)
2064 if v == nil || v.B == nil || v.B.Kind != "files" {
2065 continue
2066 }
2067 if best == nil || v.B.Seq > seq {
2068 key, ver, seq, mgen, best = k, v.ID, v.B.Seq, v.B.Gen, v.B
2069 }
2070 }
2071 if best != nil {
2072 return key, ver, seq, mgen, best
2073 }
2074 next, ok := w.genParent[p]
2075 if !ok {
2076 break
2077 }
2078 p = next
2079 }
2080 return "", 0, 0, "", nil
2081}
2082
2083// lastLandedManifest is INV-28's ground truth: of the first generation in
2084// the chain that holds a landed manifest, the one published last (the
2085// issue order the model records in the blob), whatever its sequence.
2086func (w *world) lastLandedManifest(r int, loc, gen string) *blob {
2087 _, _, _, mg, m := w.newestManifest(r, loc, gen)
2088 if m == nil {
2089 return nil
2090 }
2091 bk := w.buckets[r]
2092 last := m
2093 for _, k := range bk.keys(dataKey(loc, mg, "files/")) {
2094 if v := bk.current(k); v != nil && v.B != nil && v.B.Kind == "files" && v.B.Gen == mg && v.B.Base > last.Base {
2095 last = v.B
2096 }
2097 }
2098 return last
2099}
2100
2101// filesLive is the files live set (L13; claim §6.2a "Files in the live
2102// set"): a manifest of holder.generation, of a relocation's prepared
2103// generation, the one a running relocation's pin names, and the newest
2104// landed manifest of the admitted chain (the live manifest, which an
2105// unchanged file keeps referencing generations after it was published)
2106// is never archived by a park-generation or read as residue. It is
2107// region-qualified (rule files-live-region; joint round 1 finding 1): k
2108// is the key in region r's bucket. The mutation of files-live-set
2109// protects nothing, so a park-generation of an older generation archives
2110// the live or the pinned manifest.
2111func (w *world) filesLive(c *claim, r int, k string) bool {
2112 return on("files-live-set") && w.filesLiveIn(c, r, k, on("files-live-region") && on("live-set-region"))
2113}
2114
2115// filesLiveSet is the files live set itself, region-qualified, which the
2116// residue oracle reads whatever is mutated.
2117func (w *world) filesLiveSet(c *claim, r int, k string) bool {
2118 return w.filesLiveIn(c, r, k, true)
2119}
2120
2121// filesHome: does region r hold the files live set? Only the
2122// authoritative region does (claim rev 6.5 §6.2a, R4-3): the region of
2123// the holder's current installation, as the moves spec's §5.1 table
2124// ("The authoritative region", the claim state by claim state table)
2125// names it, united with op.exec's region, whose box runs moves' own parks
2126// (F4's keepFilesLive):
2127//
2128// - serving, pushing, reverting: op.exec's region;
2129// - relocating: its source region, for the whole operation (a
2130// relocation never leaves it; while op.exec is none, at a failover's
2131// pinning or a replay, the chain's newest manifest there is the pin's
2132// candidate: two-box seed 1215, stuck for ever under a narrower
2133// reading, INV-3);
2134// - moving (before the flip, every phase, op.exec none from stopped on):
2135// the source's region;
2136// - landing: the target's region, op.exec's, throughout; and the
2137// source's region until the claim follows the move's commit
2138// (op.Committed, followRecord's "committed" case), not only until the
2139// target's probe is admitted as §5.1's row for holder.generation
2140// reads: the probe's admission commits nothing, and until the commit
2141// a clean rollback names op.SrcGen again in the source region
2142// (moves_test.go's return CAS, Holder.Gen = op.SrcGen), whose next
2143// failover pins the source's chain there (Astra joint round 2). The
2144// record's commit (landTarget's MoveState = "committed", the CAS on
2145// the field rollback also takes) rules the rollback out; the claim
2146// follows it in the CAS that queues the source region's
2147// whole-location park-superseded, so the source stays home until
2148// that park owns it;
2149// - restoring: the source's region while the target releases
2150// (releasing, released, the dirty lane's returned: SrcPhase waiting,
2151// op.exec the target or none), until the source's return CAS (moves
2152// a5e8c13f, L18 finding 1). After a clean return the source is op.exec
2153// and names op.SrcGen again. After a dirty return the newest admission
2154// (the target's) decides until the source's probe is admitted into the
2155// return generation: the source region is not the files home then,
2156// even as op.exec's, so the return's install park (moves' D6,
2157// install-park) retires the pre-move generations with their files
2158// (dirtyReturned). The target region is retired by the release's
2159// whole-location park, which never consults the files live set;
2160// - creating, deleting, parking, purging, free: none (op.exec is none
2161// in each, and no case below names them).
2162//
2163// The mutation of files-home-move-phases keeps op.exec's region and a
2164// relocation's source only, so from a move's stopped phase to the
2165// commit, and in a rollback's released phase, a park-generation queued
2166// during the move archives the live manifest in the source region.
2167func (w *world) filesHome(c *claim, r int) bool {
2168 if dirtyReturned(c, on("dirty-return-needs-admission")) && r == c.Op.SrcRegion && on("files-home-move-phases") {
2169 return false // D6: the dirty return's install parks the pre-move generations
2170 }
2171 if e := w.execBox(c); e != nil && e.Region == r {
2172 return true
2173 }
2174 if c.Op.Kind == "relocating" && r == c.Op.SrcRegion {
2175 return true
2176 }
2177 if !on("files-home-move-phases") || r != c.Op.SrcRegion {
2178 return false
2179 }
2180 switch c.Op.Kind {
2181 case "moving", "restoring":
2182 return true
2183 case "landing":
2184 return !c.Op.Committed
2185 }
2186 return false
2187}
2188
2189// pinWatch is what INV-30 remembers of one region: the manifest version
2190// its next install would pin, and the object that manifest names when
2191// the object was current in the region's bucket.
2192type pinWatch struct {
2193 inc int
2194 loc string
2195 key string
2196 ver, base int
2197 obj string
2198}
2199
2200// servableRegions are the regions where the host could still be served
2201// next (INV-30's own reading of claim §6.2a and moves §5.1, written
2202// apart from filesHome so that it can catch filesHome): op.exec's
2203// region; a relocation's source region (it never leaves it); a move's
2204// source region before the flip; a landing's source region until the
2205// record commits (landTarget's MoveState = "committed", the CAS on the
2206// field a rollback also takes: until then a clean rollback names
2207// op.SrcGen again there); a rollback's source region. Creating,
2208// deleting, parking and purging have no executor and name none.
2209func (w *world) servableRegions(c *claim) []int {
2210 if c == nil || c.Holder == nil || dirtyReturned(c, true) {
2211 // after a dirty return of a move its target admitted, the source's
2212 // next install comes from the return lane, never from a pinned
2213 // manifest of the pre-move generations, which D6 retires by design
2214 // (moves a5e8c13f). The oracle reads the claim's admissions itself,
2215 // whatever the rules say: a dirty return the target never admitted
2216 // keeps the pre-move generation as the source's live state
2217 return nil
2218 }
2219 var out []int
2220 if e := w.execBox(c); e != nil {
2221 out = append(out, e.Region)
2222 }
2223 src := false
2224 switch c.Op.Kind {
2225 case "relocating", "moving", "restoring":
2226 src = true
2227 case "landing":
2228 src = !c.Op.Committed && !(w.rec != nil && w.rec.MoveID == c.Op.ID && w.rec.MoveState == "committed")
2229 }
2230 if src && (len(out) == 0 || out[0] != c.Op.SrcRegion) {
2231 out = append(out, c.Op.SrcRegion)
2232 }
2233 return out
2234}
2235
2236// checkFilesPreserved is INV-30 (joint round 2; claim §6.2a "Files in the
2237// live set", moves §5.1): in every region where the host could still be
2238// served next, the manifest that region's next install would pin (the
2239// newest landed manifest of the admitted chain there, newestManifest,
2240// which the failover's pin takes) and the object it names survive: once
2241// seen, the version is never archived or deleted while the region can
2242// still serve, unless a newer manifest has landed in its place. It reads
2243// the bucket each step and remembers the last step's choice, so an
2244// archive is seen as it happens, before any later read of the bucket
2245// takes the shortened chain for the truth (the fence's timerCarried and
2246// INV-28 read the bucket after the archive; a failover's stuck pin is
2247// INV-3 only much later, and only if a failover happens).
2248func (w *world) checkFilesPreserved() {
2249 c := w.claim
2250 next := map[int]pinWatch{}
2251 for _, r := range w.servableRegions(c) {
2252 loc := c.Holder.Location
2253 bk := w.buckets[r]
2254 mk, mv, _, _, m := w.newestManifest(r, loc, c.Holder.Gen)
2255 if p, ok := w.pinWatch[r]; ok && p.inc == c.Holder.Inc && p.loc == loc {
2256 gone := bk.current(p.key) == nil || bk.byID(p.key, p.ver) == nil
2257 superseded := m != nil && m.Base > p.base
2258 if gone && !superseded {
2259 w.violate("INV-30: region %d could still serve inc %d (%s %s), and the manifest its next install would pin, %s@%d, is gone from its bucket (now %q)", r, c.Holder.Inc, c.Op.Kind, c.Op.ID, p.key, p.ver, mk)
2260 return
2261 }
2262 if !gone && mk == p.key && p.obj != "" && bk.current(p.obj) == nil {
2263 w.violate("INV-30: region %d could still serve inc %d (%s %s), and the object %s its pinned manifest %s names is gone from its bucket", r, c.Holder.Inc, c.Op.Kind, c.Op.ID, p.obj, p.key)
2264 return
2265 }
2266 }
2267 if m == nil {
2268 continue
2269 }
2270 pw := pinWatch{inc: c.Holder.Inc, loc: loc, key: mk, ver: mv, base: m.Base}
2271 if m.Files != nil && m.Files.Obj != "" && bk.current(m.Files.Obj) != nil {
2272 pw.obj = m.Files.Obj
2273 }
2274 next[r] = pw
2275 }
2276 w.pinWatch = next
2277}
2278
2279// filesLiveIn is the files live set of region r. regional false is the
2280// region-blind set the rule files-live-region replaced (and every region
2281// authoritative, as live-set-region's mutation reads the whole live set):
2282// the newest manifest of the chain in any region is live in every region,
2283// so a manifest PUT that lands in a former region after a move's cleanup
2284// protects itself there for ever.
2285func (w *world) filesLiveIn(c *claim, r int, k string, regional bool) bool {
2286 if c == nil || c.Holder == nil || !isFilesKey(k) {
2287 return false
2288 }
2289 if !isManifestKey(k) {
2290 return w.objectReferenced(c, r, k, regional)
2291 }
2292 loc := c.Holder.Location
2293 if !strings.HasPrefix(k, loc) {
2294 return false
2295 }
2296 if regional && !w.filesHome(c, r) {
2297 return false
2298 }
2299 g := genOf(loc, k)
2300 if g == c.Holder.Gen {
2301 return true
2302 }
2303 if c.Op.Kind == "relocating" {
2304 if c.Op.Pin != nil && k == c.Op.Pin.ManKey {
2305 return true
2306 }
2307 if c.Op.Prepared != "" && g == c.Op.Prepared {
2308 return true
2309 }
2310 }
2311 for _, x := range w.filesRegions(r, regional) {
2312 if mk, _, _, _, _ := w.newestManifest(x, loc, c.Holder.Gen); mk == k {
2313 return true
2314 }
2315 }
2316 return false
2317}
2318
2319// filesRegions is where the live set's newest manifest is read for a key
2320// of region r: region r alone; the region-blind set reads every region.
2321func (w *world) filesRegions(r int, regional bool) []int {
2322 if regional {
2323 return []int{r}
2324 }
2325 all := make([]int, nRegions)
2326 for i := range all {
2327 all[i] = i
2328 }
2329 return all
2330}
2331
2332// pinnedManifest is the manifest the local install applies: the exact
2333// version the pin names (rule files-pin-exact; the model's versioned
2334// bucket reads it by version id, where the real store, which has none,
2335// reads the pinner's retained copy, 1b-next ruling 21); the mutation
2336// reads the newest manifest at install time, which a late PUT from the
2337// dead box may have changed since the pin.
2338func (w *world) pinnedManifest(c *claim) *blob {
2339 p := c.Op.Pin
2340 if p == nil || p.ManKey == "" || c.Holder == nil {
2341 return nil
2342 }
2343 if !on("files-pin-exact") {
2344 _, _, _, _, m := w.newestManifest(c.Op.SrcRegion, c.Holder.Location, c.Holder.Gen)
2345 return m
2346 }
2347 if v := w.buckets[c.Op.SrcRegion].byID(p.ManKey, p.ManVer); v != nil && !v.Marker {
2348 return v.B
2349 }
2350 return nil
2351}
2352
2353// regionMin is the region's minimum box count (H5): the shape's, or one
2354// for a witness's one-box region.
2355func (w *world) regionMin(r int) int {
2356 if w.oneBoxRegion[r] {
2357 return 1
2358 }
2359 return w.perRegion
2360}
2361
2362// ---------- acknowledgements (rev 4, Astra R1-4) ----------
2363
2364// acked is requireAcks: every current participant has a durable
2365// exec_ack.<box> entry in the journal. It reads only the claim; whether a
2366// participant's controller is ready now is the participant's own check
2367// before its effects (recordAck, and role()'s lease), never the barrier.
2368func acked(c *claim) bool {
2369 for _, p := range c.Op.Parts {
2370 if !contains(c.Op.Acks, p) {
2371 return false
2372 }
2373 }
2374 return true
2375}
2376
2377// recordAck is AcknowledgeExecution: a participant whose controller is
2378// ready (its execution lease current, role() renews it first) journals
2379// exec_ack.<box> once; the entry outlives a lost lease or an agent
2380// restart (1b-next rev 2 finding 10: the claim keeps the first
2381// acknowledgement). A box replaced out of the participants keeps its
2382// entry, which the barrier no longer reads.
2383func (w *world) recordAck(b *box, c *claim) {
2384 if c == nil || c.Op.Kind != "relocating" || !contains(c.Op.Parts, b.ID) || contains(c.Op.Acks, b.ID) || contains(c.Lost, b.ID) {
2385 return
2386 }
2387 if b.ExecUntil == 0 || w.localNow(b) > b.ExecUntil {
2388 return // no ready controller: nothing to acknowledge with
2389 }
2390 id := c.Op.ID
2391 // the controller's durable record exists before the acknowledgement
2392 // (1b-next Task 7: journaled before the open), and stays until a
2393 // successful scoped cleanup clears it (finishParticipation)
2394 b.execRecord, b.cleanupPending = id, true
2395 if w.cas(c, func(n *claim) bool {
2396 ok := sameOp(n.Op.ID, id) && n.Op.Kind == "relocating" && contains(n.Op.Parts, b.ID) && !contains(n.Op.Acks, b.ID)
2397 if ok {
2398 n.Op.Acks = append(n.Op.Acks, b.ID)
2399 }
2400 return ok
2401 }) {
2402 w.logf("relocation %s: box %d acknowledged execution", id, b.ID)
2403 }
2404}
2405
2406// finishParticipation is the participant's Sweep and Finish (1b-next
2407// Task 6 and 7, rulings 20): once the claim no longer runs the operation
2408// with this box as a live participant, the scoped cleanup runs (it
2409// retires an unopened P's local install of that operation, and is a
2410// no-op on a box that admitted a writer or serves the host); only when it
2411// succeeds is the durable record cleared (rule reloc-clear-after-cleanup;
2412// the mutation clears it after a failed cleanup, as rev 4 of 1b-next
2413// did). A box whose cleanup fails keeps the record, and root refuses an
2414// executable update while it exists (claim §5.10).
2415func (w *world) finishParticipation(b *box, c *claim) {
2416 if b.execRecord == "" || c == nil {
2417 return
2418 }
2419 if d := c.Op.Completed; c.Op.Kind == "serving" && d != nil && !d.Discharged && d.Box == b.ID && b.execRecord == d.Op {
2420 return // the completion box's participation is C10's to discharge (completionPass), first
2421 }
2422 if c.Op.Kind == "relocating" && c.Op.ID == b.execRecord && contains(c.Op.Parts, b.ID) && !contains(c.Lost, b.ID) {
2423 return // still a participant
2424 }
2425 if b.cleanupFails {
2426 w.stats["relocation controller: scoped cleanup failed"]++
2427 if on("reloc-clear-after-cleanup") {
2428 return // the record stays, and so does the refusal of updates
2429 }
2430 b.execRecord = "" // the mutation: cleared anyway
2431 return
2432 }
2433 if f := b.Files; f != nil && f.Gen != "" && c.Holder != nil && f.Inc == c.Holder.Inc && strings.HasPrefix(f.Gen, "P") && !(c.Op.Kind == "serving" && c.Op.Exec == b.ID) && f.Gen != c.Holder.Gen && !w.ancestorGen(c.Holder.Location, c.Holder.Gen, f.Gen) {
2434 w.retireArtifacts(b, f.Inc, f.Stamp) // an unopened P's local install of the finished operation
2435 }
2436 b.execRecord, b.cleanupPending = "", false
2437 w.stats["relocation controller: record cleared"]++
2438}
2439
2440// updateExecutable is an executable update on b (claim §5.10, region HA
2441// §5.10): root refuses it while a relocation controller's record exists.
2442// INV-29 is the oracle: an update never runs on a box whose relocation
2443// participation's scoped cleanup has not succeeded.
2444func (w *world) updateExecutable(b *box) bool {
2445 if b.Destroyed {
2446 return false
2447 }
2448 if b.execRecord != "" {
2449 w.stats["executable update refused: controller record"]++
2450 return false
2451 }
2452 if b.cleanupPending {
2453 w.violate("INV-29: box %d updated its executable while a relocation's scoped cleanup had not succeeded (claim §5.10)", b.ID)
2454 }
2455 w.stats["executable updated"]++
2456 return true
2457}
2458
2459// latestObservation is the newest observed_at the operation's journal
2460// retains (JournalPin checks every fence.<box>.observed_at, a release or
2461// not: rev 4, Astra R1-5); FenceAt covers a converted move's fence.
2462func latestObservation(op opState) int {
2463 at := op.FenceAt
2464 for _, o := range op.Observed {
2465 if o > at {
2466 at = o
2467 }
2468 }
2469 return at
2470}
2471
2472// observedAt is when the fence record says the box was observed
2473// terminated (FenceTargetWithoutSpare's deadline basis); the mutation of
2474// reloc-deadline-from-observed-at uses the termination itself, which no
2475// worker may have seen yet.
2476func observedAt(x *box) int {
2477 if !on("reloc-deadline-from-observed-at") {
2478 return x.TerminatedAt
2479 }
2480 return x.FenceObservedAt
2481}
2482
2483// objectReferenced: a live manifest (the chain's newest, the pinned one,
2484// holder.generation's or P's) names the object, or a live box's files
2485// directory does (its next manifest will): the files live set is every
2486// object the live manifests reference (claim §6.2a). Region-qualified as
2487// the manifests are: k is the key in region r's bucket, region r must be
2488// the files home (filesHome), only region r's manifests are read, and a
2489// box's directory names k only when it names region r's object. The
2490// region-blind set (regional false) reads every region's manifests and
2491// every box's directory, for a key of any region.
2492func (w *world) objectReferenced(c *claim, r int, k string, regional bool) bool {
2493 loc := c.Holder.Location
2494 names := func(m *blob) bool { return m != nil && m.Files != nil && m.Files.Obj == k }
2495 if regional && !w.filesHome(c, r) {
2496 return false
2497 }
2498 for _, x := range w.filesRegions(r, regional) {
2499 if _, _, _, _, m := w.newestManifest(x, loc, c.Holder.Gen); names(m) {
2500 return true
2501 }
2502 bk := w.buckets[x]
2503 for _, g := range []string{c.Holder.Gen, c.Op.Prepared} {
2504 if g == "" && g != c.Holder.Gen {
2505 continue
2506 }
2507 for _, mk := range bk.keys(dataKey(loc, g, "files/m-")) {
2508 if v := bk.current(mk); v != nil && names(v.B) {
2509 return true
2510 }
2511 }
2512 }
2513 }
2514 if c.Op.Kind == "relocating" && c.Op.Pin != nil && c.Op.Pin.ManKey != "" && (!regional || r == c.Op.SrcRegion) {
2515 if names(w.pinnedManifestOf(c.Op.SrcRegion, c.Op.Pin)) {
2516 return true
2517 }
2518 }
2519 for _, b := range w.liveBoxes() {
2520 if b.Files != nil && b.Files.Obj == k && (!regional || b.Files.ObjRegion == r) {
2521 return true
2522 }
2523 }
2524 return false
2525}
2526
2527// keepFilesLive wraps a moves park's keep (a push's whole-location park,
2528// a landing's install park, a rollback's return park) so it never
2529// archives the files live set (finding F4, rev 4: claim §6.2a's live set
2530// binds every park; moves' parks predate region HA's files tree). Only
2531// the two-box shape has files keys, so the one-box shape is unchanged.
2532// The mutation of files-live-set-moves-parks archives them with the rest.
2533func (w *world) keepFilesLive(r int, keep func(string) bool) func(string) bool {
2534 return func(k string) bool {
2535 if keep(k) {
2536 return true
2537 }
2538 if on("files-live-set-moves-parks") && w.filesLive(w.claim, r, k) {
2539 w.markSweepOwed(r, k)
2540 return true
2541 }
2542 return false
2543 }
2544}
2545
2546// markSweepOwed: a park kept k only because the files live set named it;
2547// once obsolete it is the at-rest sweep's (claim §6.2a, 1b-next ruling 21),
2548// so INV-10 at an operation's end exempts it as it exempts a straggler.
2549func (w *world) markSweepOwed(r int, k string) {
2550 for i := range w.buckets {
2551 if r >= 0 && i != r {
2552 continue
2553 }
2554 for _, v := range w.buckets[i].objs[k] {
2555 v.SweepOwed = true
2556 }
2557 }
2558}
2559
2560// renewController is a relocation controller's own renewal loop (hostexec
2561// renews its lease on its own clock, not when the regional worker happens
2562// to run; rev 4): an agent that is up and holds a current lease for a
2563// relocation it participates in keeps it current. A lease is still lost
2564// when the agent is down, paused, frozen or partitioned past the TTL,
2565// which is the scoped cleanup's case. Two-box shape only.
2566func (w *world) renewController(b *box) {
2567 c := w.claim
2568 if c == nil || c.Op.Kind != "relocating" || !contains(c.Op.Parts, b.ID) || !w.agentUp(b) || b.Partitioned {
2569 return
2570 }
2571 if b.ExecUntil > 0 && w.localNow(b) <= b.ExecUntil && b.ExecUntil-w.localNow(b) < execRenew {
2572 b.ExecUntil = w.localNow(b) + execTTL
2573 w.execLease[b.ID] = execLease{Op: c.Op.ID, Expires: w.now + execTTL}
2574 }
2575}
2576
2577// preparedManifestHere is moves rev 7.22 §7.6's one install routine (L16,
2578// L17; claim R18-1) for a landing or a return in the two-box shape (rev
2579// 4, claim §11 "move landing, source cleanup, then target failure with an
2580// unchanged file"): the generation's prepared files manifest, create-only
2581// at sequence 0 in this region, naming objects in this region's bucket
2582// (an unchanged file the host carried from another region is uploaded
2583// here first), acknowledged before the install's CAS. Without it a
2584// target lost right after its commit would leave no files in its region.
2585func (w *world) preparedManifestHere(b *box, c *claim, gen string) bool {
2586 h := c.Holder
2587 if !w.ensureObject(b, h, b.Files) {
2588 return false
2589 }
2590 bl := &blob{Kind: "files", Inc: h.Inc, Gen: gen, Seq: 0, Files: b.Files.clone(), Timers: append([]int(nil), b.Timers...), Text: c.Op.ID, Base: w.issued}
2591 return w.ackedPut(b, manifestKey(h.Location, gen, 0), bl, true)
2592}
2593
2594// relocJournalRelease is the participant's release (1b-next Task 7): the
2595// live executor runs its release sequence and journals it
2596// (JournalRelease), the source in releasing, a failed target in
2597// probe-failed. relocReleasing and relocProbeFailed call it for their
2598// release branch, and relocate calls it by itself while a fence is
2599// pending (rev 8; pending marks the trace line). The CAS is the union of
2600// the two phases' conditions: in releasing no release is journaled yet;
2601// in probe-failed the journaled release, if any, is another box's.
2602func (w *world) relocJournalRelease(b *box, c *claim, pending bool) {
2603 op := c.Op
2604 phase := op.Phase
2605 if phase == "probe-failed" && w.c1Fallback() {
2606 // QueueRelocationRelease needs the source in releasing: with claim
2607 // ask C1 open there is no failed-target obligation, so nothing is
2608 // queued, stopped or journaled, and the release deadline makes the
2609 // operation stuck (finding 4)
2610 w.stats["release refused: no failed-target obligation (C1 open)"]++
2611 return
2612 }
2613 r := w.relocRelease(b, c)
2614 if r == nil {
2615 return
2616 }
2617 if w.cas(c, func(n *claim) bool {
2618 ok := sameOp(n.Op.ID, op.ID) && n.Op.Kind == "relocating" && n.Op.Phase == phase && n.Op.Exec == b.ID && (n.Op.Release == nil || phase == "probe-failed" && n.Op.Release.Box != b.ID)
2619 if ok {
2620 n.Op.Release = r
2621 }
2622 return ok
2623 }) {
2624 if phase == "releasing" {
2625 w.stats["release journaled: source"]++
2626 } else {
2627 w.stats["release journaled: failed target"]++
2628 }
2629 note := ""
2630 if pending {
2631 note = ", a fence pending"
2632 }
2633 w.logf("relocation %s: box %d released the host at TXID %d (flushed=%v)%s", op.ID, b.ID, r.FinalTX, r.Flushed, note)
2634 }
2635}
2636
2637// dirtyReturned: a rollback whose source has taken the host back dirty
2638// (its return CAS written: SrcPhase past waiting) and whose probe has not
2639// yet been admitted into the return generation. From that CAS the newest
2640// admission decides (moves' movesAuthoritative, a5e8c13f): the target's,
2641// so the source region is not authoritative, and the return's install
2642// park retires the pre-move generations (D6).
2643func dirtyReturned(c *claim, needAdmission bool) bool {
2644 if c == nil || c.Holder == nil || c.Op.Kind != "restoring" || !c.Op.Dirty || c.Op.SrcPhase == "waiting" || c.Holder.Gen == c.Op.RollbackID {
2645 return false
2646 }
2647 return !needAdmission || targetAdmitted(c)
2648}
2649
2650// targetAdmitted is movesinstall.go's targetAdmitted (Astra round 4, item
2651// 2): the claim's recorded admissions hold one of this move's generation
2652// (the landing's, installGen: the move's op id). A dirty return can come
2653// without one: src_covered false makes the return dirty even when the
2654// target never admitted (releaseTarget's seal-return), and then
2655// holder.generation is still the pre-move one, the source's live state,
2656// which the return's install keeps and parks nothing of. Decided from the
2657// claim's admissions only, never from the landing phase.
2658func targetAdmitted(c *claim) bool {
2659 if c == nil || c.Holder == nil {
2660 return false
2661 }
2662 for _, a := range c.Holder.Admissions {
2663 if a.Gen == c.Op.ID {
2664 return true
2665 }
2666 }
2667 return false
2668}
2669