fix: disable waitForReady on gRPC runner client to prevent infinite block#1729
fix: disable waitForReady on gRPC runner client to prevent infinite block#1729Eitan1112 wants to merge 2 commits into
Conversation
…lock When a runner pod is killed mid-reconciliation (e.g. spot/preemptible node eviction), the gRPC client blocks forever because waitForReady: true causes it to queue RPCs and wait for the dead connection to become READY again. Since the reconciliation context has no deadline, and the runner pod will never come back, the goroutine is permanently stuck. This blocks all future reconciliations of the same Terraform CR because controller-runtime only allows one active reconciliation per object key. Even annotating with reconcile.fluxcd.io/requestedAt cannot unblock it since the new event sits in the work queue behind the stuck one. Setting waitForReady to false makes RPCs fail immediately with UNAVAILABLE when the connection is broken, which is already handled by the retry policy (4 attempts). After retries are exhausted, the reconciliation returns an error, gets re-queued, and proceeds normally with a fresh runner pod. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Hit this exact bug Cluster had been silently stuck for days with Terraform CRs frozen on Reconciling=True (Fulfilling prerequisites) + Ready=Unknown (Initializing), no logs for the affected resources, dependents looping every 90s on dependency '...' is not ready. Pulled metrics off the leader before doing anything destructive: Goroutine dump showed three workers parked in exactly the place this PR points at: Three runner pods evicted at some point in the past, three worker slots wedged ever since, no way for the controller to recover on its own. Annotating with reconcile.fluxcd.io/requestedAt did nothing — confirmed your point about controller-runtime not enqueuing a second reconcile while one is in flight. After deleting the leader pod, workqueue_longest_running_processor_seconds dropped to ~12 seconds and every stuck CR reconciled cleanly within ~15s using fresh runner pods. Same code, same workload, just no inherited stuck state. Bumping in the hope it gets reviewer attention |
|
Adding another data point in support of this fix — we hit exactly this pattern on EKS this week. Versions: controller Symptom. Three Trigger. A failed Workqueue metrics from the leader pod, ~17h after the wedge: The three "active workers" each held one of the three stuck CRs. Goroutine dump. Three Reconcile goroutines were parked in
Abbreviated stack from one of the Why annotation requeues didn't help (matches the PR description exactly): controller-runtime's workqueue dedup-coalesces an Recovery. Deleting the leader pod ( This is on EKS (so not the GCP DNS class of cause referenced in #1181). Network/DNS between the controller and runners is healthy in steady state — sibling Terraforms in the same namespaces were reconciling concurrently throughout the wedge. We can't say with certainty what made the specific runner pods go away (we don't have post-mortem logs from those pods); the most likely trigger is just node rotation — our cluster autoscaler turns nodes over routinely, and any runner pod that happened to be on a rotated node would have been killed mid-life along with everything else. Spot/preemptible eviction or the controller's own The fix in this PR ( |
Problem
When a runner pod is killed mid-reconciliation (e.g. due to spot/preemptible node eviction),
the controller goroutine blocks forever on the gRPC call and never recovers.
This happens because the gRPC service config in
getRunnerConnectionsetswaitForReady: true.Per gRPC wait-for-ready semantics,
when the connection enters
TRANSIENT_FAILURE(runner pod gone), the client queues RPCs and waitsfor the connection to become
READYagain instead of failing. Since the runner pod is dead and thereconciliation context carries no deadline, this wait is infinite.
The stuck goroutine blocks all future reconciliations of the same Terraform CR because
controller-runtime allows only one active reconciliation per object key. Annotating with
reconcile.fluxcd.io/requestedAtdoes not help — the event is enqueued but cannot beprocessed until the stuck reconciliation completes, which it never does.
Observed behavior
Terraform CR permanently stuck in
Reconciling/Initializingstate. Controller logs showthe reconciliation reaching
calling detectDrift ...→creating a planand then silence.No further logs for that CR. No runner pod present in the namespace.
Code path
Reconcilereceives a context with no deadline (tf_controller.go:113)getRunnerConnectioncreates a gRPC client withwaitForReady: true(tf_controller_runner.go:143)detectDriftcallsrunnerClient.Plan(ctx, planRequest)(tf_controller_drift_detect.go:89)TRANSIENT_FAILUREREADYReproduction conditions
tofu plan(large state, many resources)Controller logs
Runner pod logs
Terraform CR status (stuck)
Fix
Set
waitForReadytofalsein the gRPC service config. This makes RPCs fail immediatelywith
UNAVAILABLEwhen the connection is broken. The existing retry policy already handlesUNAVAILABLE(4 attempts with backoff). After retries are exhausted, the reconciliationreturns an error, the CR is re-queued by controller-runtime, and the next reconciliation
proceeds normally with a fresh runner pod.
waitForReady: falseis the gRPC default— this change restores default behavior
(the dial has a 30s timeout at
tf_controller_runner.go:109)UNAVAILABLE, 4 attempts) provides resilience against transient network blips