| index f71e93bfd..e86ac422e 100644 |
| --- a/internal/activator/activator.go |
| +++ b/internal/activator/activator.go |
| @@ -354,11 +354,16 @@ type actEntry struct { |
| route registry.Route |
| state actState |
| |
| - ready chan struct{} // waking: closed when the wake resolves; then check err |
| - err error // waking: the wake's outcome |
| - stopped chan struct{} // stopping: closed when the stop completes |
| - wakeCancel context.CancelFunc |
| - stopOutcome *stopObservation |
| + ready chan struct{} // waking: closed when the wake resolves; then check err |
| + err error // waking: the wake's outcome |
| + stopped chan struct{} // stopping: closed when the stop completes |
| + wakeCancel context.CancelFunc |
| + stopOutcome *stopObservation |
| + |
| + // stopWhy is why this entry is stopping (beginStopLocked), written under |
| + // a.mu; finishStop hands it to a run it interrupts (run spec §5.3). |
| + stopWhy string |
| + |
| sidecarUncertain bool |
| |
| // Activity tracking: an instance is idle only when nothing is in flight |
| @@ -757,12 +762,14 @@ func (a *Service) begin(ctx context.Context, rt registry.Route, stamp bool) (rel |
| // request waits for it then re-wakes — recovery is the routine |
| // wake path. |
| if repErr := e.rep.Err(); repErr != nil || e.proc.Exited() { |
| + why := StopExited |
| if repErr != nil { |
| log.Printf("activator: LEASE LOST for %s (%v) — standing down", e.host, repErr) |
| + why = StopLeaseLost |
| } else { |
| log.Printf("activator: instance %s exited unexpectedly — recycling", e.host) |
| } |
| - a.beginStopLocked(e) |
| + a.beginStopLocked(e, why) |
| a.mu.Unlock() |
| continue |
| } |
| @@ -842,12 +849,14 @@ func (a *Service) BeginSidecarRequest(ctx context.Context, rt registry.Route) er |
| // (or absent) entry and turns this request away instead of |
| // chaining into a re-wake. |
| if repErr := e.rep.Err(); repErr != nil || e.proc.Exited() { |
| + why := StopExited |
| if repErr != nil { |
| log.Printf("activator: LEASE LOST for %s (%v) — standing down", e.host, repErr) |
| + why = StopLeaseLost |
| } else { |
| log.Printf("activator: instance %s exited unexpectedly — recycling", e.host) |
| } |
| - a.beginStopLocked(e) |
| + a.beginStopLocked(e, why) |
| a.mu.Unlock() |
| continue |
| } |
| @@ -994,7 +1003,7 @@ func (a *Service) wake(e *actEntry) { |
| e.spawnVersion = w.spawnVersion |
| e.staleSince = time.Time{} |
| if a.fences[e.host] != 0 { |
| - a.beginStopLocked(e) |
| + a.beginStopLocked(e, StopFenced) |
| } else if e.bounceAfterWake { |
| // A config delivery or `carlos restart` asked for while this wake |
| // was under way (Bounce). Same spot and same reason as the fence |
| @@ -1369,9 +1378,12 @@ const actStopTimeout = 20 * time.Second |
| // idle sweep (instance has been idle past cfg.Idle), a dead child, a lost |
| // lease (open requests on either of the last two are already doomed), and |
| // Bounce (config changed — the instance may be mid-request, but the drain |
| -// still gives it actStopTimeout to finish before SIGKILL). |
| -func (a *Service) beginStopLocked(e *actEntry) { |
| +// still gives it actStopTimeout to finish before SIGKILL). Every caller says |
| +// why (the Stop* reasons in cmd.go): a run the stop interrupts tells the |
| +// member (run spec §5.3). |
| +func (a *Service) beginStopLocked(e *actEntry, why string) { |
| e.state = actStopping |
| + e.stopWhy = why |
| e.stopped = make(chan struct{}) |
| go a.stop(e) |
| } |
| @@ -1450,7 +1462,7 @@ func (a *Service) bounceLocked(e *actEntry) bool { |
| msg = "sidecar restarting (carlos restart)" |
| } |
| emitEvent(a.Logs, e.route, "info", msg) |
| - a.beginStopLocked(e) |
| + a.beginStopLocked(e, StopRestart) |
| return true |
| } |
| |
| @@ -1563,7 +1575,7 @@ func (a *Service) ConvergeAdopted(host, ver string) bool { |
| return false |
| } |
| log.Printf("activator: %s serving %s but %s is adopted — cycling onto the new build", host, e.spawnVersion, ver) |
| - a.beginStopLocked(e) |
| + a.beginStopLocked(e, StopRelease) |
| return true |
| } |
| |
| @@ -1713,12 +1725,14 @@ func (a *Service) WakeWorker(rt registry.Route) error { |
| // dead child: its proc is mid-replacement, so leave it be. |
| if !e.workerRespawning { |
| if repErr := e.rep.Err(); repErr != nil || e.proc.Exited() { |
| + why := StopExited |
| if repErr != nil { |
| log.Printf("activator: LEASE LOST for %s (%v) — standing down", e.host, repErr) |
| + why = StopLeaseLost |
| } else { |
| log.Printf("activator: worker %s exited — recycling", e.host) |
| } |
| - a.beginStopLocked(e) |
| + a.beginStopLocked(e, why) |
| a.mu.Unlock() |
| continue |
| } |
| @@ -1954,7 +1968,7 @@ func (a *Service) respawnWorkerLocked(e *actEntry, stopOld Proc, why string) { |
| // lease, so a keyed sibling can take it. Nothing re-wakes it here |
| // until the box can provide the config (the config gate refuses |
| // every wake before then). |
| - a.beginStopLocked(e) |
| + a.beginStopLocked(e, StopRestart) |
| a.mu.Unlock() |
| log.Printf("activator: worker %s NOT respawned (%s): %v — standing down and releasing its lease", host, why, refused) |
| case err != nil: |
| @@ -2069,7 +2083,7 @@ func (a *Service) SleepWorker(host string) { |
| return |
| } |
| emitEvent(a.Logs, e.route, "info", "sidecar reported idle — stopping") |
| - a.beginStopLocked(e) |
| + a.beginStopLocked(e, StopIdle) |
| } |
| |
| // WorkerToken reports the live per-incarnation token for host, or "" — the |
| @@ -2462,7 +2476,7 @@ func (a *Service) sweepOnce() { |
| } |
| if err := e.rep.Err(); err != nil { |
| log.Printf("activator: LEASE LOST for %s (%v) — standing down", e.host, err) |
| - a.beginStopLocked(e) |
| + a.beginStopLocked(e, StopLeaseLost) |
| continue |
| } |
| if e.proc.Exited() { |
| @@ -2504,7 +2518,7 @@ func (a *Service) sweepOnce() { |
| } else { |
| log.Printf("activator: instance %s exited unexpectedly — recycling", e.host) |
| } |
| - a.beginStopLocked(e) |
| + a.beginStopLocked(e, StopExited) |
| continue |
| } |
| // The idle predicate is shared verbatim; only its WINDOW can differ. |
| @@ -2544,7 +2558,7 @@ func (a *Service) sweepOnce() { |
| heldForTick := ok && time.Unix(next, 0).Sub(now) <= idleWindow |
| if !heldForTick { |
| log.Printf("activator: %s idle %s — hibernating", e.host, now.Sub(e.lastActive).Truncate(time.Second)) |
| - a.beginStopLocked(e) |
| + a.beginStopLocked(e, StopIdle) |
| continue |
| } |
| // Fall through to the convergence check below: a host held awake |
| @@ -2571,7 +2585,7 @@ func (a *Service) sweepOnce() { |
| continue |
| } |
| log.Printf("activator: %s hit its max_run of %s — stopping", e.host, e.workerMaxRun) |
| - a.beginStopLocked(e) |
| + a.beginStopLocked(e, StopIdle) |
| continue |
| } |
| // Converge onto a shipped build. A running child never re-reads |
| @@ -2608,7 +2622,7 @@ func (a *Service) sweepOnce() { |
| continue |
| } |
| log.Printf("activator: %s serving %s but live symlink is now %s — cycling onto the new build", e.host, e.spawnVersion, cur) |
| - a.beginStopLocked(e) |
| + a.beginStopLocked(e, StopRelease) |
| cycled++ |
| continue |
| } |
| @@ -2838,7 +2852,7 @@ func (a *Service) RetireExcept(keep map[string]bool) { |
| switch e.state { |
| case actAwake: |
| log.Printf("activator: %s is no longer hibernate-managed — releasing", e.host) |
| - a.beginStopLocked(e) |
| + a.beginStopLocked(e, StopRetired) |
| case actWaking: |
| // A wake can't be stopped mid-flight; finish the retire once it |
| // resolves (below, off-lock). |
| @@ -2858,7 +2872,7 @@ func (a *Service) RetireExcept(keep map[string]bool) { |
| // same host means the route was re-flagged in the meantime. |
| if a.entries[e.host] == e && e.state == actAwake { |
| log.Printf("activator: %s is no longer hibernate-managed — releasing (wake finished first)", e.host) |
| - a.beginStopLocked(e) |
| + a.beginStopLocked(e, StopRetired) |
| } |
| }(e) |
| } |
| @@ -2922,7 +2936,7 @@ func (a *Service) Shutdown(ctx context.Context) { |
| for _, e := range a.entries { |
| switch e.state { |
| case actAwake: |
| - a.beginStopLocked(e) |
| + a.beginStopLocked(e, StopShutdown) |
| waits = append(waits, e.stopped) |
| case actStopping: |
| waits = append(waits, e.stopped) |