| index 18f78573a..0226f3e8c 100644 |
| --- a/internal/host/movesrecoveryowners_test.go |
| +++ b/internal/host/movesrecoveryowners_test.go |
| @@ -960,3 +960,90 @@ func (f *rcFixture) admitThroughWriter(o hostclaim.Observed, in ClaimLaunchInput |
| } |
| return admitted |
| } |
| + |
| +// Fix round 4: the invariant lineage relies on (see its comment). While the |
| +// op is restoring or reverting, the claim admits no writer outside the |
| +// recovery owners: the writer refuses any warm admission (the reviewer's |
| +// counterexample keeps G under an outside session); only the executor may |
| +// admit; and the dispatch hands every cold admission of the op to the |
| +// recovery owners' evidence, which refuses one they would not select (an |
| +// outside owner's shape) and journals the link of any it accepts, so even a |
| +// hand-built admission that passes is in the recovery's lineage. |
| +func TestRecoveryOpAdmitsNoOutsideWriter(t *testing.T) { |
| + for _, kind := range []string{rcClean, rcRevert} { |
| + t.Run(kind, func(t *testing.T) { |
| + f := newRCFixture(t, kind) |
| + ctx := t.Context() |
| + o := f.read() |
| + in := f.selectAdmission(o) |
| + admitted := f.admitThroughWriter(o, in) |
| + outside := 0 |
| + serving := ClaimKindOwners{Inputs: f.owners, PublicationEvidence: func(context.Context, hostclaim.Observed) error { return nil }, |
| + AdmissionEvidence: func(context.Context, hostclaim.Observed, hostclaim.Admission, *genselect.Plan) error { |
| + outside++ |
| + return nil |
| + }} |
| + moves := f.owners.MovesOwners(&movesProbeAttempts{}).ClaimKindOwners |
| + dispatch := ClaimOwnerDispatch{Serving: serving, Moves: &moves} |
| + w := hostbox.Writer{Claims: f.claims, Data: f.bucket, Bucket: "data", Region: "ie", Box: "ie-box", AdmissionEvidence: dispatch.AdmissionEvidence} |
| + h := admitted.Claim.Holder |
| + plan, err := genselect.Select(ctx, f.bucket, f.owners.request(poHost, h)) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + restart := hostclaim.Admission{ObservedOpID: poOp, ObservedPhase: hostclaim.PhaseProbing, ObservedSession: h.Session, ObservedGeneration: h.Generation, |
| + Box: "ie-box", Region: "ie", PredecessorRegion: "ie", Mode: "cold", NewGeneration: "outside1", RemoteMaxTXID: plan.MaxTXID} |
| + unmoved := func() { |
| + t.Helper() |
| + now := f.read().Claim.Holder |
| + if now.Session != h.Session || now.Generation != h.Generation || now.WriterBox != h.WriterBox || len(now.Admissions) != len(h.Admissions) { |
| + t.Fatalf("the holder moved: %+v", now) |
| + } |
| + } |
| + |
| + // The counterexample's outside warm admission keeping G. |
| + warm := restart |
| + warm.Mode, warm.NewGeneration, warm.PredecessorCertified = "warm", "", true |
| + if _, err := w.Admit(ctx, admitted, warm, nil); !errors.Is(err, hostbox.ErrUnsupported) { |
| + t.Fatalf("a warm admission: %v", err) |
| + } |
| + // An outside owner's shape (serving's: no op, no phase), and one |
| + // claiming coverage the recovery's restart never claims. |
| + for name, mutate := range map[string]func(*hostclaim.Admission){ |
| + "serving's shape": func(a *hostclaim.Admission) { a.ObservedOpID, a.ObservedPhase = "", "" }, |
| + "claimed coverage": func(a *hostclaim.Admission) { a.Coverage = "clean-return" }, |
| + "a kept stamp": func(a *hostclaim.Admission) { a.KeepStamp = true }, |
| + } { |
| + a := restart |
| + mutate(&a) |
| + if _, err := w.Admit(ctx, admitted, a, plan); err == nil { |
| + t.Fatalf("%s was admitted", name) |
| + } |
| + } |
| + if outside != 0 { |
| + t.Fatalf("the dispatch reached another owner's evidence %d times", outside) |
| + } |
| + // Another box is never the executor. |
| + other := restart |
| + other.Box = "uk-box" |
| + if _, err := hostclaim.Update(ctx, f.claims, admitted, hostclaim.AdmitWriter(other)); !errors.Is(err, hostclaim.ErrWrongState) { |
| + t.Fatalf("another box: %v", err) |
| + } |
| + unmoved() |
| + |
| + // Any cold admission the op does take passed the recovery's |
| + // evidence, which journalled its link: it is in the lineage. |
| + taken, err := w.Admit(ctx, admitted, restart, plan) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + v, err := f.owners.recovery().recovery(ctx, taken) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := f.owners.recovery().lineage(poHost, v); err != nil { |
| + t.Fatalf("an admission the evidence passed: %v", err) |
| + } |
| + }) |
| + } |
| +} |