carlos / platform Public

moves: a move out of home finishes its cleanup on its source

Every place the moves box turns a placement code into an engine or claim
region now goes through park.PlacementRegion, so the home's "" becomes
park.HomeRegion: the engines the arms build, the chores they look for, the
regions they queue (the commit's source location park, the return lane,
residue, staging, revert and abort lanes), the view's authoritative-region
table and the probe's predecessor region. The console's recovery commit
maps the record's codes the same way (K9). Placement-to-placement routing
is unchanged.

The view refuses an unset region, and in production holds the roster's
region for this box to its placement's engine region (movesBoxRegion): the
roster cannot hold "", so a home box's claim names home otherwise and the
view refuses there until the claim names home as the engine does.
armMovesParks and the lane predicate take parkRegion. movesNoLiveRegion
moves out of the placement alphabet ("none" is a valid code).

Tests: the whole move out of home through the real commit, engine and view,
retiring the pre-move generation and closing; the view's table seen from
home and elsewhere; arming, the lane predicate, the recovery policy and the
console's commit from home; unset and invalid regions still refuse.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 7fb8079ae96a953bcd48efa20f0a155892531f4d parent 6dd7297
24 files changed, +497 −81
  • internal/host/movesabort.go +2 −1
  • internal/host/movescut.go +2 −1
  • internal/host/movescut_test.go +2 −2
  • internal/host/movesfinal.go +3 −3
  • internal/host/movesfinal_test.go +2 −2
  • internal/host/moveshome_region_test.go +46 −0
  • internal/host/moveshome_test.go +204 −0
  • internal/host/movesinstall.go +2 −2
  • internal/host/movesland_probe_test.go +5 −5
  • internal/host/movesland_test.go +2 −2
  • internal/host/moveslandcommit.go +2 −1
  • internal/host/moveslandpark.go +1 −1
  • internal/host/movesparkfakes_test.go +2 −2
  • internal/host/movesparks_test.go +2 −2
  • internal/host/movesphase1_harness_test.go +23 −7
  • internal/host/movespolicy.go +2 −1
  • internal/host/movesprobeowners.go +10 −0
  • internal/host/movesrecover.go +1 −1
  • internal/host/movesrevert.go +9 −7
  • internal/host/movesrollback.go +5 −5
  • internal/host/movessources.go +84 −25
  • internal/host/movessources_test.go +28 −10
  • internal/hostops/move_recovery_commit_test.go +53 −0
  • internal/hostops/move_recovery_grant.go +5 −1
