| index 5fade535d..ee19ae754 100644 |
| --- a/internal/release/adopt.go |
| +++ b/internal/release/adopt.go |
| @@ -84,6 +84,15 @@ type passConfig struct { |
| // proof is D3's gate (WithProofGate, proofgate.go). nil, the default for |
| // every existing caller and test, means no proof is read or required. |
| proof *ProofGate |
| + |
| + // pins is WithPinSource's provider: staged copies that something beyond |
| + // the registry rows, the live links and the generations file still |
| + // refers to. nil adds nothing. |
| + pins PinSource |
| + |
| + // stage is what ensureStagedChecked needs to judge a staged copy it is |
| + // about to reuse (spec §4.2). PassFilter sets it once per pass. |
| + stage stageCheck |
| } |
| |
| // NagExpiredCanaries opts a Pass/PassFilter call into spec §5's nag: one |
| @@ -279,6 +288,13 @@ func WithProofGate(g *ProofGate) PassOption { |
| return func(c *passConfig) { c.proof = g } |
| } |
| |
| +// WithPinSource adds src's references to the pin set a reused staged copy is |
| +// checked against (spec §4.2), on top of the live links, the registry rows |
| +// and the generations file, which every pass with a data dir reads itself. |
| +func WithPinSource(src PinSource) PassOption { |
| + return func(c *passConfig) { c.pins = src } |
| +} |
| + |
| // stageBlocked reports whether err is the version store refusing a write to |
| // THIS uid, rather than any of the many other ways staging fails. errors.Is |
| // unwraps the *os.PathError that mkdir/openat return. |
| @@ -366,6 +382,48 @@ func PassFilter(ctx context.Context, reg *registry.Registry, s Store, pub ed2551 |
| } |
| cfg.selected = selected |
| |
| + // The reuse check's view of what refers to a staged copy (spec §4.2). It |
| + // is asked only when a staged copy turns out to differ from its canonical |
| + // release, so a healthy pass never pays for it, and it reads fresh then, |
| + // so a host this pass has already repointed counts. |
| + cfg.stage = stageCheck{ |
| + verified: map[string]bool{}, |
| + refs: func() (StagedPaths, error) { |
| + rows, err := reg.List() |
| + if err != nil { |
| + return nil, fmt.Errorf("list routes: %w", err) |
| + } |
| + pins, err := BoxPins(optDir, rows) |
| + if err != nil { |
| + return nil, err |
| + } |
| + // Live and pending generations, from the file plan 5's agent |
| + // writes into the data dir (index amendments). Every production |
| + // pass passes the data dir as BakeDir: root's RunAdopt, the |
| + // agent's loops, and cold materialization. A pass with no data |
| + // dir (tests) has no agent beside it to ask. |
| + if cfg.bakeDir != "" { |
| + gen, err := GenerationPins(GenerationsPath(cfg.bakeDir), func() ([]registry.Route, error) { return rows, nil }).StagedPins(ctx, optDir) |
| + if err != nil { |
| + return nil, err |
| + } |
| + for p := range gen { |
| + pins.add(p) |
| + } |
| + } |
| + if cfg.pins != nil { |
| + more, err := cfg.pins.StagedPins(ctx, optDir) |
| + if err != nil { |
| + return nil, err |
| + } |
| + for p := range more { |
| + pins.add(p) |
| + } |
| + } |
| + return pins, nil |
| + }, |
| + } |
| + |
| // Resolve every distinct (Account, App, Channel) once. Verification |
| // failures kill the group (nil entry), not the pass. |
| // |
| @@ -1089,7 +1147,7 @@ func adoptHost(ctx context.Context, work hostwrite.Work, reg *registry.Registry, |
| if !admitted { |
| return |
| } |
| - m, err := ensureStaged(ctx, s, optDir, RouteAccount(rt), rt.App, ver, rt.Host) |
| + m, err := ensureStagedChecked(ctx, s, optDir, RouteAccount(rt), rt.App, ver, rt.Host, cfg.stage) |
| if err != nil { |
| fmt.Printf(" %s: %v\n", rt.Host, err) |
| // A version this uid cannot write is the one staging failure a |
| @@ -1579,6 +1637,18 @@ func capabilityPlan(rt registry.Route, m *Manifest, canRestart bool) (caps.Plan, |
| }, nil |
| } |
| |
| +// stageCheck is what ensureStagedChecked needs to judge a staged copy it is |
| +// about to reuse (spec §4.2). |
| +type stageCheck struct { |
| + // refs reports the staged copies something on this box still refers to. |
| + // nil, or an error, means unknown, and unknown never permits a restage. |
| + refs func() (StagedPaths, error) |
| + // verified holds staged paths whose bytes already matched their canonical |
| + // release this pass, so N hosts moving onto one build hash it once. nil |
| + // remembers nothing. |
| + verified map[string]bool |
| +} |
| + |
| // ensureStaged makes ver's build of app available in the version store, |
| // downloading and sha256-verifying it if needed. Idempotent across hosts in |
| // one pass. The manifest supplies the sha; the bytes are refused unless they |
| @@ -1590,9 +1660,10 @@ func capabilityPlan(rt registry.Route, m *Manifest, canRestart bool) (caps.Plan, |
| // current: a fresh download writes the sibling alongside the binary, and a |
| // binary that is ALREADY staged but missing its sibling gets one written |
| // without a re-download — the back-fill for every version staged before this |
| -// sibling existed. That sibling is a deliberately LOSSY re-marshal of the |
| -// bucket's manifest (see StageManifest's doc); the bucket object stays |
| -// canonical. |
| +// sibling existed. For copies staged by this binary or later the sibling is |
| +// the canonical manifest's exact bytes; only a copy an older binary staged |
| +// carries a lossy re-marshal (see ReadStagedManifest). The bucket object stays |
| +// canonical either way. |
| // |
| // The back-fill's own fetch failure is NOT fatal when the binary is already |
| // staged (2026-08-03 review, I2): this function's whole promise is "the |
| @@ -1615,74 +1686,131 @@ func capabilityPlan(rt registry.Route, m *Manifest, canRestart bool) (caps.Plan, |
| // and adoptHost turns that into a refusal when — and only when — capabilities |
| // are armed for the route. Every other caller keeps today's contract exactly: |
| // an error means "the binary is not on disk", nil means it is. |
| +// |
| +// A version directory that is already staged is no longer trusted by name |
| +// (zero-downtime deploys spec §4.2). A version name can be shipped again with |
| +// different bytes once retention has pruned it, and the staged path is keyed |
| +// by name alone. So a staged binary is hashed and compared with the canonical |
| +// manifest's digest for this platform: |
| +// - a match reuses it, as before; |
| +// - a mismatch restages over it only when nothing on the box refers to it |
| +// (chk.refs: live links, Version, PrevVersion, the generations file, |
| +// and whatever WithPinSource adds); otherwise the version is refused with |
| +// ErrVersionReused, so a copy under another instance's build never |
| +// changes underneath it; |
| +// - a canonical manifest that cannot be read keeps today's answer: the copy |
| +// is used as staged, because a box cannot compare what it cannot read, |
| +// and the agent runs any attempt onto it in hold (CanonicalDemand). |
| +// |
| +// ensureStaged is ensureStagedChecked with the pin set this package can read |
| +// with no registry in hand: the live links. The tests that stage one version |
| +// directly call it. |
| func ensureStaged(ctx context.Context, s Store, optDir, acct, app, ver, host string) (*Manifest, error) { |
| - staged := false |
| - if _, err := os.Stat(VersionPath(optDir, acct, ver, app)); err == nil { |
| - staged = true |
| - if _, err := os.Stat(StagedManifestPath(optDir, acct, ver, app)); err == nil { |
| - // Binary and sibling both present: the sibling IS this function's |
| - // manifest in hand on this path — read once, locally, with no |
| - // bucket round-trip, which is the whole point of staging it. This |
| - // is not the re-read the doc comment above forbids: it happens |
| - // only where the file is known to exist, and a read that fails or |
| - // refuses (a rolled-back platform binary meeting a capability |
| - // block a newer one wrote) falls through to the fetch below |
| - // rather than answering "no capabilities". |
| - m, err := ReadStagedManifest(optDir, acct, ver, app) |
| - if err == nil && m != nil { |
| - return m, nil |
| - } |
| - if err != nil { |
| - fmt.Printf(" %s: staged manifest for %s: %v — re-fetching it\n", host, ver, err) |
| - } |
| - } |
| - // Binary staged, sibling missing: fall through to fetch the manifest |
| - // and write ONLY the sibling. This is the one path where an |
| - // already-staged version's manifest is re-fetched — kept cheap by |
| - // only ever taking it when the sibling itself is absent. |
| - } |
| + return ensureStagedChecked(ctx, s, optDir, acct, app, ver, host, stageCheck{ |
| + refs: func() (StagedPaths, error) { return BoxPins(optDir, nil) }, |
| + }) |
| +} |
| + |
| +// ensureStagedChecked is ensureStaged with the reuse check's references |
| +// supplied by chk; see ensureStaged's doc for the whole contract. |
| +func ensureStagedChecked(ctx context.Context, s Store, optDir, acct, app, ver, host string, chk stageCheck) (*Manifest, error) { |
| + path := VersionPath(optDir, acct, ver, app) |
| + _, statErr := os.Stat(path) |
| + staged := statErr == nil |
| m, raw, err := GetManifestRaw(ctx, s, acct, app, ver) |
| if err != nil { |
| - if staged { |
| - // The binary IS staged — this function's whole promise — so a |
| - // fetch failure here is only ever worth a log line; see the |
| - // doc comment above and backfillManifest's per-pass retry. |
| - fmt.Printf(" %s: back-fill manifest for %s: %v\n", host, ver, err) |
| - return nil, nil |
| + if !staged { |
| + return nil, fmt.Errorf("manifest for %s/%s: %w", app, ver, err) |
| } |
| - return nil, fmt.Errorf("manifest for %s/%s: %w", app, ver, err) |
| + // The binary IS staged, which is this function's whole promise, and |
| + // the canonical manifest cannot be read to compare it with. A sibling |
| + // already on disk is the manifest in hand, exactly as before this |
| + // check existed. Without one, a fetch failure is only worth a line; |
| + // backfillManifest retries on every pass. |
| + sib, serr := ReadStagedManifest(optDir, acct, ver, app) |
| + if serr == nil && sib != nil { |
| + return sib, nil |
| + } |
| + if serr != nil { |
| + fmt.Printf(" %s: staged manifest for %s: %v\n", host, ver, serr) |
| + } |
| + fmt.Printf(" %s: back-fill manifest for %s: %v\n", host, ver, err) |
| + return nil, nil |
| } |
| - if !staged { |
| - art, ok := m.Artifacts[adoptPlatform] |
| - if !ok { |
| - // A box facing a release with no build for its platform: skip, never |
| - // install the wrong arch. The next release ships it and the box |
| - // catches up. |
| - return nil, fmt.Errorf("release %s has no %s build — skipping", ver, adoptPlatform) |
| + art, hasBuild := m.Artifacts[adoptPlatform] |
| + if staged { |
| + if chk.verified[path] { |
| + repairSibling(optDir, acct, app, ver, host, raw) |
| + return m, nil |
| } |
| - body, _, err := GetQualified(ctx, s, ArtifactKey(acct, app, ver, art.File)) |
| - if err != nil { |
| - return nil, fmt.Errorf("download %s: %w", ver, err) |
| + sum, herr := FileSHA256(path) |
| + if herr != nil { |
| + return nil, fmt.Errorf("staged %s/%s: %w", app, ver, herr) |
| + } |
| + if hasBuild && strings.EqualFold(sum, art.SHA256) { |
| + if chk.verified != nil { |
| + chk.verified[path] = true |
| + } |
| + repairSibling(optDir, acct, app, ver, host, raw) |
| + return m, nil |
| } |
| - if got := etagOf(body); got != art.SHA256 { |
| - return nil, fmt.Errorf("download %s: sha256 mismatch (refusing to stage)", ver) |
| + // Different bytes under the same name. Overwrite only a copy nothing |
| + // refers to: overwriting a referenced one would change the bytes under |
| + // another instance's build (spec §4.2). |
| + if chk.refs == nil { |
| + return nil, fmt.Errorf("%s/%s: the staged copy differs from the release and what refers to it is unknown, so it was not restaged", app, ver) |
| } |
| - if err := StageArtifact(optDir, acct, ver, app, body); err != nil { |
| - return nil, err |
| + pins, perr := chk.refs() |
| + if perr != nil { |
| + return nil, fmt.Errorf("%s/%s: the staged copy differs from the release and what refers to it is unknown (%v), so it was not restaged", app, ver, perr) |
| + } |
| + if pins.Has(path) { |
| + return nil, fmt.Errorf("%s/%s: %w", app, ver, ErrVersionReused) |
| } |
| + fmt.Printf(" %s: staged %s differs from the release in the store and nothing on this box refers to it; restaging\n", host, ver) |
| + } |
| + if !hasBuild { |
| + // A box facing a release with no build for its platform: skip, never |
| + // install the wrong arch. The next release ships it and the box |
| + // catches up. |
| + return nil, fmt.Errorf("release %s has no %s build — skipping", ver, adoptPlatform) |
| } |
| - // The sibling is the CANONICAL bytes (spec §4.2): what the bucket holds, |
| - // unknown fields and all, never a re-marshal by whichever binary staged |
| - // it. The write is best-effort: a failure here is not fatal to staging the |
| - // binary. A binary with an unreadable sibling is the leave-alone state, |
| - // which is safe (ReadStagedManifest's nil-nil-on-absence contract), and |
| - // backfillManifest gets another chance at it on every later pass. |
| + body, _, err := GetQualified(ctx, s, ArtifactKey(acct, app, ver, art.File)) |
| + if err != nil { |
| + return nil, fmt.Errorf("download %s: %w", ver, err) |
| + } |
| + if got := etagOf(body); got != art.SHA256 { |
| + return nil, fmt.Errorf("download %s: sha256 mismatch (refusing to stage)", ver) |
| + } |
| + if err := StageArtifact(optDir, acct, ver, app, body); err != nil { |
| + return nil, err |
| + } |
| + if chk.verified != nil { |
| + chk.verified[path] = true |
| + } |
| + // The sibling is the CANONICAL bytes (spec §4.2), and a restage replaces |
| + // whatever sibling the stale copy had. Best-effort: a failure here is not |
| + // fatal to staging the binary, and backfillManifest gets another chance |
| + // at it on every later pass. |
| if err := StageManifest(optDir, acct, ver, app, raw); err != nil { |
| fmt.Printf(" %s: stage manifest sibling for %s: %v\n", host, ver, err) |
| } |
| return m, nil |
| } |
| |
| +// repairSibling writes the canonical bytes beside a reused binary whose |
| +// sibling is absent or does not read: the back-fill for versions staged |
| +// before the sibling existed, and the repair for one a different binary |
| +// wrote. A sibling that reads is left alone, as before. Best-effort. |
| +func repairSibling(optDir, acct, app, ver, host string, raw []byte) { |
| + if sib, err := ReadStagedManifest(optDir, acct, ver, app); err == nil && sib != nil { |
| + return |
| + } |
| + if err := StageManifest(optDir, acct, ver, app, raw); err != nil { |
| + fmt.Printf(" %s: stage manifest sibling for %s: %v\n", host, ver, err) |
| + } |
| +} |
| + |
| // backfillManifest is adoptHost's per-pass repair for a LIVE version's |
| // staged sibling (2026-08-03 review, C1). ensureStaged's own back-fill only |
| // fires from the call inside adoptHost that sits behind the cur == ver |