controller: fix-3 dead-app alerting + fix-6 ring cap/spill/spam (WIP, pre-build)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017CDMFpFx84pfviCTVuGGhf
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
package stacks
|
||||
|
||||
import "testing"
|
||||
|
||||
// fix-3: only stopped/exited count as "down" for a deployed app. starting/unhealthy (running),
|
||||
// restarting/deploying (transient), paused (deliberate), unknown (ambiguous) must NOT alert.
|
||||
func TestIsDownState(t *testing.T) {
|
||||
down := []ContainerState{StateStopped, StateExited}
|
||||
for _, s := range down {
|
||||
if !IsDownState(s) {
|
||||
t.Errorf("IsDownState(%q) = false, want true", s)
|
||||
}
|
||||
}
|
||||
notDown := []ContainerState{
|
||||
StateRunning, StateStarting, StateUnhealthy, StateRestarting,
|
||||
StateDeploying, StatePaused, StateUnknown, StateNotDeployed, StateOrphaned,
|
||||
}
|
||||
for _, s := range notDown {
|
||||
if IsDownState(s) {
|
||||
t.Errorf("IsDownState(%q) = true, want false (must not manufacture a dead-app alert)", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,16 @@ const (
|
||||
StateOrphaned ContainerState = "orphaned"
|
||||
)
|
||||
|
||||
// IsDownState reports whether a container state means a DEPLOYED app is not running and won't recover
|
||||
// on its own (fix-3, CAMPAIGN-3). Only `stopped` and `exited` qualify — a Docker "created"/"dead"
|
||||
// container (a failed-at-boot app, the F11 case) resolves to `stopped`. Deliberately NOT `starting`
|
||||
// / `unhealthy` (running, with their own health handling), `restarting` (self-recovering),
|
||||
// `deploying` (mid-deploy), `paused` (a deliberate user action), or `unknown` (ambiguous — fail-open,
|
||||
// never manufacture a dead-app alert from an inconclusive read).
|
||||
func IsDownState(s ContainerState) bool {
|
||||
return s == StateStopped || s == StateExited
|
||||
}
|
||||
|
||||
// ContainerInfo holds status info about a single container within a stack.
|
||||
type ContainerInfo struct {
|
||||
Name string `json:"name"`
|
||||
@@ -444,9 +454,10 @@ func (m *Manager) refreshStatusLocked() error {
|
||||
totalContainers++
|
||||
}
|
||||
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] refreshStatusLocked: docker ps returned %d containers across %d projects", totalContainers, len(projectContainers))
|
||||
}
|
||||
// fix-6: refreshStatusLocked runs every 10s (the status-refresh job) — its per-cycle enumeration
|
||||
// lines are TRACE (dropped from the debug ring) so they don't eat the post-incident window. A real
|
||||
// state change is logged elsewhere at INFO; a docker error returns up the stack.
|
||||
m.logger.Printf("[TRACE] [stacks] refreshStatusLocked: docker ps returned %d containers across %d projects", totalContainers, len(projectContainers))
|
||||
|
||||
m.logger.Printf("[INFO] [stacks] Status refresh: %d containers across %d stacks", totalContainers, len(m.stacks))
|
||||
|
||||
@@ -473,7 +484,7 @@ func (m *Manager) refreshStatusLocked() error {
|
||||
}
|
||||
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] refreshStatusLocked: stack %q → state=%s containers=%d", name, stack.State, len(stack.Containers))
|
||||
m.logger.Printf("[TRACE] [stacks] refreshStatusLocked: stack %q → state=%s containers=%d", name, stack.State, len(stack.Containers))
|
||||
}
|
||||
|
||||
stack.LastUpdated = time.Now()
|
||||
|
||||
Reference in New Issue
Block a user