hostclaim: the orphan predicate (record first, then claim; no claim = legacy)
Co-Authored-By: Codex (gpt-6-astra) <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+39
−0
internal/hostclaim/orphan.go+16 −0internal/hostclaim/orphan_test.go+23 −0
diff --git a/internal/hostclaim/orphan.go b/internal/hostclaim/orphan.go| new file mode 100644 |
| index 000000000..072728710 |
| --- /dev/null |
| +++ b/internal/hostclaim/orphan.go |
| @@ -0,0 +1,16 @@ |
| +package hostclaim |
| + |
| +// IsOrphan is §5.1 step 5. The caller reads the record FIRST and this |
| +// claim AFTER it (R3-2). Each incarnation gets exactly one claim attempt, |
| +// and a record exists only after that attempt succeeded, so a claim read |
| +// after the record that does not name recordInc means recordInc can never |
| +// become the holder. A creating holder is named by Holder, so it is |
| +// covered. An absent claim is a legacy host (Q2), never an orphan. The |
| +// caller deletes only with DeleteIfMatch on the ETag it read. |
| +func IsOrphan(recordInc string, claimReadAfterRecord Observed) bool { |
| + if claimReadAfterRecord.Absent || claimReadAfterRecord.Claim == nil { |
| + return false |
| + } |
| + h := claimReadAfterRecord.Claim.Holder |
| + return h == nil || h.Incarnation != recordInc |
| +} |
diff --git a/internal/hostclaim/orphan_test.go b/internal/hostclaim/orphan_test.go| new file mode 100644 |
| index 000000000..1dc8c7a4d |
| --- /dev/null |
| +++ b/internal/hostclaim/orphan_test.go |
| @@ -0,0 +1,23 @@ |
| +package hostclaim |
| + |
| +import "testing" |
| + |
| +func TestIsOrphan(t *testing.T) { |
| + if IsOrphan("A", Observed{Absent: true}) { |
| + t.Fatal("no claim = legacy, never orphan") |
| + } |
| + held := Observed{Claim: &Claim{Holder: &Holder{Incarnation: "A"}, Op: Op{Kind: KindServing}}} |
| + if IsOrphan("A", held) { |
| + t.Fatal("the holder's own record is not an orphan") |
| + } |
| + creating := Observed{Claim: &Claim{Holder: &Holder{Incarnation: "A"}, Op: Op{Kind: KindCreating, ID: "c"}}} |
| + if IsOrphan("A", creating) { |
| + t.Fatal("a record of the creating incarnation is not an orphan") |
| + } |
| + if !IsOrphan("A", Observed{Claim: &Claim{Holder: &Holder{Incarnation: "B"}, Op: Op{Kind: KindServing}}}) { |
| + t.Fatal("another incarnation's claim makes A an orphan") |
| + } |
| + if !IsOrphan("A", Observed{Claim: &Claim{}}) { |
| + t.Fatal("a free claim makes A an orphan") |
| + } |
| +} |