Skip to content

Issue #10437: derive velero server's graceful shutdown timeout from terminationGracePeriodSeconds - #10438

Open
adonispd wants to merge 8 commits into
velero-io:mainfrom
adonispd:gh-10437-graceful-shutdown-timeout
Open

adonispd wants to merge 8 commits into
velero-io:mainfrom
adonispd:gh-10437-graceful-shutdown-timeout

Conversation

@adonispd

@adonispd adonispd commented Aug 28, 2026 •

Copy link
Copy Markdown

Summary

velero server builds its controller-runtime manager without setting GracefulShutdownTimeout, so it always inherits controller-runtime's hardcoded 30s default, regardless of how high an operator raises the pod's terminationGracePeriodSeconds. On node disruption (Karpenter drift/consolidation, cluster-autoscaler scale-down, cloud-provider node maintenance) the pod is deleted directly rather than replaced through a rollout, so the manager force-exits after only 30s, cutting off any in-flight backup/restore reconcile and leaving it Failed or PartiallyFailed.

This PR makes velero server derive its shutdown timeout automatically from the pod's own terminationGracePeriodSeconds (minus a safety buffer), falling back to today's 30s whenever derivation isn't possible, while still allowing an explicit override:

  • Adds --graceful-shutdown-timeout to override the derived value explicitly; an explicit 0 or negative value is rejected at flag-parse time.
  • Adds --graceful-shutdown-safety-buffer (default 10s) to control the buffer subtracted from terminationGracePeriodSeconds during derivation.
  • Adds the POD_NAME Downward API env var to the velero install-generated Deployment, so the self pod lookup used for derivation works for non-Helm installs too.
  • Documents the new flags and behavior on site/content/docs/main/customize-installation.md, and adds a changelog fragment.

Closes #10437.

Not addressed by this PR (tracked separately):

  • #9036 — a replacement pod marking an in-flight backup/restore Failed on startup.
  • #9062 — multi-replica / leader election.
  • The official Helm chart's Deployment template doesn't yet set POD_NAME, so Helm-based installs will hit the 30s fallback until that chart change merges. Tracked as vmware-tanzu/helm-charts#749, fixed by vmware-tanzu/helm-charts#748, sequenced to merge after this PR.

Test plan

  • go test ./pkg/cmd/server/... ./pkg/install/... — all pass, no regressions.
  • New tests added:
    • TestGracefulShutdownTimeoutFlag / TestGracefulShutdownSafetyBufferFlag (pkg/cmd/server/config/config_test.go) — flag defaults, valid explicit values, and rejection of 0/negative values.
    • TestResolveGracefulShutdownTimeout (pkg/cmd/server/server_test.go) — covers explicit-flag precedence, successful derivation from the pod spec, POD_NAME unset fallback, terminationGracePeriodSeconds unset fallback, and underflowed-derivation fallback.
    • TestDeployment (pkg/install/deployment_test.go) — updated for the new POD_NAME env var.

…ut from terminationGracePeriodSeconds

velero server built its controller-runtime manager without setting
GracefulShutdownTimeout, so it always inherited controller-runtime's
hardcoded 30s default regardless of how high an operator raised the
pod's terminationGracePeriodSeconds. On node disruption (Karpenter
drift/consolidation, cluster-autoscaler scale-down, node maintenance)
the manager force-exited after only 30s, cutting off in-flight
backup/restore reconciles.

This derives the manager's shutdown timeout automatically from the
server's own pod's terminationGracePeriodSeconds (minus a safety
buffer), falling back to the historical 30s whenever derivation isn't
possible, while still allowing an explicit override:

- Add --graceful-shutdown-timeout to override the derived value
  explicitly; an explicit 0 or negative value is rejected at flag-parse
  time.
- Add --graceful-shutdown-safety-buffer (default 10s) to control the
  buffer subtracted from terminationGracePeriodSeconds during
  derivation.
- Add the POD_NAME Downward API env var to the velero install-generated
  Deployment so the self pod lookup works for non-Helm installs too.
- Document the new flags and add a changelog fragment.

Signed-off-by: Adonis Dutra <adonis.dutra@outsystems.com>
Signed-off-by: AdonisPD <adonis.dutra@outsystems.com>
@adonispd
adonispd requested a review from a team as a code owner August 28, 2026 18:48
@netlify

netlify Bot commented Aug 28, 2026 •

Copy link
Copy Markdown

👷 Deploy request for velero pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit ebfaa07

