carlos / platform Public

activator: RetireExcept re-validates a probe's open under its final lock (Plan 3 Task 7 fix round 1, finding 2)

A probe entry opened after the held-probe snapshot was taken is kept for
this reload: its registry observation is older than the open. A seam
between the snapshot and the registry read makes the interleaving
deterministic in the test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 078a6c36b596847cb62763401d4ccff55711bf09 parent 073e1fc
3 files changed, +52 −5
  • internal/activator/activator.go +6 −1
  • internal/activator/probewake.go +13 −4
  • internal/activator/probewake_test.go +33 −0
diff --git a/internal/activator/activator.go b/internal/activator/activator.go
index 8439c71cf..d1fac2429 100644
--- a/internal/activator/activator.go
+++ b/internal/activator/activator.go
@@ -2956,7 +2956,7 @@ func (a *Service) sweepOnce() {
func (a *Service) RetireExcept(keep map[string]bool) {
// A probe's entry (ProbeWake) is held for its move while the registry
// row is: the edge manages ready rows only, so it is never in keep.
- seen, held := a.heldProbes(keep)
+ seen, held, openedAt := a.heldProbes(keep)
if retireBetweenLocks != nil {
retireBetweenLocks()
}
@@ -2971,6 +2971,11 @@ func (a *Service) RetireExcept(keep map[string]bool) {
if keep[e.host] || held[e] || (e.probe != nil && !seen[e]) {
continue
}
+ if e.probe != nil && e.probe.opened && !openedAt[e] {
+ // Opened after the snapshot: the registry read is older than the
+ // open, so it cannot judge the entry. Kept for this reload.
+ continue
+ }
// Workers are outside this reconcile's world entirely. `keep` is the
// EDGE's managed set, built by edge.Reload — which refuses a sidecar
// row on kind as the first act of its loop, so a worker's host can
diff --git a/internal/activator/probewake.go b/internal/activator/probewake.go
index 80448dc70..a36690069 100644
--- a/internal/activator/probewake.go
+++ b/internal/activator/probewake.go
@@ -96,7 +96,7 @@ func probeResultOf(o *hostclaim.Observed) ProbeResult {
// Test seams: retireBetweenLocks runs in RetireExcept between the probe
// snapshot and the retirement pass; probeWakePublished runs in ProbeWake
// after the entry is published and before its wake starts.
-var retireBetweenLocks, probeWakePublished func()
+var retireBetweenLocks, probeWakePublished, retireBeforeRegistryRead func()
// probeEntry marks an actEntry ProbeWake made.
type probeEntry struct {
@@ -204,7 +204,13 @@ func probeRefused(e *actEntry, err error) error {
// a.mu, so RetireExcept retires a probe only when this snapshot covered it
// (fix round 1, finding 6): one published after it is left for the next
// reload. A row that cannot be read keeps its entry for this reload.
-func (a *Service) heldProbes(keep map[string]bool) (seen, held map[*actEntry]bool) {
+//
+// openedAt is the open state each snapshotted entry had when the snapshot
+// was taken. An entry opened since then was opened after its registry
+// observation could have been made, so that observation is stale for it:
+// RetireExcept re-validates the open under its final lock and keeps the
+// entry for this reload (fix round 1, finding 2).
+func (a *Service) heldProbes(keep map[string]bool) (seen, held, openedAt map[*actEntry]bool) {
a.mu.Lock()
var probes []*actEntry
opened := map[*actEntry]bool{}
@@ -219,8 +225,11 @@ func (a *Service) heldProbes(keep map[string]bool) (seen, held map[*actEntry]boo
for _, e := range probes {
seen[e] = true
}
+ if retireBeforeRegistryRead != nil {
+ retireBeforeRegistryRead()
+ }
if a.Routes == nil {
- return seen, held
+ return seen, held, opened
}
for _, e := range probes {
rt, err := a.Routes.Get(e.host)
@@ -237,5 +246,5 @@ func (a *Service) heldProbes(keep map[string]bool) (seen, held map[*actEntry]boo
held[e] = true
}
}
- return seen, held
+ return seen, held, opened
}
diff --git a/internal/activator/probewake_test.go b/internal/activator/probewake_test.go
index d3192602a..34e44b6fc 100644
--- a/internal/activator/probewake_test.go
+++ b/internal/activator/probewake_test.go
@@ -491,3 +491,36 @@ func TestIdleSweepSparesAHeldProbe(t *testing.T) {
a.sweepOnce()
waitFor(t, "the opened probe to idle out", func() bool { return !a.Owns("one.example.com") })
}
+
+// Task 7 fix round 1, finding 2: the open lands between RetireExcept's probe
+// snapshot (not opened) and its registry read (ready, unheld). The
+// registry's answer is older than the entry's open, so the entry is kept,
+// not retired as a probe that never opened.
+func TestRetireExceptRevalidatesTheOpenUnderTheFinalLock(t *testing.T) {
+ a, _, _, reg := testActivator(t, time.Hour)
+ a.Routes = reg
+ if err := reg.Upsert(testRoute("one.example.com")); err != nil {
+ t.Fatal(err)
+ }
+ if err := reg.SetHold("one.example.com", "landing", pwOp); err != nil {
+ t.Fatal(err)
+ }
+ e := pwHeldEntry(t, a)
+ retireBeforeRegistryRead = func() {
+ if !a.OpenProbeEntry("one.example.com", pwOp) {
+ t.Error("not marked")
+ }
+ if ok, err := reg.ReleaseHold("one.example.com", pwOp); err != nil || !ok {
+ t.Errorf("release: %v %v", ok, err)
+ }
+ }
+ t.Cleanup(func() { retireBeforeRegistryRead = nil })
+ a.RetireExcept(map[string]bool{})
+ retireBeforeRegistryRead = nil
+ a.mu.Lock()
+ state := e.state
+ a.mu.Unlock()
+ if state != actAwake || !a.Owns("one.example.com") {
+ t.Fatal("the probe opened during the retirement was retired")
+ }
+}