Repository navigation
fix(chaos): orphan recovery undoes every fault left from before the Kates API started, and NetworkPolicies in any namespace - #374
Merged
Conversation
…ates API started, and NetworkPolicies in any namespace Startup orphan recovery skipped any fault younger than kates.chaos.orphan-recovery.min-age-sec, 900 seconds, measured from now. The chart pins one replica and rolls it with Recreate, so a restart is always the case it skipped. A plan's SCALE_DOWN lowers brokers-sigma, the pod is OOM-killed or upgraded, and the new pod is up 90 s later: the reconciler saw a 2-minute-old snapshot, left it, and never looked again. The pool stayed a broker short, and a kubernetes-provider partition kept its broker cut off, until some later restart came more than 15 minutes after the fault. NetworkPolicies outside kates.chaos.kafka.namespace, such as the ones consumer-isolation makes in kates, were never recovered at any age, although cleanup() deletes them in every namespace. The cutoff is now the process start, as for the INTERRUPTED reports, truncated to whole seconds because creationTimestamp has no more, so a policy this process makes in its first second is not taken for an orphan. min-age-sec defaults to 0, in the @ConfigProperty and in application.properties, and moves the cutoff earlier. NetworkPolicies are listed in every namespace and each is deleted from its own, in its own try, so a namespace that refuses the delete leaves the others recovered. Scale-down snapshots are still restored in the Kafka namespace only; book 07 says so. The reconciler's javadoc gives the one-replica reason. Tests cover a pool scaled down 2 minutes before the start, policies in two namespaces, and a policy stamped in the second the process started. Caveat refs into DisruptionOrphanReconciler are re-pointed through a diff line map. Books 07 and 11, the stretched-kafka guide and the resilience-scale-down example drop the 15-minute gap.
This was referenced Oct 17, 2026
bmscomp
added a commit
that referenced
this pull request
Oct 17, 2026
…l the client has read the exit status (#383) cmdProbeComparesTheOutputOfACommandThatExitedZero failed PR #374's CI with "ended without an exit status". The cause is in fabric8's mockwebserver. Its WebSocketSession queues the open messages one at a time, and its executor closes the socket as soon as no message is pending. When it sent the output before the status was queued, it closed the socket first, and exitCode() completed with null. waitFor(50) did not help, because a ready-made StatusStreamMessage keeps a delay of 0. On main, the two exit-status tests failed 1 run in 1,000. execWithStatus now also expects a client message that never comes. The mock keeps a socket open while it waits for one, so the client closes the socket itself once it has read the status: 0 failures in 4,000 runs. ProbeExecutor is unchanged. fabric8's Vert.x socket reports the close in frame order, so a status the API server sends before its close frame is always read first.
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.
Why
Startup orphan recovery left every fault that a normal restart leaves behind. Checked in the code (line numbers are main's):
reconcileNetworkPoliciesandreconcileScaleDowns(DisruptionOrphanReconciler.java:107,:148) tookSystem.currentTimeMillis() - minAgeSec * 1000.kates.chaos.orphan-recovery.min-age-secwas 900 both in the@ConfigPropertydefault and inapplication.properties:198.values.schema.jsonpinsreplicaCountto 1 andstrategy.typetoRecreate, so no peer can exist, and a restart always lands inside the window.brokers-sigma. The Kates API is OOM-killed by its in-JVM benchmark, or ahelm upgradearrives.KatesExecutor.shutdown()doesn't interrupt the plan thread, so no rollback runs. The new pod is up about 90 s later. Recovery sees a 2-minute-oldscaled-down-atsnapshot and skips it, and since it runs only once, at startup, it never looks again. The pool stays a broker short until some later restart comes more than 15 minutes after the fault, and the report says onlyINTERRUPTED. A kubernetes-provider partition behaves the same way:kates-netpol-<pod>keeps the broker cut off.kates.chaos.kafka.namespaceonly (:113,:124). The consumer-isolation playbook targets namespacekates, so its policies were never recovered at any age, althoughKubernetesChaosProvider.cleanup()deletesmanaged-by=katespoliciesinAnyNamespace().The finding's fix (cutoff at the process start, default 0, list in any namespace) needed four corrections, all applied here:
application.properties:198sets 900 explicitly, so it has to change too, not only the@ConfigPropertydefault.inAnyNamespace(), each delete has to use the policy's own namespace, notkafkaNamespace.startedAthas a gap.creationTimestamphas whole seconds, so a policy this process makes in its first second can be stamped beforestartedAtand deleted while live. The cutoff is truncated to whole seconds. Thescaled-down-atannotation is in milliseconds, so truncating doesn't change anything for scale-downs.Changes
kates/.../disruption/DisruptionOrphanReconciler.javacutoff(startedAt): the process start, whichmarkInterruptedReportsalready uses, truncated to whole seconds, minusmin-age-sec.reconcilepassesstartedAttoreconcileNetworkPoliciesandreconcileScaleDowns, and both use it in place of theircurrentTimeMillis()cutoffs.min-age-secdefault is"0".reconcileNetworkPolicieslistsmanaged-by=katespoliciesinAnyNamespace()and deletes each one from its own namespace. The log line namesnamespace/name. The method is package-private now, for the test.namespace/nameand moves on to the next policy.kates.chaos.kafka.namespaceonly.onStartsays recovery cleans faults made before this process started.kates/src/main/resources/application.propertieskates.chaos.orphan-recovery.min-age-sec=0, edited in place so that no CLI ref into the file moves.kates mcprefsDisruptionOrphanReconciler.javaare re-pointed through a diff line map:mcp_tools_chaos.go:285goes from53-54,62-69,85-92,152-203to54-55,63-70,86-93,171-222, andmcp_tools_runs.go:2391and the comment at:1931go from152-203to171-222.application.propertiesare untouched.Docs
min-age-sec, 0 by default, moving the cutoff earlier.kates.io/original-replicasannotation.docs/stretched-kafka/03-kubernetes-and-strimzi.md:581drops "only once they are 15 min old" and keeps the rest of the sentence. The scope's docs list didn't name this file, but the clause is false after this change.cli/examples/resilience-scale-down.yaml: orphan recovery brings the broker back the next time the Kates API starts.Tests
DisruptionOrphanReconcilerTest(fabric8 crud mock server,VertxPerMockClientkept), 2 new tests, 3 in all:restoresANodePoolAbandonedMidPlanAndLeavesARecentScaleDownAlone, updated:minAgeSec = 0and the cutoff fromstartedAt. A pool scaled down 2 minutes before the start (the 90-second restart) is now restored. One scaled down 1 s after the start stays at 0.deletesKatesNetworkPoliciesFromBeforeTheStartInEveryNamespace: Kates-managed policies inkafkaandkates, and one unlabelled policy. WithstartedAt60 s ago nothing is deleted. WithstartedAt60 s ahead, both Kates policies are deleted from their own namespaces, and the unlabelled one stays. The fabric8 7.2 crud handler overwritescreationTimestampon create, so the test moves the start instead (the split the scope allowed).aPolicyStampedInTheSecondTheProcessStartedIsKept:startedAtis the mock's stamp plus 700 ms, and the policy is kept. This guards the truncation.git diff --name-only origin/main...HEAD: one commit on origin/main, touching 9 paths:kates/**(DisruptionOrphanReconciler.javaand its test,application.properties),cli/**(mcp_tools_chaos.go,mcp_tools_runs.go,examples/resilience-scale-down.yaml),docs/book/**(07, 11) anddocs/stretched-kafka/03. CI jobs triggered:ci.ymlcode (Build & Test, CLI, govulncheck),lint.ymlgo/java/yaml,docs.yml,book.yml,native.yml.cd kates && ./mvnw -o test -Dquarkus.grpc.codegen.skip=true -Dquarkus.http.test-port=0: BUILD SUCCESS, 1,468 tests, 0 failures, 0 errors, 0 skipped.DisruptionOrphanReconcilerTestran all 3 tests, andResilienceExamplesTestread the editedcli/examplesfile.spotless:checkon JDK 25 (25.0.2-graalce): BUILD SUCCESS.cd cli && go test ./...: all packages ok, withcli/cmdrun fresh rather than from cache. That coversTestMCPCaveatRefAnchors, which checks the movedDisruptionOrphanReconciler.javarefs.cd cli && go vet ./...: clean.cd cli && golangci-lint run --new-from-rev=origin/main ./...: 0 issues.cd cli && govulncheck ./...: no vulnerabilities found.bash scripts/check-cli-style.shandbash scripts/check-cli-compat.sh: OK.scripts/check-book-style.sh: OK, and the structure ratchet holds for all 38 pages.scripts/gen-version-matrix.sh --checkandscripts/gen-chart-table.sh --check: in sync (the chart table check also covers thedocs/stretched-kafkachange).python3 scripts/check-book-contrast.py: all 170 colour pairs meet WCAG AA.scripts/check-mermaid.sh --quiet: 156 diagrams, 0 invalid.prepare-book-render.sh, thenquarto render docs/book --to html): 39 chapters prepared, 38 pages rendered, 0 warnings in the render log. The worktree wasn't touched.python3 scripts/check-book-fragments.pyon that render: all 7,458 fragment links in 38 pages land on an id..yamllint.yamlon.github,configandcharts(CI's yaml job scope), and oncli/examples/resilience-scale-down.yaml: 0 errors. The only warnings were already there (for examplecharts/kates/values-corporate.yaml:142empty-lines), and the edited example lints clean.--offline --include-fragments(docs.yml's link check): skipped, since lychee isn't installed locally. The docs diff adds or removes no Markdown link, so CI's run is the only check.make check-help,helm lint/helm templateandcheck-versions.sh: not triggered, since this PR changes no.github/**,scripts/**, Makefile,charts/**orversions.env.*ITtests (ci.ymlBuild & Test runs./mvnw verify, and Docker stays off locally);native.yml, triggered bykates/**);book.ymlpdf job);docs.yml);github-advanced-securityAI scan, which is red on every PR for a GitHub-side reason.Decisions
min-age-secdefaults to 0, with the cutoff truncated to whole seconds. A margin would move the cutoff earlier, so it would skip orphans the old process made in its last seconds before it died, as in an in-place OOM restart where a native boot takes about 1 s. It protects nothing the truncation doesn't already protect.=0line stays inapplication.properties:198, edited in place. Deleting it and relying on the@ConfigPropertydefault would shift four CLI refs at line 274 and below.ScaleDownSnapshots.restoreworks per namespace, while SCALE_DOWN can targetspec.targetNamespace(). Listing StatefulSets cluster-wide is a bigger RBAC and cost question. Book 07 documents the gap, and it is left as a follow-up.Deliberately not done
deleteonnetworkpolicies. The chart grants it only underrbac.directChaos, in a ClusterRole (charts/kates/templates/rbac.yaml:54-67).kates/k8s/rbac.yamlgrants onlygetandlist, so deletes there still fail with 403 and are logged, as before.creationTimestamp. If the Kates node's clock is ahead by Δ, a partition this process creates within Δ of startup can be deleted early. If it is behind, an orphan created within Δ before startup is left. NTP keeps Δ to milliseconds.kates/k8s/deployment.yaml(1 replica,Recreate) rule that out.min-age-secis still there for such a deployment.INTERRUPTED. Nothing records whether its faults were undone. This PR doesn't change that.Landing
This PR shares no file with the earlier plan PRs (#370, #371, #372, #373), so it can land at any time.