| 1 | package hostclaim |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "slices" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/regionname" |
| 11 | ) |
| 12 | |
| 13 | // Main's home-region parks (park.HomeRegion, "@home") name the home's |
| 14 | // chores by that token where moves queues them, while a relocation's region |
| 15 | // is the roster name (ie here). Under a verified home binding the two are |
| 16 | // one region (regionname.SameRegion), so a relocation in the home sees and |
| 17 | // reuses the chores moves queued under the other name. Without a binding |
| 18 | // the comparison stays exact, as main's own comparisons do. |
| 19 | |
| 20 | func TestRelocationStartGuardReadsTheHomesTwoNames(t *testing.T) { |
| 21 | serving := func(obs ...Obligation) *Claim { |
| 22 | return &Claim{Holder: &Holder{Incarnation: "i"}, Op: Op{Kind: KindServing, Exec: relSource}, Obligations: obs} |
| 23 | } |
| 24 | for _, tc := range []struct { |
| 25 | name, home, region string |
| 26 | ob Obligation |
| 27 | want error |
| 28 | }{ |
| 29 | {"park-replica under @home, home bound", "ie", "ie", Obligation{Kind: "park-replica", Region: regionname.HomeRegion}, ErrPendingChore}, |
| 30 | {"park-replica under @home, no binding", "", "ie", Obligation{Kind: "park-replica", Region: regionname.HomeRegion}, nil}, |
| 31 | {"park-replica under the roster name, home bound", "ie", "ie", Obligation{Kind: "park-replica", Region: "ie"}, ErrPendingChore}, |
| 32 | {"park-replica under @home, another region's relocation", "ie", "uk", Obligation{Kind: "park-replica", Region: regionname.HomeRegion}, nil}, |
| 33 | {"park-generation under @home (L11)", "ie", "ie", Obligation{Kind: "park-generation", Region: regionname.HomeRegion}, nil}, |
| 34 | } { |
| 35 | t.Run(tc.name, func(t *testing.T) { |
| 36 | if err := RelocationStartGuard(serving(tc.ob), tc.home, tc.region, relSource, relTarget, false); !errors.Is(err, tc.want) { |
| 37 | t.Fatalf("got %v, want %v", err, tc.want) |
| 38 | } |
| 39 | }) |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | func TestStartRelocationCarriesTheHomeBinding(t *testing.T) { |
| 44 | ctx := context.Background() |
| 45 | t.Run("recorded as the op's home_region", func(t *testing.T) { |
| 46 | s, o := relServing(t) |
| 47 | in := relStart(ReasonDrain, nil) |
| 48 | in.HomeRegion = "ie" |
| 49 | o, err := StartRelocation(ctx, s, o, in, relAuthority{}) |
| 50 | if err != nil { |
| 51 | t.Fatal(err) |
| 52 | } |
| 53 | if o.Claim.Op.Intent["home_region"] != "ie" || o.Claim.Op.Intent["region"] != "ie" { |
| 54 | t.Fatalf("intent %v", o.Claim.Op.Intent) |
| 55 | } |
| 56 | }) |
| 57 | t.Run("no binding records none", func(t *testing.T) { |
| 58 | s, o := relServing(t) |
| 59 | o, err := StartRelocation(ctx, s, o, relStart(ReasonDrain, nil), relAuthority{}) |
| 60 | if err != nil { |
| 61 | t.Fatal(err) |
| 62 | } |
| 63 | if _, ok := o.Claim.Op.Intent["home_region"]; ok { |
| 64 | t.Fatalf("intent %v", o.Claim.Op.Intent) |
| 65 | } |
| 66 | }) |
| 67 | t.Run("a park-replica under @home refuses the start", func(t *testing.T) { |
| 68 | s, o := relServing(t) |
| 69 | o = step(t, s, o, func(c *Claim) error { |
| 70 | c.Obligations = append(c.Obligations, Obligation{ID: "lane", Op: "mv-old", Kind: "park-replica", Region: regionname.HomeRegion, Owner: "region:" + regionname.HomeRegion, Retirement: "r"}) |
| 71 | return nil |
| 72 | }) |
| 73 | in := relStart(ReasonDrain, nil) |
| 74 | in.HomeRegion = "ie" |
| 75 | raw := mustJSON(t, o.Claim) |
| 76 | if _, err := StartRelocation(ctx, s, o, in, relAuthority{}); !errors.Is(err, ErrPendingChore) { |
| 77 | t.Fatalf("got %v", err) |
| 78 | } |
| 79 | if stored, err := Read(ctx, s, o.Host); err != nil || stored.ETag != o.ETag || string(raw) != string(mustJSON(t, o.Claim)) { |
| 80 | t.Fatalf("refusal wrote the claim: %v", err) |
| 81 | } |
| 82 | }) |
| 83 | t.Run("a home that is no placement code refuses", func(t *testing.T) { |
| 84 | s, o := relServing(t) |
| 85 | in := relStart(ReasonDrain, nil) |
| 86 | in.HomeRegion = regionname.HomeRegion |
| 87 | if _, err := StartRelocation(ctx, s, o, in, relAuthority{}); !errors.Is(err, ErrRelocation) { |
| 88 | t.Fatalf("got %v", err) |
| 89 | } |
| 90 | if stored, err := Read(ctx, s, o.Host); err != nil || stored.ETag != o.ETag { |
| 91 | t.Fatalf("refusal wrote the claim: %v", err) |
| 92 | } |
| 93 | }) |
| 94 | } |
| 95 | |
| 96 | // The completion parks an abandoned P in the relocation's region unless a |
| 97 | // park-generation for it is already owed there, under either of the home's |
| 98 | // names when the op is bound to the home. |
| 99 | func TestRelocationCompletionReusesAParkUnderTheHomesOtherName(t *testing.T) { |
| 100 | ctx := context.Background() |
| 101 | for _, home := range []string{"ie", ""} { |
| 102 | t.Run("home "+home, func(t *testing.T) { |
| 103 | s, o := relAtPhase(t, PhaseInstalling) |
| 104 | o, err := ReplaceTarget(ctx, s, o, "rel-1", putFence(t, s, relTarget), nil, Target{relSpare, relEnrollment(relSpare)}, "P2", relAuthority{}) |
| 105 | if err != nil { |
| 106 | t.Fatal(err) |
| 107 | } |
| 108 | o = step(t, s, o, func(c *Claim) error { |
| 109 | if home != "" { |
| 110 | c.Op.Intent["home_region"] = home |
| 111 | } |
| 112 | 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"}) |
| 113 | return nil |
| 114 | }) |
| 115 | o = step(t, s, o, ack(relSpare)) |
| 116 | o = step(t, s, o, ToInstalled("rel-1", relPrepared("P2", 40, 7))) |
| 117 | o = step(t, s, o, RecordLocalInstall("rel-1", localInstall(o.Claim))) |
| 118 | o = step(t, s, o, ToProbing("rel-1", relObserved)) |
| 119 | o = step(t, s, o, AdmitWriter(Admission{ObservedOpID: "rel-1", ObservedPhase: PhaseRelocatingProbing, ObservedSession: o.Claim.Holder.Session, ObservedGeneration: o.Claim.Holder.Generation, |
| 120 | Box: relSpare, Mode: "cold", NewGeneration: "P2", KeepStamp: true, PredecessorCertified: true, Region: "ie", PredecessorRegion: "ie"})) |
| 121 | o = step(t, s, o, RecordProbe("rel-1", Probe{Box: relSpare, Session: o.Claim.Holder.Session})) |
| 122 | o = step(t, s, o, RecordProbeStop("rel-1", ProbeStop{Box: relSpare, Session: o.Claim.Holder.Session, Receipt: "fence-1", ReceiptSHA256: strings.Repeat("a", 64)})) |
| 123 | o = step(t, s, o, CompleteRelocation("rel-1", counter())) |
| 124 | var regions []string |
| 125 | for _, ob := range o.Claim.Obligations { |
| 126 | if ob.Kind == "park-generation" && ob.Generation == "P1" { |
| 127 | regions = append(regions, ob.Region) |
| 128 | } |
| 129 | } |
| 130 | want := []string{regionname.HomeRegion} |
| 131 | if home == "" { |
| 132 | want = append(want, "ie") // exact without a binding: a second chore in the roster name |
| 133 | } |
| 134 | slices.Sort(regions) |
| 135 | slices.Sort(want) |
| 136 | if !slices.Equal(regions, want) { |
| 137 | t.Fatalf("P1's parks: %v, want %v", regions, want) |
| 138 | } |
| 139 | }) |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | // J3 into the home: the move names its destination park.HomeRegion and |
| 144 | // the fence binding names the roster's home (uk here). They are one region |
| 145 | // only under the move's home binding; the relocation's region is then the |
| 146 | // roster name, and the commit's lane park is queued in park.HomeRegion, as |
| 147 | // moves' own commit queues it. |
| 148 | func TestJ3CommittedLandingIntoTheHomeNeedsTheMovesHomeBinding(t *testing.T) { |
| 149 | ctx := context.Background() |
| 150 | for _, bound := range []bool{true, false} { |
| 151 | name := "bound" |
| 152 | if !bound { |
| 153 | name = "unbound" |
| 154 | } |
| 155 | t.Run(name, func(t *testing.T) { |
| 156 | s, o := j3Landing(t, PhaseLandingProbed) |
| 157 | o = step(t, s, o, func(c *Claim) error { |
| 158 | c.Op.Intent["to"] = regionname.HomeRegion |
| 159 | if bound { |
| 160 | c.Op.Intent["home_region"] = "uk" |
| 161 | } |
| 162 | return nil |
| 163 | }) |
| 164 | ref := j3Fence(t, s) |
| 165 | s.puts = 0 |
| 166 | after, err := ConvertCommittedLanding(ctx, s, o, moveA, j3Target, ref, j3Authority{}, &j3Guard{to: regionname.HomeRegion}, nil, counter()) |
| 167 | if !bound { |
| 168 | if !errors.Is(err, ErrFence) || s.puts != 0 { |
| 169 | t.Fatalf("got %v, writes %d", err, s.puts) |
| 170 | } |
| 171 | return |
| 172 | } |
| 173 | if err != nil { |
| 174 | t.Fatal(err) |
| 175 | } |
| 176 | c := after.Claim |
| 177 | if c.Op.Intent["region"] != "uk" || c.Op.Intent["home_region"] != "uk" || c.Op.Intent["move_to_region"] != regionname.HomeRegion { |
| 178 | t.Fatalf("intent %v", c.Op.Intent) |
| 179 | } |
| 180 | if owner, err := RelocationRecoveryOwner(c); err != nil || owner != "region:uk" { |
| 181 | t.Fatalf("owner=%q error=%v", owner, err) |
| 182 | } |
| 183 | if k := obligationKinds(c); k["park-replica/ie"] != 1 || k["park-replica/"+regionname.HomeRegion] != 1 { |
| 184 | t.Fatalf("the commit's chores: %v", k) |
| 185 | } |
| 186 | }) |
| 187 | } |
| 188 | } |
| 189 | |