hostclaim: admission retries compare the normalized predecessor region (Plan 8 fix round 2)
Co-Authored-By: Codex <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+66
−2
internal/hostclaim/lossrecord_test.go+60 −0internal/hostclaim/lost.go+6 −2
CI failed — run details
diff --git a/internal/hostclaim/lossrecord_test.go b/internal/hostclaim/lossrecord_test.go| index f274a374b..c39c878fc 100644 |
| --- a/internal/hostclaim/lossrecord_test.go |
| +++ b/internal/hostclaim/lossrecord_test.go |
| @@ -369,6 +369,66 @@ func TestLossBearingAdmissionRequiresEveryFrozenTargetAndSourceCut(t *testing.T) |
| }) |
| } |
| +func TestLossAdmissionRetryInheritsPredecessorRegion(t *testing.T) { |
| + for _, region := range []string{"ie", "uk"} { |
| + for _, fallback := range []string{"", "fr"} { |
| + t.Run(region+"/fallback="+fallback, func(t *testing.T) { |
| + ctx := t.Context() |
| + s := release.NewDirStore(t.TempDir()) |
| + pub, priv, err := ed25519.GenerateKey(nil) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + obs := servingClaim(t, ctx, s) |
| + obs = updateGenerationTest(t, ctx, s, obs, AdmitWriter(Admission{ |
| + Region: "ie", Box: obs.Claim.Op.Exec, Mode: "cold", NewGeneration: "g-target", PredecessorCertified: true, |
| + })) |
| + obs = updateGenerationTest(t, ctx, s, obs, func(c *Claim) error { |
| + c.Op = Op{Kind: KindReverting, ID: "return", Phase: "recover", Exec: "recovery", Participants: []string{"recovery"}} |
| + c.LossRefs = []string{"loss_1"} |
| + return nil |
| + }) |
| + h := obs.Claim.Holder |
| + record := validLossRecord(t) |
| + record.Host, record.HolderIncarnation = obs.Host, h.Incarnation |
| + record.HolderStamp, record.HolderSession = h.Stamp, h.Session |
| + record.OpID, record.OpKind, record.OpPhase, record.Box = "return", KindReverting, "recover", h.WriterBox |
| + record.WriteSources = []WriteSource{{Reason: "target", Box: h.WriterBox, Generation: h.Generation, Session: h.Session, Region: "ie", Baseline: "target-snapshot"}} |
| + created, err := CreateLossRecord(ctx, s, release.RootSigner(priv), record) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + committed := created.Record |
| + committed.State, committed.ResultClaimGen = "committed", obs.Claim.Gen |
| + if _, err := ReplaceLossRecord(ctx, s, release.RootSigner(priv), created, committed); err != nil { |
| + t.Fatal(err) |
| + } |
| + in := Admission{ObservedOpID: "return", ObservedPhase: "recover", ObservedSession: h.Session, ObservedGeneration: h.Generation, |
| + Box: "recovery", Mode: "cold", NewGeneration: "g-recovery", Region: region, PredecessorRegion: fallback, RemoteMaxTXID: 12} |
| + validated := []ValidatedWriteSource{{Reason: "target", Box: h.WriterBox, Generation: h.Generation, Session: h.Session, Region: "ie", Baseline: "target-snapshot", RemoteMaxTXID: 12}} |
| + admitted, err := AdmitWriterWithLoss(ctx, s, pub, obs, in, record.ID, validated, testLossRecoveryOwner{}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + last := admitted.Claim.Holder.Admissions[len(admitted.Claim.Holder.Admissions)-1] |
| + if last.PredecessorRegion != "ie" || last.Region != region { |
| + t.Fatalf("admission regions = %q / %q, want %q / %q", last.Region, last.PredecessorRegion, region, "ie") |
| + } |
| + calls := 0 |
| + owner := lossRecoveryOwnerFunc(func(c Claim, loss LossRecord, a Admission) error { |
| + calls++ |
| + return (testLossRecoveryOwner{}).ValidateLossRecovery(c, loss, a) |
| + }) |
| + retry, err := AdmitWriterWithLoss(ctx, s, pub, admitted, in, record.ID, validated, owner) |
| + if err != nil || retry.ETag != admitted.ETag || !reflect.DeepEqual(retry.Claim, admitted.Claim) || calls != 1 { |
| + t.Errorf("identical retry: err %v, owner calls %d, observation %+v", err, calls, retry) |
| + } |
| + assertAdmissionStored(t, ctx, s, admitted) |
| + }) |
| + } |
| + } |
| +} |
| + |
| func TestTargetOnlyLossAdmissionRequiresColdUncoveredRecovery(t *testing.T) { |
| ctx := context.Background() |
| s := release.NewDirStore(t.TempDir()) |
diff --git a/internal/hostclaim/lost.go b/internal/hostclaim/lost.go| index ab8ec014e..5a0ab45ff 100644 |
| --- a/internal/hostclaim/lost.go |
| +++ b/internal/hostclaim/lost.go |
| @@ -128,12 +128,16 @@ func AdmitWriterWithLoss(ctx context.Context, s release.Store, pub ed25519.Publi |
| if len(h.Admissions) > 0 { |
| predecessor = h.Admissions[len(h.Admissions)-1] |
| } |
| - for _, prior := range h.Admissions { |
| + for i, prior := range h.Admissions { |
| if prior.LossID != lossID { |
| continue |
| } |
| + predecessorRegion := in.PredecessorRegion |
| + if i > 0 { |
| + predecessorRegion = h.Admissions[i-1].Region |
| + } |
| if prior.Box != in.Box || prior.Mode != in.Mode || prior.Generation != in.NewGeneration || |
| - prior.Region != in.Region || prior.PredecessorRegion != in.PredecessorRegion || prior.Coverage != in.Coverage || |
| + prior.Region != in.Region || prior.PredecessorRegion != predecessorRegion || prior.Coverage != in.Coverage || |
| prior.Predecessor != in.ObservedGeneration || prior.WritesLost == nil || |
| prior.WritesLost.Generation != in.ObservedGeneration || prior.WritesLost.Session != in.ObservedSession || |
| prior.WritesLost.RemoteMaxTXID != in.RemoteMaxTXID || !reflect.DeepEqual(prior.AdditionalWritesLost, in.AdditionalWritesLost) { |