| 1 | package park |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "testing" |
| 7 | "time" |
| 8 | |
| 9 | "github.com/carlosframework/platform/internal/hostclaim" |
| 10 | "github.com/carlosframework/platform/internal/moves" |
| 11 | ) |
| 12 | |
| 13 | // TestParkScopeSurvivesRestart is witness wParkScopeSurvivesRestart (claim |
| 14 | // rev 6.23 §2, INV-22, Plan 5 ruling K8). A moves chore is queued with its |
| 15 | // scope bound in the queuing CAS. After a restart the caller computes its |
| 16 | // bound again, and the new one is later; neither a second queue nor a |
| 17 | // park.Engine.Bind with it moves the scope: both are refused, the claim is |
| 18 | // not written, and the first scope stands. A fresh engine then takes its |
| 19 | // deadline from the persisted scope: pending before it, acknowledged after. |
| 20 | func TestParkScopeSurvivesRestart(t *testing.T) { |
| 21 | f := newFixture(t) |
| 22 | ctx := t.Context() |
| 23 | obs, err := hostclaim.Read(ctx, f.e.Claims, f.w.Host) |
| 24 | if err != nil { |
| 25 | t.Fatal(err) |
| 26 | } |
| 27 | inc := obs.Claim.Holder.Incarnation |
| 28 | sealBound := f.ob.Park.EvidenceAt |
| 29 | first := moves.StagingScope(f.w.Host, f.w.Op, inc, sealBound) |
| 30 | obs, err = hostclaim.Update(ctx, f.e.Claims, obs, func(c *hostclaim.Claim) error { |
| 31 | c.Obligations = nil |
| 32 | return hostclaim.QueueStagingPark(f.w.Op, "ie", first, f.e.NewID)(c) |
| 33 | }) |
| 34 | if err != nil { |
| 35 | t.Fatalf("queue the lane park: %v", err) |
| 36 | } |
| 37 | if len(obs.Claim.Obligations) != 1 { |
| 38 | t.Fatalf("obligations = %+v, want the lane park alone", obs.Claim.Obligations) |
| 39 | } |
| 40 | lane := obs.Claim.Obligations[0] |
| 41 | w := Work{Host: f.w.Host, ID: lane.ID, Op: lane.Op} |
| 42 | |
| 43 | // The restart: the bound is computed again, a minute later. |
| 44 | later := moves.StagingScope(f.w.Host, f.w.Op, inc, sealBound.Add(time.Minute)) |
| 45 | stands := func(when string) { |
| 46 | t.Helper() |
| 47 | now, err := hostclaim.Read(ctx, f.e.Claims, f.w.Host) |
| 48 | if err != nil { |
| 49 | t.Fatal(err) |
| 50 | } |
| 51 | if now.ETag != obs.ETag { |
| 52 | t.Fatalf("%s: the claim was written", when) |
| 53 | } |
| 54 | if len(now.Claim.Obligations) != 1 || now.Claim.Obligations[0].Park == nil || !now.Claim.Obligations[0].Park.EvidenceAt.Equal(sealBound) { |
| 55 | t.Fatalf("%s: the first scope does not stand: %+v", when, now.Claim.Obligations) |
| 56 | } |
| 57 | } |
| 58 | if _, err := hostclaim.Update(ctx, f.e.Claims, obs, hostclaim.QueueStagingPark(f.w.Op, "ie", later, f.e.NewID)); !errors.Is(err, hostclaim.ErrWrongState) { |
| 59 | t.Fatalf("a second queue with a later bound: %v, want ErrWrongState", err) |
| 60 | } |
| 61 | stands("after the second queue") |
| 62 | if err := f.e.Bind(ctx, w, later); !errors.Is(err, ErrChanged) { |
| 63 | t.Fatalf("a bind with a later bound: %v, want ErrChanged", err) |
| 64 | } |
| 65 | stands("after the bind") |
| 66 | if err := f.e.Bind(ctx, w, first); err != nil { |
| 67 | t.Fatalf("a bind with the persisted scope: %v", err) |
| 68 | } |
| 69 | stands("after the same bind") |
| 70 | |
| 71 | // A fresh engine (the agent restarted) reads its deadline from the claim. |
| 72 | restarted := f.e |
| 73 | restarted.View = func(_ context.Context, o hostclaim.Observed) (View, error) { |
| 74 | return View{ClaimETag: o.ETag, Region: "ie", AuthoritativeRegion: "ie", MarkerKnown: true, MarkerGeneration: "unrelated", GenerationOf: func(Ref) (string, bool) { return "live", true }}, nil |
| 75 | } |
| 76 | f.b.Put("data", first.Location+"/replica/0000/data", []byte("staged")) |
| 77 | deadline := sealBound.Add(SignatureAge + first.RequestLimit) |
| 78 | f.now = deadline.Add(-time.Second) |
| 79 | restarted.After = func(step string) error { |
| 80 | if step == "verify" { |
| 81 | f.now = deadline.Add(time.Hour) |
| 82 | } |
| 83 | return nil |
| 84 | } |
| 85 | if err := restarted.Run(ctx, w); !errors.Is(err, ErrPending) { |
| 86 | t.Fatalf("a run whose first listing is before the persisted deadline: %v, want ErrPending", err) |
| 87 | } |
| 88 | after, err := hostclaim.Read(ctx, f.e.Claims, f.w.Host) |
| 89 | if err != nil { |
| 90 | t.Fatal(err) |
| 91 | } |
| 92 | if len(after.Claim.Obligations) != 1 || !after.Claim.Obligations[0].Park.EvidenceAt.Equal(sealBound) { |
| 93 | t.Fatalf("the run moved the scope: %+v", after.Claim.Obligations) |
| 94 | } |
| 95 | restarted.After = nil |
| 96 | if err := restarted.Run(ctx, w); err != nil { |
| 97 | t.Fatalf("a run after the persisted deadline: %v", err) |
| 98 | } |
| 99 | after, err = hostclaim.Read(ctx, f.e.Claims, f.w.Host) |
| 100 | if err != nil { |
| 101 | t.Fatal(err) |
| 102 | } |
| 103 | if len(after.Claim.Obligations) != 0 || !after.Claim.QuietAfter["ie"].Equal(deadline) { |
| 104 | t.Fatalf("after the deadline: obligations %+v, quiet_after %v, want none and %v", after.Claim.Obligations, after.Claim.QuietAfter["ie"], deadline) |
| 105 | } |
| 106 | } |
| 107 | |