Skip to content

fix: bound the systemctl retry budget in cse_helpers.sh - #9310

Open
Ganeshkumar Ashokavardhanan (ganeshkumarashok) wants to merge 1 commit into
mainfrom
aganeshkuma/fix-systemctl-retry-budget
Open

fix: bound the systemctl retry budget in cse_helpers.sh#9310
Ganeshkumar Ashokavardhanan (ganeshkumarashok) wants to merge 1 commit into
mainfrom
aganeshkuma/fix-systemctl-retry-budget

Conversation

@ganeshkumarashok

Copy link
Copy Markdown
Contributor

What

systemctlEnableAndStart / systemctlEnableAndStartNoBlock hardcode systemctl_restart 100 5 $timeout $service.

The retry count is not a time bound, and the 30 / 300 a caller passes is only the per-systemctl-call timeout, not a retry budget. systemctlEnableAndStart localdns 30 reads like "give up after 30s"; it can actually run for over an hour.

I measured this with a fast-failing unit:

scenario before after
critical unit fails fast 498s (rc=1) 122s (rc=2, 24 attempts)
optional unit fails fast 498s 10s

DefaultCSETimeout is 900s (pkg/agent/datamodel/const.go). A single bad unit eating 498s of that means downstream provisioning steps never run, and their specific error codes get replaced by a generic CSE timeout — which is strictly worse for diagnosis than failing fast.

This isn't hypothetical. cse_config.sh::configureLocalDNSExporterSocket already carries a per-caller workaround for exactly this problem:

# systemctlEnableAndStartNoBlock would hit its retry loop (~100 retries × 5s) for a missing unit.

The 100 5 values date to #7339 and were never deliberately tuned (#8105 only moved the status logging).

Changes

_systemctl_retry_svc_operation

  • Optional maxBudget (7th arg, default 0 = disabled) — the same convention _retry_file_curl_internal already uses in this file. Caps the per-attempt timeout to the remaining budget and returns 2 when exhausted.
  • Adds the check_cse_timeout guard that every other retry helper here already has, so we exit with a specific code instead of running the CSE window to zero.
  • Bounds the journal dump: journalctl -u $svc --no-pager -n 50. It previously ran unbounded on every failed attempt, so log volume grew quadratically with the retry count.

systemctlEnableAndStart / NoBlock

  • Optional 3rd retries param; applies a 120s budget by default.
  • systemctl enable retries 120 → 3. It only writes symlinks on disk and runs immediately after a successful restart, so a 600s tail cannot help.

Callers — a small retry count for units whose failure is already explicitly non-fatal: mig-partition, node-exporter(+.path), nvidia-dcgm(+-exporter), localdns-exporter.socket, aks-localdns-hosts-setup.timer, aks-log-collector.timer, measure-tls-bootstrapping-latency. There are enough of these that a full 120s budget each would exceed the CSE window on its own.

Worth calling out: ensureMigPartition is documented as "this is expected to fail and work only on next reboot" — and is currently retried for ~500s.

containerd, kubelet, localdns, and nvidia-gridd keep the default budget.

Compatibility

Every new parameter is optional and defaults to today's values, so old and new copies of these scripts interoperate in both directions. systemctl_stop / systemctl_disable are unchanged (budget defaults to 0). cse_helpers.sh and cse_config.sh ship in the same CustomData blob, so the shared constants are versioned together.

Testing

  • make shellspec (docker): 939 examples, 0 failures
  • 9 new examples covering the retry loop, which previously had no direct coverage — every existing caller mocked the helper. Covers: budget trips before retry exhaustion, legacy behavior when no budget is passed, success path, bounded journalctl, arg passthrough, and caller-supplied retry counts.
  • make validate-shell: shellcheck findings byte-identical to main (the gate has pre-existing failures in unrelated files).
  • go test ./pkg/agent/...: ok
  • Wall-clock numbers in the table above measured in the shellspec container.

No testdata regeneration needed — cse_helpers.sh is not embedded in the parser snapshots.

systemctlEnableAndStart/NoBlock hardcoded `systemctl_restart 100 5 $timeout`.
The retry count is not a time bound, and the `30`/`300` a caller passes is only
the per-systemctl-call timeout, not a budget. A unit that fails fast burns
100 * 5s sleeping before giving up; a unit that hangs burns retries * timeout,
which can exceed the 900s DefaultCSETimeout on its own.

Measured with a fast-failing unit: 498s to give up on a single service.

That time is taken out of the CSE window, so downstream provisioning steps
never run and their specific error codes get replaced by a generic CSE
timeout. cse_config.sh::configureLocalDNSExporterSocket already carries a
per-caller workaround for exactly this ("systemctlEnableAndStartNoBlock would
hit its retry loop (~100 retries x 5s) for a missing unit").

Changes:

- _systemctl_retry_svc_operation takes an optional maxBudget (7th arg,
  default 0 = off), matching the convention already used by
  _retry_file_curl_internal. It caps the per-attempt timeout to the
  remaining budget and returns 2 when the budget is exhausted.
- Add the global check_cse_timeout guard that every other retry helper in
  this file already has, so we fail with a specific error code instead of
  running the CSE window down.
- Bound the per-failure journal dump (`--no-pager -n 50`). It ran on every
  failed attempt with no limit, so log volume grew quadratically with retries.
- systemctlEnableAndStart/NoBlock take an optional retry count and apply a
  120s budget: 498s -> 122s for a fast-failing unit.
- Pass a small retry count for units whose failure is already explicitly
  non-fatal (mig-partition, node-exporter, dcgm, localdns-exporter.socket,
  aks-log-collector.timer, ...): 498s -> 10s each. There are enough of these
  that a full budget each would exceed the CSE window on its own. Note
  ensureMigPartition is documented as "expected to fail and work only on next
  reboot", yet retried for ~500s.
- systemctl enable retries 120 -> 3. It only writes symlinks on disk, and it
  runs right after a successful restart, so a 600s tail cannot help.

Every new parameter is optional and defaults to today's values, so old and
new copies of these scripts interoperate. systemctl_stop/systemctl_disable
are unchanged (budget defaults to 0).

Adds shellspec coverage for the retry loop, which previously had none - all
existing callers mocked the helper.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

Windows Unit Test Results

  3 files   13 suites   50s ⏱️
404 tests 404 ✅ 0 💤 0 ❌
407 runs  407 ✅ 0 💤 0 ❌

Results for commit 3b9a7d6.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Bounds systemctl retries during Linux VHD builds and node provisioning.

Changes:

  • Adds operation budgets and bounded journal output.
  • Reduces retries for optional services.
  • Adds ShellSpec coverage for retry behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
cse_helpers.sh Adds retry budgets and tuning constants.
cse_config.sh Applies reduced retries to optional services.
cse_main.sh Limits log collector timer retries.
cse_retry_helpers_spec.sh Tests budgets, passthrough, and journal limits.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +631 to +632
timeout $effectiveTimeout systemctl daemon-reload
timeout $effectiveTimeout systemctl $operation $svcname && return 0
Comment on lines +607 to +608
retries=$1; wait_sleep=$2; timeout=$3 operation=$4 svcname=$5 shouldLogRetryInfo=${6:-false} maxBudget=${7:-0}
opStartTime=$(date +%s)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants