host: the landing is bounded by the earlier of cut_deadline and landing_deadline (final-review fix 3, spec §7.4)
Every pre-commit landing phase's entry, and configured before the install and before the transition to restored, checks the earlier of the cut's absolute deadline and the flip's landing_deadline; past it, the rollback is marked with the landing's deadline sentence (MV-N169).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+108
−7
internal/host/movesland.go+32 −7internal/host/movesland_finalfix_test.go+76 −0
diff --git a/internal/host/movesland.go b/internal/host/movesland.go| index 7d5152fea..d9caab4b2 100644 |
| --- a/internal/host/movesland.go |
| +++ b/internal/host/movesland.go |
| @@ -57,8 +57,9 @@ import ( |
| // archive is extracted into (M14, M15). A failure that is permanent for this |
| // landing is a rollback (§7.8), which Plan 4 builds: until then the record |
| // carries the reason (provision.MoveMarkRollback) and this pass leaves a |
| -// marked record alone. landing_deadline passing before `restored` marks it |
| -// too. |
| +// marked record alone. The landing's deadline passing before `restored` |
| +// marks it too: the earlier of the cut's absolute cut_deadline and the |
| +// flip's landing_deadline (spec §7.4; landingDeadlineOf). |
| // |
| // Local installation (final review, findings 1 and 2). Every destructive |
| // local mutation (the files and env install, .files-gen, the database |
| @@ -198,7 +199,7 @@ func (l *landingRun) step(ctx context.Context) (bool, error) { |
| l.w.notice(l.rec.Host+"#rollback", "moves: %s: landing %s is marked for rollback (%s); the rollback is Plan 4's", l.rec.Host, l.g.ID, m.Reason) |
| return false, nil |
| } |
| - if m.LandingDeadline != nil && l.cfg().Now().After(*m.LandingDeadline) && landingBefore(m.LandingPhase, provision.LandRestored) { |
| + if landingBefore(m.LandingPhase, provision.LandRestored) && l.pastDeadline() { |
| return l.markRollback(ctx, provision.RollbackLandingDeadline) |
| } |
| switch m.LandingPhase { |
| @@ -225,10 +226,33 @@ func (l *landingRun) step(ctx context.Context) (bool, error) { |
| return false, nil // probing and later: the probe's (Plan 3) |
| } |
| -// pastDeadline reports whether landing_deadline has passed now. |
| +// landingDeadlineOf is the deadline a landing before restored works to: |
| +// the earlier of the cut's absolute deadline and landing_deadline. Spec |
| +// §7.4 has every pre-commit phase on both participants check the absolute |
| +// deadline; the flip's landing_deadline is a second, separate bound, never |
| +// an extension of it (final review, finding 3). which names the one that |
| +// applies, for the log. |
| +func landingDeadlineOf(m *provision.MoveState) (d *time.Time, which string) { |
| + if m.CutDeadline != nil { |
| + d, which = m.CutDeadline, "cut_deadline" |
| + } |
| + if m.LandingDeadline != nil && (d == nil || m.LandingDeadline.Before(*d)) { |
| + d, which = m.LandingDeadline, "landing_deadline" |
| + } |
| + return d, which |
| +} |
| + |
| +// pastDeadline reports whether the landing's deadline (landingDeadlineOf) |
| +// has passed now. It is checked at every phase's entry (step), and in |
| +// configured again before the install and before the transition that |
| +// names it complete. |
| func (l *landingRun) pastDeadline() bool { |
| - d := l.rec.Move.LandingDeadline |
| - return d != nil && l.cfg().Now().After(*d) |
| + d, which := landingDeadlineOf(l.rec.Move) |
| + if d == nil || !l.cfg().Now().After(*d) { |
| + return false |
| + } |
| + log.Printf("moves: %s: landing %s is past its %s (%s)", l.rec.Host, l.g.ID, which, d.UTC().Format(time.RFC3339)) |
| + return true |
| } |
| // landingBefore reports whether phase comes before other in LandingOrder. |
| @@ -979,7 +1003,8 @@ func (l *landingRun) local_(ctx context.Context) (bool, error) { |
| // pinned at readiness (else the rollback marker, "configuration changed |
| // during the move"); the pinned release live and proven for the held row |
| // through Plan 1's T7 hook (ruling G4: "waiting for the root stage pass" |
| -// waits, anything else waits until landing_deadline marks the rollback); |
| +// waits, anything else waits until the landing's deadline marks the |
| +// rollback); |
| // then the install the brief lists under `restored`: the built database's |
| // sha256 against the manifest, lease.CheckDB, the rename into DBPath (G34), |
| // <dbPath>.gen through hostbox's marker writer naming the prepared |
diff --git a/internal/host/movesland_finalfix_test.go b/internal/host/movesland_finalfix_test.go| index 6de1a504d..232a6774b 100644 |
| --- a/internal/host/movesland_finalfix_test.go |
| +++ b/internal/host/movesland_finalfix_test.go |
| @@ -6,10 +6,12 @@ import ( |
| "os" |
| "path/filepath" |
| "testing" |
| + "time" |
| "github.com/carlosframework/platform/internal/hostbox" |
| "github.com/carlosframework/platform/internal/hostclaim" |
| "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/moves" |
| "github.com/carlosframework/platform/internal/provision" |
| ) |
| @@ -306,3 +308,77 @@ func TestLandingInstallRefusesASuccessor(t *testing.T) { |
| } |
| }) |
| } |
| + |
| +// Finding 3: the landing is bounded by the earlier of the cut's absolute |
| +// deadline and landing_deadline (spec §7.4). The cut deadline here falls a |
| +// minute after the flip; the landing deadline is fifteen minutes away. |
| +func TestLandingCutDeadlineMarksRollback(t *testing.T) { |
| + ctx := context.Background() |
| + setCutDeadline := func(t *testing.T, h *landingHarness) time.Time { |
| + t.Helper() |
| + rec := h.read(t, p1Host) |
| + d := rec.Move.FlippedAt.Add(time.Minute) |
| + if _, err := provision.SetRecordMove(ctx, h.s, rec.Account, rec.App, rec.Host, func(r *provision.Record) error { |
| + r.Move.CutDeadline = &d |
| + return nil |
| + }); err != nil { |
| + t.Fatal(err) |
| + } |
| + if ld := h.read(t, p1Host).Move.LandingDeadline; ld == nil || !ld.After(d.Add(2*time.Minute)) { |
| + t.Fatalf("fixture: landing deadline %v is not well after the cut deadline %v", ld, d) |
| + } |
| + return d |
| + } |
| + pastCut := func(h *landingHarness, d time.Time) { |
| + h.clock.Advance(d.Sub(h.clock.Now()) + time.Second) |
| + } |
| + t.Run("at checking", func(t *testing.T) { |
| + h := newLandingHarness(t) |
| + h.flip(t) |
| + d := setCutDeadline(t, h) |
| + pastCut(h, d) |
| + h.land(t) |
| + if m := h.read(t, p1Host).Move; m.Reason != provision.RollbackLandingDeadline || m.LandingPhase != provision.LandChecking { |
| + t.Fatalf("at checking past the cut deadline: %+v", m) |
| + } |
| + }) |
| + t.Run("at configured while the release waits", func(t *testing.T) { |
| + h := newLandingHarness(t) |
| + h.flip(t) |
| + d := setCutDeadline(t, h) |
| + h.release = moves.Check{Reason: t7NotProven} |
| + h.land(t) |
| + if m := h.read(t, p1Host).Move; m.LandingPhase != provision.LandConfigured || m.Reason != "" { |
| + t.Fatalf("waiting on the proof: %+v", m) |
| + } |
| + pastCut(h, d) |
| + h.release = moves.Check{OK: true} |
| + h.land(t) |
| + m := h.read(t, p1Host).Move |
| + if m.Reason != provision.RollbackLandingDeadline || m.LandingPhase != provision.LandConfigured { |
| + t.Fatalf("at configured past the cut deadline: %+v", m) |
| + } |
| + if _, err := os.Stat(provision.DefaultDBPath(h.tdir, p1Host)); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatalf("installed past the cut deadline: %v", err) |
| + } |
| + }) |
| + t.Run("before the install", func(t *testing.T) { |
| + h := newLandingHarness(t) |
| + h.flip(t) |
| + d := setCutDeadline(t, h) |
| + h.landFault = func(p string) error { |
| + if p == "after-release-ready" { |
| + pastCut(h, d) |
| + } |
| + return nil |
| + } |
| + h.land(t) |
| + m := h.read(t, p1Host).Move |
| + if m.Reason != provision.RollbackLandingDeadline || m.LandingPhase != provision.LandConfigured || (m.Landing != nil && m.Landing.InstalledDBSHA256 != "") { |
| + t.Fatalf("past the cut deadline before the install: %+v", m) |
| + } |
| + if _, err := os.Stat(provision.DefaultDBPath(h.tdir, p1Host)); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatalf("installed past the cut deadline: %v", err) |
| + } |
| + }) |
| +} |