package hostclaim import ( "reflect" "testing" ) // TestRollbackBaselineStaysUncoveredUntilCommit ports the claim half of the // model's wRollbackBaselinePreserved (claim rev 6.14 item 5, R12-3; moves // spec §5.1 step 9 and §6.4): a landing's probe publishes the target's // generation in the target's region, and PublishGeneration, as it stands on // main, does not queue the predecessor, which is the source's generation in // the source's region, for a park. The target's admission record stays // uncovered and no chore is queued into the source's region, at either // probing phase and when the publication is replayed, so a rollback still // has the generation it restores from. Only Commit covers the record, in // the CAS that queues the source region's whole-location park // (wCommitCoverage); a rollback, the target's release and a clean return // never do. The source's own claim sequence, RollBack then TargetReleased // then ReturnToSource(clean), ends with holder.generation naming the // source's generation again, which the host half // (TestRollbackBaselineSurvivesTheCleanReturn) finds still in the bucket. func TestRollbackBaselineStaysUncoveredUntilCommit(t *testing.T) { failID := func(t *testing.T) func() string { return func() string { t.Error("the target's publication allocated an obligation id for the source's generation") return "id" } } publish := func(t *testing.T, c *Claim) { t.Helper() if err := PublishGeneration(c.Op.ID, c.Op.Phase, c.Holder.Session, c.Holder.Generation, "uk", failID(t))(c); err != nil { t.Fatalf("publish at %s: %v", c.Op.Phase, err) } } // baselineKept is the invariant: the admission record's predecessor is // the source's generation in ie, nothing is queued into ie, and the // record is uncovered. baselineKept := func(t *testing.T, c *Claim, where string) { t.Helper() srcGen := c.Op.Journal[JournalSrcGen] if srcGen == "" || len(c.Holder.Admissions) != 1 { t.Fatalf("%s: fixture: src_gen %q, admissions %+v", where, srcGen, c.Holder.Admissions) } a := c.Holder.Admissions[0] if a.Box != "uk-box" || a.PredecessorRegion != "ie" || a.Predecessor != srcGen { t.Fatalf("%s: the target's record does not name the source's generation %q in ie: %+v", where, srcGen, a) } if a.Covered { t.Fatalf("%s: the target's record is covered: %+v", where, a) } for _, o := range c.Obligations { if o.Region == "ie" || o.Box == "ie-box" { t.Fatalf("%s: an obligation reaches the source's region: %+v", where, o) } if o.Kind == "park-generation" { t.Fatalf("%s: a generation park was queued: %+v", where, o) } } } for _, from := range []string{"probing", "probed"} { t.Run("publication at "+from, func(t *testing.T) { c := targetWroteLanding(t) c.Op.Phase = from publish(t, c) baselineKept(t, c, "after the publication") // A replay changes nothing. before := mustJSON(t, c) publish(t, c) if string(mustJSON(t, c)) != string(before) { t.Fatalf("the replayed publication changed the claim:\nbefore %s\nafter %s", before, mustJSON(t, c)) } validAfterWrite(t, c) }) } t.Run("the rollback and the clean return", func(t *testing.T) { c := targetWroteLanding(t) publish(t, c) baselineKept(t, c, "after the publication") srcGen := c.Op.Journal[JournalSrcGen] if err := RollBack(moveA, rbA)(c); err != nil { t.Fatal(err) } baselineKept(t, c, "after the rollback") if err := TargetReleased(moveA, "uk-box", "uk", stoppedTarget, releaseScope(c), counter())(c); err != nil { t.Fatal(err) } // The release queues the target region's own park and covers only // records whose predecessor lives there. baselineKept(t, c, "after the target's release") if k := obligationKinds(c); k["park-replica/uk"] != 1 || len(c.Obligations) != 1 { t.Fatalf("the release owes %+v", c.Obligations) } if err := ReturnToSource(moveA, "ie-box", true, ReturnFence{})(c); err != nil { t.Fatal(err) } baselineKept(t, c, "after the clean return") if c.Op.Kind != KindRestoring || c.Op.Phase != PhaseClaimed || c.Op.Exec != "ie-box" { t.Fatalf("op: %+v", c.Op) } if c.Holder.Generation != srcGen || c.Holder.Stamp != 3 { t.Fatalf("the return names generation %q stamp %d, want the source's %q and 3", c.Holder.Generation, c.Holder.Stamp, srcGen) } validAfterWrite(t, c) }) t.Run("only the commit covers the record", func(t *testing.T) { c := targetWroteLanding(t) publish(t, c) baselineKept(t, c, "after the publication") src, stg := commitScopes(c) if err := Commit(moveA, "ie", "ie-box", "uk", src, stg, counter())(c); err != nil { t.Fatal(err) } if !c.Holder.Admissions[0].Covered { t.Fatalf("the commit left the record uncovered: %+v", c.Holder.Admissions[0]) } if k := obligationKinds(c); k["park-replica/ie"] != 1 { t.Fatalf("the commit queued %+v, want the source region's whole-location park", c.Obligations) } for _, o := range c.Obligations { if o.Kind == "park-generation" { t.Fatalf("the commit queued a generation park: %+v", o) } } // A publication after the commit changes nothing more. before := mustJSON(t, c) publish2 := PublishGeneration(c.Op.ID, c.Op.Phase, c.Holder.Session, c.Holder.Generation, "uk", failID(t)) if err := publish2(c); err != nil || !reflect.DeepEqual(mustJSON(t, c), before) { t.Fatalf("a publication after the commit: %v", err) } }) }