@codecov

codecov Bot commented Aug 28, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.36620% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/cmd/server/server.go 86.20% 3 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

Comment thread pkg/cmd/server/server.go
return defaultGracefulShutdownTimeout
}

derived := terminationGracePeriod - cfg.GracefulShutdownSafetyBuffer

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If derived is the default 30, wouldn't this forcibly shrink it to 20 effectively?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch — fixed in 6d637fe: now short-circuits to the 30s default whenever terminationGracePeriodSeconds <= 30s, so default installs no longer shrink.

Comment thread pkg/cmd/server/server.go
return defaultGracefulShutdownTimeout
}

return derived

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A logline to note the derived value may be helpful:

logger.WithFields(logrus.Fields{
    "gracefulShutdownTimeout": derived,
    "terminationGracePeriod": terminationGracePeriod,
    "safetyBuffer": cfg.GracefulShutdownSafetyBuffer,
}).Info("Resolved graceful shutdown timeout")

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added, thanks — logs gracefulShutdownTimeout, terminationGracePeriod, and safetyBuffer at Info level in 6d637fe.

Comment thread pkg/cmd/server/config/config.go Outdated
CredentialsDirectory: credentials.DefaultStoreDirectory(),
ItemBlockWorkerCount: DefaultItemBlockWorkerCount,
ConcurrentBackups: DefaultConcurrentBackups,
GracefulShutdownSafetyBuffer: defaultGracefulShutdownSafetyBuffer,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

validation to ensure this field is non-negative could be needed
otherwise we end up with a higher value that actual (3600 - (-30)) = 3630 which is higher that k8s value

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed — --graceful-shutdown-safety-buffer now rejects negative values via a custom pflag.Value, same pattern as --graceful-shutdown-timeout.

…imeout

- Don't shrink the derived timeout below defaultGracefulShutdownTimeout
  when terminationGracePeriodSeconds is at or below Kubernetes' own
  30s default, preserving today's behavior for installs that never
  set it (NFR2).
- Reject a negative --graceful-shutdown-safety-buffer, which would
  otherwise inflate the derived timeout past terminationGracePeriodSeconds.
- Log the resolved graceful shutdown timeout, termination grace period,
  and safety buffer at Info level.
- Rename the changelog fragment to match the actual PR number (10438).
- Add test coverage for all of the above.

Signed-off-by: AdonisPD <adonis.dutra@outsystems.com>
@adonispd
adonispd requested a review from Joeavaikath August 29, 2026 20:34
Joeavaikath
Joeavaikath previously approved these changes Aug 31, 2026

@kaovilai kaovilai left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Avoid using the entire terminationGracePeriodSeconds as the application’s shutdown timeout. Reserve a safety margin and account for any preStop hook: the termination grace-period countdown begins before the hook runs, and SIGTERM is sent only after the hook completes. Both therefore consume the same budget, after which Kubernetes may forcibly kill the container.

The current safety-buffer approach is appropriate, but the documentation should clarify that operators with a preStop hook must configure the buffer to cover the hook’s maximum duration plus a small final shutdown margin.

