| index 6c03b26b5..5d1f4e8f8 100644 |
| --- a/internal/host/movesinstall.go |
| +++ b/internal/host/movesinstall.go |
| @@ -47,10 +47,12 @@ import ( |
| // never compared with move.inputs (J2-R2-5); |
| // - local: a landing parks an earlier stay's artifacts tagged S'; a return |
| // parks the source's own pre-move installation before anything is |
| -// extracted (J13): RetireArtifacts(S) when the claim's inventory lists |
| -// paths of S, then the moves-owned rename (H5(b), parkLocalArtifacts) of |
| -// every pre-move artifact still at the flat paths. placeStaged and |
| -// restore then find nothing there, and refuse anything that appears; |
| +// extracted (J13, fix round 1): RetireArtifacts for S and for every |
| +// stamp the claim shows superseded on this box, a stuck mark for any |
| +// other stamp, then the moves-owned rename (H5(b), parkLocalArtifacts) |
| +// of every pre-move artifact still at the flat paths, inside the host |
| +// guard. placeStaged and restore then find nothing there, and refuse |
| +// anything that appears; |
| // - the stand-in park view's authoritative region (J24): the source's for |
| // a landing, the target's for a return, each the other side of the move |
| // before this side's probe is admitted. |
| @@ -82,6 +84,7 @@ const ( |
| refuseInstallMismatch // configured: the build does not verify |
| refuseConfigChanged // configured: a landing's render is not its pin |
| refuseDeadline // configured: a landing ran past its deadline |
| + refuseUnknownStamp // local: a return found paths under a stamp it cannot account for |
| ) |
| |
| // installSource is one kind of install: what the six phases need from the |
| @@ -609,8 +612,14 @@ func (ir *installRun) local_(ctx context.Context) (bool, error) { |
| } |
| if !j.Done(landStepRetired) { |
| if ir.src.Role == roleSource { |
| - if err := ir.retirePreMove(ctx, obs, captured, j, mark); err != nil { |
| + outcome, err := ir.retirePreMove(ctx, captured, j, mark) |
| + switch { |
| + case err != nil: |
| return false, err |
| + case outcome == restoreMarked: |
| + return true, nil // the record changed: the refusal |
| + case outcome == restoreWaiting: |
| + return false, nil |
| } |
| } else if raw, ok := j.Steps[landStepPrevious]; ok { |
| earlier, err := strconv.ParseUint(raw, 10, 64) |
| @@ -744,49 +753,154 @@ func installArtifactPaths(host string) []string { |
| return []string{db, db + ".gen", db + "-wal", db + "-shm", db + ".corrupt", ".activator-" + db + "-litestream", host, host + ".env"} |
| } |
| |
| -// retirePreMove is a dirty return's local sub-step (2) (ruling J13): the |
| -// source's own pre-move installation leaves the flat paths before anything |
| -// of the return's is placed there, so placeStaged and restore find them |
| -// empty and never install over them. When the claim's inventory lists paths |
| -// of S, the source's installation stamp (the holder's after ReturnToSource), |
| -// they are retired through park.Local under the host guard against the |
| -// park-local the source's claimed queued (RetireArtifacts completes it); |
| -// with paths of S listed and no park-local owed it stops, since only the |
| -// claimed CAS queues it. A park-local owed with nothing of S listed (a replay |
| -// after the retirement) is completed. Then every pre-move artifact still at |
| -// the flat paths, registered or not, is parked by the moves-owned journalled |
| -// rename (H5(b), parkLocalArtifacts) into the engine's layout. Retired |
| -// stamps other than S are not the source's to find here: RecordInventory |
| -// refuses a path still listed under one, and the install stops there. |
| -func (ir *installRun) retirePreMove(ctx context.Context, obs hostclaim.Observed, captured hostguard.Captured, j *moves.Journal, mark func(step, value string) error) error { |
| +// retirePreMove is a dirty return's local sub-step (2) (ruling J13; fix |
| +// round 1): the source's own pre-move installation, and anything an earlier |
| +// installation of this holder left on this box, leaves the flat paths before |
| +// anything of the return's is placed there, so placeStaged and restore find |
| +// them empty and never install over them, and the return's registration |
| +// never meets a path tagged with another stamp. |
| +// |
| +// First the claim's inventory for this box, read fresh each round, is |
| +// accounted for stamp by stamp, before anything moves: |
| +// - S, the source's installation stamp (the holder's after |
| +// ReturnToSource): retired through park.Local under the host guard |
| +// against the park-local the source's claimed queued |
| +// (RetireArtifacts completes it); paths of S listed with no park-local |
| +// owed is an error, since only the claimed CAS queues it; |
| +// - a stamp the claim shows superseded on this box (supersededOnBox: a |
| +// cold admission here issued it, and it is neither S nor the holder's |
| +// live session; spec §7.8, rev 7.11): retired the same way, against a |
| +// park-local queued here for it, so its inventory entries clear; |
| +// - any other stamp (live, unknown, another holder's): nothing is moved, |
| +// and the return stops for an operator (StuckReturnUnknownStamp); a |
| +// retry after the operator has accounted for it runs this again. |
| +// |
| +// A park-local owed with nothing listed (a replay after a retirement) is |
| +// completed. Then every pre-move artifact still at the flat paths that no |
| +// inventory entry claims is parked by the moves-owned journalled rename |
| +// (H5(b), parkLocalArtifacts), its plan and its renames inside the host |
| +// guard with the claim and the record read fresh there (fix round 1, ruling |
| +// 1), so a call that resumes after authority moved on renames nothing. |
| +func (ir *installRun) retirePreMove(ctx context.Context, captured hostguard.Captured, j *moves.Journal, mark func(step, value string) error) (restoreOutcome, error) { |
| cfg := ir.cfg() |
| - host, id := ir.rec.Host, ir.g.ID |
| + host, id, phase := ir.rec.Host, ir.g.ID, ir.src.Phases[3] |
| stamp := captured.Stamp |
| - var chore *hostclaim.Obligation |
| - for i := range obs.Claim.Obligations { |
| - if o := &obs.Claim.Obligations[i]; o.Op == id && o.Kind == "park-local" && o.Box == cfg.Box { |
| - chore = o |
| + for round := 0; ; round++ { |
| + if round > 64 { |
| + return restoreWaiting, fmt.Errorf("move %s: the source's inventory did not clear", id) |
| + } |
| + obs, err := ir.sync(ctx, phase) |
| + if err != nil { |
| + return restoreWaiting, err |
| } |
| + c := obs.Claim |
| + var retire []uint64 |
| + for _, x := range inventoryStamps(c, cfg.Box) { |
| + switch { |
| + case x == stamp: |
| + retire = append([]uint64{x}, retire...) |
| + case supersededOnBox(c, cfg.Box, x): |
| + retire = append(retire, x) |
| + default: |
| + log.Printf("moves: %s: this box's inventory lists paths under stamp %d, which the return of %s cannot account for; nothing is moved", host, x, id) |
| + ok, err := ir.refuse(ctx, refuseUnknownStamp) |
| + if err != nil || !ok { |
| + return restoreWaiting, err |
| + } |
| + return restoreMarked, nil |
| + } |
| + } |
| + var chore *hostclaim.Obligation |
| + for i := range c.Obligations { |
| + if o := &c.Obligations[i]; o.Op == id && o.Kind == "park-local" && o.Box == cfg.Box { |
| + chore = o |
| + } |
| + } |
| + if len(retire) == 0 { |
| + if chore != nil { |
| + if _, err := hostclaim.Update(ctx, ir.s, obs, hostclaim.CompleteLocalPark(id, cfg.Box, stamp)); err != nil { |
| + return restoreWaiting, fmt.Errorf("complete the source's local park: %w", err) |
| + } |
| + } |
| + break |
| + } |
| + x := retire[0] |
| + if chore == nil { |
| + if x == stamp { |
| + return restoreWaiting, fmt.Errorf("move %s: this box's inventory lists the source's pre-move paths at stamp %d, and the claim owes no park-local for them", id, stamp) |
| + } |
| + if obs, err = hostclaim.Update(ctx, ir.s, obs, hostclaim.QueueLocalPark(id, cfg.Box, obligationID)); err != nil { |
| + return restoreWaiting, fmt.Errorf("queue the local park of stamp %d: %w", x, err) |
| + } |
| + for i := range obs.Claim.Obligations { |
| + if o := &obs.Claim.Obligations[i]; o.Op == id && o.Kind == "park-local" && o.Box == cfg.Box { |
| + chore = o |
| + } |
| + } |
| + if chore == nil { |
| + return restoreWaiting, fmt.Errorf("move %s: the local park of stamp %d was not queued", id, x) |
| + } |
| + } |
| + if err := ir.local.RetireArtifacts(ctx, host, captured, chore.ID, id, x); err != nil { |
| + return restoreWaiting, fmt.Errorf("retire the source's artifacts of stamp %d: %w", x, err) |
| + } |
| + log.Printf("moves: %s: the source's artifacts of stamp %d are retired", host, x) |
| } |
| - registered := hostclaim.InventoryHasStamp(obs.Claim, cfg.Box, stamp) |
| - switch { |
| - case registered && chore != nil: |
| - if err := ir.local.RetireArtifacts(ctx, host, captured, chore.ID, id, stamp); err != nil { |
| - return fmt.Errorf("retire the source's pre-move installation: %w", err) |
| + if err := ir.fault("before-" + installStepPreMove); err != nil { |
| + return restoreWaiting, err |
| + } |
| + paths := installArtifactPaths(host) |
| + unclaimed := func(c *hostclaim.Claim) error { |
| + for _, p := range paths { |
| + if _, ok := c.Inventory[cfg.Box].Paths[p]; ok { |
| + return fmt.Errorf("%w: the inventory claims %s", errLandingChanged, p) |
| + } |
| } |
| - log.Printf("moves: %s: the source's pre-move installation (stamp %d) is retired", host, stamp) |
| - case registered: |
| - return fmt.Errorf("move %s: this box's inventory lists the source's pre-move paths at stamp %d, and the claim owes no park-local for them", id, stamp) |
| - case chore != nil: |
| - if _, err := hostclaim.Update(ctx, ir.s, obs, hostclaim.CompleteLocalPark(id, cfg.Box, stamp)); err != nil { |
| - return fmt.Errorf("complete the source's local park: %w", err) |
| + return nil |
| + } |
| + err := ir.guardedWith(ctx, captured, phase, unclaimed, func() error { |
| + _, err := parkLocalArtifacts(ctx, j, mark, localArtifactPark{DataDir: cfg.DataDir, Incarnation: captured.Incarnation, Host: host, |
| + Op: ir.src.GenerationOp, Step: installStepPreMove, Paths: paths, NewID: obligationID, Fault: ir.fault}) |
| + return err |
| + }) |
| + if err != nil { |
| + return restoreWaiting, fmt.Errorf("park the source's pre-move artifacts: %w", err) |
| + } |
| + return restoreInstalled, nil |
| +} |
| + |
| +// inventoryStamps are the distinct stamps box's inventory tags paths with, |
| +// in order. |
| +func inventoryStamps(c *hostclaim.Claim, box string) []uint64 { |
| + seen := map[uint64]bool{} |
| + var out []uint64 |
| + for _, s := range c.Inventory[box].Paths { |
| + if !seen[s] { |
| + seen[s] = true |
| + out = append(out, s) |
| } |
| } |
| - if _, err := parkLocalArtifacts(ctx, j, mark, localArtifactPark{DataDir: cfg.DataDir, Incarnation: captured.Incarnation, Host: host, |
| - Op: ir.src.GenerationOp, Step: installStepPreMove, Paths: installArtifactPaths(host), NewID: obligationID, Fault: ir.fault}); err != nil { |
| - return fmt.Errorf("park the source's pre-move artifacts: %w", err) |
| + sort.Slice(out, func(i, k int) bool { return out[i] < out[k] }) |
| + return out |
| +} |
| + |
| +// supersededOnBox reports whether the claim shows stamp as an installation |
| +// stamp this holder issued on box and has since moved past: a cold |
| +// admission's stamp is its record's session (hostclaim.AdmitWriter), so a |
| +// cold admission record on box with that session names it, and it is |
| +// neither the holder's stamp nor its live writer session. A stamp the claim |
| +// has no such record of is not this box's to retire. |
| +func supersededOnBox(c *hostclaim.Claim, box string, stamp uint64) bool { |
| + h := c.Holder |
| + if h == nil || stamp == 0 || stamp == h.Stamp || stamp == h.Session { |
| + return false |
| + } |
| + for _, a := range h.Admissions { |
| + if a.Mode == "cold" && a.Box == box && a.Session == stamp { |
| + return true |
| + } |
| } |
| - return nil |
| + return false |
| } |
| |
| // configured (§7.6, §9.1, §9.2): the held row rendered (RenderHeld, the |
| @@ -963,6 +1077,8 @@ func recoveryInstallSource(w *movesWorker, s release.Store, rec provision.Record |
| reason = provision.StuckReturnFilesOccupied |
| case refuseInstallMismatch: |
| reason = provision.StuckReturnInstallMismatch |
| + case refuseUnknownStamp: |
| + reason = provision.StuckReturnUnknownStamp |
| default: |
| return false, "", fmt.Errorf("move %s: a return's install has no refusal %d", g.ID, why) |
| } |