| index 5d15da4b4..e3d31deda 100644 |
| --- a/internal/host/movesprobeowners.go |
| +++ b/internal/host/movesprobeowners.go |
| @@ -3,6 +3,8 @@ package host |
| import ( |
| "bytes" |
| "context" |
| + "crypto/sha256" |
| + "encoding/hex" |
| "encoding/json" |
| "errors" |
| "fmt" |
| @@ -41,9 +43,12 @@ import ( |
| // replication and the publication sequence (claimwakeassembly.go). This |
| // owner set supplies what moves knows: |
| // |
| -// - Select (read-only, before admission): the launch-input pin at spawn |
| -// (§7.7, round-2 finding 14): the live release and the launch |
| -// environment's report are the pinned ones, else ProbeInputsChanged |
| +// - Select (read-only, before admission): the landing is still |
| +// admissible (its deadline, the record attempt within what T11 |
| +// admitted, the install the record vouches for); the launch-input pin |
| +// at spawn (§7.7, round-2 finding 14): the live release and the report |
| +// of the selected launch's own environment are the pinned ones, else |
| +// ProbeInputsChanged |
| // with the recorded sentence "configuration changed during the move", |
| // which the landing turns into its rollback (Task 6); then the |
| // admission per §7.14. The first probe is a cold admission into the |
| @@ -59,18 +64,25 @@ import ( |
| // once it has (H14). The launch identity is the pinned config and |
| // release (H4), CARLOS_MOVE_PROBE=1 is in the child's environment (H18) |
| // and the readiness path is the app's moves.ready_path (H12). |
| -// - AdmissionEvidence: the same decisions, re-derived and compared, |
| -// against a baseline selected again, immediately before the CAS. |
| -// - Materialize, before every start, under the admitted spawn selection: |
| -// the held render and the held release, each under its own claim |
| -// selection, held-config and held-release, through the existing held |
| -// paths (appconfig.RenderHeldUnder, release.MaterializeHeldUnder) and |
| -// MovesClaimPolicy's probing rows (H13); the rendered etags must be the |
| -// pins (the authoritative check); then the §5.1 start check |
| -// (moves.StartCheck) against the claim after the admission's CAS. |
| -// - Publish: the prepared generation's marker, before replication: the |
| -// move id with the build's pin, superseding the current marker by its |
| -// exact ETag or created when there is none. PublishFenced: a restarted |
| +// - AdmissionEvidence: the same decisions and the same admissibility, |
| +// re-derived and compared, pin included, against a baseline selected |
| +// again, immediately before the CAS. |
| +// - Materialize, before every start, under the admitted spawn selection, |
| +// in the order ruled for C1 (fix round 1, finding 8): the held render |
| +// and the held release, each under its own claim selection, held-config |
| +// and held-release, through the existing held paths |
| +// (appconfig.RenderHeldUnder, release.MaterializeHeldUnder) and |
| +// MovesClaimPolicy's probing rows (H13), the render's etags compared |
| +// with the pins before anything is selected or written; then the |
| +// verified prepared marker, published under the admitted writer's |
| +// fence; then the §5.1 start check (moves.StartCheck, not weakened) |
| +// against the claim after the admission's CAS; only then the spawn. |
| +// - Publish: Group D's call before replication finds the prepared marker |
| +// Materialize published and records it; it writes only under the fence |
| +// Materialize recorded for the session. The marker names the move id |
| +// with the build's pin and install.json's digest, superseding the |
| +// current marker (an earlier stay's, which Plan 2 keeps) by its exact |
| +// ETag, or created when there is none. PublishFenced: a restarted |
| // attempt's fresh generation, after its own first root, as serving |
| // publishes it. PublicationEvidence: this process's receipt for the |
| // admitted session, read back. |
| @@ -93,9 +105,20 @@ type movesProbeOwners struct { |
| // Launch is the held row's launch: the live executable, its digest, |
| // arguments, environment and version (the claim's launch snapshot). |
| Launch func(context.Context, registry.Route) (ClaimServingLaunch, error) |
| - // EnvReport is moves.ReportEnv's digest of the held row's launch |
| - // environment, the value Inputs.EnvReport pinned at readiness. |
| - EnvReport func(context.Context, registry.Route) (string, error) |
| + // EnvKey is the env report's HMAC key (moves.ReportKeyFromSecrets). The |
| + // pin is checked against moves.ReportEnv(EnvKey, env) of the selected |
| + // launch's own environment, so no report can be decoupled from what |
| + // runs (fix round 1, finding 3). Normalisation contract: env is the |
| + // launch's K=V list, last value wins (childEnv's dedupe), with the |
| + // probe-only variables (probeOnlyVars: CARLOS_MOVE_PROBE) removed, |
| + // since the source never ran with them and the probe adds |
| + // CARLOS_MOVE_PROBE=1 after the pin. The production Launch must |
| + // therefore be the env the readiness pin was reported over |
| + // (ExecBackend.LaunchEnvironment's composition; claim Plan 10's launch |
| + // accessor). |
| + EnvKey []byte |
| + // Admitted is the number of probe attempts T11 admitted (H10). |
| + Admitted func(context.Context, *provision.Record) (int, error) |
| // Works selects a held action's claim work at the admitted capture |
| // (hostbox.WorkSource.SelectAt). |
| Works func(ctx context.Context, host, action string, captured hostguard.Captured) (hostwrite.Work, error) |
| @@ -106,6 +129,9 @@ type movesProbeOwners struct { |
| |
| mu sync.Mutex |
| published map[string]ClaimPublicationReceipt |
| + // fences are the admitted spawn selections Materialize ran under, by |
| + // host and session: the live fence for the session's marker writes. |
| + fences map[string]hostwrite.Work |
| } |
| |
| var ( |
| @@ -141,7 +167,7 @@ func (m *movesProbeOwners) MovesOwners(ops OperationAttempts) ClaimMovesOwners { |
| |
| func (m *movesProbeOwners) valid() bool { |
| return m != nil && m.Claims != nil && m.Records != nil && m.ReadyPath != nil && m.Data != nil && m.Bucket != "" && m.Region != "" && m.Box != "" && |
| - m.DataDir != "" && m.FilesDir != nil && m.Markers != nil && m.Route != nil && m.Launch != nil && m.EnvReport != nil && m.Works != nil && |
| + m.DataDir != "" && m.FilesDir != nil && m.Markers != nil && m.Route != nil && m.Launch != nil && len(m.EnvKey) != 0 && m.Admitted != nil && m.Works != nil && |
| m.Render != nil && m.Release != nil && m.NewID != nil && m.Now != nil |
| } |
| |
| @@ -215,14 +241,85 @@ func heldRoute(rt registry.Route, host, op string) error { |
| return nil |
| } |
| |
| -// opAdmitted reports whether the holder already admitted a writer into the |
| -// move id: a restarted attempt then admits a fresh generation. |
| -func opAdmitted(h *hostclaim.Holder, op string) bool { |
| - if h.Generation == op { |
| - return true |
| +// firstAdmission reports whether the holder's writer is still the move's |
| +// source: the flip leaves the source's session in place, and every |
| +// admission on this box makes it the writer. A first admission is the |
| +// prepared one; any later one is a restarted attempt into a fresh |
| +// generation. It reads no admission history, so trimming cannot change it. |
| +func firstAdmission(h *hostclaim.Holder, box string) bool { |
| + return h.WriterBox != box |
| +} |
| + |
| +// admissible is the landing's own word that an admission may happen now |
| +// (fix round 1, finding 1): before the landing's deadline (the earlier of |
| +// cut_deadline and landing_deadline, H8), at a record attempt within what |
| +// T11 admitted (H10), over the install the record vouches for. |
| +func (m *movesProbeOwners) admissible(ctx context.Context, v probeLanding) error { |
| + mv := v.rec.Move |
| + d, _ := landingDeadlineOf(mv) |
| + if d == nil || !m.Now().Before(*d) { |
| + return fmt.Errorf("%w: the landing is past its deadline", hostbox.ErrChanged) |
| + } |
| + admitted, err := m.Admitted(ctx, v.rec) |
| + if err != nil { |
| + return err |
| + } |
| + if mv.Landing == nil || mv.Landing.Attempts < 1 || mv.Landing.Attempts > moves.AttemptsFor(moves.ProbeLanding, admitted) { |
| + return fmt.Errorf("%w: the record's probe attempt is not one T11 admitted", hostbox.ErrChanged) |
| + } |
| + if mv.Landing.InstalledDBSHA256 != "sha256:"+v.manifest.DBSHA256 { |
| + return fmt.Errorf("%w: the record does not vouch for this install", hostbox.ErrChanged) |
| + } |
| + return nil |
| +} |
| + |
| +// probeOnlyVars are the variables the probe adds to the launch and the |
| +// readiness pin never saw. |
| +var probeOnlyVars = map[string]bool{"CARLOS_MOVE_PROBE": true} |
| + |
| +// envDigest is the env report digest of a launch environment, normalised |
| +// as EnvKey's comment states. |
| +func envDigest(key []byte, env []string) string { |
| + vars := map[string]string{} |
| + for _, kv := range env { |
| + k, v, ok := strings.Cut(kv, "=") |
| + if !ok || probeOnlyVars[k] { |
| + continue |
| + } |
| + vars[k] = v |
| + } |
| + return moves.ReportEnv(key, vars).Digest |
| +} |
| + |
| +// manifestDigest is install.json's digest: the build writes the manifest |
| +// as json.Marshal of the journalled value (moves.BuildGeneration). |
| +func manifestDigest(m moves.InstallManifest) (string, error) { |
| + raw, err := json.Marshal(m) |
| + if err != nil { |
| + return "", err |
| + } |
| + sum := sha256.Sum256(raw) |
| + return hex.EncodeToString(sum[:]), nil |
| +} |
| + |
| +func manifestPin(m moves.InstallManifest) genselect.Pin { |
| + return genselect.Pin{SnapshotKey: m.SnapshotKey, SnapshotVersion: m.SnapshotVersion, SnapshotTXID: m.N, SnapshotCRC64: m.SnapshotCRC64} |
| +} |
| + |
| +// landingMarker reports whether the location's current marker already |
| +// names this landing: the move id's verified prepared marker, or the fresh |
| +// generation of a later admission on this box under it. Anything else (an |
| +// earlier stay's, another incarnation's) is superseded by the prepared one. |
| +func (m *movesProbeOwners) landingMarker(marker *genselect.Marker, host string, v probeLanding, digest string) bool { |
| + h := v.holder |
| + if marker == nil || marker.Incarnation != h.Incarnation || marker.Host != host || marker.Box != m.Box { |
| + return false |
| + } |
| + if marker.Op == v.op { |
| + return marker.Pin == manifestPin(v.manifest) && marker.ManifestSHA256 == digest |
| } |
| for _, a := range h.Admissions { |
| - if a.Generation == op { |
| + if a.Generation == marker.Op && a.Box == m.Box && a.Region == m.Region && a.Mode == "cold" { |
| return true |
| } |
| } |
| @@ -243,6 +340,7 @@ func (m *movesProbeOwners) request(host string, h *hostclaim.Holder) genselect.R |
| } |
| |
| func preparedPin(v probeLanding) genselect.PreparedPin { |
| + // The build's own pin, from the journalled manifest. |
| return genselect.PreparedPin{Generation: v.op, SnapshotKey: v.manifest.SnapshotKey, SnapshotVersion: v.manifest.SnapshotVersion, SnapshotTXID: v.manifest.N, SnapshotCRC64: v.manifest.SnapshotCRC64} |
| } |
| |
| @@ -271,17 +369,22 @@ func (m *movesProbeOwners) currentMarker(ctx context.Context, h *hostclaim.Holde |
| |
| // baseline is the admission's baseline (H14): the build's pin for the |
| // prepared generation; for a restarted attempt, the prepared pin while the |
| -// location has no marker (the move id published nothing beyond the build) |
| -// and the certified remote once it has one. |
| +// location's marker is not this landing's (the move id published nothing |
| +// beyond the build; an earlier stay's pin is never a baseline), and the |
| +// certified remote once it is. |
| func (m *movesProbeOwners) baseline(ctx context.Context, host string, v probeLanding, prepared bool) (*genselect.Plan, error) { |
| if prepared { |
| return genselect.SelectPrepared(ctx, m.Data, m.request(host, v.holder), preparedPin(v)) |
| } |
| - current, err := m.currentMarker(ctx, v.holder) |
| + digest, err := manifestDigest(v.manifest) |
| + if err != nil { |
| + return nil, err |
| + } |
| + marker, err := m.readMarker(ctx, v.holder) |
| if err != nil { |
| return nil, err |
| } |
| - if current == nil { |
| + if !m.landingMarker(marker, host, v, digest) { |
| return genselect.SelectPrepared(ctx, m.Data, m.request(host, v.holder), preparedPin(v)) |
| } |
| return genselect.Select(ctx, m.Data, m.request(host, v.holder)) |
| @@ -350,8 +453,11 @@ func (m *movesProbeOwners) Select(ctx context.Context, rt registry.Route, o host |
| if err := heldRoute(rt, o.Host, v.op); err != nil { |
| return ClaimLaunchInputs{}, err |
| } |
| + if err := m.admissible(ctx, v); err != nil { |
| + return ClaimLaunchInputs{}, err |
| + } |
| h := v.holder |
| - prepared := !opAdmitted(h, v.op) |
| + prepared := firstAdmission(h, m.Box) |
| admission := hostclaim.Admission{ObservedOpID: v.op, ObservedPhase: o.Claim.Op.Phase, ObservedSession: h.Session, ObservedGeneration: h.Generation, Box: m.Box, Mode: "cold", |
| KeepStamp: h.Stamp > h.Session, Region: m.Region, PredecessorRegion: predecessorRegion(h, v.rec.Move.From)} |
| if prepared { |
| @@ -368,7 +474,7 @@ func (m *movesProbeOwners) Select(ctx context.Context, rt registry.Route, o host |
| } |
| } else { |
| admission.NewGeneration = m.NewID() |
| - if !claimFreshGeneration(h, admission.NewGeneration) { |
| + if admission.NewGeneration == v.op || !claimFreshGeneration(h, admission.NewGeneration) { |
| return ClaimLaunchInputs{}, hostwrite.ErrUnavailable |
| } |
| } |
| @@ -393,11 +499,7 @@ func (m *movesProbeOwners) Select(ctx context.Context, rt registry.Route, o host |
| if l.Version != pins.Release { |
| return ClaimLaunchInputs{}, probeInputsChanged() |
| } |
| - report, err := m.EnvReport(ctx, rt) |
| - if err != nil { |
| - return ClaimLaunchInputs{}, err |
| - } |
| - if report == "" || report != pins.EnvReport { |
| + if pins.EnvReport == "" || envDigest(m.EnvKey, l.Env) != pins.EnvReport { |
| return ClaimLaunchInputs{}, probeInputsChanged() |
| } |
| return ClaimLaunchInputs{Roles: []string{"main"}, Identity: launchIdentity(pins), Baseline: plan, Admission: admission, |
| @@ -412,20 +514,25 @@ func (m *movesProbeOwners) AdmissionEvidence(ctx context.Context, o hostclaim.Ob |
| if err != nil { |
| return err |
| } |
| + if err := m.admissible(ctx, v); err != nil { |
| + return err |
| + } |
| h := v.holder |
| if in.ObservedOpID != v.op || in.ObservedPhase != o.Claim.Op.Phase || in.ObservedSession != h.Session || in.ObservedGeneration != h.Generation || in.Box != m.Box || in.Region != m.Region || |
| in.Mode != "cold" || in.PredecessorCertified || in.LossID != "" || len(in.AdditionalWritesLost) != 0 || in.KeepStamp != (h.Stamp > h.Session) || |
| in.PredecessorRegion != predecessorRegion(h, v.rec.Move.From) || baseline == nil || in.RemoteMaxTXID != baseline.MaxTXID { |
| return hostbox.ErrChanged |
| } |
| - prepared := !opAdmitted(h, v.op) |
| + prepared := firstAdmission(h, m.Box) |
| switch { |
| case prepared: |
| coverage := "" |
| if v.seal.FromDB { |
| coverage = "sealed" |
| } |
| - if in.NewGeneration != v.op || in.Coverage != coverage { |
| + pin := manifestPin(v.manifest) |
| + if in.NewGeneration != v.op || in.Coverage != coverage || baseline.Generation != v.op || baseline.Pin == nil || *baseline.Pin != pin || |
| + baseline.MaxTXID != v.manifest.N || v.manifest.N != v.seal.N { |
| return hostbox.ErrChanged |
| } |
| rt, err := m.route(o.Host, v.op) |
| @@ -448,7 +555,7 @@ func (m *movesProbeOwners) AdmissionEvidence(ctx context.Context, o hostclaim.Ob |
| if err != nil { |
| return err |
| } |
| - if fresh.Generation != baseline.Generation || fresh.MaxTXID != baseline.MaxTXID || !reflect.DeepEqual(fresh.Files, baseline.Files) { |
| + if fresh.Generation != baseline.Generation || fresh.MaxTXID != baseline.MaxTXID || !reflect.DeepEqual(fresh.Files, baseline.Files) || !reflect.DeepEqual(fresh.Pin, baseline.Pin) { |
| return hostbox.ErrChanged |
| } |
| return nil |
| @@ -491,8 +598,12 @@ func (w heldIdentityWork) With(ctx context.Context, _ hostwrite.Identity, fn fun |
| return w.Work.With(ctx, w.ID, fn) |
| } |
| |
| -// Materialize runs before every start of the admitted probe: the held |
| -// render and release under their own selections, then the start check. |
| +// Materialize runs before every start of the admitted probe, in the ruled |
| +// order: the held render (its etags compared with the pins before anything |
| +// is selected or written) and the held release under their own selections, |
| +// the launch environment's pin again over the exact env that will run, the |
| +// verified prepared marker under the admitted writer's fence, then the |
| +// start check. The spawn follows only if all of it passed. |
| func (m *movesProbeOwners) Materialize(ctx context.Context, o hostclaim.Observed, in ClaimLaunchInputs, work hostwrite.Work) error { |
| v, err := m.landing(ctx, o) |
| if err != nil { |
| @@ -508,8 +619,9 @@ func (m *movesProbeOwners) Materialize(ctx context.Context, o hostclaim.Observed |
| if in.Identity != launchIdentity(pins) { |
| return hostbox.ErrChanged |
| } |
| + fence := writerFence(work, in.Identity) |
| // The admitted spawn selection still holds under the launch identity. |
| - if err := work.With(ctx, in.Identity, func() error { return nil }); err != nil { |
| + if err := fence(ctx); err != nil { |
| return err |
| } |
| rt, err := m.route(o.Host, v.op) |
| @@ -522,6 +634,12 @@ func (m *movesProbeOwners) Materialize(ctx context.Context, o hostclaim.Observed |
| h := v.holder |
| captured := hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp} |
| hr, err := m.Render(ctx, rt, v.op, func(pre appconfig.HeldRender) (hostwrite.Work, error) { |
| + // The pin decides before any selection or write (fix round 1, |
| + // finding 4): a changed configuration is the landing's rollback |
| + // sentence, never the policy's refusal of the write. |
| + if !sameBundle(pre.BundleETags, pins.BundleETags) || pre.OverlayETag != pins.OverlayETag { |
| + return nil, probeInputsChanged() |
| + } |
| w, err := m.Works(ctx, o.Host, movesActionHeldConfig, captured) |
| if err != nil { |
| return nil, err |
| @@ -541,9 +659,123 @@ func (m *movesProbeOwners) Materialize(ctx context.Context, o hostclaim.Observed |
| if err := m.Release(ctx, rt, pins.Release, heldIdentityWork{Work: released, ID: hostwrite.Identity{Release: pins.Release}}); err != nil { |
| return err |
| } |
| + if envDigest(m.EnvKey, in.Env) != pins.EnvReport { |
| + return probeInputsChanged() |
| + } |
| + m.mu.Lock() |
| + if m.fences == nil { |
| + m.fences = map[string]hostwrite.Work{} |
| + } |
| + m.fences[claimPublishedKey(o.Host, h.Session)] = work |
| + m.mu.Unlock() |
| + receipt, err := m.preparedMarker(ctx, o.Host, v, fence) |
| + if err != nil { |
| + return err |
| + } |
| + if in.Prepared { |
| + m.record(o.Host, h.Session, receipt) |
| + } |
| return m.startCheck(ctx, o, in) |
| } |
| |
| +// writerFence is the admitted writer's live fence over its spawn selection: |
| +// the attempt's lease, the session, stamp and generation, and the policy, |
| +// checked afresh on every call (claimLeaseWork over hostbox's selection). |
| +func writerFence(work hostwrite.Work, id hostwrite.Identity) func(context.Context) error { |
| + return func(ctx context.Context) error { |
| + if err := ctx.Err(); err != nil { |
| + return err |
| + } |
| + return work.With(ctx, id, func() error { return nil }) |
| + } |
| +} |
| + |
| +// preparedMarker establishes the verified prepared marker under fence: the |
| +// install.json the build wrote is the journalled manifest, and the location's |
| +// generation.json names this landing (left as it is) or is superseded by the |
| +// move id's marker with the build's pin and that manifest's digest, by |
| +// If-Match on the current version or If-None-Match when there is none. The |
| +// store runs fence before every request attempt. It returns the receipt for |
| +// the move id's marker, or the zero receipt when a later landing generation's |
| +// marker is current. |
| +func (m *movesProbeOwners) preparedMarker(ctx context.Context, host string, v probeLanding, fence func(context.Context) error) (ClaimPublicationReceipt, error) { |
| + h := v.holder |
| + digest, err := manifestDigest(v.manifest) |
| + if err != nil { |
| + return ClaimPublicationReceipt{}, err |
| + } |
| + bounded, cancel := context.WithTimeout(ctx, hostbox.RequestLimit) |
| + defer cancel() |
| + if err := m.verifyInstall(bounded, h, v, digest); err != nil { |
| + return ClaimPublicationReceipt{}, err |
| + } |
| + pin := manifestPin(v.manifest) |
| + root := genselect.File{Key: v.manifest.SnapshotKey, VersionID: v.manifest.SnapshotVersion, Level: 9, MinTXID: 1, MaxTXID: v.manifest.N} |
| + current, err := m.currentMarker(bounded, h) |
| + if err != nil { |
| + return ClaimPublicationReceipt{}, err |
| + } |
| + if current != nil { |
| + marker, err := m.readMarker(bounded, h) |
| + if err != nil { |
| + return ClaimPublicationReceipt{}, err |
| + } |
| + if m.landingMarker(marker, host, v, digest) { |
| + if marker.Op != v.op { |
| + return ClaimPublicationReceipt{}, nil |
| + } |
| + return ClaimPublicationReceipt{MarkerVersion: current.ID, Root: root}, nil |
| + } |
| + } |
| + body, err := json.Marshal(genselect.Marker{Op: v.op, Incarnation: h.Incarnation, Host: host, Box: m.Box, InstalledAt: m.Now().UTC(), ManifestSHA256: digest, Pin: pin}) |
| + if err != nil { |
| + return ClaimPublicationReceipt{}, err |
| + } |
| + expected := "" |
| + if current != nil { |
| + expected = current.ETag |
| + } |
| + if err := fence(bounded); err != nil { |
| + return ClaimPublicationReceipt{}, err |
| + } |
| + version, err := m.Markers.PutMarker(bounded, m.Bucket, markerKey(h), body, expected, fence) |
| + if err != nil { |
| + return ClaimPublicationReceipt{}, err |
| + } |
| + if version == "" { |
| + return ClaimPublicationReceipt{}, hostwrite.ErrUnavailable |
| + } |
| + return ClaimPublicationReceipt{MarkerVersion: version, Root: root}, nil |
| +} |
| + |
| +// verifyInstall requires the generation's current install.json to be the |
| +// journalled manifest, byte for byte (its digest is the marker's). |
| +func (m *movesProbeOwners) verifyInstall(ctx context.Context, h *hostclaim.Holder, v probeLanding, digest string) error { |
| + key := moves.InstallManifestKey(moves.GenerationPrefix(h.Location, v.op)) |
| + versions, err := m.Data.ListVersions(ctx, m.Bucket, key) |
| + if err != nil { |
| + return err |
| + } |
| + id := "" |
| + for _, ver := range versions { |
| + if ver.Key == key && ver.IsLatest && !ver.DeleteMarker { |
| + id = ver.VersionID |
| + } |
| + } |
| + if id == "" { |
| + return fmt.Errorf("%w: the generation has no install.json", hostbox.ErrChanged) |
| + } |
| + raw, err := m.Data.GetVersion(ctx, m.Bucket, key, id) |
| + if err != nil { |
| + return err |
| + } |
| + sum := sha256.Sum256(raw) |
| + if hex.EncodeToString(sum[:]) != digest { |
| + return fmt.Errorf("%w: install.json is not the journalled manifest", hostbox.ErrChanged) |
| + } |
| + return nil |
| +} |
| + |
| // startCheck is §5.1's check against the claim after the admission's CAS, |
| // read fresh: generation.json from this region's location, .files-gen and |
| // <dbPath>.gen from this box. |
| @@ -588,9 +820,10 @@ func (m *movesProbeOwners) EmptyAuthority(context.Context, hostclaim.Observed, C |
| return hostbox.ErrUnsupported |
| } |
| |
| -// Publish writes the prepared generation's marker before replication: the |
| -// move id with the build's pin. A marker this box already wrote for the |
| -// move id is a replay and records the receipt again. |
| +// Publish is Group D's call for a prepared admission, before replication. |
| +// Materialize has already published the verified prepared marker; this |
| +// finds it and records the receipt. It writes only under the fence |
| +// Materialize recorded for the admitted session, and refuses without one. |
| func (m *movesProbeOwners) Publish(ctx context.Context, o hostclaim.Observed, in ClaimLaunchInputs) error { |
| v, err := m.landing(ctx, o) |
| if err != nil { |
| @@ -603,36 +836,20 @@ func (m *movesProbeOwners) Publish(ctx context.Context, o hostclaim.Observed, in |
| if !in.Prepared || in.Admission.NewGeneration != v.op { |
| return hostbox.ErrChanged |
| } |
| - pin := genselect.Pin{SnapshotKey: v.manifest.SnapshotKey, SnapshotVersion: v.manifest.SnapshotVersion, SnapshotTXID: v.manifest.N, SnapshotCRC64: v.manifest.SnapshotCRC64} |
| - root := genselect.File{Key: v.manifest.SnapshotKey, VersionID: v.manifest.SnapshotVersion, Level: 9, MinTXID: 1, MaxTXID: v.manifest.N} |
| - bounded, cancel := context.WithTimeout(ctx, hostbox.RequestLimit) |
| - defer cancel() |
| - current, err := m.currentMarker(bounded, h) |
| - if err != nil { |
| - return err |
| - } |
| - if current != nil { |
| - if marker, err := m.readMarker(bounded, h); err == nil && marker != nil && marker.Op == v.op && marker.Box == m.Box && marker.Incarnation == h.Incarnation && marker.Host == o.Host && marker.Pin == pin { |
| - m.record(o.Host, h.Session, ClaimPublicationReceipt{MarkerVersion: current.ID, Root: root}) |
| - return nil |
| - } |
| - } |
| - body, err := json.Marshal(genselect.Marker{Op: v.op, Incarnation: h.Incarnation, Host: o.Host, Box: m.Box, InstalledAt: m.Now().UTC(), Pin: pin}) |
| - if err != nil { |
| - return err |
| - } |
| - expected := "" |
| - if current != nil { |
| - expected = current.ETag |
| + m.mu.Lock() |
| + work := m.fences[claimPublishedKey(o.Host, h.Session)] |
| + m.mu.Unlock() |
| + if work == nil { |
| + return fmt.Errorf("%w: no admitted writer's fence for session %d", hostbox.ErrChanged, h.Session) |
| } |
| - version, err := m.Markers.PutMarker(bounded, m.Bucket, markerKey(h), body, expected, func(ctx context.Context) error { return ctx.Err() }) |
| + receipt, err := m.preparedMarker(ctx, o.Host, v, writerFence(work, in.Identity)) |
| if err != nil { |
| return err |
| } |
| - if version == "" { |
| - return hostwrite.ErrUnavailable |
| + if receipt.MarkerVersion == "" { |
| + return hostbox.ErrChanged |
| } |
| - m.record(o.Host, h.Session, ClaimPublicationReceipt{MarkerVersion: version, Root: root}) |
| + m.record(o.Host, h.Session, receipt) |
| return nil |
| } |
| |
| @@ -740,8 +957,14 @@ func (m *movesProbeOwners) PublicationEvidence(ctx context.Context, o hostclaim. |
| if plan.Generation != h.Generation || plan.Marker == nil || plan.MarkerVersion != receipt.MarkerVersion || plan.Marker.Op != h.Generation || plan.Marker.Box != m.Box || plan.Marker.Incarnation != h.Incarnation || plan.Marker.Host != o.Host { |
| return hostbox.ErrChanged |
| } |
| - if h.Generation == v.op && (plan.Marker.SnapshotKey != v.manifest.SnapshotKey || plan.Marker.SnapshotVersion != v.manifest.SnapshotVersion || plan.Marker.SnapshotTXID != v.manifest.N) { |
| - return hostbox.ErrChanged |
| + if h.Generation == v.op { |
| + digest, err := manifestDigest(v.manifest) |
| + if err != nil { |
| + return err |
| + } |
| + if plan.Marker.Pin != manifestPin(v.manifest) || plan.Marker.ManifestSHA256 != digest { |
| + return hostbox.ErrChanged |
| + } |
| } |
| versions, err := m.Data.ListVersions(ctx, m.Bucket, receipt.Root.Key) |
| if err != nil { |