package park import ( "context" "errors" "testing" "time" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/moves" ) // TestParkScopeSurvivesRestart is witness wParkScopeSurvivesRestart (claim // rev 6.23 ยง2, INV-22, Plan 5 ruling K8). A moves chore is queued with its // scope bound in the queuing CAS. After a restart the caller computes its // bound again, and the new one is later; neither a second queue nor a // park.Engine.Bind with it moves the scope: both are refused, the claim is // not written, and the first scope stands. A fresh engine then takes its // deadline from the persisted scope: pending before it, acknowledged after. func TestParkScopeSurvivesRestart(t *testing.T) { f := newFixture(t) ctx := t.Context() obs, err := hostclaim.Read(ctx, f.e.Claims, f.w.Host) if err != nil { t.Fatal(err) } inc := obs.Claim.Holder.Incarnation sealBound := f.ob.Park.EvidenceAt first := moves.StagingScope(f.w.Host, f.w.Op, inc, sealBound) obs, err = hostclaim.Update(ctx, f.e.Claims, obs, func(c *hostclaim.Claim) error { c.Obligations = nil return hostclaim.QueueStagingPark(f.w.Op, "ie", first, f.e.NewID)(c) }) if err != nil { t.Fatalf("queue the lane park: %v", err) } if len(obs.Claim.Obligations) != 1 { t.Fatalf("obligations = %+v, want the lane park alone", obs.Claim.Obligations) } lane := obs.Claim.Obligations[0] w := Work{Host: f.w.Host, ID: lane.ID, Op: lane.Op} // The restart: the bound is computed again, a minute later. later := moves.StagingScope(f.w.Host, f.w.Op, inc, sealBound.Add(time.Minute)) stands := func(when string) { t.Helper() now, err := hostclaim.Read(ctx, f.e.Claims, f.w.Host) if err != nil { t.Fatal(err) } if now.ETag != obs.ETag { t.Fatalf("%s: the claim was written", when) } if len(now.Claim.Obligations) != 1 || now.Claim.Obligations[0].Park == nil || !now.Claim.Obligations[0].Park.EvidenceAt.Equal(sealBound) { t.Fatalf("%s: the first scope does not stand: %+v", when, now.Claim.Obligations) } } if _, err := hostclaim.Update(ctx, f.e.Claims, obs, hostclaim.QueueStagingPark(f.w.Op, "ie", later, f.e.NewID)); !errors.Is(err, hostclaim.ErrWrongState) { t.Fatalf("a second queue with a later bound: %v, want ErrWrongState", err) } stands("after the second queue") if err := f.e.Bind(ctx, w, later); !errors.Is(err, ErrChanged) { t.Fatalf("a bind with a later bound: %v, want ErrChanged", err) } stands("after the bind") if err := f.e.Bind(ctx, w, first); err != nil { t.Fatalf("a bind with the persisted scope: %v", err) } stands("after the same bind") // A fresh engine (the agent restarted) reads its deadline from the claim. restarted := f.e restarted.View = func(_ context.Context, o hostclaim.Observed) (View, error) { return View{ClaimETag: o.ETag, Region: "ie", AuthoritativeRegion: "ie", MarkerKnown: true, MarkerGeneration: "unrelated", GenerationOf: func(Ref) (string, bool) { return "live", true }}, nil } f.b.Put("data", first.Location+"/replica/0000/data", []byte("staged")) deadline := sealBound.Add(SignatureAge + first.RequestLimit) f.now = deadline.Add(-time.Second) restarted.After = func(step string) error { if step == "verify" { f.now = deadline.Add(time.Hour) } return nil } if err := restarted.Run(ctx, w); !errors.Is(err, ErrPending) { t.Fatalf("a run whose first listing is before the persisted deadline: %v, want ErrPending", err) } after, err := hostclaim.Read(ctx, f.e.Claims, f.w.Host) if err != nil { t.Fatal(err) } if len(after.Claim.Obligations) != 1 || !after.Claim.Obligations[0].Park.EvidenceAt.Equal(sealBound) { t.Fatalf("the run moved the scope: %+v", after.Claim.Obligations) } restarted.After = nil if err := restarted.Run(ctx, w); err != nil { t.Fatalf("a run after the persisted deadline: %v", err) } after, err = hostclaim.Read(ctx, f.e.Claims, f.w.Host) if err != nil { t.Fatal(err) } if len(after.Claim.Obligations) != 0 || !after.Claim.QuietAfter["ie"].Equal(deadline) { t.Fatalf("after the deadline: obligations %+v, quiet_after %v, want none and %v", after.Claim.Obligations, after.Claim.QuietAfter["ie"], deadline) } }