carlos / platform Public

activator: one pending-stop reaper, its proof chosen by the flag

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev ac9f3fefa5792865ecadfdaaf2800859746e6664 parent d699653
2 files changed, +26 −52
  • internal/activator/activator.go +12 −21
  • internal/activator/opset.go +14 −31
diff --git a/internal/activator/activator.go b/internal/activator/activator.go
index 5279718d8..460c2b8a0 100644
--- a/internal/activator/activator.go
+++ b/internal/activator/activator.go
@@ -508,9 +508,9 @@ type actEntry struct {
// exited, so an intentional stop is never read as a crash (spec §6.4).
// Written under a.mu.
settleCancel context.CancelFunc
- // genReaping is true while reapCurrentGen (or, flag off,
- // reapUncontainedStop) is running for this pending stop, so
- // finishPendingStops does not start a second one. Written under a.mu.
+ // genReaping is true while reapCurrentGen is running for this pending
+ // stop, so finishPendingStops does not start a second one. Written
+ // under a.mu.
genReaping bool
// sidecarJoin is a sidecar start a flag-off terminal stop stopped
// waiting for (sidecarStartJoinWait) while it was still in flight: the
@@ -3574,7 +3574,7 @@ func (a *Service) stop(e *actEntry) {
// the lease, until the start returns and has stopped what it started
// (the restart goroutine stops it before it closes sidecarStartDone).
// Only then is the host cgroup proven empty and the lease released
- // (reapUncontainedStop).
+ // (reapCurrentGen).
a.mu.Lock()
startDone, hook := e.sidecarStartDone, a.joinWaiting
a.mu.Unlock()
@@ -3750,7 +3750,7 @@ func (a *Service) stop(e *actEntry) {
// Flag off, the leader's exit says nothing of what it forked (Task
// 8, F6): the host cgroup is proven empty first. Unproven, the stop
// stays pending with the lease, as for ErrStillRunning, and the
- // sweep retries the proof (reapUncontainedStop).
+ // sweep retries the proof (reapCurrentGen).
log.Printf("activator: stop %s: its host cgroup is not proven empty; keeping the lease until it is", e.host)
a.mu.Lock()
a.keepPendingLocked(e, outcome)
@@ -3821,28 +3821,19 @@ func (a *Service) finishPendingStops() {
if !e.pendingStop || len(e.ops.members) > 0 || !a.respawnSettledLocked(e) {
continue
}
- if a.gens && (e.proc == nil || e.proc.Exited()) {
+ if e.proc == nil || e.proc.Exited() {
// Members gone and the main process (if any) exited: the stop
- // finishes once the retain-until-proven rule holds (releasable,
- // in reapCurrentGen). A process still alive falls through to
- // main's re-stop cadence below.
- if !e.genReaping {
+ // finishes once its termination proof holds (reapCurrentGen):
+ // flag on, the retain-until-proven rule (releasable); flag off,
+ // once a sidecar start it left in flight has returned, the host
+ // cgroup proven empty (Task 8, F6). A process still alive falls
+ // through to main's re-stop cadence below.
+ if !e.genReaping && (a.gens || sidecarStartJoined(e.sidecarJoin)) {
e.genReaping = true
go a.reapCurrentGen(e)
}
continue
}
- if !a.gens && (e.proc == nil || e.proc.Exited()) {
- // Flag off, the main process (if any) exited: the stop finishes
- // once a sidecar start it left in flight has returned and the
- // host cgroup is proven empty (Task 8, F6; reapUncontainedStop).
- // A process still alive falls through to the re-stop cadence.
- if !e.genReaping && sidecarStartJoined(e.sidecarJoin) {
- e.genReaping = true
- go a.reapUncontainedStop(e)
- }
- continue
- }
// Still running: spec §6.3 says sweepOnce retries proc.Stop on its
// own cadence until done closes, and that applies here too, not only
// once termination has already been proven some other way. Without
diff --git a/internal/activator/opset.go b/internal/activator/opset.go
index 08e1ef81d..6439d29c9 100644
--- a/internal/activator/opset.go
+++ b/internal/activator/opset.go
@@ -404,32 +404,6 @@ func (a *Service) stopHostProven(e *actEntry) bool {
return false
}
-// reapUncontainedStop finishes a flag-off pending stop whose main process
-// has exited (finishPendingStops): the host cgroup is proven empty
-// (stopHostProven) before finishPendingStop releases the lease. A failed
-// proof leaves the stop pending, and the next sweep tries again. The
-// caller set e.genReaping; this clears it.
-func (a *Service) reapUncontainedStop(e *actEntry) {
- if !a.stopHostProven(e) {
- a.mu.Lock()
- e.genReaping = false
- a.mu.Unlock()
- return
- }
- a.mu.Lock()
- e.genReaping, e.pendingStop = false, false
- outcome, p := e.stopOutcome, e.proc
- a.mu.Unlock()
- if outcome == nil {
- outcome = &stopObservation{}
- }
- if p == nil {
- a.finishStop(e, outcome)
- return
- }
- a.finishPendingStop(e, outcome)
-}
-
// hostCgroupIsTheWakes reports whether a failed wake of rt may have run in
// carlos-exec@<host>, so that cgroup must be proven empty: an exec child,
// that is, a flat or foreign row. A unit-backed row runs under systemd
@@ -670,11 +644,18 @@ func (a *Service) keepPendingLocked(e *actEntry, outcome *stopObservation) {
e.stopRetriedAt = a.now()
}
-// reapCurrentGen finishes a pending stop whose main process has exited, once
-// the retain-until-proven rule holds (releasable). A failed proof leaves the
-// stop pending, and the next sweep tries again.
+// reapCurrentGen finishes a pending stop whose main process has exited
+// (finishPendingStops), once its termination proof holds: with generations
+// on, the retain-until-proven rule (releasable); with them off, the host
+// cgroup proven empty (stopHostProven, Task 8, F6). A failed proof leaves
+// the stop pending, and the next sweep tries again. The caller set
+// e.genReaping; this clears it. Never called with a.mu held.
func (a *Service) reapCurrentGen(e *actEntry) {
- if !a.releasable(e) {
+ proven := a.stopHostProven
+ if a.gens {
+ proven = a.releasable
+ }
+ if !proven(e) {
a.mu.Lock()
e.genReaping = false
a.mu.Unlock()
@@ -688,7 +669,9 @@ func (a *Service) reapCurrentGen(e *actEntry) {
outcome = &stopObservation{}
}
if p == nil {
- a.finishStop(e, outcome) // only the barrier left it pending
+ // Only the barrier (flag on) or the host proof (flag off) left it
+ // pending.
+ a.finishStop(e, outcome)
return
}
a.finishPendingStop(e, outcome)