host: commit as a record CAS, the claim half, the conditional open (Plan 3 Task 7, §7.5, §7.9 steps 1 and 2, H7 to H9)
probed commits through MoveCommit; its refusal past the deadline or after a recorded rollback is the commit-deadline mark. The target's committed arm, every pass while committed: the claim half (a claim lagging at probing stepped to probed first, m-2; Commit under KeepWriterSession), then, under the hold lock with record and claim re-read and committed, the probe's live entry is marked open before the hold is released, then MoveOpened. The source's location park is scoped from the flip (H7), the staging lane from the seal's bound. Census rows for the two new box-side record writes. Unarmed in production with the landing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
10 files changed,
+422
−5
approved_moves_copy_test.go+1 −0internal/host/movesfinal.go+2 −0internal/host/movesland.go+2 −4internal/host/movesland_commit_test.go+152 −0internal/host/movesland_probe_test.go+1 −0internal/host/movesland_test.go+36 −0internal/host/moveslandcommit.go+212 −0internal/host/movespass.go+5 −0internal/host/movesphase1.go+7 −1internal/provision/plan9_b8_census_test.go+4 −0
diff --git a/approved_moves_copy_test.go b/approved_moves_copy_test.go| index 43d039478..9776e290b 100644 |
| --- a/approved_moves_copy_test.go |
| +++ b/approved_moves_copy_test.go |
| @@ -580,6 +580,7 @@ var movesScanFiles = []string{ |
| "internal/host/movesinstall.go", |
| "internal/host/moveshold.go", |
| "internal/host/movesland.go", |
| + "internal/host/moveslandcommit.go", |
| "internal/host/moveslandinstall.go", |
| "internal/host/moveslandpark.go", |
| "internal/host/moveslandprobe.go", |
diff --git a/internal/host/movesfinal.go b/internal/host/movesfinal.go| index a339f7694..17f33638d 100644 |
| --- a/internal/host/movesfinal.go |
| +++ b/internal/host/movesfinal.go |
| @@ -145,6 +145,8 @@ func (w *movesWorker) participantRound(ctx context.Context, s release.Store, rec |
| switch { |
| case role == roleSource && rec.Move.State == provision.MoveCommitted: |
| return w.finalizeSource(ctx, s, rec) |
| + case role == roleTarget && rec.Move.State == provision.MoveCommitted: |
| + return w.committedTarget(ctx, s, rec) |
| case role == roleTarget && rec.Move.State == provision.MoveRollingBack: |
| return w.rollbackTarget(ctx, s, rec) |
| case role == roleTarget && rec.Move.State == provision.MoveReverting: |
diff --git a/internal/host/movesland.go b/internal/host/movesland.go| index 3ed35df5c..6367404db 100644 |
| --- a/internal/host/movesland.go |
| +++ b/internal/host/movesland.go |
| @@ -231,10 +231,8 @@ func (l *landingRun) step(ctx context.Context) (bool, error) { |
| case provision.LandProbing: |
| return l.probing(ctx) |
| case provision.LandProbed: |
| - // The probe passed. The commit is Task 7's; this phase ends at the |
| - // claim's probed step. |
| - _, err := l.syncClaimPhase(ctx) |
| - return false, err |
| + // The probe passed: the commit (moveslandcommit.go). |
| + return l.probed(ctx) |
| } |
| return false, nil |
| } |
diff --git a/internal/host/movesland_commit_test.go b/internal/host/movesland_commit_test.go| new file mode 100644 |
| index 000000000..74446c39a |
| --- /dev/null |
| +++ b/internal/host/movesland_commit_test.go |
| @@ -0,0 +1,152 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "reflect" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/provision" |
| +) |
| + |
| +// probedLanding is a landing driven from the flip through every phase with |
| +// the real transitions, to probed. |
| +func probedLanding(t *testing.T) (*landingHarness, *probeLog) { |
| + t.Helper() |
| + h, p := installedLanding(t, 2) |
| + h.land(t) |
| + h.atPhase(t, provision.LandProbed) |
| + h.holdProbed = false |
| + return h, p |
| +} |
| + |
| +// committedOpened asserts the end state of the commit and the open: the |
| +// record committed with opened_at, the claim committed with the admitted |
| +// writer kept and the commit's parks queued, the row ready, one probe. |
| +func committedOpened(t *testing.T, h *landingHarness, p *probeLog) { |
| + t.Helper() |
| + m := h.read(t, p1Host).Move |
| + if m.State != provision.MoveCommitted || m.CommittedAt == nil || m.OpenedAt == nil { |
| + t.Fatalf("record: %+v", m) |
| + } |
| + c := h.claim(t) |
| + if c.Op.Kind != hostclaim.KindLanding || c.Op.Phase != hostclaim.PhaseCommitted { |
| + t.Fatalf("claim: %+v", c.Op) |
| + } |
| + if c.Holder.Session != p.last.Session || c.Holder.Stamp != p.last.Stamp || c.Holder.Generation != p.last.Generation { |
| + t.Fatalf("the commit changed the admitted writer %+v: %+v", p.last, c.Holder) |
| + } |
| + kinds := map[string]int{} |
| + m2 := h.read(t, p1Host).Move |
| + for _, o := range c.Obligations { |
| + kinds[o.Kind+"/"+o.Region]++ |
| + if o.Kind == "park-replica" && o.Region == "ie" && (o.Park == nil || o.Park.Location != c.Holder.Location || !o.Park.EvidenceAt.Equal(m2.FlippedAt.UTC())) { |
| + t.Fatalf("the source's park is not scoped from the flip: %+v", o.Park) |
| + } |
| + if o.Kind == "park-replica" && o.Region == "uk" && (o.Park == nil || o.Park.Location != "moves/"+p1Host+"/"+p1ID) { |
| + t.Fatalf("the staging park: %+v", o.Park) |
| + } |
| + } |
| + // The source region's location (scoped from the flip, H7) and the |
| + // target region's staging lane (scoped to the seal's bound). |
| + if kinds["park-replica/ie"] != 1 || kinds["park-replica/uk"] != 1 { |
| + t.Fatalf("the commit's parks: %+v", c.Obligations) |
| + } |
| + if h.route.Status != "ready" || h.route.HoldOp != "" { |
| + t.Fatalf("the row is not open: %+v", h.route) |
| + } |
| + if len(p.configs) != 1 { |
| + t.Fatalf("probed %d times", len(p.configs)) |
| + } |
| +} |
| + |
| +// TestCommitThenOpenInOrder (brief; witness wCommitBeforeOpen): from the |
| +// flip through every landing phase with the real transitions, the commit is |
| +// the record's CAS, then the claim's; the probe's live entry is marked open, |
| +// and only then is the hold released: the row is ready only after the record |
| +// and the claim both say committed. |
| +func TestCommitThenOpenInOrder(t *testing.T) { |
| + h, p := probedLanding(t) |
| + h.land(t) |
| + committedOpened(t, h, p) |
| + want := []string{"mark:committed/committed/landing", "release:committed/committed/landing"} |
| + if !reflect.DeepEqual(h.openLog, want) { |
| + t.Fatalf("the open: %v, want %v", h.openLog, want) |
| + } |
| +} |
| + |
| +// TestOpenAfterCrashDoesNotReprobe (brief, Review Focus 3): a worker |
| +// restarted on a record at committed with the claim still landing/probed |
| +// writes the claim half, opens, and never calls the probe. |
| +func TestOpenAfterCrashDoesNotReprobe(t *testing.T) { |
| + h, p := probedLanding(t) |
| + h.landAt = "after-record-committed" |
| + h.crashLand(t) |
| + if m := h.read(t, p1Host).Move; m.State != provision.MoveCommitted || h.claim(t).Op.Phase != hostclaim.PhaseLandingProbed || h.route.Status != "landing" { |
| + t.Fatalf("the crash point: %+v claim %s row %s", m, h.claim(t).Op.Phase, h.route.Status) |
| + } |
| + h.freshTarget() |
| + h.land(t) |
| + committedOpened(t, h, p) |
| +} |
| + |
| +// The open repeats while the record is committed: a pass that finds the row |
| +// already open changes nothing, opened_at included. |
| +func TestCommittedOpenRepeatsConditionally(t *testing.T) { |
| + h, p := probedLanding(t) |
| + h.land(t) |
| + committedOpened(t, h, p) |
| + opened := *h.read(t, p1Host).Move.OpenedAt |
| + h.clock.Advance(time.Minute) |
| + h.land(t) |
| + committedOpened(t, h, p) |
| + if m := h.read(t, p1Host).Move; !m.OpenedAt.Equal(opened) { |
| + t.Fatalf("opened_at moved: %v, was %v", m.OpenedAt, opened) |
| + } |
| +} |
| + |
| +// TestLateCommitRollsBack (brief, Review Focus 2; H8): a commit that would |
| +// come after the landing's deadline is refused by the record's CAS and |
| +// becomes the commit-deadline rollback, at the one conversion site; the row |
| +// never opens. |
| +func TestLateCommitRollsBack(t *testing.T) { |
| + h, _ := probedLanding(t) |
| + h.landFault = func(point string) error { |
| + if point == "before-commit" { |
| + h.clock.Advance(cutLandingWindow + time.Minute) |
| + } |
| + return nil |
| + } |
| + h.land(t) |
| + if m := h.read(t, p1Host).Move; m.State != provision.MoveLanding || m.Reason != provision.RollbackCommitDeadline { |
| + t.Fatalf("a late commit: %+v", m) |
| + } |
| + h.landFault = nil |
| + h.land(t) |
| + rolledBack(t, h, provision.RollbackCommitDeadline, provision.LandProbed) |
| + if h.route.Status == "ready" || len(h.openLog) != 0 { |
| + t.Fatalf("a rolled-back landing opened: %+v %v", h.route, h.openLog) |
| + } |
| +} |
| + |
| +// m-2: a claim lagging at probing behind a probed record is walked to probed |
| +// before the commit, on the admitted session, so Commit (probed only) holds. |
| +func TestCommitReassertsALaggingProbedClaim(t *testing.T) { |
| + h, p := probedLanding(t) |
| + rewriteClaimOp(t, h.s, func(op *hostclaim.Op) { op.Phase = hostclaim.PhaseLandingProbing }) |
| + h.land(t) |
| + committedOpened(t, h, p) |
| +} |
| + |
| +// Unarmed: with no release of the hold the committed arm waits; the commit |
| +// itself is the record's and is made. |
| +func TestCommittedOpenWaitsUnarmed(t *testing.T) { |
| + h, _ := probedLanding(t) |
| + h.noRelease = true |
| + h.land(t) |
| + if m := h.read(t, p1Host).Move; m.State != provision.MoveCommitted || m.OpenedAt != nil || h.route.Status != "landing" { |
| + t.Fatalf("an unarmed open: %+v %+v", m, h.route) |
| + } |
| + _ = context.Background |
| +} |
diff --git a/internal/host/movesland_probe_test.go b/internal/host/movesland_probe_test.go| index 78f53dd0d..2da13677f 100644 |
| --- a/internal/host/movesland_probe_test.go |
| +++ b/internal/host/movesland_probe_test.go |
| @@ -124,6 +124,7 @@ func installedLanding(t *testing.T, attempts int) (*landingHarness, *probeLog) { |
| } |
| p := &probeLog{t: t, h: h} |
| h.probe, h.probeLive = p.wake, p.liveProbe |
| + h.holdProbed = true |
| return h, p |
| } |
diff --git a/internal/host/movesland_test.go b/internal/host/movesland_test.go| index efad7f361..50fdde0a4 100644 |
| --- a/internal/host/movesland_test.go |
| +++ b/internal/host/movesland_test.go |
| @@ -77,6 +77,15 @@ type landingHarness struct { |
| probe func(context.Context, activator.ProbeConfig) (activator.ProbeResult, error) |
| // probeLive, when set, is the activator's live probe entry. |
| probeLive func(string) (string, activator.ProbeResult, bool) |
| + // openLog records the target's open (Plan 3 Task 7): the probe entry's |
| + // mark and the hold's release, in order, with the record and claim |
| + // state each saw. |
| + openLog []string |
| + // noRelease leaves ReleaseHeld unwired (the open unarmed). |
| + noRelease bool |
| + // holdProbed stops every pass at probed, before the commit (Task 6's |
| + // tests, which end there). |
| + holdProbed bool |
| } |
| // fakeParkLeases refuses a second acquisition of a held location. |
| @@ -356,8 +365,26 @@ func (h *landingHarness) landingCfg(t *testing.T) moveCfg { |
| if h.probeLive != nil { |
| c.ProbeLive = h.probeLive |
| } |
| + c.ReleaseHeld = func(host, op string) (bool, error) { |
| + h.openLog = append(h.openLog, "release:"+h.openState(t)) |
| + if host != p1Host || !registry.IsHold(h.route.Status) || h.route.HoldOp != op { |
| + return false, nil |
| + } |
| + h.route.Status, h.route.HoldOp = "ready", "" |
| + return true, nil |
| + } |
| + if h.noRelease { |
| + c.ReleaseHeld = nil |
| + } |
| + c.MarkProbeOpen = func(host, op string) bool { |
| + h.openLog = append(h.openLog, "mark:"+h.openState(t)) |
| + return host == p1Host && op == p1ID |
| + } |
| c.landFault = func(point string) error { |
| h.landHits = append(h.landHits, point) |
| + if h.holdProbed && point == "before-commit" { |
| + return errHoldProbed |
| + } |
| if h.landFault != nil { |
| if err := h.landFault(point); err != nil { |
| return err |
| @@ -1533,3 +1560,12 @@ func TestLandingBuildIsCancelledWhenTheLeaseIsLostMidRequest(t *testing.T) { |
| h.land(t) |
| h.installed(t) |
| } |
| + |
| +// openState is the record's state, the claim's phase and the row's status |
| +// at a step of the open. |
| +func (h *landingHarness) openState(t *testing.T) string { |
| + return h.read(t, p1Host).Move.State + "/" + h.claim(t).Op.Phase + "/" + h.route.Status |
| +} |
| + |
| +// errHoldProbed is holdProbed's stop before the commit. |
| +var errHoldProbed = errors.New("held at probed by the test") |
diff --git a/internal/host/moveslandcommit.go b/internal/host/moveslandcommit.go| new file mode 100644 |
| index 000000000..9ee12ca27 |
| --- /dev/null |
| +++ b/internal/host/moveslandcommit.go |
| @@ -0,0 +1,212 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "fmt" |
| + "log" |
| + |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/moves" |
| + "github.com/carlosframework/platform/internal/provision" |
| + "github.com/carlosframework/platform/internal/registry" |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +// The commit and the open on the target (moves spec §7.5, §7.9 steps 1 and |
| +// 2; Plan 3 Task 7; preflight H6 to H9). |
| +// |
| +// probed: the commit is the record's CAS (provision.MoveCommit), which |
| +// refuses a recorded rollback and a commit after the landing's deadline |
| +// (H8); that refusal is the commit-deadline rollback mark, converted at the |
| +// pass's one rollback site. Then the committed arm runs in the same pass. |
| +// |
| +// The committed arm (H9), routed for the target by the post-flip participant |
| +// rule, runs every pass while the record is committed: |
| +// - the claim half: a claim lagging at probing is stepped to probed first |
| +// (platform-1e m-2), then hostclaim.Commit (probed only) under |
| +// KeepWriterSession, so the admitted probe session is the serving one |
| +// (H3); idempotent once committed. The source's location park is scoped |
| +// from the flip (H7), the staging lane from the seal's bound; |
| +// - the open, conditional: under the host's hold lock, with the record and |
| +// the claim re-read there and both committed, the probe's live activator |
| +// entry is marked open (MarkProbeOpen), and only then is the hold |
| +// released (ReleaseHeld), so an edge reload in between keeps the entry |
| +// (Task 5's open race). The entry from the probe is the serving process: |
| +// it is never restarted; |
| +// - MoveOpened, the first time only. |
| +// |
| +// Unarmed in production with the rest of the landing: landingArmed fails |
| +// there (ruling G17), and the open needs ReleaseHeld. |
| + |
| +// probed commits the landing: the record's CAS, then the committed arm. |
| +func (l *landingRun) probed(ctx context.Context) (bool, error) { |
| + obs, err := l.syncClaimPhase(ctx) |
| + if err != nil { |
| + return false, err |
| + } |
| + if err := l.requirePhase(obs, provision.LandProbed); err != nil { |
| + return false, err |
| + } |
| + if err := l.fault("before-commit"); err != nil { |
| + return false, err |
| + } |
| + fresh, err := provision.SetRecordMove(ctx, l.s, l.rec.Account, l.rec.App, l.rec.Host, provision.MoveCommit(l.g, l.cfg().Now())) |
| + switch { |
| + case errors.Is(err, provision.ErrCutDeadline): |
| + log.Printf("moves: %s: the commit of landing %s came after its deadline", l.rec.Host, l.g.ID) |
| + return l.markRollback(ctx, provision.RollbackCommitDeadline) |
| + case errors.Is(err, provision.ErrMoveConflict): |
| + // A replay finds committed; anything else is another writer's. |
| + if fresh, err = provision.GetRecord(ctx, l.s, l.rec.Account, l.rec.App, l.rec.Host); err != nil { |
| + return false, fmt.Errorf("re-read the record: %w", err) |
| + } |
| + if fresh == nil || fresh.Move == nil || fresh.Move.ID != l.g.ID || fresh.Move.State != provision.MoveCommitted { |
| + log.Printf("moves: %s: the commit of landing %s is refused; the record moved on", l.rec.Host, l.g.ID) |
| + return false, nil |
| + } |
| + case err != nil: |
| + return false, fmt.Errorf("the commit: %w", err) |
| + } |
| + log.Printf("moves: %s: landing %s is committed", l.rec.Host, l.g.ID) |
| + if err := l.fault("after-record-committed"); err != nil { |
| + return true, err |
| + } |
| + return true, l.w.committedTarget(ctx, l.s, *fresh) |
| +} |
| + |
| +// committedTarget is the committed arm on the target (see the file comment). |
| +func (w *movesWorker) committedTarget(ctx context.Context, s release.Store, rec provision.Record) error { |
| + cfg := w.cfg |
| + m := rec.Move |
| + if m == nil || m.Legacy() || m.State != provision.MoveCommitted || m.To != cfg.Region || m.From == cfg.Region { |
| + return nil |
| + } |
| + if !landingArmed(cfg) { |
| + w.notice(rec.Host+"#committed", "moves: %s: the open of %s waits: this box's landing is not armed", rec.Host, m.ID) |
| + return nil |
| + } |
| + l := &landingRun{w: w, s: s, rec: rec, g: provision.MoveGuard{ID: m.ID, Incarnation: m.Incarnation}, guard: cfg.Guard} |
| + if err := l.commitClaim(ctx); err != nil { |
| + return err |
| + } |
| + if err := l.fault("after-claim-committed"); err != nil { |
| + return err |
| + } |
| + if cfg.ReleaseHeld == nil { |
| + w.notice(rec.Host+"#open", "moves: %s: the open of %s waits: this box cannot release the hold", rec.Host, m.ID) |
| + return nil |
| + } |
| + if err := l.open(ctx); err != nil { |
| + return err |
| + } |
| + if m.OpenedAt == nil { |
| + if _, err := provision.SetRecordMove(ctx, s, rec.Account, rec.App, rec.Host, provision.MoveOpened(l.g, cfg.Now())); err != nil { |
| + return fmt.Errorf("the record's opened_at: %w", err) |
| + } |
| + log.Printf("moves: %s: landing %s is open", rec.Host, m.ID) |
| + nudgeSerial(ctx, s) |
| + } |
| + return nil |
| +} |
| + |
| +// commitClaim is the claim half of the commit (§7.5): written by whoever |
| +// sees the record ahead. A claim already committed is done. |
| +func (l *landingRun) commitClaim(ctx context.Context) error { |
| + cfg := l.cfg() |
| + id := l.g.ID |
| + obs, err := l.claim(ctx) |
| + if err != nil { |
| + return err |
| + } |
| + op := obs.Claim.Op |
| + if op.Kind != hostclaim.KindLanding { |
| + return fmt.Errorf("move %s: the claim is %s/%q, not landing", id, op.Kind, op.Phase) |
| + } |
| + if op.Phase == hostclaim.PhaseLandingCommitted { |
| + return nil |
| + } |
| + if cfg.Box == "" || op.Exec != cfg.Box { |
| + return fmt.Errorf("move %s: the claim's executor is %q, not this box", id, op.Exec) |
| + } |
| + if op.Phase == hostclaim.PhaseLandingProbing { |
| + // m-2: the commit follows probed; a claim lagging behind the |
| + // record's probed is stepped there on the same session. |
| + if obs, err = hostclaim.Update(ctx, l.s, obs, hostclaim.KeepWriterSession(id, hostclaim.SetMovePhase(id, hostclaim.KindLanding, hostclaim.PhaseLandingProbing, hostclaim.PhaseLandingProbed))); err != nil { |
| + return fmt.Errorf("move %s: the claim's probed before the commit: %w", id, err) |
| + } |
| + } |
| + source, staging, err := l.commitScopes(obs.Claim) |
| + if err != nil { |
| + return err |
| + } |
| + if _, err := hostclaim.Update(ctx, l.s, obs, hostclaim.KeepWriterSession(id, hostclaim.Commit(id, l.rec.Move.From, obs.Claim.Op.Intent["source"], cfg.Region, source, staging, obligationID))); err != nil { |
| + return fmt.Errorf("move %s: the claim's commit: %w", id, err) |
| + } |
| + log.Printf("moves: %s: the claim of landing %s is committed", l.rec.Host, id) |
| + return nil |
| +} |
| + |
| +// commitScopes are the parks the commit queues: the source region's location |
| +// scoped from the flip (H7), and the staging lane scoped to the seal's |
| +// bound. |
| +func (l *landingRun) commitScopes(c *hostclaim.Claim) (source, staging hostclaim.ParkScope, err error) { |
| + if source, err = sourceLocationParkScope(l.rec, *c.Holder); err != nil { |
| + return source, staging, err |
| + } |
| + seal, err := journalledSeal(c) |
| + if err != nil { |
| + return source, staging, err |
| + } |
| + staging = hostclaim.ParkScope{Location: moves.ScopeLocation(l.rec.Host, l.g.ID), Incarnation: c.Holder.Incarnation, EvidenceAt: seal.Bound().UTC(), RequestLimit: cutRequestLimit} |
| + return source, staging, nil |
| +} |
| + |
| +// open is the conditional open: under the host's hold lock, with the record |
| +// and the claim re-read and both committed, the probe's entry is marked open |
| +// and then the hold is released. A row already open is a replay. |
| +func (l *landingRun) open(ctx context.Context) error { |
| + cfg := l.cfg() |
| + host, id := l.rec.Host, l.g.ID |
| + var err error |
| + provision.WithHostHoldLock(host, func() { |
| + var fresh *provision.Record |
| + if fresh, err = provision.GetRecord(ctx, l.s, l.rec.Account, l.rec.App, host); err != nil { |
| + return |
| + } |
| + if fresh == nil || fresh.Move == nil || fresh.Move.ID != id || fresh.Move.Incarnation != l.g.Incarnation || fresh.Move.State != provision.MoveCommitted { |
| + err = fmt.Errorf("move %s: the record is no longer committed", id) |
| + return |
| + } |
| + var obs hostclaim.Observed |
| + if obs, err = l.claim(ctx); err != nil { |
| + return |
| + } |
| + if op := obs.Claim.Op; op.Kind != hostclaim.KindLanding || op.Phase != hostclaim.PhaseLandingCommitted { |
| + err = fmt.Errorf("move %s: the claim is %s/%q, not committed", id, op.Kind, op.Phase) |
| + return |
| + } |
| + var rt *registry.Route |
| + if rt, err = cfg.Route(host); err != nil { |
| + return |
| + } |
| + switch { |
| + case rt == nil: |
| + err = fmt.Errorf("move %s: this box has no row to open", id) |
| + return |
| + case rt.HoldOp == "": |
| + return // open already |
| + case rt.HoldOp != id: |
| + err = fmt.Errorf("move %s: the row is held %s by %q", id, rt.Status, rt.HoldOp) |
| + return |
| + } |
| + if cfg.MarkProbeOpen != nil && !cfg.MarkProbeOpen(host, id) { |
| + log.Printf("moves: %s: no live probe entry for %s; the next request wakes it under the commit", host, id) |
| + } |
| + _, err = cfg.ReleaseHeld(host, id) |
| + }) |
| + if err != nil { |
| + return fmt.Errorf("open the row: %w", err) |
| + } |
| + return nil |
| +} |
diff --git a/internal/host/movespass.go b/internal/host/movespass.go| index 050161923..ff753f0a9 100644 |
| --- a/internal/host/movespass.go |
| +++ b/internal/host/movespass.go |
| @@ -566,6 +566,11 @@ type moveCfg struct { |
| // ProbeLive is the activator's LiveProbe: the awake probe entry for a |
| // host, its op and admitted session. Nil in production with Probe. |
| ProbeLive func(host string) (op string, res activator.ProbeResult, ok bool) |
| + // MarkProbeOpen is the activator's OpenProbeEntry: the target's open |
| + // marks the probe's live entry open before it releases the hold, so an |
| + // edge reload in between keeps it (Plan 3 Task 7). Nil in production |
| + // with Probe. |
| + MarkProbeOpen func(host, op string) bool |
| // landFault and parkFault are test seams: the landing's crash points |
| // and the park engine's (park.Engine.After). Nil in production. |
| landFault func(point string) error |
diff --git a/internal/host/movesphase1.go b/internal/host/movesphase1.go| index d997b5f3f..72a76a5ac 100644 |
| --- a/internal/host/movesphase1.go |
| +++ b/internal/host/movesphase1.go |
| @@ -200,8 +200,14 @@ func phase1MoveRound(ctx context.Context, s release.Store, w *movesWorker, rec p |
| // admits it here. checking to restored is Task 10's (movesland.go); |
| // the probe and the commit are Plan 3's. |
| return w.landingPass(ctx, s, rec) |
| + case provision.MoveCommitted: |
| + // The target's committed arm (Plan 3 Task 7, H9): the claim half |
| + // and the conditional open, every pass while committed. |
| + if movesParticipantRecord(w.cfg, rec) == roleTarget { |
| + return w.committedTarget(ctx, s, rec) |
| + } |
| } |
| - return nil // committed and later: Plans 3 to 5 |
| + return nil // later: Plans 4 and 5 |
| } |
| // srcRound is what one preparing round learned for the source report. |
diff --git a/internal/provision/plan9_b8_census_test.go b/internal/provision/plan9_b8_census_test.go| index a147485c9..67ffe1226 100644 |
| --- a/internal/provision/plan9_b8_census_test.go |
| +++ b/internal/provision/plan9_b8_census_test.go |
| @@ -501,6 +501,10 @@ var plan9B8CensusInventory = []censusEntry{ |
| {"internal/host/movesinstall.go", "installRun.transition", "record-writer", "key:SetRecordMove", "5679de7898730c831906e5350529a67259d85ad0a94132ac86175a1102e21753"}, |
| // The refusal callback marks the return stuck through SetRecordMove; other callbacks only read or mutate memory. |
| {"internal/host/movesinstall.go", "recoveryInstallSource", "record-writer", "key:SetRecordMove", "b4bb0798a032c1599b9063a165656ab76c00b6ea349cb17b5f6a95769432bd5f"}, |
| + // Commits the landing through SetRecordMove (MoveCommit), its refusal the rollback mark. |
| + {"internal/host/moveslandcommit.go", "landingRun.probed", "record-writer", "key:SetRecordMove", "6f961790d8fe169af68e2f70790c400d37c2388a4f62c26921f22b8b47c859de"}, |
| + // Stamps opened_at through SetRecordMove (MoveOpened) after the target's open. |
| + {"internal/host/moveslandcommit.go", "movesWorker.committedTarget", "record-writer", "key:SetRecordMove", "f112b24fdc86aebcefe90f959a0d72a82aace3e0631e60ad8261ab76f7760eb8"}, |
| // Closes the rollback record through SetRecordMove; claim release and local journal parking use separate storage. |
| {"internal/host/movesrecover.go", "recoverRun.finalize", "record-writer", "key:SetRecordMove", "fff2e28c78db68bccb16da6ab70ca877b51243b0aa56b9cb2c513c6f5660f7f7"}, |
| // Advances the source recovery phase through SetRecordMove and its sequenced writer. |