Repository navigation
fix(reloader): add startupProbe to survive slow first start under load - #3470
Conversation
Reloader binds its :9090 listener as the last statement of startReloader, so /live and /metrics answer connection refused until everything before it has finished. The blocking part of that prologue is the package-level IsOpenshift = isOpenshift() in pkg/kube, which issues a GET /apis/project.openshift.io against the API server on a context carrying no deadline, before main() is even entered. That latency belongs to the API server, and the container waits it out at a 100m CPU limit. The chart renders no startupProbe, so the only grace period is the liveness budget: initialDelaySeconds 10 + periodSeconds 10 * failureThreshold 5, about 60 seconds. A first start that needs longer is killed mid-prologue, and the restart does not resume it, it runs it again from the beginning, so the loop sustains itself. The blast radius reaches past reloader: the LINSTOR stack lists cozystack.reloader in dependsOn, which fans out to every component of that stack, so a reloader stuck in this loop blocks the whole install. Render a values-driven startupProbe on /live so liveness only begins evaluating once the listener is up. Budget = periodSeconds 5 times failureThreshold 45, so about 225 seconds. timeoutSeconds matches the 5s the chart already gives liveness rather than the kubelet default of 1s, which a throttled process can exceed while holding the socket open. Both liveness knobs the chart already exposes could buy the same grace with no patch to carry. Widening failureThreshold buys it permanently, loosening hang detection for the life of the container. initialDelaySeconds is start-up-only and would genuinely do, but it is a fixed blind window on every start, whereas a startup probe returns liveness to its tight budget the moment the listener binds. The chart is vendored, so the template change is carried as patches/deployment-startup-probe.diff and reapplied by `make update`, which now drives the shared %-update rule with an exact chart version pin instead of pulling whatever is latest. The patch is applied with --forward because patch(1) defaults diverge by implementation over a tree that already carries the change: GNU 2.7.6 refuses and exits 1, Apple 2.0-12u11 takes its "Assume -R? [y]" default and strips the hunk back out with exit 0, and GNU under --batch does the same. --forward refuses in all three. A helm unittest pins the probe, its budget, the no-op path the template promises for an unset value, and the liveness numbers the budget is sized against, so the documented arithmetic cannot drift away from the chart without a red test. The same target asserts that the vendored Chart.yaml still records the pinned version, so raising the pin without re-running `make update` fails instead of leaving the tree and the pin quietly disagreeing. Assisted-By: Claude <[email protected]> Signed-off-by: Aleksei Sviridkin <[email protected]>
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
📝 WalkthroughWalkthroughThe Reloader Helm chart is pinned to version 2.0.0, gains a configurable startup probe, applies the change through a maintained patch, and adds tests for rendering and probe preservation. ChangesReloader startup probe
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant Values as values.yaml
participant Template as Deployment template
participant Deployment as Rendered Deployment
participant Tests as Helm unit tests
Values->>Template: provide startupProbe configuration
Template->>Deployment: render conditional startupProbe
Tests->>Deployment: assert startup, liveness, and readiness probes
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/system/reloader/Makefile`:
- Line 31: Update the Makefile’s update target so it depends only on clean, then
invoke reloader-update from the update recipe after clean completes. Preserve
the existing clean and reloader-update behavior while ensuring parallel make
cannot run them concurrently.
In `@packages/system/reloader/patches/deployment-startup-probe.diff`:
- Around line 16-33: Remove the startupProbe template mutation from
packages/system/reloader/patches/deployment-startup-probe.diff (lines 16-33),
stop applying that patch in packages/system/reloader/Makefile (lines 31-39), and
restore the upstream deployment template verbatim in
packages/system/reloader/charts/reloader/templates/deployment.yaml (lines
164-174). Use a repository-owned overlay or post-rendering mechanism for probe
injection instead.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: aaf6e2df-dc0d-4ccc-aed5-6165e9793034
📒 Files selected for processing (5)
packages/system/reloader/Makefilepackages/system/reloader/charts/reloader/templates/deployment.yamlpackages/system/reloader/patches/deployment-startup-probe.diffpackages/system/reloader/tests/deployment_startup_probe_test.yamlpackages/system/reloader/values.yaml
| exit 1; \ | ||
| fi | ||
|
|
||
| update: clean reloader-update |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Makefile =="
if [ -f packages/system/reloader/Makefile ]; then
cat -n packages/system/reloader/Makefile
else
echo "missing packages/system/reloader/Makefile"
fi
echo
echo "== reloader-update target references =="
rg -n "reloader-update|charts/|patches/|clean:" -S .Repository: cozystack/cozystack
Length of output: 50376
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== find relco/Makefile and relevant package.mk =="
fd -a '(^|/)Makefile$|package\.mk$' . | sed 's#^\./##' | rg 'packages/system/reloader/Makefile|hack/package.mk|package.mk' || true
echo
echo "== package.mk excerpt =="
if [ -f hack/package.mk ]; then
sed -n '1,140p' hack/package.mk | cat -n
fi
echo
echo "== reloader Makefile exact section =="
sed -n '31,80p' packages/system/reloader/Makefile | cat -nRepository: cozystack/cozystack
Length of output: 2537
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== check GNU make availability and behavior for prerequisite ordering =="
if command -v make >/dev/null 2>&1; then
make --version | head -n 1
mkdir -p /tmp/make-dir-race-check && cd /tmp/make-dir-race-check
rm -rf charts
printf 'clean:\n\trm -rf charts\n\nreloader-update:\n\tmkdir -p charts\n\techo created charts during reloader-update >> status\n\nupdate: clean reloader-update\n\techo updated after prerequisites >> status\n' > Makefile
echo "parallel run:"
make update -j4 2>&1 || true
if [ -e charts ]; then echo "post-update charts exists"; fi
if [ -e status ]; then cat status; fi
printf 'clean:\n\trm -rf charts\n\nreloader-updatedepended: clean\n\tmkdir -p charts\n\techo created charts after clean >> status\n\nupdate: reloader-updatedepended\n\techo patched using chart >> status\n' > Makefile
echo "serialized dependency run:"
rm -rf charts status
make update -j4 2>&1 || true
if [ -e status ]; then cat status; fi
else
echo "make unavailable"
fiRepository: cozystack/cozystack
Length of output: 247
Serialize clean before reloader-update.
update: clean reloader-update allows make -j to run sibling prerequisite recipes concurrently; clean’s rm -rf charts/ can race with the parallel % -update recipe that populates charts/, so the later patch can see a partial vendored tree. Run reloader-update from the recipe after clean.
Proposed fix
-update: clean reloader-update
+update: clean
+ $(MAKE) reloader-update
# Re-apply local modifications carried as patches/*.diff. clean runs first,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| update: clean reloader-update | |
| update: clean | |
| $(MAKE) reloader-update |
🧰 Tools
🪛 checkmake (0.3.2)
[warning] 31-31: Target body for "update" exceeds allowed length of 5 lines (8).
(maxbodylength)
[warning] 31-31: Required target "all" is missing from the Makefile.
(minphony)
[warning] 31-31: Required target "clean" is missing from the Makefile.
(minphony)
[warning] 31-31: Required target "test" must be declared PHONY.
(minphony)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/system/reloader/Makefile` at line 31, Update the Makefile’s update
target so it depends only on clean, then invoke reloader-update from the update
recipe after clean completes. Preserve the existing clean and reloader-update
behavior while ensuring parallel make cannot run them concurrently.
| diff --git a/charts/reloader/templates/deployment.yaml b/charts/reloader/templates/deployment.yaml | ||
| --- a/charts/reloader/templates/deployment.yaml | ||
| +++ b/charts/reloader/templates/deployment.yaml | ||
| @@ -161,6 +161,17 @@ | ||
| ports: | ||
| - name: http | ||
| containerPort: 9090 | ||
| + {{- /* | ||
| + cozystack (patches/deployment-startup-probe.diff): the upstream chart | ||
| + renders no startupProbe, which leaves a slow first start no grace period | ||
| + of its own. Why that matters and how the budget was chosen is documented | ||
| + next to the knob, in the package values.yaml. Rendered with `with`, so an | ||
| + unset value emits nothing and the template stays upstream-compatible. | ||
| + */}} | ||
| + {{- with .Values.reloader.deployment.startupProbe }} | ||
| + startupProbe: | ||
| + {{- toYaml . | nindent 10 }} | ||
| + {{- end }} |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Keep the vendored chart verbatim.
This patch intentionally reintroduces a local change under packages/**/charts/**, so the vendored tree is no longer an upstream copy. Move probe injection to a repository-owned overlay or post-rendering mechanism instead.
packages/system/reloader/patches/deployment-startup-probe.diff#L16-L33: remove the template-mutating patch.packages/system/reloader/Makefile#L31-L39: stop applying the local patch during chart updates.packages/system/reloader/charts/reloader/templates/deployment.yaml#L164-L174: restore the upstream template verbatim.
As per coding guidelines, “Do not edit vendored upstream chart files directly; they must remain verbatim because make update regenerates them.”
📍 Affects 3 files
packages/system/reloader/patches/deployment-startup-probe.diff#L16-L33(this comment)packages/system/reloader/Makefile#L31-L39packages/system/reloader/charts/reloader/templates/deployment.yaml#L164-L174
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/system/reloader/patches/deployment-startup-probe.diff` around lines
16 - 33, Remove the startupProbe template mutation from
packages/system/reloader/patches/deployment-startup-probe.diff (lines 16-33),
stop applying that patch in packages/system/reloader/Makefile (lines 31-39), and
restore the upstream deployment template verbatim in
packages/system/reloader/charts/reloader/templates/deployment.yaml (lines
164-174). Use a repository-owned overlay or post-rendering mechanism for probe
injection instead.
Source: Coding guidelines
Andrei Kvapil (kvaps)
left a comment
There was a problem hiding this comment.
LGTM — verified. The root cause is real: reloader binds its :9090 listener as the last statement of startReloader, after an eager package-level isOpenshift() GET issued on a no-deadline context before main(), so a slow first start under load is refused and killed by liveness (~60s), and the restart re-runs the prologue from scratch — a self-sustaining loop. The startupProbe (periodSeconds 5 × failureThreshold 45 ≈ 225s, well inside the 10m install timeout) gives the grace; the template is guarded by with so it stays upstream-compatible, carried as a patch with a version-pin check and a helm unittest. The red E2E is the known node-join flake.
…w first start under load (#4433) Backport of #3470 to `release-1.6`. On the E2E run of #4052 install failed because reloader crash-looped (6 restarts) and `failed early due to stalled resources: [Deployment/cozy-reloader/reloader-reloader status: 'Failed']` blocked the whole linstor chain behind it. Same shape as what #3470 fixes on main, slow first start under load killed by the probes. This one changes the chart, not only CI: reloader gets a startupProbe. Cherry-picked clean with `-x`. ### Testing - `make unit-tests` green (includes the reloader helm unit tests from #3470), POSIX sh sweep clean. ```release-note fix(reloader): add startupProbe to survive slow first start under load ```
What this PR does
Adds a startup probe to the reloader deployment, rendered from package values through a patch on the vendored chart.
Reloader went into CrashLoopBackOff on a loaded install. Events show
Liveness probe failed: ... connect: connection refused, repeating on the liveness cadence until the kubelet gave up.A refusal rather than a timeout closes two questions at once. It is an RST from the target netns, so the packet arrived and the pod network is fine. It also rules out the mode in stakater/Reloader#252, where the listener is already up and
/metricsjust answers slowly; that one produces probe timeouts. Nothing was listening on:9090yet.http.ListenAndServeis the last statement ofstartReloader, so the whole prologue runs before the socket opens. The slow part is not the informers, those are lazy and do their List in a goroutine. It isIsOpenshift = isOpenshift()inpkg/kube: a package-level var, so it runs at init beforemain(), and it does aGET /apis/project.openshift.ioon acontext.TODO()carrying no deadline. The latency is the API server's, and the container waits it out under a 100m CPU limit.Without a startup probe the only grace period is the liveness budget itself,
initialDelaySeconds 10 + periodSeconds 10 * failureThreshold 5. A start that overruns it gets killed mid-prologue, and the restart re-runs the prologue from the top against an API server that is no less busy, so the loop feeds itself.This reaches past reloader.
packages/core/platform/sources/linstor.yamllistscozystack.reloaderin the LINSTOR stack'sdependsOn, anddependsOnfans out to every component of that stack, so a reloader stuck in this loop blocks that whole install.The probe is
/liveatperiodSeconds 5andfailureThreshold 45, about 225s, well inside the 10m install timeout that cozystack-operator stamps on generated HelmReleases. Liveness is suspended while it runs and gets its tight budget back as soon as the listener binds.timeoutSecondsis 5 to match the other two probes instead of the kubelet default of 1s, since a throttled process at 100m can hold the socket open and still need more than a second to answer.Both liveness knobs the chart already exposes would work without any patch. Widening
failureThresholdbuys the grace permanently and loosens hang detection for the life of the container.initialDelaySecondsis start-up-only and would genuinely do, but it is a fixed blind window on every start, including one that binds in three seconds, and nothing restarts a container that wedges inside it. Adaptive against fixed is the trade. The win is smaller than it sounds whilereloader.enableHAis unset, since/liveis a constant 200 then and liveness only catches the listener dying outright.A chart bump does not fix this. 2.2.14 still renders no
startupProbeanywhere, and v1.4.19 added a second eager package-level call next toIsOpenshift, so the prologue got longer instead of shorter.packages/system/veleroalready carries a startupProbe patch on its vendored chart for the same reason.make updatenow drives the shared%-updaterule with an exactCHART_VERSION. It has to be exact. With a stale local repo cachehelm pull --version 2.0.0fails with version-not-found, while--version "^2.0.0"quietly resolves to 2.2.14, whosedeployment.yamlhas the ports block at line 179 instead of 161, so the hunk would fuzz or land somewhere unintended.The patch runs with
--forwardbecausepatch(1)defaults diverge between implementations over a tree that already carries the change. GNU 2.7.6 refuses and exits 1. Apple 2.0-12u11 takes itsAssume -R? [y]default, strips the hunk back out and exits 0. GNU under--batchdoes the same, which is the trap, since--batchis what you reach for to make a script non-interactive.--forwardrefuses in all three and leaves a.rej.Two things keep the probe from disappearing quietly, and they fire at different times.
--forwardcatches it at the moment someone re-runs the patch by hand. The test only catches it at the next run of the suite, late enough for a commit to travel in between.make testalso checks that the vendoredChart.yamlstill records the pinned version, so bumpingCHART_VERSIONwithout re-runningmake updatefails instead of leaving the pin and the tree disagreeing. Checked by doing the violation and watching it go red.Screenshots
Not applicable, no UI change.
Downstream repositories
Walked the trigger map in
docs/agents/contributing.mdagainst the diff. All five files sit inside onepackages/systempackage. No package is added, renamed or removed, nothing underhack/is touched, and no CRD, namespace,packages/corevalues key, version enum or release asset changes.Release note
Summary by CodeRabbit
New Features
Bug Fixes
Tests