feat(controller): detect slow restart loops via termination reason - #74
Merged
Merged
Conversation
watchReasons only sees containers that are waiting. Kubelet resets its restart backoff once a container has stayed up longer than roughly twice the maximum backoff, so a container that survives beyond that between deaths restarts immediately every time and never enters CrashLoopBackOff. A memory leak has exactly that shape, and such a workload can reach hundreds of restarts while remaining invisible. Adds watchTerminationReasons, empty by default so nothing changes for existing policies, matching on why the container last exited rather than on what it is waiting for. A match needs the restart threshold reached, a listed reason, and the exit to fall inside the new restartWindow. That window is not optional: the restart count is cumulative for a pod's whole life and never decays, so without it a workload that misbehaved last month would still be scaled down. A pod in this state is running when observed, so the change is not confined to the failure predicate. It needs a second field index, a widened watch predicate, and a shared predicate between the reconcile loop, allReplicasFailing and policyWouldAct, since the sibling check runs under allReplicasFailing, which defaults to true and would otherwise leave the feature inert. Guards against acting on pods being deleted, which would sweep every replica of a rolling update in at once, on completed pods, on containers still inside their startup probe, and on classic init containers, which run once and cannot loop. Sidecars declared with restartPolicy Always do count. Also documents that termination reasons never appear as waiting reasons, so putting OOMKilled in watchReasons silently matches nothing. Closes #67 Signed-off-by: Simon Lauger <simon@lauger.de>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
watchReasonsonly sees containers that are waiting. Kubelet resets its restart backoff once a container has stayed up longer than roughly twice the maximum backoff, so a container that survives beyond that between deaths restarts immediately every time and never entersCrashLoopBackOff. A memory leak has exactly that shape: run, grow, get OOM-killed, restart at once, repeat. Such a workload can reach hundreds of restarts while staying completely invisible to this operator.Adds
watchTerminationReasons, matching on why the container last exited rather than on what it is waiting for. Empty by default, so nothing changes for existing policies.A match requires all three of: the restart threshold reached, a listed termination reason, and that exit falling inside the new
restartWindow(default1h). The window is not optional.RestartCountis cumulative for a pod's whole life and never decays, so without it a workload that misbehaved last month would still be scaled down today.Why this is not a one-line predicate change
A pod in this state is running when observed, so it never reaches the existing candidate set. Adding the check to
podHasFailureReasonalone would have been dead code. The change therefore also needed:allReplicasFailingandpolicyWouldAct. This one is subtle:allReplicasFailingdefaults totrueand checks siblings with its own predicate, so a narrower one there would have made the whole feature inert under default settings while appearing to work in isolation.allReplicasFailingnow takes the policy rather than a reason list, which makes disagreement impossible rather than merely unlikely.Guards
Not counted: pods being deleted, which would otherwise sweep every replica of a rolling update into the candidate set at once; completed pods; containers still inside their startup probe; and classic init containers, which run once and cannot loop. Init containers declared with
restartPolicy: Alwaysare sidecars and do count.Documentation
Also records something that would otherwise keep biting people: termination reasons never appear as waiting reasons, so putting
OOMKilledintowatchReasonsmatches nothing and fails silently. The README previously invited exactly that by sayingwatchReasons"matches the container's waiting reason exactly".Closes #67
Test plan
newLoopingPodfixture builds the state this loop actually produces: running now, high restart count, recent exit carrying the reason, never in a watched waiting state.make cipasses; coverage rose from 61.2 to 64.3 percent.