carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package park
2
3import (
4 "context"
5 "errors"
6 "strings"
7 "testing"
8 "time"
9
10 "github.com/carlosframework/platform/internal/hostclaim"
11 "github.com/carlosframework/platform/internal/release"
12)
13
14// Ruling R-PARK (settings-4 Task 8b): <holder.Location>/build-state.json,
15// every version of it, is live state beside the lease, the marker's twin.
16// The engine skips it wherever it skips the marker key at the holder
17// location, and refuses a whole-location park of the live holder location
18// because of it, exactly as for the marker. Anywhere else (a retired
19// location, a moves lane) it is residue like any other object.
20
21// TestHolderBuildStateIsNeverACandidate: a sweep never makes the live
22// location's build state a candidate and never parks a version of it; the
23// same key at a retired location is a candidate and is parked with the
24// location, and a lane's copy stays a moves/<host> candidate.
25func TestHolderBuildStateIsNeverACandidate(t *testing.T) {
26 f, obs, v, _ := sweepFixture(t)
27 key := release.BuildStateKey(f.ob.Park.Location)
28 older := f.b.Put("data", key, []byte("older"))
29 current := f.b.Put("data", key, []byte("current"))
30 retiredInc := strings.Repeat("a", 64)
31 retired := "instances/" + f.w.Host + "/" + retiredInc
32 stale := release.BuildStateKey(retired)
33 f.b.Put("data", stale, []byte("stale"))
34 lane := "moves/" + f.w.Host + "/mv_1/state/build-state.json"
35 f.b.Put("data", lane, []byte("lane"))
36 vs, err := f.e.Discover(t.Context(), f.w.Host)
37 if err != nil {
38 t.Fatal(err)
39 }
40 d, err := Decide(obs, v, vs, f.ob.Park.Incarnation)
41 if err != nil {
42 t.Fatalf("Decide: %v", err)
43 }
44 want := []Candidate{
45 {Location: f.ob.Park.Location, Incarnation: f.ob.Park.Incarnation, Generation: "g0"},
46 {Location: retired, Incarnation: retiredInc},
47 {Location: "moves/" + f.w.Host, Incarnation: f.ob.Park.Incarnation},
48 }
49 if len(d.Alerts) != 0 || len(d.Candidates) != len(want) {
50 t.Fatalf("decision %+v, want %+v", d, want)
51 }
52 for _, w := range want {
53 found := false
54 for _, c := range d.Candidates {
55 found = found || c == w
56 }
57 if !found {
58 t.Fatalf("decision %+v lacks %+v", d.Candidates, w)
59 }
60 }
61 f.e.View = func(_ context.Context, o hostclaim.Observed) (View, error) {
62 x := v
63 x.ClaimETag = o.ETag
64 return x, nil
65 }
66 if _, err := f.e.Sweep(t.Context(), f.w.Host, f.ob.Park.Incarnation, f.ob.Park.EvidenceAt, time.Second); err != nil {
67 t.Fatalf("Sweep: %v", err)
68 }
69 for _, id := range []string{older, current} {
70 if _, err := f.b.GetVersion(t.Context(), "data", key, id); err != nil {
71 t.Fatalf("the live build state version %s was parked: %v", id, err)
72 }
73 }
74 left, err := f.b.ListVersions(t.Context(), "data", retired+"/")
75 if err != nil || len(left) != 0 {
76 t.Fatalf("the retired location still holds %+v (%v)", left, err)
77 }
78}
79
80// twinFixture is a holder location, serving, holding one generation that
81// is not live (g0) beside its build state, with one chore of kind at the
82// holder location, seen from an authoritative region or not.
83func twinFixture(t *testing.T, kind string, authoritative, withState bool) (*fixture, string) {
84 t.Helper()
85 f := newFixture(t)
86 p := f.ob.Park.Location + "/"
87 f.ob.Kind = kind
88 if kind == "park-generation" {
89 f.ob.Generation = "g0"
90 }
91 obs, err := hostclaim.Read(t.Context(), f.e.Claims, f.w.Host)
92 if err != nil {
93 t.Fatal(err)
94 }
95 if _, err := hostclaim.Update(t.Context(), f.e.Claims, obs, func(c *hostclaim.Claim) error {
96 c.Op = hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box"}
97 c.Holder.Generation, c.Holder.Session = "g1", 1
98 c.Holder.Admissions = []hostclaim.AdmissionRecord{{Session: 1, Generation: "g1", Mode: "cold", Region: "ie"}}
99 c.Obligations = []hostclaim.Obligation{f.ob}
100 return nil
101 }); err != nil {
102 t.Fatal(err)
103 }
104 f.b.Put("data", p+"g0/data", []byte("g0"))
105 key := release.BuildStateKey(f.ob.Park.Location)
106 if withState {
107 f.b.Put("data", key, []byte("older"))
108 f.b.Put("data", key, []byte("current"))
109 }
110 classify := func(r Ref) (string, bool) {
111 rel, ok := strings.CutPrefix(r.Key, p)
112 if !ok {
113 return "", false
114 }
115 g, _, ok := strings.Cut(rel, "/")
116 return g, ok
117 }
118 f.e.View = func(_ context.Context, o hostclaim.Observed) (View, error) {
119 v := View{ClaimETag: o.ETag, Region: "ie", AuthoritativeRegion: "uk", GenerationOf: classify}
120 if authoritative {
121 v.AuthoritativeRegion, v.MarkerKnown, v.MarkerGeneration = "ie", true, "g1"
122 v.Marker = Ref{Key: p + "generation.json"}
123 }
124 return v, nil
125 }
126 return f, key
127}
128
129// TestHolderBuildStateIsParkedOnlyAsTheMarkerIs: at the live holder
130// location a generation's park passes over the build state in either
131// region; a whole-location park refuses because of it in the authoritative
132// region (the control without it completes), and in a region that is not
133// authoritative parks it with everything else, as it parks the marker.
134func TestHolderBuildStateIsParkedOnlyAsTheMarkerIs(t *testing.T) {
135 versionsAt := func(t *testing.T, f *fixture, key string) int {
136 t.Helper()
137 vs, err := f.b.ListVersions(t.Context(), "data", key)
138 if err != nil {
139 t.Fatal(err)
140 }
141 n := 0
142 for _, v := range vs {
143 if v.Key == key {
144 n++
145 }
146 }
147 return n
148 }
149 for name, authoritative := range map[string]bool{"authoritative": true, "not authoritative": false} {
150 t.Run(name+": a generation's park passes it over", func(t *testing.T) {
151 f, key := twinFixture(t, "park-generation", authoritative, true)
152 if err := f.e.Run(t.Context(), f.w); err != nil {
153 t.Fatalf("Run: %v", err)
154 }
155 f.done0(t)
156 if n := versionsAt(t, f, key); n != 2 {
157 t.Fatalf("%d versions of the build state left, want both", n)
158 }
159 if n := versionsAt(t, f, f.ob.Park.Location+"/g0/data"); n != 0 {
160 t.Fatalf("g0 was not parked: %d versions", n)
161 }
162 })
163 }
164 t.Run("authoritative: a whole-location park refuses because of it", func(t *testing.T) {
165 control, _ := twinFixture(t, "park-replica", true, false)
166 if err := control.e.Run(t.Context(), control.w); err != nil {
167 t.Fatalf("control without the build state: %v", err)
168 }
169 f, _ := twinFixture(t, "park-replica", true, true)
170 before := f.b.Snapshot()
171 if err := f.e.Run(t.Context(), f.w); !errors.Is(err, ErrUnsafe) {
172 t.Fatalf("Run = %v, want ErrUnsafe", err)
173 }
174 if f.b.Snapshot() != before {
175 t.Fatal("the refused park changed storage")
176 }
177 })
178 t.Run("not authoritative: a whole-location park parks it", func(t *testing.T) {
179 f, key := twinFixture(t, "park-replica", false, true)
180 if err := f.e.Run(t.Context(), f.w); err != nil {
181 t.Fatalf("Run: %v", err)
182 }
183 f.done0(t)
184 if n := versionsAt(t, f, key); n != 0 {
185 t.Fatalf("%d versions of the build state left, want none", n)
186 }
187 })
188}
189
190// done0 asserts the chore is acknowledged.
191func (f *fixture) done0(t *testing.T) {
192 t.Helper()
193 obs, err := hostclaim.Read(t.Context(), f.e.Claims, f.w.Host)
194 if err != nil {
195 t.Fatal(err)
196 }
197 if len(obs.Claim.Obligations) != 0 {
198 t.Fatalf("obligation still open: %+v", obs.Claim.Obligations)
199 }
200}
201