carlos / platform Public

hostclaim: op-id guard (C2) and the start guard (one op, CC11)

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 8ef0099163cf0f099dc4bb9d3f20f1c5d2e6f585 parent 1ca5f37
2 files changed, +77 −0
  • internal/hostclaim/ops.go +44 −0
  • internal/hostclaim/ops_test.go +33 −0
diff --git a/internal/hostclaim/ops.go b/internal/hostclaim/ops.go
new file mode 100644
index 000000000..85b3e740c
--- /dev/null
+++ b/internal/hostclaim/ops.go
@@ -0,0 +1,44 @@
+package hostclaim
+
+import (
+ "errors"
+ "fmt"
+)
+
+var (
+ ErrStaleOp = errors.New("hostclaim: the claim is under a different operation")
+ ErrWrongState = errors.New("hostclaim: transition not legal from the claim's state")
+ ErrPendingChore = errors.New("hostclaim: an obligation is pending in a location this operation writes")
+)
+
+func RequireOp(id string) func(*Claim) error {
+ return func(c *Claim) error {
+ if id == "" || c.Op.ID != id {
+ return fmt.Errorf("%w: have %q, want %q", ErrStaleOp, c.Op.ID, id)
+ }
+ return nil
+ }
+}
+
+// StartGuard is §4: one operation at a time, started from serving, and
+// CC11: never over a chore in a region or box the operation will write.
+func StartGuard(c *Claim, regions, boxes []string) error {
+ if c.Holder == nil || c.Op.Kind != KindServing {
+ return fmt.Errorf("%w: kind %q", ErrWrongState, c.Op.Kind)
+ }
+ inR, inB := set(regions), set(boxes)
+ for _, o := range c.Obligations {
+ if (o.Region != "" && inR[o.Region]) || (o.Box != "" && inB[o.Box]) {
+ return fmt.Errorf("%w: %s %s%s", ErrPendingChore, o.Kind, o.Region, o.Box)
+ }
+ }
+ return nil
+}
+
+func set(xs []string) map[string]bool {
+ m := make(map[string]bool, len(xs))
+ for _, x := range xs {
+ m[x] = true
+ }
+ return m
+}
diff --git a/internal/hostclaim/ops_test.go b/internal/hostclaim/ops_test.go
new file mode 100644
index 000000000..91c8396ee
--- /dev/null
+++ b/internal/hostclaim/ops_test.go
@@ -0,0 +1,33 @@
+package hostclaim
+
+import (
+ "errors"
+ "testing"
+)
+
+func TestRequireOpRefusesStale(t *testing.T) {
+ c := &Claim{Op: Op{Kind: KindDeleting, ID: "op_new"}}
+ if err := RequireOp("op_old")(c); !errors.Is(err, ErrStaleOp) {
+ t.Fatalf("stale op = %v", err)
+ }
+ if err := RequireOp("op_new")(c); err != nil {
+ t.Fatalf("current op = %v", err)
+ }
+ if err := RequireOp("")(c); !errors.Is(err, ErrStaleOp) {
+ t.Fatalf("empty id must never match: %v", err)
+ }
+}
+
+func TestStartGuard(t *testing.T) {
+ h := &Holder{Incarnation: "i"}
+ if err := StartGuard(&Claim{Holder: h, Op: Op{Kind: KindDeleting, ID: "x"}}, nil, nil); !errors.Is(err, ErrWrongState) {
+ t.Fatalf("start over a running op = %v", err)
+ }
+ busy := &Claim{Holder: h, Op: Op{Kind: KindServing}, Obligations: []Obligation{{Kind: "park-replica", Region: "uk"}}}
+ if err := StartGuard(busy, []string{"uk"}, nil); !errors.Is(err, ErrPendingChore) {
+ t.Fatalf("start over a pending chore = %v", err)
+ }
+ if err := StartGuard(busy, []string{"ie"}, nil); err != nil {
+ t.Fatalf("start elsewhere = %v", err)
+ }
+}