fix(validator): bound absent-resource retries in health checks of deployment validation#1324
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR adds validator client QPS/Burst defaults and a 30s AbsentResourceGracePeriod; implements helpers to classify NotFound and choose substantive vs fallback errors with a NotFound-specific deadline gate; updates assert/error retry loops and the runner to preserve substantive failures on timeout/cancellation and to apply the absent-resource grace window; and rebuilds the typed Kubernetes client with raised QPS/Burst. Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
validators/chainsaw/inprocess.go (1)
164-185:⚠️ Potential issue | 🟠 Major | ⚡ Quick winGrace window is anchored too early and can prematurely fail transient recreate flows.
absentDeadlineis fixed at function start (Line 164), then any laterErrCodeNotFounduses that stale cutoff (Line 184/Line 281). If a resource appears (or returns non-NotFound) and later briefly disappears during rollout, this path can fail immediately even though the resource is not “entirely absent.”
Please make the grace window stateful (only for consecutive initial NotFound, or reset/disable grace after first non-NotFound). Apply the same fix pattern invalidators/chainsaw/runner.goLine 169-193.Also applies to: 280-285
🤖 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 `@validators/chainsaw/inprocess.go` around lines 164 - 185, The current loop anchors absentDeadline at function start (absentDeadline := time.Now().Add(defaults.AbsentResourceGracePeriod)) which causes notFoundGraceDeadline(lastErr, absentDeadline, deadline) to treat later transient NotFound errors as still within the original grace window; change the logic to set/clear the absentDeadline only when you observe a NotFound result and to reset/disable it when you observe the first non-NotFound (i.e., make the grace window stateful): remove the fixed absentDeadline initialization, introduce a nil/zero absentDeadline variable updated inside the loop when evaluateAssert returns an ErrCodeNotFound, and clear/zero that variable when evaluateAssert returns any non-NotFound error so subsequent notFoundGraceDeadline calls use the updated state; apply the same change to the analogous loop in validators/chainsaw/runner.go (the block around evaluateAssert/evaluate and notFoundGraceDeadline).
🤖 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.
Outside diff comments:
In `@validators/chainsaw/inprocess.go`:
- Around line 164-185: The current loop anchors absentDeadline at function start
(absentDeadline := time.Now().Add(defaults.AbsentResourceGracePeriod)) which
causes notFoundGraceDeadline(lastErr, absentDeadline, deadline) to treat later
transient NotFound errors as still within the original grace window; change the
logic to set/clear the absentDeadline only when you observe a NotFound result
and to reset/disable it when you observe the first non-NotFound (i.e., make the
grace window stateful): remove the fixed absentDeadline initialization,
introduce a nil/zero absentDeadline variable updated inside the loop when
evaluateAssert returns an ErrCodeNotFound, and clear/zero that variable when
evaluateAssert returns any non-NotFound error so subsequent
notFoundGraceDeadline calls use the updated state; apply the same change to the
analogous loop in validators/chainsaw/runner.go (the block around
evaluateAssert/evaluate and notFoundGraceDeadline).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Enterprise
Run ID: 90f66859-9279-41f1-b516-9674ced6b783
📒 Files selected for processing (6)
pkg/defaults/k8s.gopkg/defaults/timeouts.govalidators/chainsaw/grace_test.govalidators/chainsaw/inprocess.govalidators/chainsaw/runner.govalidators/context.go
7c5bcb5 to
2388e4d
Compare
|
@coderabbitai Addressed in 2388e4d. Good catch on the grace window being anchored too early. Made the grace stateful: a Applied the same pattern in both |
|
Tip For best results, initiate chat on the files or code changes.
(ᵔ◡ᵔ)🐇 |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
validators/chainsaw/runner.go (1)
169-201:⚠️ Potential issue | 🟠 Major | ⚡ Quick winNotFound grace is bypassed on raw-resource asserts because fetch errors are re-coded to internal.
At Line 201, the new grace path depends on
isNotFoundErr(lastErr), butassertSingleDocumentat Line 266 wrapsfetcher.Fetcherrors asErrCodeInternal. That clobbersErrCodeNotFound, so absent resources in raw YAML mode won’t hit the 30s grace and will continue using the full timeout.💡 Proposed fix (preserve structured codes from fetcher)
diff --git a/validators/chainsaw/runner.go b/validators/chainsaw/runner.go @@ actual, err := fetcher.Fetch(ctx, apiVersion, kind, namespace, name) if err != nil { - return errors.Wrap(errors.ErrCodeInternal, - fmt.Sprintf("failed to fetch %s %s/%s", kind, namespace, name), err) + return errors.PropagateOrWrap( + err, + errors.ErrCodeInternal, + fmt.Sprintf("failed to fetch %s %s/%s", kind, namespace, name), + ) }Based on learnings, structured errors should be propagated as-is to preserve their codes, and this grace logic depends on preserving
ErrCodeNotFound.🤖 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 `@validators/chainsaw/runner.go` around lines 169 - 201, The NotFound-grace logic in notFoundGraceDeadline is bypassed because assertSingleDocument currently re-wraps errors from fetcher.Fetch as ErrCodeInternal, which destroys ErrCodeNotFound; update assertSingleDocument so it preserves and returns structured errors from fetcher.Fetch (i.e., if the fetch error already carries a structured error code such as ErrCodeNotFound, return it as-is) and only wrap truly unknown/non-structured errors as ErrCodeInternal; this will ensure assertAllDocuments sees the original ErrCodeNotFound (so isNotFoundErr(lastErr) and the absentDeadline logic work correctly).Source: Learnings
🤖 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.
Outside diff comments:
In `@validators/chainsaw/runner.go`:
- Around line 169-201: The NotFound-grace logic in notFoundGraceDeadline is
bypassed because assertSingleDocument currently re-wraps errors from
fetcher.Fetch as ErrCodeInternal, which destroys ErrCodeNotFound; update
assertSingleDocument so it preserves and returns structured errors from
fetcher.Fetch (i.e., if the fetch error already carries a structured error code
such as ErrCodeNotFound, return it as-is) and only wrap truly
unknown/non-structured errors as ErrCodeInternal; this will ensure
assertAllDocuments sees the original ErrCodeNotFound (so isNotFoundErr(lastErr)
and the absentDeadline logic work correctly).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Enterprise
Run ID: a94c9af4-9adf-4368-a1a0-18dc1b31542e
📒 Files selected for processing (7)
pkg/defaults/k8s.gopkg/defaults/timeouts.govalidators/chainsaw/grace_test.govalidators/chainsaw/inprocess.govalidators/chainsaw/runner.govalidators/chainsaw/substantive_test.govalidators/context.go
2388e4d to
8b9cc20
Compare
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
validators/chainsaw/runner.go (1)
265-268:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPreserve
Fetcherror codes on the raw-resource path.
assertRawResources()now decides both the short absent-resource grace and the final surfaced failure code fromlastErr, but this wrap collapses everyfetcher.Fetch()failure intoErrCodeInternal. That means raw YAML asserts never hit the newErrCodeNotFoundfast-fail path, and transientErrCodeUnavailablefetch failures are also misclassified as internal failures. In practice, raw-resource checks still burn the full timeout and can reportotherinstead of the intended structured failure.Suggested fix
actual, err := fetcher.Fetch(ctx, apiVersion, kind, namespace, name) if err != nil { - return errors.Wrap(errors.ErrCodeInternal, - fmt.Sprintf("failed to fetch %s %s/%s", kind, namespace, name), err) + return errors.PropagateOrWrap(err, errors.ErrCodeInternal, + fmt.Sprintf("failed to fetch %s %s/%s", kind, namespace, name)) }Based on learnings, errors that already carry the correct structured code should be propagated as-is, or via
errors.PropagateOrWrap(...), instead of being re-wrapped under a new code.🤖 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 `@validators/chainsaw/runner.go` around lines 265 - 268, The current wrap in the fetch path (inside the block handling fetcher.Fetch in runner.go) always converts any fetch error into ErrCodeInternal which prevents assertRawResources() from seeing original codes like ErrCodeNotFound or ErrCodeUnavailable; change the error handling so that errors returned by fetcher.Fetch (used by functions like assertRawResources and assigned to lastErr) preserve their structured code—either by using errors.PropagateOrWrap(...) with the descriptive message or by checking the error's code and returning it directly instead of wrapping into ErrCodeInternal—so existing ErrCodeNotFound/ErrCodeUnavailable values flow through unchanged while still annotating the failure with the "failed to fetch <kind> <namespace>/<name>" context.Source: Learnings
🤖 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.
Outside diff comments:
In `@validators/chainsaw/runner.go`:
- Around line 265-268: The current wrap in the fetch path (inside the block
handling fetcher.Fetch in runner.go) always converts any fetch error into
ErrCodeInternal which prevents assertRawResources() from seeing original codes
like ErrCodeNotFound or ErrCodeUnavailable; change the error handling so that
errors returned by fetcher.Fetch (used by functions like assertRawResources and
assigned to lastErr) preserve their structured code—either by using
errors.PropagateOrWrap(...) with the descriptive message or by checking the
error's code and returning it directly instead of wrapping into
ErrCodeInternal—so existing ErrCodeNotFound/ErrCodeUnavailable values flow
through unchanged while still annotating the failure with the "failed to fetch
<kind> <namespace>/<name>" context.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Enterprise
Run ID: 73d5cce9-985b-4b34-8f1a-260e0748f656
📒 Files selected for processing (7)
pkg/defaults/k8s.gopkg/defaults/timeouts.govalidators/chainsaw/grace_test.govalidators/chainsaw/inprocess.govalidators/chainsaw/runner.govalidators/chainsaw/substantive_test.govalidators/context.go
8b9cc20 to
d9a91e2
Compare
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
validators/chainsaw/runner.go (1)
173-205:⚠️ Potential issue | 🟠 Major | ⚡ Quick winFail fast on terminal assertion-spec errors in the raw-resource retry loop.
assertSingleDocumentnow classifies malformed/non-evaluable checks asErrCodeInvalidRequest, butassertRawResourcesstill retries until timeout instead of exiting immediately on terminal errors. This can unnecessarily consumeChainsawMaxParallelslots and prolong runs.Suggested fix
for { lastErr = assertAllDocuments(ctx, docs, fetcher) if lastErr == nil { result.Passed = true slog.Info("health check passed", "component", ca.Name) return result } + if isTerminalAssertErr(lastErr) { + reason := preferSubstantiveErr(lastSubstantiveErr, lastErr) + result.Output = reason.Error() + result.Error = reason + slog.Warn("health check failed", "component", ca.Name, "error", reason) + return result + } // Record the failure seen while the context is still live; after // cancellation assertAllDocuments returns a context / rate-limiter🤖 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 `@validators/chainsaw/runner.go` around lines 173 - 205, After assertAllDocuments returns a terminal assertion-spec error (e.g. ErrCodeInvalidRequest produced by assertSingleDocument), we should stop retrying and free the ChainsawMaxParallel slot instead of looping to timeout; detect the terminal code (use the same error-code helper you have for assertions, e.g. compare assertionErrorCode(lastErr) to ErrCodeInvalidRequest or add an isTerminalAssertionErr helper), and when true set the result as a clean failure (populate the result/Error or result.Message similarly to other failure paths), log the terminal error, and return immediately from the loop (or break to the same exit path) inside the assertRawResources retry loop that calls assertAllDocuments so retries do not continue on non-retryable assertion errors.
🤖 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.
Outside diff comments:
In `@validators/chainsaw/runner.go`:
- Around line 173-205: After assertAllDocuments returns a terminal
assertion-spec error (e.g. ErrCodeInvalidRequest produced by
assertSingleDocument), we should stop retrying and free the ChainsawMaxParallel
slot instead of looping to timeout; detect the terminal code (use the same
error-code helper you have for assertions, e.g. compare
assertionErrorCode(lastErr) to ErrCodeInvalidRequest or add an
isTerminalAssertionErr helper), and when true set the result as a clean failure
(populate the result/Error or result.Message similarly to other failure paths),
log the terminal error, and return immediately from the loop (or break to the
same exit path) inside the assertRawResources retry loop that calls
assertAllDocuments so retries do not continue on non-retryable assertion errors.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Enterprise
Run ID: a63d40ba-3105-4eeb-b214-030fc87a3adf
📒 Files selected for processing (7)
pkg/defaults/k8s.gopkg/defaults/timeouts.govalidators/chainsaw/grace_test.govalidators/chainsaw/inprocess.govalidators/chainsaw/runner.govalidators/chainsaw/substantive_test.govalidators/context.go
|
@coderabbitai The raw-resource path finding (assertSingleDocument collapsing Fetch errors to ErrCodeInternal) is fixed in d9a91e2.
Added |
|
Tip For best results, initiate chat on the files or code changes.
|
The expected-resources deployment validator retried entirely-absent resources for the full ChainsawAssertTimeout (6m), saturating the ChainsawMaxParallel worker pool, starving healthy components, and pushing the check past the Job deadline so it reported an opaque "other" instead of a clean failure. - Add AbsentResourceGracePeriod (30s) bounding only the NotFound case; not-ready (shape mismatch) and transient (Unavailable) keep the full 6m. - Make the grace stateful: once any non-NotFound result is observed (resource exists / transient error), disable the grace so a later transient NotFound (e.g. a pod recreate mid-rollout) keeps the full readiness budget instead of fast-failing on the stale grace window. - Preserve the substantive assertion error at the deadline so the verdict is a clean failed with the real reason, not a context-cancellation wrap. - Raise validator REST client QPS/Burst (50/100) above the client-go defaults so the multi-component enumeration sweep is not throttled. - Tests: preferSubstantiveErr, notFoundGraceDeadline (incl. recreate), and a canceled-context retry that asserts the substantive error surfaces. Fixes NVIDIA#1323
d9a91e2 to
1000736
Compare
Summary
Fix the deployment validator's
expected-resourcescheck so it fails fast and cleanly when a recipe-declared resource is absent or in a different namespace than the recipe expects — instead of retrying to the Job deadline and reporting an opaqueother.Fixes #1323
Problem
On a cluster where a recipe component is absent or misplaced,
expected-resourcesretried the missing resource for the fullChainsawAssertTimeout(6m). WithChainsawMaxParallel = 4worker slots, a few absent components starved the healthy ones and pushed the check past the Job'sactiveDeadlineSeconds; the pod was killed and the result surfaced asstatus=other(~8m) with a misleadingclient rate limiter Wait … context deadline exceededmessage rather than an actionable failure.Changes
pkg/defaults.AbsentResourceGracePeriod = 30s): bound only the entirely-absent (ErrCodeNotFound) case. A not-ready resource (shape mismatch →ErrCodeInternal) and a transient API failure (ErrCodeUnavailable) keep the full readiness budget, so slow-but-healthy rollouts are not failed prematurely.ErrCodeInternal), so a later transientNotFound(e.g. a pod recreate mid-rollout) keeps the full deadline. A transientErrCodeUnavailabledoes not latch (it proves nothing about existence), so a cleanNotFoundafter an API blip still gets the fast-fail.ErrCodeInternalcontext-cancellation/rate-limiter artifact — so the verdict is a cleanfailedwith the real cause, not an opaqueother.assertSingleDocumentnow propagates the fetcher's structured code (NotFound/Unavailable) instead of blanket-wrapping asErrCodeInternal, so the grace/latch logic works identically in both the in-process and raw paths.QPS 50 / Burst 100) above the client-go defaults so the multi-component enumeration sweep is not throttled. apiserver APF still protects the server.Tests
preferSubstantiveErr,notFoundGraceDeadline(incl. the recreate case),resourceObservedErr(incl.Unavailabledoes-not-latch),assertSingleDocumentpropagates the structured code, and a canceled-context retry that asserts the substantive error is surfaced. Build, lint, and-racetests pass.Verified on live clusters
failedin 1m26s, clean reason (Nodewright tuning: not found …)Same divergent GB300 cluster, before vs after: 8m24s → 1m26s,
other→failedwith the real cause.Risk
NotFound) case; not-ready and transient paths keep their full timeout. Easy to revert.