diff --git a/internal/host/movesabort.go b/internal/host/movesabort.go
index 98742ef20..6e3b0097a 100644
--- a/internal/host/movesabort.go
+++ b/internal/host/movesabort.go
@@ -12,6 +12,7 @@ import (
"github.com/carlosframework/platform/internal/hostclaim"
"github.com/carlosframework/platform/internal/lease"
"github.com/carlosframework/platform/internal/moves"
+ "github.com/carlosframework/platform/internal/park"
"github.com/carlosframework/platform/internal/provision"
"github.com/carlosframework/platform/internal/release"
)
@@ -234,7 +235,7 @@ func (w *movesWorker) abortClaim(ctx context.Context, s release.Store, rec provi
}
}
scope := moves.AbortScope(host, id, obs.Claim.Holder.Incarnation, bound)
- if _, err := hostclaim.Update(ctx, s, obs, hostclaim.Abort(id, rec.Move.To, scope, obligationID)); err != nil {
+ if _, err := hostclaim.Update(ctx, s, obs, hostclaim.Abort(id, park.PlacementRegion(rec.Move.To), scope, obligationID)); err != nil {
return false, fmt.Errorf("abort the claim: %w", err)
}
log.Printf("moves: %s: the claim aborts move %s; the staging lane in %s is queued for its park there", host, id, rec.Move.To)
diff --git a/internal/host/movescut.go b/internal/host/movescut.go
index b373f2e85..9095c7284 100644
--- a/internal/host/movescut.go
+++ b/internal/host/movescut.go
@@ -20,6 +20,7 @@ import (
"github.com/carlosframework/platform/internal/hostclaim"
"github.com/carlosframework/platform/internal/lease"
"github.com/carlosframework/platform/internal/moves"
+ "github.com/carlosframework/platform/internal/park"
"github.com/carlosframework/platform/internal/provision"
"github.com/carlosframework/platform/internal/registry"
"github.com/carlosframework/platform/internal/release"
@@ -1231,7 +1232,7 @@ func (w *movesWorker) revertClaim(ctx context.Context, s release.Store, rec prov
}
}
scope := moves.StagingScope(rec.Host, id, obs.Claim.Holder.Incarnation, bound)
- if _, err := hostclaim.Update(ctx, s, obs, hostclaim.Revert(id, cfg.Box, rec.Move.To, &scope, obligationID)); err != nil {
+ if _, err := hostclaim.Update(ctx, s, obs, hostclaim.Revert(id, cfg.Box, park.PlacementRegion(rec.Move.To), &scope, obligationID)); err != nil {
return fmt.Errorf("revert the claim: %w", err)
}
log.Printf("moves: %s: the claim reverts to this box; the staging lane in %s is queued for its park", rec.Host, rec.Move.To)
diff --git a/internal/host/movescut_test.go b/internal/host/movescut_test.go
index a94566d51..8ce97ea5f 100644
--- a/internal/host/movescut_test.go
+++ b/internal/host/movescut_test.go
@@ -91,9 +91,9 @@ var _ lease.FilePutIfAbsent = (*fakeVersioned)(nil)
const lastTXKey = "0000/0000000000000003-0000000000000003.ltx"
-func newCutHarness(t *testing.T) *cutHarness {
+func newCutHarness(t *testing.T, opts ...p1Option) *cutHarness {
t.Helper()
- h := &cutHarness{phase1Harness: newPhase1Harness(t), ev: &fakeEvidence{stopped: true}, localPos: 3,
+ h := &cutHarness{phase1Harness: newPhase1Harness(t, opts...), ev: &fakeEvidence{stopped: true}, localPos: 3,
bundle: map[string]string{"default": "e1", "environment": ""}}
h.timers = []registry.Timer{{Host: p1Host, Name: "nightly", Kind: "cron", Spec: "0 3 * * *", NextAt: 1000}}
last := ltxForTest(t, 4096, 4, 3, 3)
diff --git a/internal/host/movesfinal.go b/internal/host/movesfinal.go
index c6401c950..0689533fe 100644
--- a/internal/host/movesfinal.go
+++ b/internal/host/movesfinal.go
@@ -387,12 +387,12 @@ func (f *sourceFinal) engine(vs lease.VersionedStore) park.Engine {
if err := f.sourceOf(obs); err != nil {
return err
}
- if o.Op != f.g.ID || o.Region != cfg.Region {
+ if o.Op != f.g.ID || o.Region != cfg.parkRegion() {
return fmt.Errorf("%w: not this finalization's chore", park.ErrUnsafe)
}
return nil
}
- return park.Engine{Store: vs, Claims: f.s, Bucket: cfg.OwnBucket, Region: cfg.Region, Leases: cfg.ParkLeases,
+ return park.Engine{Store: vs, Claims: f.s, Bucket: cfg.OwnBucket, Region: cfg.parkRegion(), Leases: cfg.ParkLeases,
Now: f.now, NewID: obligationID, View: cfg.ParkView, Authorize: movesArmAuthorize(cfg.ParkAuthorize, own), After: cfg.parkFault}
}
@@ -406,7 +406,7 @@ func (f *sourceFinal) remote(ctx context.Context, obs hostclaim.Observed) (bool,
var chore *hostclaim.Obligation
for i := range obs.Claim.Obligations {
o := &obs.Claim.Obligations[i]
- if o.Op == id && o.Kind == "park-replica" && o.Region == cfg.Region && o.Park != nil && !isStagingLocation(o.Park.Location) {
+ if o.Op == id && o.Kind == "park-replica" && o.Region == cfg.parkRegion() && o.Park != nil && !isStagingLocation(o.Park.Location) {
chore = o
}
}
diff --git a/internal/host/movesfinal_test.go b/internal/host/movesfinal_test.go
index c80997ab3..08014d0bb 100644
--- a/internal/host/movesfinal_test.go
+++ b/internal/host/movesfinal_test.go
@@ -45,9 +45,9 @@ type finalHarness struct {
deletes int // DeleteHeld calls that deleted
}
-func newFinalHarness(t *testing.T) *finalHarness {
+func newFinalHarness(t *testing.T, opts ...p1Option) *finalHarness {
t.Helper()
- h := &finalHarness{landingHarness: newLandingHarness(t)}
+ h := &finalHarness{landingHarness: newLandingHarness(t, opts...)}
h.flip(t)
h.land(t)
h.installed(t)
diff --git a/internal/host/moveshome_region_test.go b/internal/host/moveshome_region_test.go
new file mode 100644
index 000000000..cbafb5a28
--- /dev/null
+++ b/internal/host/moveshome_region_test.go
@@ -0,0 +1,46 @@
+package host
+
+import (
+ "errors"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/park"
+ "github.com/carlosframework/platform/internal/provision"
+)
+
+// TestMovesBoxRegionIsThePlacements: the park view's region is this box's
+// as the claim knows it, and it must be the engine region of this box's own
+// placement code. A home box whose claim names home any other way (the
+// roster's region, a named code, the unset region) refuses rather than
+// reading a home record from a region that is not HomeRegion.
+func TestMovesBoxRegionIsThePlacements(t *testing.T) {
+ for _, tc := range []struct {
+ placement, claims string
+ ok bool
+ }{
+ {"", park.HomeRegion, true},
+ {"uk", "uk", true},
+ {"", "ie", false},
+ {"", "", false},
+ {"uk", "ie", false},
+ {"uk", park.HomeRegion, false},
+ {"IE", "IE", false},
+ {park.HomeRegion, park.HomeRegion, false},
+ } {
+ f := newVwFixture(t)
+ f.rec.Region = ""
+ f.rec.Move = &provision.MoveState{ID: vwMove, Incarnation: f.inc, From: "", To: "uk", State: provision.MoveCutting}
+ obs := vwObs(&hostclaim.Claim{Holder: f.holder(""), Op: hostclaim.Op{Kind: hostclaim.KindMoving, ID: vwMove, Phase: hostclaim.PhaseMoving, Exec: "box"}})
+ v, err := movesParkViewAt(f.records(), f.bucket, "data", movesBoxRegion(tc.placement, fixedRegion(tc.claims)))(t.Context(), obs)
+ if tc.ok != (err == nil) {
+ t.Fatalf("placement %q, claim region %q: view %+v, %v", tc.placement, tc.claims, v, err)
+ }
+ if tc.ok && v.Region != tc.claims {
+ t.Fatalf("placement %q: the view's region %q", tc.placement, v.Region)
+ }
+ if !tc.ok && !errors.Is(err, park.ErrUnsafe) {
+ t.Fatalf("placement %q, claim region %q: %v, want ErrUnsafe", tc.placement, tc.claims, err)
+ }
+ }
+}
diff --git a/internal/host/moveshome_test.go b/internal/host/moveshome_test.go
new file mode 100644
index 000000000..bc129fd11
--- /dev/null
+++ b/internal/host/moveshome_test.go
@@ -0,0 +1,204 @@
+package host
+
+import (
+ "errors"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/park"
+ "github.com/carlosframework/platform/internal/provision"
+)
+
+// Home-region parks, part 1 (Paul, 2026-10-11): a move out of the home
+// placement finishes its cleanup on its source box. The home's placement
+// code is ""; its engine region is park.HomeRegion, mapped once where a
+// placement code becomes an engine region (park.PlacementRegion).
+
+// TestMoveFromHomeClosesThroughTheSourcePark: the whole move out of home,
+// through the real paths. The target commits through Plan 3's committed arm,
+// which queues the source's location park in HomeRegion; the source box,
+// whose placement is "", runs it through the real park engine and the real
+// park view seen from home, retiring the pre-move generation from its
+// bucket; and the move closes.
+func TestMoveFromHomeClosesThroughTheSourcePark(t *testing.T) {
+ h := newCloseHarness(t, fromHome)
+ h.cfg.ParkView = movesParkView(MovesRecordsFrom(h.s), h.vs, "data-ie", park.HomeRegion)
+ h.cfg.ParkAuthorize = allowParkAuthorize
+ if rec := h.read(t, p1Host); rec.Move.From != "" || h.cfg.Region != "" {
+ t.Fatalf("fixture: not a move out of home: from %q, source region %q", rec.Move.From, h.cfg.Region)
+ }
+ h.land(t)
+ h.committedAndOpen(t)
+ src := h.chore(t, park.HomeRegion, false)
+ if src == nil || src.Owner != "region:"+park.HomeRegion || src.Park.Location != cutRoot {
+ t.Fatalf("the commit queued the source's location park as %+v, want it in %s", src, park.HomeRegion)
+ }
+ for _, o := range h.claim(t).Obligations {
+ if o.Region == "" || (o.Kind == "park-replica" && o.Region != park.HomeRegion && o.Region != "uk") {
+ t.Fatalf("the commit queued %+v", o)
+ }
+ }
+ // The target's probe recorded the home generation's region as home's,
+ // and the commit's location park covered it.
+ covered := false
+ for _, a := range h.claim(t).Holder.Admissions {
+ if a.Region == "uk" && a.Predecessor == "cur" {
+ if a.PredecessorRegion != park.HomeRegion || !a.Covered {
+ t.Fatalf("the probe's admission %+v: the home generation is not home's, or not covered", a)
+ }
+ covered = true
+ }
+ }
+ if !covered {
+ t.Fatalf("no admission retires the home generation: %+v", h.claim(t).Holder.Admissions)
+ }
+ h.clock.Advance(24 * time.Hour)
+ h.land(t)
+ if h.chore(t, "uk", true) != nil {
+ t.Fatal("the staging park was not acknowledged")
+ }
+ h.notClosed(t, "the source has not parked")
+ h.sourcePass(t)
+ h.done(t)
+ for k := range h.f.buckets["data-ie"] {
+ if strings.HasPrefix(k, cutRoot+"/") {
+ t.Fatalf("still at the home location: %s", k)
+ }
+ }
+ pre := 0
+ for k := range h.f.buckets["data-ie"] {
+ if strings.HasPrefix(k, "parked/"+h.inc+"/"+p1Host+"/"+src.Retirement+"/instances/") && strings.Contains(k, "/g-cur/") {
+ pre++
+ }
+ }
+ if pre == 0 {
+ t.Fatal("the pre-move generation was not archived under the location park's retirement")
+ }
+ q := h.claim(t).QuietAfter
+ if _, ok := q[park.HomeRegion]; !ok {
+ t.Fatalf("quiet_after %v names no %s", q, park.HomeRegion)
+ }
+ if _, ok := q[""]; ok {
+ t.Fatalf("quiet_after %v names the unset region", q)
+ }
+ if m := h.read(t, p1Host).LastMove; m == nil || m.Outcome != "done" {
+ t.Fatalf("the move did not close: %+v", m)
+ }
+}
+
+// TestHomeParkViewNamesHomeAsARegion: the real park view for a move out of
+// home names HomeRegion where the spec's table names the source, seen from
+// home and from elsewhere; before the commit, home stays authoritative on
+// home (J2/F4). An unset region, a named region spelled "home" and a legacy
+// home holder with no admission still refuse.
+func TestHomeParkViewNamesHomeAsARegion(t *testing.T) {
+ type row struct {
+ name, seen string
+ claim func(f *vwFixture) *hostclaim.Claim
+ state string
+ want string // "" wants a refusal
+ }
+ op := func(kind, phase string) hostclaim.Op {
+ return hostclaim.Op{Kind: kind, ID: vwMove, Phase: phase, Exec: "box"}
+ }
+ home := park.HomeRegion
+ rows := []row{
+ {"moving, a legacy holder, seen from home", home, func(f *vwFixture) *hostclaim.Claim {
+ return &hostclaim.Claim{Holder: f.holder(""), Op: op(hostclaim.KindMoving, hostclaim.PhaseMoving)}
+ }, provision.MoveCutting, home},
+ {"moving, admitted at home, seen from us", "us", func(f *vwFixture) *hostclaim.Claim {
+ return &hostclaim.Claim{Holder: f.holder("cur", "cur@"+home), Op: op(hostclaim.KindMoving, hostclaim.PhaseMoving)}
+ }, provision.MoveCutting, home},
+ {"reverting, seen from home", home, func(f *vwFixture) *hostclaim.Claim {
+ return &hostclaim.Claim{Holder: f.holder("cur", "cur@"+home), Op: op(hostclaim.KindReverting, "")}
+ }, provision.MoveReverting, home},
+ {"landing, the target's probe admitted, seen from home", home, func(f *vwFixture) *hostclaim.Claim {
+ return &hostclaim.Claim{Holder: f.holder(vwMove, "cur@"+home, vwMove+"@uk"), Op: op(hostclaim.KindLanding, provision.LandProbing)}
+ }, provision.MoveLanding, home},
+ {"landing, the target's probe admitted, seen from us", "us", func(f *vwFixture) *hostclaim.Claim {
+ return &hostclaim.Claim{Holder: f.holder(vwMove, "cur@"+home, vwMove+"@uk"), Op: op(hostclaim.KindLanding, provision.LandProbing)}
+ }, provision.MoveLanding, "uk"},
+ {"landing committed, seen from home", home, func(f *vwFixture) *hostclaim.Claim {
+ return &hostclaim.Claim{Holder: f.holder(vwMove, "cur@"+home, vwMove+"@uk"), Op: op(hostclaim.KindLanding, hostclaim.PhaseCommitted)}
+ }, provision.MoveCommitted, "uk"},
+ {"restoring before any admission", home, func(f *vwFixture) *hostclaim.Claim {
+ return &hostclaim.Claim{Holder: f.holder(""), Op: op(hostclaim.KindRestoring, hostclaim.PhaseReleasing)}
+ }, provision.MoveRollingBack, home},
+ {"moving, admitted in a named region spelled home", home, func(f *vwFixture) *hostclaim.Claim {
+ return &hostclaim.Claim{Holder: f.holder("cur", "cur@home"), Op: op(hostclaim.KindMoving, hostclaim.PhaseMoving)}
+ }, provision.MoveCutting, ""},
+ {"moving, seen from the unset region", "", func(f *vwFixture) *hostclaim.Claim {
+ return &hostclaim.Claim{Holder: f.holder(""), Op: op(hostclaim.KindMoving, hostclaim.PhaseMoving)}
+ }, provision.MoveCutting, ""},
+ {"serving, a legacy home holder with no admission", home, func(f *vwFixture) *hostclaim.Claim {
+ return &hostclaim.Claim{Holder: f.holder(""), Op: hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box"}}
+ }, "", ""},
+ }
+ for _, r := range rows {
+ t.Run(r.name, func(t *testing.T) {
+ f := newVwFixture(t)
+ f.rec.Region = ""
+ if r.state != "" {
+ f.rec.Move = &provision.MoveState{ID: vwMove, Incarnation: f.inc, From: "", To: "uk", State: r.state}
+ }
+ obs := vwObs(r.claim(f))
+ v, err := movesParkView(f.records(), f.bucket, "data", r.seen)(t.Context(), obs)
+ if r.want == "" {
+ if !errors.Is(err, park.ErrUnsafe) {
+ t.Fatalf("view %+v, %v; want a refusal", v, err)
+ }
+ return
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ if v.AuthoritativeRegion != r.want || v.Region != r.seen {
+ t.Fatalf("view seen from %q names %q authoritative, want %q", v.Region, v.AuthoritativeRegion, r.want)
+ }
+ if (r.want == r.seen) != v.MarkerKnown {
+ t.Fatalf("the marker was read %v, seen from %s with %s authoritative", v.MarkerKnown, r.seen, r.want)
+ }
+ })
+ }
+}
+
+// TestHomeLaneIsArmed: the lane park's arming takes the home's "" as a
+// region (the lane predicate), and refuses a code that is no placement.
+func TestHomeLaneIsArmed(t *testing.T) {
+ c := newRevertHarness(t).laneCfg()
+ for code, armed := range map[string]bool{"uk": true, "": true, "home": true, "IE": false, park.HomeRegion: false, "a/b": false} {
+ c.Region = code
+ if revertLaneArmed(c) != armed {
+ t.Fatalf("lane armed for %q: %v, want %v", code, !armed, armed)
+ }
+ }
+}
+
+// TestMovesClaimPolicyRecoveryFromHome: a rollback's or a revert's recovery
+// on a home source is granted to the source's selection in park.HomeRegion,
+// the claim's engine region for home, and refused to one in a named region,
+// one named "home" among them.
+func TestMovesClaimPolicyRecoveryFromHome(t *testing.T) {
+ type base struct {
+ claim func() *hostclaim.Claim
+ record func() *provision.Record
+ }
+ for name, b := range map[string]base{
+ "restoring probing": {func() *hostclaim.Claim { return rpRestoringClaim(rpClaimPhase(provision.RbSourceProbing)) },
+ func() *provision.Record { return rpRestoringRecord(provision.RbSourceProbing) }},
+ "reverting probing": {func() *hostclaim.Claim { return rpRevertingClaim(hostclaim.PhaseProbing) }, rpRevertingRecord},
+ } {
+ for region, granted := range map[string]bool{park.HomeRegion: true, "home": false, "ie": false, "uk": false} {
+ c, rec := b.claim(), b.record()
+ rec.Move.From = ""
+ s := rpSelection(c, "spawn")
+ s.Region = region
+ err := MovesClaimPolicy((&mpReader{rec: rec}).read)(t.Context(), c, s, rpIdentity("spawn"))
+ if (err == nil) != granted {
+ t.Fatalf("%s from home, selection in %q: %v, want granted %v", name, region, err, granted)
+ }
+ }
+ }
+}
diff --git a/internal/host/movesinstall.go b/internal/host/movesinstall.go
index 78f2aedb2..e9259a026 100644
--- a/internal/host/movesinstall.go
+++ b/internal/host/movesinstall.go
@@ -479,7 +479,7 @@ func (ir *installRun) parking(ctx context.Context) (bool, error) {
return false, err
}
scope := moves.ResidueScope(holder, *ir.rec.Move.FlippedAt)
- if obs, err = hostclaim.Update(ctx, ir.s, obs, hostclaim.QueueResidueParks(id, cfg.Region, scope, gens, obligationID)); err != nil {
+ if obs, err = hostclaim.Update(ctx, ir.s, obs, hostclaim.QueueResidueParks(id, cfg.parkRegion(), scope, gens, obligationID)); err != nil {
return false, fmt.Errorf("queue the residue parks: %w", err)
}
if err := ir.fault("after-residue-queued"); err != nil {
@@ -488,7 +488,7 @@ func (ir *installRun) parking(ctx context.Context) (bool, error) {
eng := ir.engine(vs)
pending := 0
for _, o := range obs.Claim.Obligations {
- if o.Op != id || o.Kind != "park-generation" || o.Region != cfg.Region {
+ if o.Op != id || o.Kind != "park-generation" || o.Region != cfg.parkRegion() {
continue
}
w := park.Work{Host: host, ID: o.ID, Op: id}
diff --git a/internal/host/movesland_probe_test.go b/internal/host/movesland_probe_test.go
index 6896b4a21..aae06d1fd 100644
--- a/internal/host/movesland_probe_test.go
+++ b/internal/host/movesland_probe_test.go
@@ -53,7 +53,8 @@ func (p *probeLog) wake(ctx context.Context, cfg activator.ProbeConfig) (activat
if p.before != nil {
p.before(n)
}
- k := p.h.read(p.t, p1Host).Move.Landing.Attempts
+ m := p.h.read(p.t, p1Host).Move
+ k := m.Landing.Attempts
obs, err := hostclaim.Read(ctx, p.h.s, p1Host)
if err != nil {
return activator.ProbeResult{}, err
@@ -63,10 +64,9 @@ func (p *probeLog) wake(ctx context.Context, cfg activator.ProbeConfig) (activat
if h.WriterBox == "uk-box" {
gen = "fresh" + strconv.Itoa(n)
}
- from := "ie"
- if len(h.Admissions) > 0 {
- from = h.Admissions[len(h.Admissions)-1].Region
- }
+ // The predecessor's region is the probe owners' own rule: the last
+ // admission's region, or the move's source's engine region.
+ from := predecessorRegion(h, m.From)
admitted, err := hostclaim.Update(ctx, p.h.s, obs, hostclaim.AdmitWriter(hostclaim.Admission{ObservedOpID: p1ID, ObservedPhase: provision.LandProbing,
ObservedSession: h.Session, ObservedGeneration: h.Generation, Box: "uk-box", Mode: "cold", NewGeneration: gen, KeepStamp: h.Stamp > h.Session, Region: "uk", PredecessorRegion: from}))
if err != nil {
diff --git a/internal/host/movesland_test.go b/internal/host/movesland_test.go
index e4c13a06d..688694ed3 100644
--- a/internal/host/movesland_test.go
+++ b/internal/host/movesland_test.go
@@ -273,9 +273,9 @@ func countRowsForTest(t *testing.T, path string) int {
return n
}
-func newLandingHarness(t *testing.T) *landingHarness {
+func newLandingHarness(t *testing.T, opts ...p1Option) *landingHarness {
t.Helper()
- h := &landingHarness{cutHarness: newCutHarness(t), leases: &fakeParkLeases{}, release: moves.Check{OK: true}}
+ h := &landingHarness{cutHarness: newCutHarness(t, opts...), leases: &fakeParkLeases{}, release: moves.Check{OK: true}}
// The real replica replaces the harness's fake level files under the
// source's lineage; the final checkpoint adds nothing (the database is
// quiet at N).
diff --git a/internal/host/moveslandcommit.go b/internal/host/moveslandcommit.go
index 60f6b17cb..a8e63313c 100644
--- a/internal/host/moveslandcommit.go
+++ b/internal/host/moveslandcommit.go
@@ -8,6 +8,7 @@ import (
"github.com/carlosframework/platform/internal/hostclaim"
"github.com/carlosframework/platform/internal/moves"
+ "github.com/carlosframework/platform/internal/park"
"github.com/carlosframework/platform/internal/provision"
"github.com/carlosframework/platform/internal/registry"
"github.com/carlosframework/platform/internal/release"
@@ -162,7 +163,7 @@ func (l *landingRun) commitClaim(ctx context.Context) error {
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 {
+ if _, err := hostclaim.Update(ctx, l.s, obs, hostclaim.KeepWriterSession(id, hostclaim.Commit(id, park.PlacementRegion(l.rec.Move.From), obs.Claim.Op.Intent["source"], cfg.parkRegion(), 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)
diff --git a/internal/host/moveslandpark.go b/internal/host/moveslandpark.go
index 76244f0b9..0c6890a52 100644
--- a/internal/host/moveslandpark.go
+++ b/internal/host/moveslandpark.go
@@ -135,7 +135,7 @@ func isHexString(s string) bool {
// check (the arming requires all three).
func (ir *installRun) engine(vs lease.VersionedStore) park.Engine {
cfg := ir.cfg()
- return park.Engine{Store: vs, Claims: ir.s, Bucket: cfg.OwnBucket, Region: cfg.Region, Leases: cfg.ParkLeases,
+ return park.Engine{Store: vs, Claims: ir.s, Bucket: cfg.OwnBucket, Region: cfg.parkRegion(), Leases: cfg.ParkLeases,
Now: cfg.Now, NewID: obligationID, View: cfg.ParkView, Authorize: movesArmAuthorize(cfg.ParkAuthorize, ir.authorizeObligation), After: cfg.parkFault}
}
diff --git a/internal/host/movesparkfakes_test.go b/internal/host/movesparkfakes_test.go
index fbaca8928..09db434e7 100644
--- a/internal/host/movesparkfakes_test.go
+++ b/internal/host/movesparkfakes_test.go
@@ -29,9 +29,9 @@ func standInView(region, role string, m *provision.MoveState) func(context.Conte
var authoritative string
switch role {
case roleTarget:
- authoritative = m.From
+ authoritative = park.PlacementRegion(m.From)
case roleSource:
- authoritative = m.To
+ authoritative = park.PlacementRegion(m.To)
default:
return park.View{}, park.ErrUnsafe
}
diff --git a/internal/host/movesparks_test.go b/internal/host/movesparks_test.go
index ec4613b75..3f2051ac1 100644
--- a/internal/host/movesparks_test.go
+++ b/internal/host/movesparks_test.go
@@ -26,9 +26,9 @@ import (
// source region's acknowledgement yet (C4's regional runner), so
// regionalAck stands in for it.
-func newCloseHarness(t *testing.T) *finalHarness {
+func newCloseHarness(t *testing.T, opts ...p1Option) *finalHarness {
t.Helper()
- h := newFinalHarness(t)
+ h := newFinalHarness(t, opts...)
if _, err := provision.SetRecordMove(context.Background(), h.s, "bab", "app", p1Host, func(rec *provision.Record) error {
rec.Move.CutAttempts = 2
return nil
diff --git a/internal/host/movesphase1_harness_test.go b/internal/host/movesphase1_harness_test.go
index 0aea92faf..39d17362c 100644
--- a/internal/host/movesphase1_harness_test.go
+++ b/internal/host/movesphase1_harness_test.go
@@ -19,6 +19,7 @@ import (
"github.com/carlosframework/platform/internal/hostclaim"
"github.com/carlosframework/platform/internal/lease"
"github.com/carlosframework/platform/internal/moves"
+ "github.com/carlosframework/platform/internal/park"
"github.com/carlosframework/platform/internal/provision"
"github.com/carlosframework/platform/internal/registry"
"github.com/carlosframework/platform/internal/release"
@@ -329,11 +330,26 @@ type phase1Harness struct {
// worker1 is the source box's worker and worker2 the target's, each
// kept across passes as one agent keeps its own.
worker1, worker2 *movesWorker
+ // src is the source box's placement code: "ie", or the home's "" for a
+ // move out of home (fromHome).
+ src string
}
-func newPhase1Harness(t *testing.T) *phase1Harness {
+// p1Option adjusts a Phase 1 harness before it is built.
+type p1Option func(*phase1Harness)
+
+// fromHome builds the harness for a move out of the home placement: the
+// source box, its record and its move name the home's code, "" (home-region
+// parks, part 1). The source box keeps its name and bucket.
+func fromHome(h *phase1Harness) { h.src = "" }
+
+func newPhase1Harness(t *testing.T, opts ...p1Option) *phase1Harness {
t.Helper()
- h := &phase1Harness{moveHarness: newMoveHarness(t), lease: &fakeStagingLease{}, clock: &stepClock{now: time.Date(2026, 10, 7, 9, 0, 0, 0, time.UTC)}}
+ h := &phase1Harness{moveHarness: newMoveHarness(t), lease: &fakeStagingLease{}, clock: &stepClock{now: time.Date(2026, 10, 7, 9, 0, 0, 0, time.UTC)}, src: "ie"}
+ for _, o := range opts {
+ o(h)
+ }
+ h.cfg.Region = h.src
h.vs = &fakeVersioned{f: h.f}
h.lease.until = h.clock.now.Add(moves.StagingLeaseTTL)
inc, err := hostclaim.Incarnation("bab", "app", p1Host, p1Created)
@@ -352,7 +368,7 @@ func newPhase1Harness(t *testing.T) *phase1Harness {
h.optDir = t.TempDir()
h.route = registry.Route{Host: p1Host, Kind: "instance", App: "app", Account: "bab", Hibernate: true, Status: "ready",
Socket: provision.DefaultSocket(p1Host), DBPath: provision.DefaultDBPath(h.cfg.DataDir, p1Host), Provisioned: true}
- h.env = map[string]string{"CARLOS_REGION": "ie", "CARLOS_CONTROL_SOCKET": "/run/carlos/" + p1Host + ".sock.control", "APP_MODE": "prod"}
+ h.env = map[string]string{"CARLOS_REGION": h.src, "CARLOS_CONTROL_SOCKET": "/run/carlos/" + p1Host + ".sock.control", "APP_MODE": "prod"}
h.probe, h.strict = moves.Check{OK: true}, moves.Check{OK: true}
ctx := context.Background()
@@ -417,7 +433,7 @@ func newPhase1Harness(t *testing.T) *phase1Harness {
// The source's edges file: ie publishes its data bucket too (T10).
if _, err := h.s.Put(ctx, "control/edges/ie-box.json",
- []byte(`{"v":1,"box":"ie-box","eip":"1.2.3.5","placement":"ie","data_bucket":"data-ie","data_bucket_region":"eu-west-1","hosts":{}}`), ""); err != nil {
+ []byte(`{"v":1,"box":"ie-box","eip":"1.2.3.5","placement":"`+h.src+`","data_bucket":"data-ie","data_bucket_region":"eu-west-1","hosts":{}}`), ""); err != nil {
t.Fatal(err)
}
// The app's instances manifest: it declares no readiness path.
@@ -455,19 +471,19 @@ func pointLiveForTest(t *testing.T, optDir, host, v string) {
func (h *phase1Harness) start(t *testing.T, cut string) provision.Record {
t.Helper()
ctx := context.Background()
- rec := provision.Record{Schema: 1, Host: p1Host, Slug: "s", App: "app", Account: "bab", Region: "ie", Channel: "stable", CreatedAt: p1Created}
+ rec := provision.Record{Schema: 1, Host: p1Host, Slug: "s", App: "app", Account: "bab", Region: h.src, Channel: "stable", CreatedAt: p1Created}
if err := provision.CreateRecord(ctx, h.s, rec); err != nil {
t.Fatal(err)
}
if _, err := provision.SetRecordMove(ctx, h.s, "bab", "app", p1Host, provision.MoveStartTransition(provision.MoveStart{
- ID: p1ID, Incarnation: h.inc, From: "ie", To: "uk", Cut: cut, By: "op", Now: h.clock.Now()})); err != nil {
+ ID: p1ID, Incarnation: h.inc, From: h.src, To: "uk", Cut: cut, By: "op", Now: h.clock.Now()})); err != nil {
t.Fatal(err)
}
obs, err := hostclaim.Update(ctx, h.s, hostclaim.Observed{Host: p1Host, Absent: true}, func(c *hostclaim.Claim) error {
c.Holder = &hostclaim.Holder{Family: "instance", Key: provision.RecordKey("bab", "app", p1Host), Account: "bab", App: "app",
Incarnation: h.inc, CreatedAt: p1Created, Location: "instances/" + p1Host + "/" + h.inc, Generation: "cur", Stamp: 1}
c.Op = hostclaim.Op{Kind: hostclaim.KindServing, Exec: "ie-box"}
- c.Touched = hostclaim.Touched{Complete: true, Regions: []string{"ie"}, Boxes: []string{"ie-box"}}
+ c.Touched = hostclaim.Touched{Complete: true, Regions: []string{park.PlacementRegion(h.src)}, Boxes: []string{"ie-box"}}
return nil
})
if err != nil {
diff --git a/internal/host/movespolicy.go b/internal/host/movespolicy.go
index b5eaa465b..de2b1fc22 100644
--- a/internal/host/movespolicy.go
+++ b/internal/host/movespolicy.go
@@ -10,6 +10,7 @@ import (
"github.com/carlosframework/platform/internal/hostbox"
"github.com/carlosframework/platform/internal/hostclaim"
"github.com/carlosframework/platform/internal/hostwrite"
+ "github.com/carlosframework/platform/internal/park"
"github.com/carlosframework/platform/internal/provision"
"github.com/carlosframework/platform/internal/registry"
"github.com/carlosframework/platform/internal/release"
@@ -436,7 +437,7 @@ func movesRecoveryPolicy(ctx context.Context, read MovesRecordReader, c *hostcla
if rec == nil || rec.Host != c.Host || rec.Move == nil || rec.Move.ID != s.Op || rec.Move.Incarnation != c.Holder.Incarnation {
return refuse("the record is not this move", nil)
}
- if rec.Move.From != s.Region {
+ if park.PlacementRegion(rec.Move.From) != s.Region {
return refuse("this region is not the move's source region", nil)
}
var (
diff --git a/internal/host/movesprobeowners.go b/internal/host/movesprobeowners.go
index 137d32f57..c147a9eb2 100644
--- a/internal/host/movesprobeowners.go
+++ b/internal/host/movesprobeowners.go
@@ -26,6 +26,7 @@ import (
"github.com/carlosframework/platform/internal/hostwrite"
"github.com/carlosframework/platform/internal/moves"
"github.com/carlosframework/platform/internal/moves/archive"
+ "github.com/carlosframework/platform/internal/park"
"github.com/carlosframework/platform/internal/provision"
"github.com/carlosframework/platform/internal/registry"
"github.com/carlosframework/platform/internal/release"
@@ -390,6 +391,15 @@ func predecessorRegion(h *hostclaim.Holder, from string) string {
if n := len(h.Admissions); n > 0 {
return h.Admissions[n-1].Region
}
+ // from is the move's source placement code. Its engine region is the
+ // predecessor's (park.HomeRegion for the home's ""): the claim reads a
+ // predecessor region of "" as holding nothing anywhere and covers it
+ // with no chore, which a home generation does not (home-region parks,
+ // part 1). A code that is no placement is kept as it is: it names no
+ // region, so nothing covers it by mistake.
+ if r := park.PlacementRegion(from); r != "" {
+ return r
+ }
return from
}
diff --git a/internal/host/movesrecover.go b/internal/host/movesrecover.go
index 74bc04486..52fced74a 100644
--- a/internal/host/movesrecover.go
+++ b/internal/host/movesrecover.go
@@ -672,7 +672,7 @@ func (r *recoverRun) probed(ctx context.Context) (bool, error) {
return false, err
}
scope := moves.ReturnLaneScope(host, r.rb, obs.Claim.Holder.Incarnation, seal.Bound())
- lane = hostclaim.QueueReturnLanePark(id, cfg.Region, scope, obligationID)
+ lane = hostclaim.QueueReturnLanePark(id, cfg.parkRegion(), scope, obligationID)
}
switch obs.Claim.Op.Phase {
case hostclaim.PhaseProbing:
diff --git a/internal/host/movesrevert.go b/internal/host/movesrevert.go
index 0a0346de1..91ac4b200 100644
--- a/internal/host/movesrevert.go
+++ b/internal/host/movesrevert.go
@@ -158,11 +158,13 @@ func revertSourceArmed(cfg moveCfg) bool {
}
// revertLaneArmed reports whether this box can run the target region's lane
-// park: the claim's park leases, view and authorizer, and this box's own
-// bucket. In production armMovesParks sets the three only with claim Plan
-// 12, so this answers false there until it lands.
+// park: an engine region (parkRegion, the home's "" included as
+// park.HomeRegion; a code that is no placement is not one), the claim's
+// park leases, view and authorizer, and this box's own bucket. In
+// production armMovesParks sets the three only with claim Plan 12, so this
+// answers false there until it lands.
func revertLaneArmed(cfg moveCfg) bool {
- return cfg.Box != "" && cfg.Region != "" && cfg.OwnBucket != "" && parkHooksArmed(cfg) && cfg.Versioned != nil
+ return cfg.Box != "" && cfg.parkRegion() != "" && cfg.OwnBucket != "" && parkHooksArmed(cfg) && cfg.Versioned != nil
}
// restatusHold is moveCfg.RestatusHold in production: an op's own hold
@@ -899,7 +901,7 @@ func (w *movesWorker) laneParkTarget(ctx context.Context, s release.Store, rec p
lane := moves.ScopeLocation(host, id)
var chores []hostclaim.Obligation
for _, o := range obs.Claim.Obligations {
- if o.Op == id && o.Kind == "park-replica" && o.Region == cfg.Region && o.Park != nil && o.Park.Location == lane {
+ if o.Op == id && o.Kind == "park-replica" && o.Region == cfg.parkRegion() && o.Park != nil && o.Park.Location == lane {
chores = append(chores, o)
}
}
@@ -917,12 +919,12 @@ func (w *movesWorker) laneParkTarget(ctx context.Context, s release.Store, rec p
own := func(_ context.Context, obs hostclaim.Observed, o hostclaim.Obligation) error {
c := obs.Claim
if obs.Absent || c == nil || !inOp(c.Op) || (!committed && c.Op.Exec == cfg.Box) ||
- o.Op != id || o.Kind != "park-replica" || o.Region != cfg.Region || o.Park == nil || o.Park.Location != lane {
+ o.Op != id || o.Kind != "park-replica" || o.Region != cfg.parkRegion() || o.Park == nil || o.Park.Location != lane {
return fmt.Errorf("%w: not this move's lane park", park.ErrUnsafe)
}
return nil
}
- e := park.Engine{Store: vs, Claims: s, Bucket: cfg.OwnBucket, Region: cfg.Region, Leases: cfg.ParkLeases,
+ e := park.Engine{Store: vs, Claims: s, Bucket: cfg.OwnBucket, Region: cfg.parkRegion(), Leases: cfg.ParkLeases,
Now: w.now, NewID: obligationID, View: cfg.ParkView, Authorize: movesArmAuthorize(cfg.ParkAuthorize, own), After: cfg.parkFault}
for _, o := range chores {
err := e.Run(ctx, park.Work{Host: host, ID: o.ID, Op: id})
diff --git a/internal/host/movesrollback.go b/internal/host/movesrollback.go
index 326a9ae91..2093e8405 100644
--- a/internal/host/movesrollback.go
+++ b/internal/host/movesrollback.go
@@ -556,7 +556,7 @@ func (r *rollbackRun) claimReleased(ctx context.Context, evidence string) error
}
holder := obs.Claim.Holder
scope := moves.TargetLocationScope(*holder, report.CheckedAt)
- if _, err := hostclaim.Update(ctx, r.s, obs, hostclaim.TargetReleased(r.g.ID, cfg.Box, cfg.Region, evidence, scope, obligationID)); err != nil {
+ if _, err := hostclaim.Update(ctx, r.s, obs, hostclaim.TargetReleased(r.g.ID, cfg.Box, cfg.parkRegion(), evidence, scope, obligationID)); err != nil {
return fmt.Errorf("the claim half of the release: %w", err)
}
log.Printf("moves: %s: the target gave execution up for rollback %s, citing %s", r.rec.Host, r.rb, evidence)
@@ -1103,7 +1103,7 @@ func (r *rollbackRun) remote(ctx context.Context, obs hostclaim.Observed, j *mov
return false, err
}
scope := moves.StagingScope(host, id, obs.Claim.Holder.Incarnation, staged.Bound())
- if _, err := hostclaim.Update(ctx, r.s, obs, hostclaim.QueueStagingPark(id, cfg.Region, scope, obligationID)); err != nil {
+ if _, err := hostclaim.Update(ctx, r.s, obs, hostclaim.QueueStagingPark(id, cfg.parkRegion(), scope, obligationID)); err != nil {
return false, fmt.Errorf("queue the move's lane park: %w", err)
}
if err := mark(rbStepLaneQueued, ""); err != nil {
@@ -1121,7 +1121,7 @@ func (r *rollbackRun) remote(ctx context.Context, obs hostclaim.Observed, j *mov
e := r.engine(vs)
waiting := 0
for _, o := range obs.Claim.Obligations {
- if o.Op != id || o.Kind != "park-replica" || o.Region != cfg.Region || o.Park == nil {
+ if o.Op != id || o.Kind != "park-replica" || o.Region != cfg.parkRegion() || o.Park == nil {
continue
}
work := park.Work{Host: host, ID: o.ID, Op: id}
@@ -1158,7 +1158,7 @@ func (r *rollbackRun) engine(vs lease.VersionedStore) park.Engine {
}
return r.authorizeChore(obs.Claim, o)
}
- return park.Engine{Store: vs, Claims: r.s, Bucket: cfg.OwnBucket, Region: cfg.Region, Leases: cfg.ParkLeases,
+ return park.Engine{Store: vs, Claims: r.s, Bucket: cfg.OwnBucket, Region: cfg.parkRegion(), Leases: cfg.ParkLeases,
Now: r.now, NewID: obligationID, View: cfg.ParkView, Authorize: movesArmAuthorize(cfg.ParkAuthorize, own), After: cfg.parkFault}
}
@@ -1173,7 +1173,7 @@ func (r *rollbackRun) authorizeChore(c *hostclaim.Claim, o hostclaim.Obligation)
return fmt.Errorf("%w: not this rollback's chore", park.ErrUnsafe)
}
switch {
- case o.Kind == "park-replica" && o.Region == cfg.Region:
+ case o.Kind == "park-replica" && o.Region == cfg.parkRegion():
case o.Kind == "park-local" && o.Box == cfg.Box:
default:
return fmt.Errorf("%w: not this target's chore", park.ErrUnsafe)
diff --git a/internal/host/movessources.go b/internal/host/movessources.go
index e3caad3ed..474ee048c 100644
--- a/internal/host/movessources.go
+++ b/internal/host/movessources.go
@@ -57,9 +57,43 @@ import (
// movesNoLiveRegion is the authoritative region the view names when the
// spec's table says none: a claim with no holder, or a creating, deleting,
-// parking or purging op. No region code is "none", so the engine protects
+// parking or purging op. The "@" keeps it outside the placement-code
+// alphabet (provision.ValidRegion; "none" itself is a valid code) and apart
+// from park.HomeRegion, so no box's region is ever it and the engine protects
// nothing in any region, which is what "nothing is live" means.
-const movesNoLiveRegion = "none"
+const movesNoLiveRegion = "@none"
+
+// parkRegion is this box's engine region: its placement code through
+// park.PlacementRegion, so the home's "" is park.HomeRegion (home-region
+// parks, part 1). Every park engine a moves arm builds, every chore it looks
+// for or queues in its own region, and the region its claim's engine must
+// have go through it. An invalid code maps to "", which arms nothing and
+// which every engine entry refuses.
+func (c moveCfg) parkRegion() string { return park.PlacementRegion(c.Region) }
+
+// movesBoxRegion is the park view's region resolver in production: this box's
+// region as the claim knows it (resolve, the verified roster row), accepted
+// only when it is the engine region of this box's own placement code. A home
+// box (placement "") whose claim names it anything but park.HomeRegion, the
+// roster's own name for home among them, refuses: the view would otherwise
+// read a home record's "" as HomeRegion while this box's engine runs under
+// another name, and protect nothing of home's live set here.
+func movesBoxRegion(placement string, resolve func(context.Context) (string, error)) func(context.Context) (string, error) {
+ return func(ctx context.Context) (string, error) {
+ want := park.PlacementRegion(placement)
+ if want == "" {
+ return "", fmt.Errorf("%w: this box's placement %q is not a placement code", park.ErrUnsafe, placement)
+ }
+ got, err := resolve(ctx)
+ if err != nil {
+ return "", err
+ }
+ if got != want {
+ return "", fmt.Errorf("%w: the claim names this box's region %q, its placement's engine region is %q", park.ErrUnsafe, got, want)
+ }
+ return got, nil
+ }
+}
// movesClaimPlan12 is what claim Plan 12 adds to the MoveParks capability.
// Nothing on main implements it.
@@ -141,8 +175,11 @@ func cachedRegion(resolve func(context.Context) (string, error)) func(context.Co
// movesParkViewAt is movesParkView seen from the region regionOf answers.
// Production's is the claim's verified roster row's region
// (productionRosterRegion), the region the claim's engine runs in, so the
-// view's region is the claim's by construction (M17; fix round 1). The home
-// region, "", is a region like any other.
+// view's region is the claim's by construction (M17; fix round 1), held to
+// this box's placement by movesBoxRegion. Every region it names is an engine
+// region: the home is park.HomeRegion, never "", and a region of "" (unset)
+// refuses. The record's placement codes are mapped once, in
+// movesAuthoritative.
func movesParkViewAt(records MovesRecordReader, data genselect.Reader, bucket string, regionOf func(context.Context) (string, error)) func(context.Context, hostclaim.Observed) (park.View, error) {
return func(ctx context.Context, obs hostclaim.Observed) (park.View, error) {
if ctx == nil || records == nil || data == nil || bucket == "" || regionOf == nil {
@@ -155,6 +192,9 @@ func movesParkViewAt(records MovesRecordReader, data genselect.Reader, bucket st
if err != nil {
return park.View{}, fmt.Errorf("this box's region: %w", err)
}
+ if region == "" {
+ return park.View{}, fmt.Errorf("%w: this box's region is unset", park.ErrUnsafe)
+ }
auth, err := movesAuthoritative(ctx, records, obs, region)
if err != nil {
return park.View{}, err
@@ -365,6 +405,14 @@ func readMarker(ctx context.Context, data genselect.Reader, bucket, host, locati
// (the record-first close, M4), when the newest admission decides alone. A
// region the claim's history contradicts refuses. So does any other kind
// (placing, relocating): it has no row in the table.
+//
+// The move's From and To are placement codes; they are compared and
+// answered as engine regions (park.PlacementRegion), so a move out of home
+// names park.HomeRegion, which no named region equals, and a code that is no
+// placement refuses. region is already an engine region. A serving holder
+// with no admission still takes the record's region as it is, and a record
+// with the home's "" there still refuses: that row is the claim's sweep of
+// every legacy home host, not a move's, and is left as it was.
func movesAuthoritative(ctx context.Context, records MovesRecordReader, obs hostclaim.Observed, region string) (string, error) {
c := obs.Claim
h := c.Holder
@@ -404,6 +452,10 @@ func movesAuthoritative(ctx context.Context, records MovesRecordReader, obs host
}
return "", fmt.Errorf("%w: the record does not carry move %s", park.ErrUnsafe, c.Op.ID)
}
+ from, to := park.PlacementRegion(m.From), park.PlacementRegion(m.To)
+ if from == "" || to == "" {
+ return "", fmt.Errorf("%w: move %s names %q and %q, which are not both placement codes", park.ErrUnsafe, m.ID, m.From, m.To)
+ }
contradicts := func(want ...string) error {
if !known {
return nil
@@ -417,38 +469,38 @@ func movesAuthoritative(ctx context.Context, records MovesRecordReader, obs host
}
switch c.Op.Kind {
case hostclaim.KindMoving, hostclaim.KindReverting:
- if err := contradicts(m.From); err != nil {
+ if err := contradicts(from); err != nil {
return "", err
}
- return m.From, nil
+ return from, nil
case hostclaim.KindLanding:
- if err := contradicts(m.From, m.To); err != nil {
+ if err := contradicts(from, to); err != nil {
return "", err
}
- if known && admitted == m.To {
- if region == m.From && c.Op.Phase != hostclaim.PhaseCommitted && m.State != provision.MoveCommitted {
+ if known && admitted == to {
+ if region == from && c.Op.Phase != hostclaim.PhaseCommitted && m.State != provision.MoveCommitted {
// J2/F4 (Paul's #236 ruling): the source's live set, its
// files live set among it, stays protected on the source
// until the record commits. Seen from the source's region the
// source stays authoritative, so the engine protects there
// too (and refuses a park of the location, whose current
// generation the source's marker does not name).
- return m.From, nil
+ return from, nil
}
- return m.To, nil
+ return to, nil
}
if c.Op.Phase == hostclaim.PhaseCommitted || m.State == provision.MoveCommitted {
- return "", fmt.Errorf("%w: move %s committed with no admission in %s", park.ErrUnsafe, m.ID, m.To)
+ return "", fmt.Errorf("%w: move %s committed with no admission in %s", park.ErrUnsafe, m.ID, to)
}
- return m.From, nil
+ return from, nil
default: // restoring
- if err := contradicts(m.From, m.To); err != nil {
+ if err := contradicts(from, to); err != nil {
return "", err
}
if known {
return admitted, nil
}
- return m.From, nil
+ return from, nil
}
}
@@ -575,9 +627,11 @@ func registerMovesParks(src movesParkSources) error {
// armMovesParks sets the moves arms' park hooks from the claim's MoveParks
// capability (M17): its engine's leases, view and authorizer, only when the
-// engine's bucket and region are this box's own bucket and placement
-// region, and claim Plan 12's RunHeld and local authorizer (M5, M9). Any
-// refusal leaves every hook unset, so every arm stays unarmed.
+// engine's bucket and region are this box's own bucket and its placement's
+// engine region (parkRegion: park.HomeRegion for the home's ""), and claim
+// Plan 12's RunHeld and local authorizer (M5, M9). An engine region of ""
+// never arms. Any refusal leaves every hook unset, so every arm stays
+// unarmed.
func armMovesParks(ctx context.Context, cfg *moveCfg, providers ClaimProviders, plan12 func() (movesClaimPlan12, error)) error {
if ctx == nil || cfg == nil || plan12 == nil {
return hostwrite.ErrUnavailable
@@ -586,8 +640,8 @@ func armMovesParks(ctx context.Context, cfg *moveCfg, providers ClaimProviders,
if err != nil {
return fmt.Errorf("the claim's MoveParks: %w", err)
}
- if engine.Bucket != cfg.OwnBucket || engine.Region != cfg.Region {
- return fmt.Errorf("%w: the claim's park engine is %s in %q, this box's moves use %s in %q", hostbox.ErrChanged, engine.Bucket, engine.Region, cfg.OwnBucket, cfg.Region)
+ if want := cfg.parkRegion(); want == "" || engine.Bucket != cfg.OwnBucket || engine.Region != want {
+ return fmt.Errorf("%w: the claim's park engine is %s in %q, this box's moves use %s in %q (placement %q)", hostbox.ErrChanged, engine.Bucket, engine.Region, cfg.OwnBucket, want, cfg.Region)
}
if engine.Leases == nil || engine.View == nil || engine.Authorize == nil {
return errors.Join(hostwrite.ErrUnavailable, errors.New("the claim's park engine lacks its leases, view or authorizer"))
@@ -609,10 +663,14 @@ func armMovesParks(ctx context.Context, cfg *moveCfg, providers ClaimProviders,
// on (ruling H1), over the deployment store's records, this box's data
// bucket read by exact version (lease.NewVersionedS3, through rootaws), and
// the region regionOf answers, behind the claim Plan 12 guard. Production's
-// regionOf is productionRosterRegion: the claim's verified roster row, the
-// home region "" included (fix round 1). Off, it registers nothing and the
-// claim keeps its named refusals. Task 5's evidence goes through refusals,
-// which pages its anomalies once (movesrefusals.go).
+// regionOf is productionRosterRegion: the claim's verified roster row (fix
+// round 1), held by movesBoxRegion to the engine region of this box's own
+// placement, CARLOS_REGION. The roster refuses a row with region "", so on
+// a home box it names home otherwise than park.HomeRegion, and the view
+// refuses there until the claim names home as the engine does (home-region
+// parks, part 1). Off, it registers nothing and the claim keeps its named
+// refusals. Task 5's evidence goes through refusals, which pages its
+// anomalies once (movesrefusals.go).
func registerProductionMovesParks(ctx context.Context, getenv func(string) string, store release.Store, hib activator.Config, regionOf func(context.Context) (string, error), refusals *movesRefusals) error {
if !hostwrite.Enabled(getenv) || !moves.Enabled(getenv) {
return nil
@@ -626,7 +684,8 @@ func registerProductionMovesParks(ctx context.Context, getenv func(string) strin
}
records := MovesRecordsFrom(store)
evidence := refusals.evidence(movesRetirementEvidence{Records: records, Now: time.Now})
- src, err := newMovesParkSources(records, data, hib.Bucket, cachedRegion(regionOf), evidence, movesPlan12Parks)
+ region := movesBoxRegion(strings.TrimSpace(getenv("CARLOS_REGION")), cachedRegion(regionOf))
+ src, err := newMovesParkSources(records, data, hib.Bucket, region, evidence, movesPlan12Parks)
if err != nil {
return err
}
diff --git a/internal/host/movessources_test.go b/internal/host/movessources_test.go
index dcef3995a..11c52d3b3 100644
--- a/internal/host/movessources_test.go
+++ b/internal/host/movessources_test.go
@@ -553,30 +553,48 @@ func TestRegisterProductionMovesParksNeedsBothSwitches(t *testing.T) {
}
}
-// TestHomeBoxArms (fix round 1, region source): the home region, "", is a
-// region like any other. A home box's view is seen from "" and its arms take
-// an engine whose region is "".
+// TestHomeBoxArms (fix round 1, region source; home-region parks, part 1):
+// a home box, whose placement code is "", is seen from park.HomeRegion and
+// its arms take an engine whose region is park.HomeRegion. An engine region
+// of "" (unset) never arms, and neither does a box whose code is no
+// placement, even when the engine's region spells the same string.
func TestHomeBoxArms(t *testing.T) {
f := newVwFixture(t)
f.rec.Region = ""
- view := movesParkViewAt(f.records(), f.bucket, "data", fixedRegion(""))
+ view := movesParkViewAt(f.records(), f.bucket, "data", fixedRegion(park.HomeRegion))
obs := vwObs(&hostclaim.Claim{Holder: f.holder(""), Op: hostclaim.Op{Kind: hostclaim.KindMoving, ID: vwMove, Phase: hostclaim.PhaseMoving, Exec: "box"}})
f.rec.Move = &provision.MoveState{ID: vwMove, Incarnation: f.inc, From: "", To: "uk", State: provision.MoveCutting}
v, err := view(t.Context(), obs)
- if err != nil || v.Region != "" || v.AuthoritativeRegion != "" || !v.MarkerKnown {
+ if err != nil || v.Region != park.HomeRegion || v.AuthoritativeRegion != park.HomeRegion || !v.MarkerKnown {
t.Fatalf("the home view %+v, %v", v, err)
}
auth := func(context.Context, hostclaim.Observed, hostclaim.Obligation) error { return nil }
- providers := ClaimProviders{moveParks: &claimMoveParks{
- engine: park.Engine{Bucket: "data-home", Region: "", Leases: &fakeParkLeases{}, View: view, Authorize: auth},
- leases: movesLeaseSet("data-home"), check: func(context.Context) error { return nil }}}
- c := moveCfg{OwnBucket: "data-home", Region: ""}
- if err := armMovesParks(t.Context(), &c, providers, func() (movesClaimPlan12, error) { return &fakePlan12{}, nil }); err != nil {
+ arm := func(engineRegion, placement string) (moveCfg, error) {
+ providers := ClaimProviders{moveParks: &claimMoveParks{
+ engine: park.Engine{Bucket: "data-home", Region: engineRegion, Leases: &fakeParkLeases{}, View: view, Authorize: auth},
+ leases: movesLeaseSet("data-home"), check: func(context.Context) error { return nil }}}
+ c := moveCfg{OwnBucket: "data-home", Region: placement}
+ err := armMovesParks(t.Context(), &c, providers, func() (movesClaimPlan12, error) { return &fakePlan12{}, nil })
+ return c, err
+ }
+ c, err := arm(park.HomeRegion, "")
+ if err != nil {
t.Fatalf("a home box did not arm: %v", err)
}
if !parkHooksArmed(c) {
t.Fatal("hooks unset")
}
+ if c, err := arm("uk", "uk"); err != nil || !parkHooksArmed(c) {
+ t.Fatalf("a named box did not arm: %v", err)
+ }
+ for _, tc := range [][2]string{{"", ""}, {"ie", ""}, {"home", ""}, {park.HomeRegion, "home"}, {park.HomeRegion, park.HomeRegion}, {"IE", "IE"}, {"", "uk"}} {
+ if c, err := arm(tc[0], tc[1]); !errors.Is(err, hostbox.ErrChanged) || parkHooksArmed(c) {
+ t.Fatalf("engine in %q, placement %q: armed (%v)", tc[0], tc[1], err)
+ }
+ }
+ if provision.ValidRegion(movesNoLiveRegion) || movesNoLiveRegion == park.HomeRegion {
+ t.Fatalf("the no-live sentinel %q can be a box's region", movesNoLiveRegion)
+ }
}
// TestParkViewRegionIsTheRosters (fix round 1, region source): the view
diff --git a/internal/hostops/move_recovery_commit_test.go b/internal/hostops/move_recovery_commit_test.go
index e2045a9bc..36cc707b1 100644
--- a/internal/hostops/move_recovery_commit_test.go
+++ b/internal/hostops/move_recovery_commit_test.go
@@ -6,6 +6,7 @@ import (
"github.com/carlosframework/platform/internal/hostclaim"
"github.com/carlosframework/platform/internal/moves"
+ "github.com/carlosframework/platform/internal/park"
"github.com/carlosframework/platform/internal/provision"
)
@@ -59,3 +60,55 @@ func TestMoveRecoveryCommitUsesSharedSealValidation(t *testing.T) {
})
}
}
+
+// Home-region parks, part 1: the console's commit of a move out of home
+// queues the source's location park in park.HomeRegion, exactly as the
+// box's commit does (K9), and covers the home generation's admission. The
+// record names home by its code, "", which the claim would refuse.
+func TestMoveRecoveryCommitFromHome(t *testing.T) {
+ w := newLossWorld(t, "landing", func(c *hostclaim.Claim, r *provision.Record, _ *AppAdmission) {
+ c.Op.Phase = hostclaim.PhaseLandingProbed
+ r.Move.State = provision.MoveCommitted
+ r.Move.CommittedAt = r.Move.FlippedAt
+ r.Move.From = ""
+ for i := range c.Holder.Admissions {
+ a := &c.Holder.Admissions[i]
+ if a.Region == "ie" {
+ a.Region = park.HomeRegion
+ }
+ if a.PredecessorRegion == "ie" {
+ a.PredecessorRegion = park.HomeRegion
+ }
+ }
+ })
+ before := w.claim(t)
+ err := w.control.RunRecovery(context.Background(), func(ctx context.Context) error {
+ return runMoveRecovery(ctx, before, func(ctx context.Context, g *MoveRecoveryGrant) error {
+ return g.Claim(ctx, RecoveryMoveCommit)
+ })
+ })
+ after := w.claim(t)
+ if err != nil || after.Claim.Op.Phase != hostclaim.PhaseCommitted {
+ t.Fatalf("commit: %v, claim=%+v", err, after.Claim.Op)
+ }
+ found := false
+ for _, o := range after.Claim.Obligations {
+ if o.Region == "" {
+ t.Fatalf("a chore in the unset region: %+v", o)
+ }
+ if o.Kind == "park-replica" && o.Park != nil && o.Park.Location == "instances/"+lossHost {
+ if o.Region != park.HomeRegion || o.Owner != "region:"+park.HomeRegion {
+ t.Fatalf("the source's location park %+v, want it in %s", o, park.HomeRegion)
+ }
+ found = true
+ }
+ }
+ if !found {
+ t.Fatalf("no location park: %+v", after.Claim.Obligations)
+ }
+ for _, a := range after.Claim.Holder.Admissions {
+ if a.PredecessorRegion == park.HomeRegion && !a.Covered {
+ t.Fatalf("the home generation's admission is not covered: %+v", a)
+ }
+ }
+}
diff --git a/internal/hostops/move_recovery_grant.go b/internal/hostops/move_recovery_grant.go
index 690ed4eb5..419b0c6db 100644
--- a/internal/hostops/move_recovery_grant.go
+++ b/internal/hostops/move_recovery_grant.go
@@ -11,6 +11,7 @@ import (
"github.com/carlosframework/platform/internal/hostclaim"
"github.com/carlosframework/platform/internal/moves"
+ "github.com/carlosframework/platform/internal/park"
"github.com/carlosframework/platform/internal/provision"
"github.com/carlosframework/platform/internal/release"
)
@@ -425,7 +426,10 @@ func (g *MoveRecoveryGrant) Claim(ctx context.Context, action MoveRecoveryClaimA
return err
}
n := 0
- transition = hostclaim.Commit(g.o.OpID, m.From, c.Op.Intent["source"], m.To, source, staging, func() string { n++; return id + strconv.Itoa(n) })
+ // The record's codes become engine regions as the box's commit
+ // makes them (park.PlacementRegion; K9): a move out of home queues
+ // its location park in park.HomeRegion, never "".
+ transition = hostclaim.Commit(g.o.OpID, park.PlacementRegion(m.From), c.Op.Intent["source"], park.PlacementRegion(m.To), source, staging, func() string { n++; return id + strconv.Itoa(n) })
default:
return ErrIdentity
}