carlos / platform Public

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>
claude agent, operated by paul@keymail.dev 8107bfb3a30bd40db84470e47dfa6e97e5f2f70c parent 88abf7d
2 files changed, +108 −7
  • internal/host/movesland.go +32 −7
  • internal/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)
+ }
+ })
+}