package hostclaim import ( "context" "errors" "slices" "strings" "testing" "github.com/carlosframework/platform/internal/regionname" ) // Main's home-region parks (park.HomeRegion, "@home") name the home's // chores by that token where moves queues them, while a relocation's region // is the roster name (ie here). Under a verified home binding the two are // one region (regionname.SameRegion), so a relocation in the home sees and // reuses the chores moves queued under the other name. Without a binding // the comparison stays exact, as main's own comparisons do. func TestRelocationStartGuardReadsTheHomesTwoNames(t *testing.T) { serving := func(obs ...Obligation) *Claim { return &Claim{Holder: &Holder{Incarnation: "i"}, Op: Op{Kind: KindServing, Exec: relSource}, Obligations: obs} } for _, tc := range []struct { name, home, region string ob Obligation want error }{ {"park-replica under @home, home bound", "ie", "ie", Obligation{Kind: "park-replica", Region: regionname.HomeRegion}, ErrPendingChore}, {"park-replica under @home, no binding", "", "ie", Obligation{Kind: "park-replica", Region: regionname.HomeRegion}, nil}, {"park-replica under the roster name, home bound", "ie", "ie", Obligation{Kind: "park-replica", Region: "ie"}, ErrPendingChore}, {"park-replica under @home, another region's relocation", "ie", "uk", Obligation{Kind: "park-replica", Region: regionname.HomeRegion}, nil}, {"park-generation under @home (L11)", "ie", "ie", Obligation{Kind: "park-generation", Region: regionname.HomeRegion}, nil}, } { t.Run(tc.name, func(t *testing.T) { if err := RelocationStartGuard(serving(tc.ob), tc.home, tc.region, relSource, relTarget, false); !errors.Is(err, tc.want) { t.Fatalf("got %v, want %v", err, tc.want) } }) } } func TestStartRelocationCarriesTheHomeBinding(t *testing.T) { ctx := context.Background() t.Run("recorded as the op's home_region", func(t *testing.T) { s, o := relServing(t) in := relStart(ReasonDrain, nil) in.HomeRegion = "ie" o, err := StartRelocation(ctx, s, o, in, relAuthority{}) if err != nil { t.Fatal(err) } if o.Claim.Op.Intent["home_region"] != "ie" || o.Claim.Op.Intent["region"] != "ie" { t.Fatalf("intent %v", o.Claim.Op.Intent) } }) t.Run("no binding records none", func(t *testing.T) { s, o := relServing(t) o, err := StartRelocation(ctx, s, o, relStart(ReasonDrain, nil), relAuthority{}) if err != nil { t.Fatal(err) } if _, ok := o.Claim.Op.Intent["home_region"]; ok { t.Fatalf("intent %v", o.Claim.Op.Intent) } }) t.Run("a park-replica under @home refuses the start", func(t *testing.T) { s, o := relServing(t) o = step(t, s, o, func(c *Claim) error { c.Obligations = append(c.Obligations, Obligation{ID: "lane", Op: "mv-old", Kind: "park-replica", Region: regionname.HomeRegion, Owner: "region:" + regionname.HomeRegion, Retirement: "r"}) return nil }) in := relStart(ReasonDrain, nil) in.HomeRegion = "ie" raw := mustJSON(t, o.Claim) if _, err := StartRelocation(ctx, s, o, in, relAuthority{}); !errors.Is(err, ErrPendingChore) { t.Fatalf("got %v", err) } if stored, err := Read(ctx, s, o.Host); err != nil || stored.ETag != o.ETag || string(raw) != string(mustJSON(t, o.Claim)) { t.Fatalf("refusal wrote the claim: %v", err) } }) t.Run("a home that is no placement code refuses", func(t *testing.T) { s, o := relServing(t) in := relStart(ReasonDrain, nil) in.HomeRegion = regionname.HomeRegion if _, err := StartRelocation(ctx, s, o, in, relAuthority{}); !errors.Is(err, ErrRelocation) { t.Fatalf("got %v", err) } if stored, err := Read(ctx, s, o.Host); err != nil || stored.ETag != o.ETag { t.Fatalf("refusal wrote the claim: %v", err) } }) } // The completion parks an abandoned P in the relocation's region unless a // park-generation for it is already owed there, under either of the home's // names when the op is bound to the home. func TestRelocationCompletionReusesAParkUnderTheHomesOtherName(t *testing.T) { ctx := context.Background() for _, home := range []string{"ie", ""} { t.Run("home "+home, func(t *testing.T) { s, o := relAtPhase(t, PhaseInstalling) o, err := ReplaceTarget(ctx, s, o, "rel-1", putFence(t, s, relTarget), nil, Target{relSpare, relEnrollment(relSpare)}, "P2", relAuthority{}) if err != nil { t.Fatal(err) } o = step(t, s, o, func(c *Claim) error { if home != "" { c.Op.Intent["home_region"] = home } c.Obligations = append(c.Obligations, Obligation{ID: "earlier", Op: "mv-old", Kind: "park-generation", Region: regionname.HomeRegion, Owner: "region:" + regionname.HomeRegion, Generation: "P1", Retirement: "r-earlier"}) return nil }) o = step(t, s, o, ack(relSpare)) o = step(t, s, o, ToInstalled("rel-1", relPrepared("P2", 40, 7))) o = step(t, s, o, RecordLocalInstall("rel-1", localInstall(o.Claim))) o = step(t, s, o, ToProbing("rel-1", relObserved)) o = step(t, s, o, AdmitWriter(Admission{ObservedOpID: "rel-1", ObservedPhase: PhaseRelocatingProbing, ObservedSession: o.Claim.Holder.Session, ObservedGeneration: o.Claim.Holder.Generation, Box: relSpare, Mode: "cold", NewGeneration: "P2", KeepStamp: true, PredecessorCertified: true, Region: "ie", PredecessorRegion: "ie"})) o = step(t, s, o, RecordProbe("rel-1", Probe{Box: relSpare, Session: o.Claim.Holder.Session})) o = step(t, s, o, RecordProbeStop("rel-1", ProbeStop{Box: relSpare, Session: o.Claim.Holder.Session, Receipt: "fence-1", ReceiptSHA256: strings.Repeat("a", 64)})) o = step(t, s, o, CompleteRelocation("rel-1", counter())) var regions []string for _, ob := range o.Claim.Obligations { if ob.Kind == "park-generation" && ob.Generation == "P1" { regions = append(regions, ob.Region) } } want := []string{regionname.HomeRegion} if home == "" { want = append(want, "ie") // exact without a binding: a second chore in the roster name } slices.Sort(regions) slices.Sort(want) if !slices.Equal(regions, want) { t.Fatalf("P1's parks: %v, want %v", regions, want) } }) } } // J3 into the home: the move names its destination park.HomeRegion and // the fence binding names the roster's home (uk here). They are one region // only under the move's home binding; the relocation's region is then the // roster name, and the commit's lane park is queued in park.HomeRegion, as // moves' own commit queues it. func TestJ3CommittedLandingIntoTheHomeNeedsTheMovesHomeBinding(t *testing.T) { ctx := context.Background() for _, bound := range []bool{true, false} { name := "bound" if !bound { name = "unbound" } t.Run(name, func(t *testing.T) { s, o := j3Landing(t, PhaseLandingProbed) o = step(t, s, o, func(c *Claim) error { c.Op.Intent["to"] = regionname.HomeRegion if bound { c.Op.Intent["home_region"] = "uk" } return nil }) ref := j3Fence(t, s) s.puts = 0 after, err := ConvertCommittedLanding(ctx, s, o, moveA, j3Target, ref, j3Authority{}, &j3Guard{to: regionname.HomeRegion}, nil, counter()) if !bound { if !errors.Is(err, ErrFence) || s.puts != 0 { t.Fatalf("got %v, writes %d", err, s.puts) } return } if err != nil { t.Fatal(err) } c := after.Claim if c.Op.Intent["region"] != "uk" || c.Op.Intent["home_region"] != "uk" || c.Op.Intent["move_to_region"] != regionname.HomeRegion { t.Fatalf("intent %v", c.Op.Intent) } if owner, err := RelocationRecoveryOwner(c); err != nil || owner != "region:uk" { t.Fatalf("owner=%q error=%v", owner, err) } if k := obligationKinds(c); k["park-replica/ie"] != 1 || k["park-replica/"+regionname.HomeRegion] != 1 { t.Fatalf("the commit's chores: %v", k) } }) } }