Comment thread pkg/cmd/server/server.go Outdated
Comment on lines +478 to +482
if terminationGracePeriod <= defaultGracefulShutdownTimeout {
// Subtracting the safety buffer here would shrink the timeout below today's
// hardcoded default for installs that never set terminationGracePeriodSeconds
// (Kubernetes itself defaults it to 30s), which would be a behavior change (NFR2).
return defaultGracefulShutdownTimeout

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The derived timeout does not always preserve a safety margin.

When terminationGracePeriodSeconds <= 30s, this branch returns a full 30-second manager timeout:

For a 30-second Pod grace period, that consumes the entire termination budget; below 30 seconds, it exceeds it. Kubernetes may therefore send SIGKILL before controller-runtime finishes waiting.

Also, a preStop hook runs before SIGTERM and consumes the same termination grace period. The current derivation does not inspect the hook, so the default 10-second buffer may be insufficient when one is configured.

Could we ensure the resolved timeout is always strictly below the Pod’s grace period? At minimum, the documentation should state that --graceful-shutdown-safety-buffer must cover the maximum preStop duration plus a final safety margin. The explicit timeout override should carry the same warning because it can equal or exceed the Pod’s total budget.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Hey @kaovilai, really appreciate the careful read here — this caught a real gap. I pushed a fix in the latest commit that I'd love your eyes on:

  • Removed the terminationGracePeriodSeconds <= 30s → return 30s short-circuit entirely. resolveGracefulShutdownTimeout now uses one uniform formula for every case, derived = terminationGracePeriodSeconds - safetyBuffer, with no exceptions — including pods at Kubernetes' own 30s default, which are no longer exempt. So the resolved timeout should now always be strictly below the pod's grace period, not just "never above" it.
  • Lowered --graceful-shutdown-safety-buffer's default from 10s to 2s, so that change stays small for existing installs (30s → 28s instead of 30s → 20s).
  • The derived <= 0 fallback (buffer doesn't fit within terminationGracePeriodSeconds at all) now returns terminationGracePeriodSeconds itself with zero margin, instead of a flat 30s — so it won't discard a grace period an operator deliberately raised above 30s just because the buffer happened not to fit.

I also went and checked the Kubernetes container lifecycle hooks docs, and they back up your point nicely: the grace period countdown starts before preStop runs, and its execution time counts against terminationGracePeriodSeconds rather than being additional to it — Kubernetes' own worked example (60s grace period, 55s hook, 10s app shutdown → killed before finishing) is pretty much the failure mode you described. One thing worth flagging, in case it's useful context: --graceful-shutdown-safety-buffer itself isn't a Kubernetes-documented mechanism as such — the k8s docs describe the operator's responsibility to size terminationGracePeriodSeconds generously, but don't have a notion of the app subtracting its own buffer. Ours is a Velero-side defensive margin for the "last mile" after controller-runtime's manager gives up waiting on reconciles (process exit, log flush, scheduling jitter), rather than something Kubernetes itself prescribes — I added a note about that distinction in customize-installation.md so it's not misread as an official guideline.

One question I'd genuinely like your take on: should we also try to read the preStop hook itself (e.g. Sleep.Seconds off the pod spec) and fold its duration into the derivation automatically? I'm currently leaning toward not doing that, mainly because:

  • Kubernetes doesn't seem to model this as something an app introspects — the convention is that SIGTERM arrival is the "start shutting down" signal, and sizing the budget around a preStop hook reads more like the operator's job via the pod spec than the app's at runtime.
  • Sleep is the only hook type with a bounded duration in the API; Exec/HTTPGet don't have one, so auto-detection would only ever cover a fraction of real configurations.
  • Before this PR, velero server had zero awareness of terminationGracePeriodSeconds or preStop at all, so documenting the buffer's role (which I've now done) already feels like a solid improvement over the status quo.

But I hold that opinion loosely — if you think auto-detecting Sleep is worth the added complexity, happy to explore it further.

Thanks again for pushing on this, it made the fix noticeably more correct.

… shutdown timeout

- Remove the terminationGracePeriodSeconds <= 30s short-circuit
  entirely. It returned a flat 30s even for pods with a shorter grace
  period, overshooting the pod's own budget. resolveGracefulShutdownTimeout
  now uses one uniform formula for every case, including pods at
  Kubernetes' own 30s default, so the resolved timeout is always
  strictly below the pod's grace period.
- Lower --graceful-shutdown-safety-buffer's default from 10s to 2s, so
  the behavior change for default installs stays small (30s -> 28s
  instead of 30s -> 20s).
- Change the derived<=0 fallback (safety buffer doesn't fit within
  terminationGracePeriodSeconds at all) to return
  terminationGracePeriodSeconds itself with zero margin, instead of a
  flat 30s, so it no longer discards a grace period an operator
  deliberately raised above 30s.
- Document that the safety buffer is a velero-side defensive margin,
  not a Kubernetes-documented mechanism, and note the preStop hook
  interaction kaovilai raised.
- Update test coverage for the new derivation behavior.

Signed-off-by: AdonisPD <adonis.dutra@outsystems.com>
@reasonerjt

Copy link
Copy Markdown
Contributor

Let's target this one to post v1.19 as we are controlling the scope of it to ensure meeting the FC milestone.

adonispd and others added 3 commits September 14, 2026 09:54
…ceful-shutdown-timeout

# Conflicts:
#	pkg/cmd/server/config/config.go
…t names

The Velero.io word list check in the Pull Request Codespell Check
workflow rejects a lowercase " kubernetes ", which two test case names
in TestResolveGracefulShutdownTimeout were tripping.

Signed-off-by: AdonisPD <adonis.dutra@outsystems.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

velero server's shutdown grace period is hardcoded to 30s, ignoring terminationGracePeriodSeconds

4 participants