fix: Prevent backoff bypass in Manifest controller error loops - #3562
Conversation
Two issues combined to defeat exponential backoff when multiple module
resources fail concurrently (e.g. namespace in Terminating state):
1. ConcurrentDefaultSSA.Run collected per-resource errors from goroutines
in channel-receive order, which is non-deterministic. errors.Join then
produced a different string on every reconcile cycle, causing
HasStatusDiff to return true on each iteration and triggering a status
patch every cycle.
2. For(&Manifest{}) had no predicate, so every status subresource patch
(which does not increment metadata.generation) fired a MODIFIED watch
event that called queue.Add, bypassing AddRateLimited and defeating
the exponential backoff entirely.
Fix: sort per-resource errors before joining to make the string
deterministic, and add GenerationChangedPredicate to the Manifest
self-watch to filter out status-only updates.
|
👋 Hi — I'm PR Bot, your SAP code review assistant. I'll automatically review your pull requests for code quality, security, and SAP compliance. Get an overview of what I do → What I do
Key commands
Configure me for your teamCreate {
"$schema": "https://devops-insights-pr-bot.cfapps.eu10-004.hana.ondemand.com/schema/pull_request_bot.json",
"features": {
"control_panel": false,
"summarize": {
"auto_generate_summary": true,
"auto_insert_summary": true,
"auto_run_on_draft_pr": true,
"use_custom_summarize_prompt": false,
"use_custom_summarize_output_template": false,
"excluded_paths": [],
"auto_exclude_authors": []
},
"review": {
"auto_generate_review": true,
"auto_run_on_draft_pr": false,
"use_custom_review_focus": false,
"excluded_paths": [],
"auto_exclude_authors": []
},
"sonar_fix": {
"enable": true,
"excluded_rules": []
},
"pipeline_fix": {
"enable": true
}
},
"excluded_paths": []
}*This introduction message will be shown to you only once, you will not see it in future PRs. |
Reproduction testTo verify locally, drop these two files into make -f tests/e2e/backoff_bypass_test.mk testWith both fixes reverted the
|
The sort.Slice path added in the backoff-bypass fix is not covered by existing unit tests; threshold updated to reflect actual 29.8%.
Verifies that errors.Join output is identical across runs when multiple resources fail concurrently, covering the sort.Slice fix. Updates coverage threshold to reflect the new 38.1%.
There was a problem hiding this comment.
Pull request overview
This PR addresses an operational issue in the Manifest controller where exponential backoff could be effectively bypassed during persistent/concurrent failures, leading to tight reconcile loops and excessive load on KLM and the KCP API server.
Changes:
- Make joined SSA errors deterministic by sorting per-resource errors before
errors.Join, preventing status “operation” churn across reconcile cycles. - Filter Manifest self-watch events by generation changes to avoid status-only updates triggering immediate requeues that bypass the rate limiter.
- Add a unit test to ensure the SSA error string remains stable across repeated runs with concurrent failures.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| unit-test-coverage-lifecycle-manager.yaml | Updates expected unit test coverage numbers for internal/manifest/skrresources. |
| internal/manifest/skrresources/ssa.go | Sorts collected SSA errors prior to joining to stabilize the combined error string. |
| internal/manifest/skrresources/ssa_test.go | Adds a regression test ensuring SSA error string determinism under concurrent failures. |
| internal/controller/manifest/setup.go | Adds a generation-based predicate to the Manifest controller’s primary watch to filter status-only updates. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…cate GenerationChangedPredicate blocked Update events where only deletionTimestamp changes (generation is not incremented on delete), which would have prevented the delete pipeline from being triggered. Replace with a targeted Funcs predicate that allows updates when either the generation or the deletionTimestamp changes, and blocks status-only patches in all other cases.
Label changes (e.g. skip-reconciliation toggle) do not increment metadata.generation, so the previous predicate would silently drop them, delaying reaction by up to the success requeue interval. Add labelChanged as a third OR condition in the UpdateFunc predicate, consistent with the Kyma controller which uses LabelChangedPredicate for the same reason.
|
Follow up issue created: #3563 |
Summary
When module resources fail concurrently (e.g. a namespace is Terminating), two issues combined to completely defeat the Manifest controller's exponential backoff:
Root cause 1 — non-deterministic error string (
ssa.go)ConcurrentDefaultSSA.Runcollects per-resource errors from goroutines via a channel. The receive order is non-deterministic, soerrors.Joinproduces a different combined error string on every reconcile cycle when ≥2 resources fail.HasStatusDiffcomparesLastOperation.Operationstrings, so it returnstrueon every cycle, triggering a status patch each time.Root cause 2 — missing predicate (
setup.go)For(&Manifest{})had no predicate. Status subresource patches don't incrementmetadata.generation, so every patch fired aMODIFIEDwatch event that calledqueue.Adddirectly — bypassingAddRateLimitedand resetting the backoff timer.Together, these produced a tight loop (~2–5 reconciles/second per manifest) instead of the intended 5–30s exponential backoff. In a production incident this caused ~700k error log entries in 12h from 7 stuck manifests, 7.5× CPU baseline, and ~2500 KCP API ops/s.
Changes
ConcurrentDefaultSSA.Run: sort per-resource errors beforeerrors.Joinso the combined string is deterministic from cycle 2 onward —HasStatusDiffreturns false, no status patch fires, backoff accumulates normally.SetupWithManager: addGenerationChangedPredicateto the Manifest self-watch so status-only updates are filtered and never bypass the rate limiter.Verification
Reproduced locally with a k3d KCP+SKR setup: deployed template-operator, forced
template-operator-systeminto Terminating, then bumped the module version to trigger concurrent SSA failures. With both fixes reverted themanifest_unauthorizedrequeue counter reached ~120–300 in 60 seconds (backoff bypassed). With both fixes applied it stayed ≤15 (proper 1s→10s backoff schedule).closes #3523