Support Configurable Max Exponential Backoff Delay for Direct Controllers - #13653
lovelace-coder-bot wants to merge 1 commit into
Conversation
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
|
🤖 AI Factory started investigating CI check failures for this pull request. Note: We recommend waiting for the 'ready-for-human' label before leaving review comments. Comments added while the system is actively working may be associated with outdated commits once a new commit is pushed, causing them to be ignored. |
Investigating tests-preview failureRun: 36983352926 Investigating tests-e2e-fixtures-monitoring failureRun: 36983352926 (This report was generated by overseer) |
maqiuyujoyce
left a comment
There was a problem hiding this comment.
Thanks for implementing configurable exponential backoff and retry halting for direct controllers!
Here are the detailed findings and suggested improvements:
1. Add BackoffMaxDelayInSecondsAnnotation to UnderlyingResourceOutOfSyncPredicate
- Issue: In
pkg/controller/predicate/predicate.go, updating annotations on an existing object does not changemetadata.generation. BecauseBackoffMaxDelayInSecondsAnnotationis not checked inUnderlyingResourceOutOfSyncPredicate.Update(), any patch that adds, modifies, or removes this annotation on an existing resource is filtered out by controller-runtime. If retries are halted ("0"), changing or deleting the annotation will never wake up the controller. - Fix: Add the annotation check to
UnderlyingResourceOutOfSyncPredicate.Update()(similar toReconcileIntervalInSecondsAnnotation):if oldValue, newValue := e.ObjectOld.GetAnnotations()[k8s.BackoffMaxDelayInSecondsAnnotation], e.ObjectNew.GetAnnotations()[k8s.BackoffMaxDelayInSecondsAnnotation]; oldValue != newValue { return true }
2. Fix Memory Leak on Resource Deletion in DynamicRateLimiter
- Issue:
failures map[types.NamespacedName]intinpkg/controller/ratelimiter/dynamicratelimiter.goonly clears entries viaForget()on reconciliation success. If a failing resource is deleted from Kubernetes,r.Get()returnsapierrors.IsNotFound(err)and exits without callingForget(), leaking map entries permanently. - Fix: Call
r.rateLimiter.Forget(request.NamespacedName)in theapierrors.IsNotFound(err)branch ofReconcile():if apierrors.IsNotFound(err) { if r.rateLimiter != nil { r.rateLimiter.Forget(request.NamespacedName) } return reconcile.Result{}, nil }
3. Align Duration Parsing with int32 Convention to Prevent Overflow
- Issue:
ParseBackoffMaxDelayparses withstrconv.ParseInt(val, 10, 64). Extremely large 64-bit values can overflowtime.Durationinto negative numbers when multiplied bytime.Second. - Fix: Follow the convention used by
MeanReconcileReenqueuePeriodFromAnnotationinpkg/controller/reconciliationinterval/reconciliationinterval.goand parse withstrconv.ParseInt(val, 10, 32):Anseconds, err := strconv.ParseInt(val, 10, 32) if err != nil { return nil, fmt.Errorf("invalid value %q for annotation %s: must be an integer", val, k8s.BackoffMaxDelayInSecondsAnnotation) } if seconds < 0 { return nil, fmt.Errorf("invalid value %q for annotation %s: must be non-negative", val, k8s.BackoffMaxDelayInSecondsAnnotation) } d := time.Duration(seconds) * time.Second return &d, nil
int32second value can never overflowtime.Durationnanoseconds.
4. Decouple Error Metrics from Controller-Runtime Return Value
- Issue:
DirectReconciler.Reconcilehas named return values(result reconcile.Result, err error)with deferredr.RecordReconcileMetrics(..., &err). Returningreconcile.Result{}, nilto halt retries resets the namederrtonil. At function exit,RecordReconcileMetricsevaluates*reconcileErr == niland recordsstatus = "OK", corrupting Prometheus error metrics by reporting failed reconciliations as successful. - Fix: Track the reconciliation failure in a separate variable so failure metrics are recorded even when returning
nilto controller-runtime for workqueue control:var reconcileErr error defer r.RecordReconcileMetrics(ctx, r.gvk, request.Namespace, request.Name, startTime, &reconcileErr) ... requeue, err := runCtx.doReconcile(ctx, obj) if err != nil { reconcileErr = err ... return reconcile.Result{}, nil }
5. Scenario Test Runner & Test Flow
- Introduce
APPLY-10-SEC-NO-EXPORTand reverttests/e2e/export.go:- Adding
AllowExportError: trueinsideexportResourceAsUnstructuredglobally silences export failures across all scenario tests, causing broken exporters to be skipped silently rather than failing golden export checks. - In
tests/e2e/script_test.go, introduce anAPPLY-10-SEC-NO-EXPORTstep (or anEXPORT: falsedirective) to skip export on resources that failed initial creation on GCP while still sleeping 10 seconds to collect HTTP retries:case "APPLY-10-SEC-NO-EXPORT": applyObject(h, obj) time.Sleep(10 * time.Second) exportResource = nil // Skip export verification as resource does not exist on GCP shouldGetKubeObject = true // Capture _object<N>.yaml to verify status condition
- Adding
- Update Scenario Test Flow in
script.yaml:- Currently, Step 0 already has the annotation set to
"0"and Step 1 is an identical re-apply withAPPLY-NO-WAIT(which doesn't sleep and produces an empty_http01.log). - Restructure
tests/e2e/testdata/scenarios/backoff_max_delay_zero/script.yamlto demonstrate clear before-and-after behavior:- Step 0 (
APPLY-10-SEC-NO-EXPORT): Apply invalid resource without the annotation. Captures a few retry attempts (e.g. 2–3 GET/POST pairs) in_http00.log, proving active exponential retry behavior. - Step 1 (
APPLY-10-SEC-NO-EXPORT): Addcnrm.cloud.google.com/backoff-max-delay-in-seconds: "0". Triggers 1 reconciliation attempt and halts. Zero retries occur over the 10-second sleep, producing only that single initial GET/POST pair in_http01.log. - Step 2 (
APPLY): Fix.specwith a valid filter. Generation change immediately triggers reconciliation, creates the resource on GCP, transitions toReady=True, and exports_export2.yaml. - Step 3 (
DELETE): Clean teardown.
- Step 0 (
- Currently, Step 0 already has the annotation set to
- Run
hack/record-gcpagainst Real GCP:- Once the scenario test structure is updated, re-run
hack/record-gcp TestE2EScript/backoff_max_delay_zeroto record authentic GCP HTTP logs for this scenario and ensure MockGCP alignment.
- Once the scenario test structure is updated, re-run
6. Clarify Scope in pkg/k8s/constants.go
- Add a doc comment on
BackoffMaxDelayInSecondsAnnotationinpkg/k8s/constants.goclarifying that this annotation is currently supported exclusively for direct controllers.
|
🤖 AI Factory started addressing review feedback for this pull request. |
b3e3791 to
0889070
Compare
|
Thank you for the detailed and helpful review! All points have been addressed in the latest commit:
(This comment was generated by overseer) |
|
🤖 AI Factory started investigating CI check failures for this pull request. Note: We recommend waiting for the 'ready-for-human' label before leaving review comments. Comments added while the system is actively working may be associated with outdated commits once a new commit is pushed, causing them to be ignored. |
…lers - Add cnrm.cloud.google.com/backoff-max-delay-in-seconds annotation in pkg/k8s/constants.go with doc comments clarifying direct controller scope. - Implement ParseBackoffMaxDelay (using int32 parsing to prevent overflow) and DynamicRateLimiter in pkg/controller/ratelimiter/dynamicratelimiter.go. - Add BackoffMaxDelayInSecondsAnnotation check to UnderlyingResourceOutOfSyncPredicate in pkg/controller/predicate/predicate.go. - Integrate dynamic rate limiting and retry halting in DirectReconciler in pkg/controller/direct/directbase/directbase_controller.go, ensuring rateLimiter cleanup on NotFound and decoupled Prometheus error metrics. - Add unit tests in pkg/controller/predicate/predicate_test.go, pkg/controller/ratelimiter/dynamicratelimiter_test.go, and pkg/controller/direct/directbase/directbase_controller_test.go. - Add APPLY-10-SEC-NO-EXPORT in tests/e2e/script_test.go and E2E scenario test in tests/e2e/testdata/scenarios/backoff_max_delay_zero/. Fix: 13649
0889070 to
561f88b
Compare
Investigating tests-e2e-fixtures-container-2-of-2 failureRun: 37079683754 Investigating presubmit-gatekeeper failureRun: 37079683754 (This report was generated by overseer) |
Summary
Support configurable maximum exponential backoff delay ceiling and complete automated retry halting for direct controllers via the resource annotation:
cnrm.cloud.google.com/backoff-max-delay-in-secondsDetails & Behavior
cnrm.cloud.google.com/backoff-max-delay-in-seconds"600"): Calculates per-resource exponential backoff (baseDelay * 2^(failures-1)capped at the specified max delay) and returnsreconcile.Result{RequeueAfter: nextDelay}, nil."0": Halts automated error retries by recordingReady=Falsecondition and returningreconcile.Result{}, nil. No periodic drift re-enqueue is scheduled until.specis modified, metadata/annotations change, or the controller pod restarts.deletionTimestamp != nilbypass custom backoff and retry halting, returning errors directly to controller-runtime for prompt deletion retries.Forget(request.NamespacedName))..specedit: Generation changes trigger immediate reconciliation through controller-runtime watch predicates.pkg/controller/ratelimiter/dynamicratelimiter_test.go.pkg/controller/direct/directbase/directbase_controller_test.goverifying"0"retry halt, positive max delay capping, malformed input fallback, success resets, and deletion bypass.tests/e2e/testdata/scenarios/backoff_max_delay_zero/verifying retry halt on failure and immediate recovery upon.specupdate.Fixes #13649
This PR was generated by the overseer agent (powered by the gemini-3.7-flash model).