| index a0951db29..5046de730 100644 |
| --- a/internal/activator/activator.go |
| +++ b/internal/activator/activator.go |
| @@ -545,6 +545,23 @@ type actEntry struct { |
| // not release the lease while a spawn against the same DB is in flight. |
| workerRespawning bool |
| respawnDone chan struct{} |
| + // shed is a respawn child, refused at its installation with generations |
| + // off, whose exit is not proven yet (final review R2-1). With |
| + // generations on the operation set holds such a child as a member. The |
| + // entry keeps it, and its build pinned, until the sweep proves it |
| + // exited (reapShedLocked): a terminal stop keeps the lease until then, |
| + // and no respawn execs beside it. |
| + shed *shedChild |
| +} |
| + |
| +// shedChild is a respawn child that was exec'd and then refused at its |
| +// installation, kept until its exit is proven (final review R2-1). Every |
| +// field is written under a.mu. reaping marks a re-stop or proof in flight. |
| +type shedChild struct { |
| + proc Proc |
| + build release.StagedIdentity |
| + reaping bool |
| + retriedAt time.Time |
| } |
| |
| // Sidecar restart pacing — Restart=always semantics without a supervisor: the |
| @@ -2344,6 +2361,11 @@ func (a *Service) convergeWorkerLocked(e *actEntry) { |
| // minutes, not the entry forever. |
| const workerSpawnTimeout = 2 * time.Minute |
| |
| +// respawnJoinBound bounds a terminal stop's wait on a supervised respawn: the |
| +// respawn goroutine's own worst case end to end (retiring the old child, the |
| +// exec, and shedding a raced one). A var so tests can shrink it. |
| +var respawnJoinBound = actStopTimeout + workerSpawnTimeout + sidecarStopTimeout |
| + |
| // respawnWorkerLocked replaces a worker's child on the SAME entry, keeping its |
| // lease and replication: only the process is new. The caller holds a.mu. |
| // stopOld is the child to retire first (nil when it is already dead). |
| @@ -2450,6 +2472,13 @@ func (a *Service) respawnWorkerLocked(e *actEntry, stopOld Proc, why string, ont |
| if a.gens { |
| oldGen.Worker = true |
| retired = a.genTerminated(rt, oldGen, stopOld) |
| + } else { |
| + // Generations off, an earlier respawn's shed child that is not |
| + // proven gone may still run beside this exec (final review |
| + // R2-1): nothing execs until the sweep has proven it exited. |
| + a.mu.Lock() |
| + retired = e.shed == nil |
| + a.mu.Unlock() |
| } |
| |
| // Still wanted? Asked HERE, before anything is spawned: past this |
| @@ -2508,7 +2537,7 @@ func (a *Service) respawnWorkerLocked(e *actEntry, stopOld Proc, why string, ont |
| switch { |
| case owned && !retired && live: |
| a.mu.Unlock() |
| - log.Printf("activator: worker %s NOT respawned (%s): the old generation's termination is not proven (its host cgroup may still hold a process, or the proof could not run), so nothing execs beside it; the sweep retries", host, why) |
| + log.Printf("activator: worker %s NOT respawned (%s): an earlier child's termination is not proven (its host cgroup or a shed respawn child may still hold a process, or the proof could not run), so nothing execs beside it; the sweep retries", host, why) |
| case !wanted || !live: |
| a.mu.Unlock() |
| reason := ownErr |
| @@ -2533,9 +2562,31 @@ func (a *Service) respawnWorkerLocked(e *actEntry, stopOld Proc, why string, ont |
| shed = true |
| a.dropMember(e, m) |
| } else { |
| + // Generations off (final review R2-1): the entry takes |
| + // the child, and its build moves from the pending record |
| + // to it in the same critical section, BEFORE the Stop. |
| + // A terminal stop that gives up waiting on this respawn |
| + // still finds it, and keeps the lease; the sweep reaps it |
| + // once its exit is proven (reapShedLocked). |
| + s := &shedChild{proc: proc, build: ident} |
| + a.mu.Lock() |
| + e.shed, s.retriedAt = s, a.now() |
| + a.dropPendingGenerationLocked(pend) |
| + a.mu.Unlock() |
| + pend = 0 |
| shedCtx, shedCancel := context.WithTimeout(context.Background(), sidecarStopTimeout) |
| - _ = proc.Stop(shedCtx) |
| + serr := proc.Stop(shedCtx) |
| shedCancel() |
| + a.mu.Lock() |
| + gone := proc.Exited() |
| + if gone && e.shed == s { |
| + e.shed = nil |
| + } |
| + a.mu.Unlock() |
| + if !gone { |
| + log.Printf("activator: worker %s: the shed respawn child's exit is not proven (%v); the entry keeps it and its build until the sweep proves it", host, serr) |
| + } |
| + _ = a.persistGenerations() |
| } |
| } |
| if pend != 0 { |
| @@ -2745,11 +2796,11 @@ func (a *Service) stop(e *actEntry) { |
| // The bound covers the respawn goroutine's OWN worst case end to end — |
| // retiring the old child, the exec, and shedding a raced one — so this |
| // wait can never give up on a respawn that is still making progress. |
| - bound := time.NewTimer(actStopTimeout + workerSpawnTimeout + sidecarStopTimeout) |
| + bound := time.NewTimer(respawnJoinBound) |
| select { |
| case <-waitRespawn: |
| case <-bound.C: |
| - log.Printf("activator: %s respawn did not finish before its stop — proceeding without it", e.host) |
| + log.Printf("activator: %s respawn did not finish before its stop; stopping the rest, the lease is kept until it has", e.host) |
| } |
| bound.Stop() // the common case closes in milliseconds; don't leave a 2m timer standing |
| } |
| @@ -2902,6 +2953,21 @@ func (a *Service) stop(e *actEntry) { |
| a.mu.Unlock() |
| return |
| } |
| + a.mu.Lock() |
| + settled := a.respawnSettledLocked(e) |
| + if !settled { |
| + // A respawn still running past the bounded wait above, or the |
| + // child it shed and could not prove gone (final review R2-1): the |
| + // lease stays held, as for ErrStillRunning, and finishPendingStops |
| + // finishes this stop once the respawn is done and its child proven |
| + // exited. |
| + a.keepPendingLocked(e, outcome) |
| + } |
| + a.mu.Unlock() |
| + if !settled { |
| + log.Printf("activator: stop %s: a respawn or the child it shed is not proven finished; keeping the lease until it is", e.host) |
| + return |
| + } |
| if a.gens && !a.releasable(e) { // swapsim:M8 |
| // THE retain-until-proven rule (spec §7.4): unproven, the stop stays |
| // pending with the lease held, exactly as for ErrStillRunning, and |
| @@ -2957,10 +3023,13 @@ func (a *Service) finishPendingStops() { |
| // Members first (zero-downtime spec §7.4): a generation whose stop |
| // could not be proven is finished here once it exits. |
| a.reapMembersLocked(e) |
| + // And a shed respawn child kept with generations off (final review |
| + // R2-1). |
| + a.reapShedLocked(e) |
| // A parent restore that failed when the last member left keeps its |
| // obligation and is retried here (final review F11). |
| a.retryParentRestoreLocked(e) |
| - if !e.pendingStop || len(e.ops.members) > 0 { |
| + if !e.pendingStop || len(e.ops.members) > 0 || !a.respawnSettledLocked(e) { |
| continue |
| } |
| if a.gens && (e.proc == nil || e.proc.Exited()) { |
| @@ -3005,6 +3074,65 @@ func (a *Service) finishPendingStops() { |
| } |
| } |
| |
| +// respawnDoneLocked reports whether e has no supervised respawn in flight. |
| +// The caller holds a.mu. |
| +func respawnDoneLocked(e *actEntry) bool { |
| + if e.respawnDone == nil { |
| + return true |
| + } |
| + select { |
| + case <-e.respawnDone: |
| + return true |
| + default: |
| + return false |
| + } |
| +} |
| + |
| +// respawnSettledLocked reports whether e's supervised respawns are done |
| +// with every child they made: none is in flight, and no child one shed is |
| +// kept unproven (final review R2-1). A terminal stop finishes, and gives |
| +// up the lease, only then. The caller holds a.mu. |
| +func (a *Service) respawnSettledLocked(e *actEntry) bool { |
| + return e.shed == nil && respawnDoneLocked(e) |
| +} |
| + |
| +// reapShedLocked finishes a shed respawn child kept with generations off |
| +// (final review R2-1), once the respawn that shed it is done. An exited |
| +// child is forgotten and its build unpinned. One still running is stopped |
| +// again, bounded, every actStopTimeout, the cadence finishPendingStops |
| +// keeps for the main process. The caller holds a.mu. |
| +func (a *Service) reapShedLocked(e *actEntry) { |
| + s := e.shed |
| + if s == nil || s.reaping || !respawnDoneLocked(e) { |
| + return |
| + } |
| + if !s.proc.Exited() && a.now().Sub(s.retriedAt) < actStopTimeout { |
| + return |
| + } |
| + s.reaping, s.retriedAt = true, a.now() |
| + go func() { |
| + if !s.proc.Exited() { |
| + ctx, cancel := context.WithTimeout(context.Background(), actStopTimeout) |
| + err := s.proc.Stop(ctx) |
| + cancel() |
| + if err != nil && !errors.Is(err, ErrStillRunning) { |
| + log.Printf("activator: %s: retry stop of the shed respawn child: %v", e.host, err) |
| + } |
| + } |
| + a.mu.Lock() |
| + s.reaping = false |
| + gone := s.proc.Exited() |
| + if gone && e.shed == s { |
| + e.shed = nil |
| + } |
| + a.mu.Unlock() |
| + if gone { |
| + log.Printf("activator: %s: the shed respawn child has exited", e.host) |
| + _ = a.persistGenerations() |
| + } |
| + }() |
| +} |
| + |
| // retryPendingStop re-issues proc.Stop for a pendingStop entry that is |
| // still running, off a.mu like finishPendingStop's own call. Unlike that |
| // one, a return here proves nothing by itself. Done closing is still what |