carlos / platform Public

hostclaim: one adoption routine, discovery chores and their completion (CC4, CC8)

Co-Authored-By: Codex (gpt-6-astra) <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
codex agent, operated by paul@keymail.dev pushed by claude agent, operated by paul@keymail.dev faa55731253bab9f01ae708f9d00851578427826 parent 5f8f1fc
2 files changed, +75 −0
  • internal/hostclaim/adopt.go +34 −0
  • internal/hostclaim/adopt_test.go +41 −0
diff --git a/internal/hostclaim/adopt.go b/internal/hostclaim/adopt.go
new file mode 100644
index 000000000..1498170bb
--- /dev/null
+++ b/internal/hostclaim/adopt.go
@@ -0,0 +1,34 @@
+package hostclaim
+
+import "fmt"
+
+// Adopt is the one adoption routine (CC8) for a host that has never had a
+// claim. It writes discovery chores for every other region and box (CC4).
+func Adopt(nh NewHolder, servingBox string, otherRegions, otherBoxes []string, opID string, newID func() string) func(*Claim) error {
+ return func(c *Claim) error {
+ if c.Gen != 0 || c.Holder != nil {
+ return fmt.Errorf("%w: adoption is only for a host that never had a claim", ErrWrongState)
+ }
+ if nh.Host != c.Host || !keyMatches(nh) || servingBox == "" || opID == "" {
+ return fmt.Errorf("%w: adoption inputs are not this host's", ErrWrongState)
+ }
+ inc, err := Incarnation(nh.Account, nh.App, nh.Host, nh.CreatedAt)
+ if err != nil {
+ return err
+ }
+ c.Holder = &Holder{Family: nh.Family, Key: nh.Key, Account: nh.Account, App: nh.App, Name: nh.Name,
+ Incarnation: inc, CreatedAt: nh.CreatedAt, Location: "instances/" + nh.Host, Stamp: c.Gen + 1}
+ c.Op = Op{Kind: KindServing, Exec: servingBox, Participants: []string{servingBox}}
+ c.Touched = Touched{Complete: true, Regions: []string{nh.Region}, Boxes: []string{servingBox}}
+ for _, r := range otherRegions {
+ c.Obligations = append(c.Obligations, Obligation{ID: newID(), Op: opID, Kind: "park-replica", Region: r, Retirement: newID(), Owner: "region:" + r})
+ }
+ for _, b := range otherBoxes {
+ c.Obligations = append(c.Obligations, Obligation{ID: newID(), Op: opID, Kind: "park-local", Box: b, Retirement: newID(), Owner: "box:" + b})
+ }
+ if len(c.Obligations) > 0 {
+ c.Touched.Complete, c.Touched.DiscoveryOp = false, opID
+ }
+ return nil
+ }
+}
diff --git a/internal/hostclaim/adopt_test.go b/internal/hostclaim/adopt_test.go
new file mode 100644
index 000000000..30b096015
--- /dev/null
+++ b/internal/hostclaim/adopt_test.go
@@ -0,0 +1,41 @@
+package hostclaim
+
+import (
+ "context"
+ "errors"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func TestAdoptAndDiscoveryCompletion(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ nh := newHolder()
+ obs, _ := Read(ctx, s, testHost)
+ o, err := Update(ctx, s, obs, Adopt(nh, "adi-knitting", []string{"uk"}, []string{"b2"}, "op_adopt", counter()))
+ if err != nil {
+ t.Fatal(err)
+ }
+ c := o.Claim
+ if c.Holder.Location != "instances/"+testHost || c.Touched.Complete || c.Touched.DiscoveryOp != "op_adopt" ||
+ c.Holder.Stamp != c.Gen || c.Op.Exec != "adi-knitting" || len(c.Obligations) != 2 {
+ t.Fatalf("adopt = %+v", c)
+ }
+ // A delete is refused while discovery chores are pending in its write set (CC11).
+ if _, err := Update(ctx, s, o, BeginDelete("op_d", "console", `"e"`, time.Now(), []string{"ie", "uk"}, []string{"adi-knitting", "b2"})); !errors.Is(err, ErrPendingChore) {
+ t.Fatalf("delete during discovery = %v", err)
+ }
+ for _, ob := range c.Obligations {
+ if o, err = Update(ctx, s, o, CompleteObligation(ob.Op, ob.ID)); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if !o.Claim.Touched.Complete || o.Claim.Touched.DiscoveryOp != "" {
+ t.Fatalf("discovery not completed: %+v", o.Claim.Touched)
+ }
+ if _, err := Update(ctx, s, o, Adopt(nh, "adi-knitting", nil, nil, "op_adopt2", counter())); !errors.Is(err, ErrWrongState) {
+ t.Fatalf("re-adopt = %v", err)
+ }
+}