| index ea7f2a9de..8c9cc3713 100644 |
| --- a/internal/sim/hostlife/protocol_test.go |
| +++ b/internal/sim/hostlife/protocol_test.go |
| @@ -675,6 +675,16 @@ func (w *world) resumeOpen(b *box, c *claim, o *openState) bool { |
| if db == nil { |
| w.stats["restore refused"]++ |
| w.logf("box %d: nothing to restore for a %s admission from %q", b.ID, o.Mode, o.Source) |
| + if o.startedAt == 0 { |
| + o.startedAt = w.now |
| + } |
| + if o.Probe && w.now-o.startedAt >= probeStartWindow { |
| + // a probe attempt whose restore cannot proceed within its |
| + // window fails like one whose app will not start (§7.7) |
| + b.open = nil |
| + w.stats["probe attempt failed: restore"]++ |
| + w.probeAttemptFailed(b, cur) |
| + } |
| return false |
| } |
| w.logf("box %d restored %s: TXID %d at position %d, %d writes (%s admission into %q)", b.ID, dataKey(loc, o.Source, ""), db.TX, db.Pos, len(db.Log), o.Mode, o.Gen) |
| @@ -923,8 +933,19 @@ func (w *world) publishGeneration(b *box, c *claim, observedOpID, observedPhase |
| id = fmt.Sprintf("session:%d", session) |
| } |
| for i := range n.Holder.Admissions { |
| - if e := &n.Holder.Admissions[i]; e.Mode == "cold" && e.Session <= session { |
| - e.Covered = true // its predecessor is parked or in this dead set |
| + e := &n.Holder.Admissions[i] |
| + if e.Mode != "cold" || e.Session > session || e.Covered { |
| + continue |
| + } |
| + // covered: its predecessor is in this dead set (a chore in this |
| + // region), or is the published generation, or holds no object in |
| + // any region. A predecessor whose files live in another region |
| + // (a move's source generation, seen by the target's publication) |
| + // stays uncovered for that region's next PublishGeneration: a |
| + // chore queued there from here would park a generation the |
| + // rollback may still need (seeds 33646, 40148, 1296 / rev 7.12) |
| + if e.Predecessor.Gen == published || !w.hasFilesAnywhere(n.Holder.Location, e.Predecessor.Gen) || hasStr(dead, e.Predecessor.Gen) && w.hasFilesIn(region, n.Holder.Location, e.Predecessor.Gen) { |
| + e.Covered = true |
| } |
| } |
| var keep []admissionRecord |
| @@ -940,22 +961,25 @@ func (w *world) publishGeneration(b *box, c *claim, observedOpID, observedPhase |
| keep = append([]admissionRecord{e}, keep...) |
| } |
| n.Holder.Admissions = keep |
| - next: |
| for _, retire := range dead { |
| - if retire == published { |
| - continue |
| + if retire == published || !w.hasFilesIn(region, n.Holder.Location, retire) { |
| + continue // nothing of it here: an empty generation, or one whose objects are another region's |
| } |
| ret := retire |
| if ret == "" { |
| ret = "@root" // the sentinel for the origin generation (claim rev 6.10) |
| } |
| + queued := false |
| for _, o := range n.Obl { |
| if o.Kind == "park-generation" && o.Region == region && o.Ret == ret && o.Prefix == n.Holder.Location { |
| - continue next // already queued |
| + queued = true // already queued |
| } |
| } |
| + if queued { |
| + continue |
| + } |
| oid := newID |
| - if k := len(n.Obl); k > 0 && n.Obl[k-1].ID == oid || hasObl(n.Obl, oid) { |
| + if hasObl(n.Obl, oid) { |
| oid = fmt.Sprintf("%s-%s", newID, ret) // one caller id, one chore per dead generation |
| } |
| n.Obl = append(n.Obl, obligation{ID: oid, Op: id, Kind: "park-generation", Region: region, Box: -1, Prefix: n.Holder.Location, Ret: ret, Quiet: w.now + horizonH}) |
| @@ -964,6 +988,28 @@ func (w *world) publishGeneration(b *box, c *claim, observedOpID, observedPhase |
| }) |
| } |
| |
| +// hasFilesIn is hasFiles for a region. |
| +func (w *world) hasFilesIn(r int, loc, gen string) bool { |
| + for _, k := range w.buckets[r].keys(dataKey(loc, gen, "")) { |
| + if genOf(loc, k) == gen { |
| + return true |
| + } |
| + } |
| + return false |
| +} |
| + |
| +// hasFilesAnywhere: the generation holds an object in some region. |
| +func (w *world) hasFilesAnywhere(loc, gen string) bool { |
| + for r := range w.buckets { |
| + for _, k := range w.buckets[r].keys(dataKey(loc, gen, "")) { |
| + if genOf(loc, k) == gen { |
| + return true |
| + } |
| + } |
| + } |
| + return false |
| +} |
| + |
| func hasObl(obl []obligation, id string) bool { |
| for _, o := range obl { |
| if o.ID == id { |
| @@ -1357,6 +1403,7 @@ func (w *world) admitWriter(b *box, c *claim, want string, probe bool) (admitInp |
| bk := w.buckets[b.Region] |
| a.RemoteMaxTXID = w.remoteMax(bk, dataKey(h.Location, h.Gen, "")) |
| a.PredecessorCertified = w.certificate(b, c) != nil |
| + |
| if want == "" || want == h.Gen || w.ancestorGen(h.Location, h.Gen, want) { |
| // holder.generation, or an operation's prepared generation that a |
| // writer already opened (it is holder.generation or an ancestor of |
| @@ -1367,9 +1414,14 @@ func (w *world) admitWriter(b *box, c *claim, want string, probe bool) (admitInp |
| return a, true |
| } |
| a.Mode, a.NewGeneration = "cold", w.newID("rec") |
| - if c.Op.Kind == "restoring" && c.Op.DirtyKnown && !c.Op.Dirty && c.Op.LostAt == 0 && c.Op.SrcCovered && b.ID == c.Op.Src && h.Gen == c.Op.SrcGen && on("clean-return-identity") { |
| + if l := lastAdmission(h); c.Op.Kind == "restoring" && c.Op.DirtyKnown && !c.Op.Dirty && c.Op.LostAt == 0 && c.Op.SrcCovered && b.ID == c.Op.Src && h.Gen == c.Op.SrcGen && |
| + l != nil && (l.Box == c.Op.Tgt || l.Session == c.Op.CutSession) && on("clean-return-identity") { |
| // (a return made clean by a declared-lost target is not covered: |
| - // the target may have written, and its session is declared) |
| + // the target may have written, and its session is declared; and |
| + // only the target's probe session or the source's own cut |
| + // session is covered by the seal's src_covered: a later session |
| + // of the recovered source that dies uncertified is declared as |
| + // any other, seed 95491 / 100,000) |
| // a clean rollback's return: the target wrote nothing and the |
| // source's replica covered the stop, so the predecessor's writes |
| // are preserved and nothing is declared (claim rev 6.12, R11-4) |