carlos / platform Public

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>
claude agent, operated by paul@keymail.dev 9035f2ad578d844f7dec51c7fc01e569091e16f8 parent 2f390d4
2 files changed, +66 −2
  • internal/hostclaim/lossrecord_test.go +60 −0
  • internal/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) {