| index 512a1c360..3ea0cf25e 100644 |
| --- a/internal/host/movesprobeowners.go |
| +++ b/internal/host/movesprobeowners.go |
| @@ -1388,53 +1388,103 @@ func pinnedLaunchIdentity(in *provision.MoveInputs) hostwrite.Identity { |
| return hostwrite.Identity{Config: committedEnvPrefix + in.EnvReport, Release: in.Release} |
| } |
| |
| -// repairFilesGen repairs a publication interrupted between its marker write |
| -// and its .files-gen refresh (fix round 2, item 1). After the admission and |
| -// before the start check, inside the admitted writer's fence: when the |
| -// location's published marker names G, G is the admitted generation's cold |
| -// predecessor (Task 4's rule), the local .files-gen names an older generation |
| -// of G's own ancestry, and the files on disk are proved G's (the landing's |
| -// placement and, fix round 3, the content: the plain digest G's install |
| -// record holds), .files-gen is moved to G. No recorded digest, or anything |
| -// else, is left alone, for the unchanged start check to decide. |
| +// repairFilesGen brings .files-gen to the admitted generation's cold |
| +// predecessor T before the start check, inside the admitted writer's fence, |
| +// in two cases, each verified by content (the files tree's plain digest |
| +// against a recorded one, with the landing's placement), never by identity |
| +// alone: |
| +// |
| +// - an interrupted refresh (Task 7 fix round 2): the published marker |
| +// names T itself, and .files-gen names an older generation of T's |
| +// ancestry; the files must match T's recorded digest. |
| +// - unpublished cold admissions (final review, ruling 1): the published |
| +// marker names an older generation P, and T reaches P through cold |
| +// admissions on this box and region, none of which published (a |
| +// publication would have superseded P's marker); .files-gen names P or |
| +// one of those admissions. The files must match P's recorded digest: |
| +// every admission since P restored from P's data and its files are P's. |
| +// |
| +// Anything else is left alone, for the unchanged start check to decide. |
| func (m *movesProbeOwners) repairFilesGen(ctx context.Context, o hostclaim.Observed, v probeLanding, work hostwrite.Work, id hostwrite.Identity) error { |
| h := o.Claim.Holder |
| marker, err := m.readMarker(ctx, h) |
| if err != nil || marker == nil { |
| return err |
| } |
| - g := marker.Op |
| - if g == "" || marker.Incarnation != h.Incarnation || marker.Host != o.Host { |
| + published := marker.Op |
| + if published == "" || marker.Incarnation != h.Incarnation || marker.Host != o.Host { |
| return nil |
| } |
| - if pred, ok := coldPredecessor(h, h.Generation); !ok || pred != g { |
| + t, ok := coldPredecessor(h, h.Generation) |
| + if !ok || t == "" { |
| return nil |
| } |
| dir, _ := m.FilesDir(o.Host) |
| files, _, err := m.localMarkers(o.Host, filepath.Join(dir, ".none")) |
| - if err != nil || files == nil || files.Op == g || files.Incarnation != h.Incarnation || !ancestorOf(h, g, files.Op) { |
| + if err != nil || files == nil || files.Op == t || files.Incarnation != h.Incarnation { |
| return nil |
| } |
| - want, ok := m.recordedFilesDigest(o.Host, v, g) |
| + var vouch string // the published generation whose recorded digest the files must match |
| + switch { |
| + case published == t: |
| + if !ancestorOf(h, t, files.Op) { |
| + return nil |
| + } |
| + vouch = t |
| + default: |
| + path, ok := m.unpublishedPath(h, t, published) |
| + if !ok || !containsString(path[1:], files.Op) { |
| + return nil |
| + } |
| + vouch = published |
| + } |
| + want, ok := m.recordedFilesDigest(o.Host, v, vouch) |
| if !ok { |
| - return nil // nothing recorded for G: the start check decides |
| + return nil // nothing recorded: the start check decides |
| } |
| - body, err := json.Marshal(moves.FilesGen{Op: g, Incarnation: h.Incarnation}) |
| + body, err := json.Marshal(moves.FilesGen{Op: t, Incarnation: h.Incarnation}) |
| if err != nil { |
| return err |
| } |
| return work.With(ctx, id, func() error { |
| if m.verifyFilesContent(o.Host, v, want) != nil { |
| - return nil // not G's files: the start check decides |
| + return nil // not the vouching generation's files: the start check decides |
| } |
| if err := syncPath(dir); err != nil { |
| return err |
| } |
| - log.Printf("moves: %s: .files-gen repaired from %s to the published %s", o.Host, files.Op, g) |
| + log.Printf("moves: %s: .files-gen repaired from %s to %s (vouched by the published %s)", o.Host, files.Op, t, vouch) |
| return writeFileAtomic(filepath.Join(dir, archive.MarkerName), body, 0o644) |
| }) |
| } |
| |
| +// unpublishedPath walks from back to the published generation to through |
| +// cold admissions, each into the generation walked and on this box and |
| +// region. It returns the generations from from to to, inclusive, or false |
| +// when the chain leaves this box or region, breaks, loops, or never |
| +// reaches to. |
| +func (m *movesProbeOwners) unpublishedPath(h *hostclaim.Holder, from, to string) ([]string, bool) { |
| + path := []string{from} |
| + for g := from; g != to; { |
| + if len(path) > len(h.Admissions)+1 { |
| + return nil, false |
| + } |
| + var found *hostclaim.AdmissionRecord |
| + for i := len(h.Admissions) - 1; i >= 0; i-- { |
| + if a := h.Admissions[i]; a.Generation == g && a.Mode == "cold" { |
| + found = &h.Admissions[i] |
| + break |
| + } |
| + } |
| + if found == nil || found.Box != m.Box || found.Region != m.Region || found.Predecessor == "" { |
| + return nil, false |
| + } |
| + g = found.Predecessor |
| + path = append(path, g) |
| + } |
| + return path, true |
| +} |
| + |
| // coldPredecessor is the predecessor of the newest cold admission into g. |
| func coldPredecessor(h *hostclaim.Holder, g string) (string, bool) { |
| for i := len(h.Admissions) - 1; i >= 0; i-- { |