carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostclaim
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "reflect"
9 "testing"
10 "time"
11
12 "github.com/carlosframework/platform/internal/release"
13)
14
15func TestAdmitWriterReturnRegion(t *testing.T) {
16 for _, mode := range []string{"warm", "cold"} {
17 for _, region := range []string{"ie", ""} {
18 t.Run(mode+"/region="+region, func(t *testing.T) {
19 ctx := context.Background()
20 s := release.NewDirStore(t.TempDir())
21 o := servingClaim(t, ctx, s)
22 o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{Region: "uk", Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: "GT"}))
23 target := o.Claim.Holder.Admissions[0]
24 o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error {
25 c.Op = Op{Kind: KindReverting, ID: "move", Phase: "return", Exec: "boxS"}
26 return nil
27 })
28 in := Admission{
29 ObservedOpID: "move", ObservedPhase: "return", ObservedSession: target.Session, ObservedGeneration: "GT",
30 Box: "boxS", Mode: mode, Region: region, PredecessorRegion: "ignored",
31 PredecessorCertified: true, Coverage: "clean-return",
32 }
33 if mode == "cold" {
34 in.NewGeneration = "GS2"
35 }
36 next, err := Update(ctx, s, o, AdmitWriter(in))
37 if mode == "warm" || region == "" {
38 if !errors.Is(err, ErrWrongState) {
39 t.Fatalf("return admission = %v, want ErrWrongState", err)
40 }
41 assertAdmissionStored(t, ctx, s, o)
42 return
43 }
44 if err != nil {
45 t.Fatal(err)
46 }
47 last := next.Claim.Holder.Admissions[1]
48 if last.Region != "ie" || last.PredecessorRegion != "uk" || last.Predecessor != "GT" || last.Generation != "GS2" || last.Mode != "cold" || last.Covered {
49 t.Fatalf("cold return = %+v", last)
50 }
51 assertAdmissionStored(t, ctx, s, next)
52 })
53 }
54 }
55}
56
57func TestAdmitWriterPendingParkCoverage(t *testing.T) {
58 for _, name := range []string{"whole predecessor region", "scoped holder location", "no park", "other region", "generation only", "local only", "scoped other location"} {
59 t.Run(name, func(t *testing.T) {
60 ctx := context.Background()
61 s := release.NewDirStore(t.TempDir())
62 o := servingClaim(t, ctx, s)
63 o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{Region: "uk", Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: "GT"}))
64 o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error {
65 c.Op = Op{Kind: KindReverting, ID: "move", Phase: "return", Exec: "boxS"}
66 ob := Obligation{ID: "park", Op: "release", Kind: "park-replica", Region: "uk", Owner: "region:uk", Retirement: "ret"}
67 switch name {
68 case "no park":
69 return nil
70 case "other region":
71 ob.Region, ob.Owner = "ie", "region:ie"
72 case "generation only":
73 ob.Kind, ob.Generation = "park-generation", "GT"
74 case "local only":
75 ob.Kind, ob.Region, ob.Owner, ob.Box = "park-local", "", "box:boxT", "boxT"
76 case "scoped holder location", "scoped other location":
77 loc := c.Holder.Location
78 if name == "scoped other location" {
79 loc = "moves/" + c.Host + "/oldmove"
80 }
81 ob.Park = &ParkScope{Location: loc, Incarnation: c.Holder.Incarnation, EvidenceAt: time.Unix(1, 0).UTC(), RequestLimit: time.Second}
82 }
83 c.Obligations = []Obligation{ob}
84 return nil
85 })
86 before := o
87 o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{
88 ObservedOpID: "move", ObservedPhase: "return", ObservedSession: o.Claim.Holder.Session, ObservedGeneration: "GT",
89 Box: "boxS", Mode: "cold", Region: "ie", NewGeneration: "GS2",
90 }))
91 last := o.Claim.Holder.Admissions[1]
92 if last.Covered != (name == "whole predecessor region" || name == "scoped holder location") || last.PredecessorRegion != "uk" || last.WritesLost == nil {
93 t.Fatalf("return admission = %+v", last)
94 }
95 if !reflect.DeepEqual(before.Claim.Obligations, o.Claim.Obligations) {
96 t.Fatal("admission changed park obligations")
97 }
98 })
99 }
100}
101
102func TestPublishGenerationServingCyclesBoundAdmissions(t *testing.T) {
103 ctx := context.Background()
104 s := release.NewDirStore(t.TempDir())
105 o := servingClaim(t, ctx, s)
106 newID := counter()
107 for cycle := 1; cycle <= 20; cycle++ {
108 generation := fmt.Sprintf("G%d", cycle)
109 o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{
110 Region: "ie", PredecessorRegion: "ie", ObservedSession: o.Claim.Holder.Session, ObservedGeneration: o.Claim.Holder.Generation,
111 // Exercise trimming without unarchived loss evidence.
112 Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: generation, PredecessorCertified: true,
113 }))
114 o = updateGenerationTest(t, ctx, s, o, PublishGeneration("", "", o.Claim.Holder.Session, generation, "ie", newID))
115 for _, record := range o.Claim.Holder.Admissions {
116 if !record.Covered {
117 t.Fatalf("cycle %d left superseded predecessor uncovered: %+v", cycle, record)
118 }
119 }
120 if len(o.Claim.Obligations) != 1 {
121 t.Fatalf("cycle %d chores = %+v", cycle, o.Claim.Obligations)
122 }
123 predecessor := RootGeneration
124 if cycle > 1 {
125 predecessor = fmt.Sprintf("G%d", cycle-1)
126 }
127 ob := o.Claim.Obligations[0]
128 if ob.Generation != predecessor {
129 t.Fatalf("retiring %q, want %q", ob.Generation, predecessor)
130 }
131 var err error
132 o, err = Update(ctx, s, o, CompleteObligation(ob.Op, ob.ID))
133 if err != nil {
134 t.Fatal(err)
135 }
136 o, err = Read(ctx, s, testHost)
137 if err != nil {
138 t.Fatal(err)
139 }
140 o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{
141 Region: "ie", ObservedSession: o.Claim.Holder.Session, ObservedGeneration: generation,
142 Box: o.Claim.Op.Exec, Mode: "warm", PredecessorCertified: true,
143 }))
144 o = updateGenerationTest(t, ctx, s, o, PublishGeneration("", "", o.Claim.Holder.Session, generation, "ie", newID))
145 records := o.Claim.Holder.Admissions
146 if !records[len(records)-1].Covered || len(o.Claim.Obligations) != 0 {
147 t.Fatalf("cycle %d left warm entry uncovered or retired live predecessor", cycle)
148 }
149 if len(records) > 8 {
150 t.Fatalf("cycle %d admissions grew to %d", cycle, len(records))
151 }
152 }
153}
154
155func TestPublishGenerationReturnedSourceRetiredLater(t *testing.T) {
156 ctx := context.Background()
157 s := release.NewDirStore(t.TempDir())
158 o := servingClaim(t, ctx, s)
159 o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error {
160 c.Holder.Generation = "GS"
161 c.Holder.Admissions = []AdmissionRecord{
162 {Region: "uk", Generation: "GT", Predecessor: "GS", PredecessorRegion: "ie"},
163 {Region: "ie", Generation: "GS", Predecessor: "GT", PredecessorRegion: "uk"},
164 }
165 return nil
166 })
167 o = updateGenerationTest(t, ctx, s, o, PublishGeneration("", "", o.Claim.Holder.Session, "GS", "ie", counter()))
168 if o.Claim.Holder.Admissions[0].Covered || o.Claim.Holder.Admissions[1].Covered || len(o.Claim.Obligations) != 0 {
169 t.Fatal("source publication covered a live or foreign predecessor")
170 }
171 o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{
172 Region: "ie", ObservedSession: o.Claim.Holder.Session, ObservedGeneration: "GS",
173 Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: "GS2",
174 }))
175 o = updateGenerationTest(t, ctx, s, o, PublishGeneration("", "", o.Claim.Holder.Session, "GS2", "ie", counter()))
176 if !o.Claim.Holder.Admissions[0].Covered || o.Claim.Holder.Admissions[1].Covered || !o.Claim.Holder.Admissions[2].Covered || len(o.Claim.Obligations) != 1 || o.Claim.Obligations[0].Generation != "GS" {
177 t.Fatalf("source retirement after return = %+v; admissions = %+v", o.Claim.Obligations, o.Claim.Holder.Admissions)
178 }
179}
180
181func TestCoverParkedPredecessors(t *testing.T) {
182 for _, kind := range []string{KindServing, KindParking} {
183 t.Run(kind, func(t *testing.T) {
184 ctx := context.Background()
185 s := release.NewDirStore(t.TempDir())
186 o := servingClaim(t, ctx, s)
187 o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error {
188 if kind == KindParking {
189 c.Op = Op{Kind: kind, ID: "sweep", Phase: "park", Intent: map[string]string{"park_sweep": "v1"}}
190 }
191 c.Holder.Admissions = []AdmissionRecord{
192 {Region: "ie", PredecessorRegion: "ie", Predecessor: "G1"},
193 {Region: "uk", PredecessorRegion: "ie", Predecessor: ""},
194 {Region: "ie", PredecessorRegion: "ie", Predecessor: RootGeneration},
195 {Region: "ie", PredecessorRegion: "uk", Predecessor: "G1"},
196 {Region: "ie", PredecessorRegion: "ie", Predecessor: "G2"},
197 {Region: "ie", Predecessor: "G1"},
198 {Region: "ie", PredecessorRegion: "ie", Predecessor: "G3", Covered: true},
199 }
200 c.Obligations = []Obligation{{ID: "existing", Op: "old", Kind: "park-generation", Region: "ie", Generation: "G1", Retirement: "ret", Owner: "region:ie"}}
201 return nil
202 })
203 before := o
204 for _, generations := range [][]string{nil, {}, {""}} {
205 o = updateGenerationTest(t, ctx, s, o, CoverParkedPredecessors(o.Claim.Op.ID, o.Claim.Op.Phase, "ie", generations))
206 if !reflect.DeepEqual(o.Claim.Holder, before.Claim.Holder) {
207 t.Fatalf("empty selection changed records: %+v", o.Claim.Holder.Admissions)
208 }
209 }
210 want := *o.Claim
211 h := *want.Holder
212 want.Holder = &h
213 h.Admissions = append([]AdmissionRecord(nil), h.Admissions...)
214 for i := 0; i < 3; i++ {
215 h.Admissions[i].Covered = true
216 }
217 for repeat := 0; repeat < 2; repeat++ {
218 o = updateGenerationTest(t, ctx, s, o, CoverParkedPredecessors(o.Claim.Op.ID, o.Claim.Op.Phase, "ie", []string{"G1", RootGeneration, "G1", "absent"}))
219 want.Gen++
220 if !reflect.DeepEqual(o.Claim, &want) {
221 t.Fatalf("sweep changed more than selected coverage: %+v", o.Claim)
222 }
223 }
224 })
225 }
226}
227
228func TestCoverParkedPredecessorsRefusals(t *testing.T) {
229 for _, name := range []string{"stale op", "stale phase", "nil holder", "empty region"} {
230 t.Run(name, func(t *testing.T) {
231 ctx := context.Background()
232 s := release.NewDirStore(t.TempDir())
233 o := servingClaim(t, ctx, s)
234 o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error {
235 c.Op = Op{Kind: KindParking, ID: "sweep", Phase: "park"}
236 c.Holder.Admissions = []AdmissionRecord{{Region: "ie", PredecessorRegion: "ie", Predecessor: "G1"}}
237 if name == "nil holder" {
238 c.Holder, c.Op = nil, Op{}
239 }
240 return nil
241 })
242 op, phase, region := o.Claim.Op.ID, o.Claim.Op.Phase, "ie"
243 switch name {
244 case "stale op":
245 op = "old"
246 case "stale phase":
247 phase = "old"
248 case "empty region":
249 region = ""
250 }
251 if _, err := Update(ctx, s, o, CoverParkedPredecessors(op, phase, region, []string{"G1"})); !errors.Is(err, ErrWrongState) {
252 t.Fatalf("sweep = %v", err)
253 }
254 assertAdmissionStored(t, ctx, s, o)
255 })
256 }
257}
258
259func TestCoverParkedPredecessorsComposesWithRetirement(t *testing.T) {
260 ctx := context.Background()
261 s := release.NewDirStore(t.TempDir())
262 o := servingClaim(t, ctx, s)
263 cover := CoverParkedPredecessors("", "", "ie", []string{"G1"})
264 o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error {
265 c.Holder.Admissions = []AdmissionRecord{{Region: "uk", PredecessorRegion: "ie", Predecessor: "G1"}}
266 c.Obligations = append(c.Obligations, Obligation{ID: "park", Op: "sweep", Kind: "park-generation", Region: "ie", Generation: "G1", Retirement: "ret", Owner: "region:ie"})
267 return cover(c)
268 })
269 if !o.Claim.Holder.Admissions[0].Covered {
270 t.Fatal("queue CAS did not cover predecessor")
271 }
272 o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error {
273 c.Holder.Admissions = append(c.Holder.Admissions, AdmissionRecord{Region: "uk", PredecessorRegion: "ie", Predecessor: "G1"})
274 return nil
275 })
276 next, err := Update(ctx, s, o, func(c *Claim) error {
277 if err := cover(c); err != nil {
278 return err
279 }
280 return CompleteObligation("sweep", "park")(c)
281 })
282 if err != nil {
283 t.Fatal(err)
284 }
285 stored, err := Read(ctx, s, testHost)
286 if err != nil {
287 t.Fatal(err)
288 }
289 if stored.ETag != next.ETag || len(stored.Claim.Obligations) != 0 || !stored.Claim.Holder.Admissions[1].Covered {
290 t.Fatal("acknowledgement CAS did not cover later predecessor")
291 }
292}
293
294func TestAdmitWriterAdditionalWritesLost(t *testing.T) {
295 ctx := context.Background()
296 s := release.NewDirStore(t.TempDir())
297 o := servingClaim(t, ctx, s)
298 o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{Region: "ie", Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: "GS", PredecessorCertified: true}))
299 cut := o.Claim.Holder.Session
300 o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error {
301 c.Op = Op{Kind: KindLanding, ID: "move", Phase: "probe", Exec: "boxT"}
302 return nil
303 })
304 var target []AdmissionRecord
305 for _, mode := range []string{"cold", "warm", "warm"} {
306 in := Admission{Region: "uk", ObservedOpID: "move", ObservedPhase: "probe", ObservedSession: o.Claim.Holder.Session,
307 ObservedGeneration: o.Claim.Holder.Generation, Box: "boxT", Mode: mode, PredecessorCertified: true}
308 if mode == "cold" {
309 in.NewGeneration, in.Coverage = "GT", "sealed"
310 }
311 o = updateGenerationTest(t, ctx, s, o, AdmitWriter(in))
312 target = append(target, o.Claim.Holder.Admissions[len(o.Claim.Holder.Admissions)-1])
313 }
314 o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error {
315 c.Holder.Generation = "GS"
316 c.Op = Op{Kind: KindReverting, ID: "move", Phase: "return", Exec: "boxS"}
317 return nil
318 })
319 additional := []WritesLost{}
320 for i, record := range target[:2] {
321 if record.Session <= cut {
322 t.Fatal("target session does not follow the cut")
323 }
324 additional = append(additional, WritesLost{Box: record.Box, Session: record.Session, Generation: record.Generation, RemoteMaxTXID: uint64(10 + i)})
325 }
326 wantAdditional := append([]WritesLost(nil), additional...)
327 o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{
328 Region: "ie", ObservedOpID: "move", ObservedPhase: "return", ObservedSession: o.Claim.Holder.Session, ObservedGeneration: "GS",
329 Box: "boxS", Mode: "cold", NewGeneration: "GS2", RemoteMaxTXID: 42, AdditionalWritesLost: additional,
330 }))
331 last := o.Claim.Holder.Admissions[len(o.Claim.Holder.Admissions)-1]
332 wantPrimary := &WritesLost{Box: "boxT", Session: target[2].Session, Generation: "GT", RemoteMaxTXID: 42}
333 if !reflect.DeepEqual(last.WritesLost, wantPrimary) || !reflect.DeepEqual(last.AdditionalWritesLost, wantAdditional) {
334 t.Fatalf("loss declarations = %+v, %+v", last.WritesLost, last.AdditionalWritesLost)
335 }
336 additional[0].Box = "changed"
337 if !reflect.DeepEqual(last.AdditionalWritesLost, wantAdditional) {
338 t.Fatal("record aliases the caller's loss declarations")
339 }
340 raw, err := json.Marshal(last)
341 if err != nil {
342 t.Fatal(err)
343 }
344 var fields map[string]json.RawMessage
345 if err := json.Unmarshal(raw, &fields); err != nil {
346 t.Fatal(err)
347 }
348 var decoded []WritesLost
349 if err := json.Unmarshal(fields["additional_writes_lost"], &decoded); err != nil {
350 t.Fatal(err)
351 }
352 if !reflect.DeepEqual(decoded, wantAdditional) {
353 t.Fatalf("additional loss JSON = %s", raw)
354 }
355 assertAdmissionStored(t, ctx, s, o)
356}
357
358func TestAdoptLostTargetReturnDeclaresLegacySource(t *testing.T) {
359 ctx := t.Context()
360 s := release.NewDirStore(t.TempDir())
361 o, err := Read(ctx, s, testHost)
362 if err != nil {
363 t.Fatal(err)
364 }
365 o = updateGenerationTest(t, ctx, s, o, Adopt(newHolder(), "boxS", nil, nil, "adopt", counter()))
366 cut := WritesLost{Box: o.Claim.Holder.WriterBox, Session: o.Claim.Holder.Session, Generation: o.Claim.Holder.Generation, RemoteMaxTXID: 17}
367 if cut.Box != "boxS" || cut.Session != 0 || cut.Generation != "" {
368 t.Fatalf("legacy cut = %+v", cut)
369 }
370 o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error {
371 if err := StartGuard(c, []string{"ie", "uk"}, []string{"boxS", "boxT"}, ""); err != nil {
372 return err
373 }
374 c.Op = Op{Kind: KindMoving, ID: "move", Phase: "cut", Exec: "boxS", Participants: []string{"boxS", "boxT"}, Journal: map[string]string{"src_covered": "false"}}
375 return nil
376 })
377 o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error {
378 c.Op.Kind, c.Op.Phase, c.Op.Exec = KindLanding, "probe", "boxT"
379 return nil
380 })
381 o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{
382 ObservedOpID: "move", ObservedPhase: "probe", ObservedSession: cut.Session, ObservedGeneration: cut.Generation,
383 Region: "uk", PredecessorRegion: "ie", Box: "boxT", Mode: "cold", NewGeneration: "GT", RemoteMaxTXID: cut.RemoteMaxTXID,
384 }))
385 target := o.Claim.Holder.Admissions[0]
386 if !reflect.DeepEqual(target.WritesLost, &cut) {
387 t.Fatalf("primary legacy loss = %+v, want %+v", target.WritesLost, cut)
388 }
389 o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error {
390 c.Holder.Generation = cut.Generation
391 c.Lost = append(c.Lost, "boxT")
392 c.Op.Kind, c.Op.Phase, c.Op.Exec = KindReverting, "return", "boxS"
393 return nil
394 })
395 in := Admission{
396 ObservedOpID: "move", ObservedPhase: "return", ObservedSession: target.Session, ObservedGeneration: cut.Generation,
397 Region: "ie", Box: "boxS", Mode: "cold", NewGeneration: "GS2", RemoteMaxTXID: 42, AdditionalWritesLost: []WritesLost{cut},
398 }
399 o = updateGenerationTest(t, ctx, s, o, AdmitWriter(in))
400 last := o.Claim.Holder.Admissions[len(o.Claim.Holder.Admissions)-1]
401 wantPrimary := &WritesLost{Box: target.Box, Session: target.Session, Generation: target.Generation, RemoteMaxTXID: 42}
402 if !reflect.DeepEqual(last.WritesLost, wantPrimary) || !reflect.DeepEqual(last.AdditionalWritesLost, []WritesLost{cut}) {
403 t.Fatalf("return losses = %+v, %+v", last.WritesLost, last.AdditionalWritesLost)
404 }
405 if _, err := Update(ctx, s, o, AdmitWriter(in)); !errors.Is(err, ErrWrongState) {
406 t.Fatalf("repeated admission = %v", err)
407 }
408 assertAdmissionStored(t, ctx, s, o)
409}
410
411func TestAdmitWriterAdditionalWritesLostValidation(t *testing.T) {
412 for _, name := range []string{"empty box", "zero session", "empty generation", "duplicate session", "duplicate predecessor", "same box different session", "same session different box", "root generation", "legacy identity", "legacy empty box", "duplicate legacy", "warm", "sealed"} {
413 t.Run(name, func(t *testing.T) {
414 ctx := context.Background()
415 s := release.NewDirStore(t.TempDir())
416 o := servingClaim(t, ctx, s)
417 o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{Region: "uk", Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: "GT"}))
418 loss := WritesLost{Box: "boxT", Session: 42, Generation: "GT", RemoteMaxTXID: 0}
419 in := Admission{Region: "ie", ObservedSession: o.Claim.Holder.Session, ObservedGeneration: "GT", Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: "GS"}
420 switch name {
421 case "empty box":
422 loss.Box = ""
423 case "zero session":
424 loss.Session = 0
425 case "empty generation":
426 loss.Generation = ""
427 case "duplicate predecessor":
428 loss.Box, loss.Session = o.Claim.Holder.WriterBox, o.Claim.Holder.Session
429 case "root generation":
430 loss.Generation = RootGeneration
431 case "legacy identity", "legacy empty box", "duplicate legacy":
432 loss.Session, loss.Generation = 0, ""
433 if name == "legacy empty box" {
434 loss.Box = ""
435 }
436 case "warm":
437 in.Mode, in.NewGeneration, in.PredecessorCertified = "warm", "", true
438 in.Region = "uk"
439 case "sealed":
440 in.Coverage = "sealed"
441 }
442 in.AdditionalWritesLost = []WritesLost{loss}
443 switch name {
444 case "duplicate legacy":
445 in.AdditionalWritesLost = append(in.AdditionalWritesLost, loss)
446 case "duplicate session", "same box different session", "same session different box":
447 second := loss
448 second.Generation, second.RemoteMaxTXID = "other", 99
449 if name == "same box different session" {
450 second.Session++
451 }
452 if name == "same session different box" {
453 second.Box = "boxU"
454 }
455 in.AdditionalWritesLost = append(in.AdditionalWritesLost, second)
456 }
457 next, err := Update(ctx, s, o, AdmitWriter(in))
458 switch name {
459 case "empty box", "zero session", "empty generation", "duplicate session", "duplicate predecessor", "legacy empty box", "duplicate legacy":
460 if !errors.Is(err, ErrWrongState) {
461 t.Fatalf("invalid loss admission = %v", err)
462 }
463 assertAdmissionStored(t, ctx, s, o)
464 default:
465 if err != nil {
466 t.Fatal(err)
467 }
468 last := next.Claim.Holder.Admissions[len(next.Claim.Holder.Admissions)-1]
469 if !reflect.DeepEqual(last.AdditionalWritesLost, in.AdditionalWritesLost) {
470 t.Fatalf("loss declarations = %+v", last.AdditionalWritesLost)
471 }
472 assertAdmissionStored(t, ctx, s, next)
473 }
474 })
475 }
476}
477
478func TestAdmitWriterWarmBornCovered(t *testing.T) {
479 for _, generation := range []string{"", "G1"} {
480 t.Run("generation="+generation, func(t *testing.T) {
481 ctx := context.Background()
482 s := release.NewDirStore(t.TempDir())
483 o := servingClaim(t, ctx, s)
484 if generation != "" {
485 o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{
486 Region: "ie", PredecessorRegion: "ie", Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: generation,
487 }))
488 }
489 o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{
490 Region: "ie", PredecessorRegion: "ie", ObservedSession: o.Claim.Holder.Session, ObservedGeneration: generation,
491 Box: o.Claim.Op.Exec, Mode: "warm", PredecessorCertified: true,
492 }))
493 records := o.Claim.Holder.Admissions
494 last := records[len(records)-1]
495 if !last.Covered || last.Mode != "warm" || last.Generation != generation || last.Predecessor != generation || last.WritesLost != nil || len(o.Claim.Obligations) != 0 {
496 t.Fatalf("warm birth = %+v; chores = %+v", last, o.Claim.Obligations)
497 }
498 if len(records) > 1 && records[0].Covered {
499 t.Fatal("warm birth covered an earlier cold entry")
500 }
501 assertAdmissionStored(t, ctx, s, o)
502 })
503 }
504}
505
506func TestCompleteObligationDoesNotCoverAdmissions(t *testing.T) {
507 for _, kind := range []string{"park-replica", "park-generation"} {
508 t.Run(kind, func(t *testing.T) {
509 ctx := context.Background()
510 s := release.NewDirStore(t.TempDir())
511 o := servingClaim(t, ctx, s)
512 o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{
513 Region: "ie", PredecessorRegion: "ie", Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: "G1",
514 }))
515 o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error {
516 ob := Obligation{ID: "park", Op: "cleanup", Kind: kind, Region: "ie", Owner: "region:ie", Retirement: "ret"}
517 if kind == "park-generation" {
518 ob.Generation = RootGeneration
519 }
520 c.Obligations = append(c.Obligations, ob)
521 return nil
522 })
523 before := o.Claim.Holder.Admissions
524 if before[0].Covered {
525 t.Fatal("queueing a park alone covered its predecessor")
526 }
527 next, err := Update(ctx, s, o, CompleteObligation("cleanup", "park"))
528 if err != nil {
529 t.Fatal(err)
530 }
531 stored, err := Read(ctx, s, testHost)
532 if err != nil {
533 t.Fatal(err)
534 }
535 if stored.ETag != next.ETag || len(stored.Claim.Obligations) != 0 || !reflect.DeepEqual(stored.Claim.Holder.Admissions, before) {
536 t.Fatalf("acknowledgement changed admissions: %+v", stored.Claim)
537 }
538 })
539 }
540}
541
542// A created incarnation's first admission declares nothing: its predecessor
543// has no writer, so no write was ever acknowledged (moves 7.17 note 2).
544func TestAdmitWriterFirstAdmissionAfterCreateDeclaresNothing(t *testing.T) {
545 ctx := context.Background()
546 s := release.NewDirStore(t.TempDir())
547 o := servingClaim(t, ctx, s)
548 if o.Claim.Holder.WriterBox != "" || len(o.Claim.Holder.Admissions) != 0 {
549 t.Fatalf("fixture is not a fresh create: %+v", o.Claim.Holder)
550 }
551 o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{Region: "ie", Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: "G1"}))
552 first := o.Claim.Holder.Admissions[0]
553 if first.WritesLost != nil || len(first.AdditionalWritesLost) != 0 {
554 t.Fatalf("first admission declared a loss: %+v", first)
555 }
556 // The next uncertified cold admission does declare its predecessor.
557 o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{Region: "ie", ObservedSession: o.Claim.Holder.Session, ObservedGeneration: "G1", Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: "G2"}))
558 second := o.Claim.Holder.Admissions[1]
559 if second.WritesLost == nil || second.WritesLost.Box == "" || second.WritesLost.Generation != "G1" {
560 t.Fatalf("second admission = %+v", second)
561 }
562}
563
564// writes_lost keeps its wire shape when a real predecessor is declared.
565func TestWritesLostJSONShape(t *testing.T) {
566 b, err := json.Marshal(AdmissionRecord{Region: "ie", Session: 4, Box: "boxB", Generation: "G2", Mode: "cold",
567 WritesLost: &WritesLost{Box: "boxA", Generation: "G1", Session: 3, RemoteMaxTXID: 42}})
568 if err != nil {
569 t.Fatal(err)
570 }
571 var got map[string]any
572 if err := json.Unmarshal(b, &got); err != nil {
573 t.Fatal(err)
574 }
575 want := map[string]any{"box": "boxA", "generation": "G1", "session": float64(3), "remote_max_txid": float64(42)}
576 if !reflect.DeepEqual(got["writes_lost"], want) {
577 t.Fatalf("writes_lost = %v, want %v", got["writes_lost"], want)
578 }
579}
580