Skip to content

fix: prevent authentication controller deadlock under OOM/webhook pressure#184

Open
laurafitzgerald wants to merge 4 commits into
opendatahub-io:devfrom
laurafitzgerald:fix/RHOAIENG-61483-auth-controller-deadlock-resilience
Open

fix: prevent authentication controller deadlock under OOM/webhook pressure#184
laurafitzgerald wants to merge 4 commits into
opendatahub-io:devfrom
laurafitzgerald:fix/RHOAIENG-61483-auth-controller-deadlock-resilience

Conversation

@laurafitzgerald

@laurafitzgerald laurafitzgerald commented May 19, 2026

Copy link
Copy Markdown

Summary

The authentication controller can permanently hang on clusters experiencing HTTP/2 stream errors from the API server. A single blocked API call (Update for finalizer, or CreateOrUpdate for HTTPRoute/ReferenceGrant) deadlocks the controller's only worker goroutine, preventing all RayClusters from progressing past the AuthenticationReady gate.

Root cause: On multi-tenant clusters, the operator watches all ConfigMaps cluster-wide. Large List responses break HTTP/2 streams (INTERNAL_ERROR), poisoning the shared connection pool. The auth controller's API calls then block indefinitely on dead connections. With MaxConcurrentReconciles: 1 and no context timeout, a single stuck call permanently blocks the entire controller — even across pod restarts.

Changes

  1. Context timeout (60s) on every reconcile — Ensures blocked API calls are cancelled rather than hanging indefinitely.

  2. Increased MaxConcurrentReconciles to 3 — One stuck worker no longer blocks all clusters.

  3. Safety requeue (5m) on success — Periodically re-reconciles to detect drift in cross-namespace resources (HTTPRoute, ReferenceGrant) that lack owner references.

  4. Removed no-op Owns() for HTTPRoute and ReferenceGrant — These never triggered reconciliation because cross-namespace owner references are impossible (HTTPRoute) and ReferenceGrant uses reference-counting cleanup without owner refs. Updated comments to accurately describe the SafetyRequeueDelay's purpose.

  5. Scoped ConfigMap informer to only kuberay-managed ConfigMaps — Adds app.kubernetes.io/created-by=kuberay-operator label selector to the manager cache (same pattern as Jobs). Eliminates cluster-wide ConfigMap List that triggers HTTP/2 stream errors.

  6. Moved enforceEnableIngressFalse from auth controller to RayCluster controller — The auth controller should not Update RayCluster spec fields; that triggers the mutating webhook and is the primary deadlock vector. The RayCluster controller is the natural owner of spec enforcement — it already handles ingress reconciliation and has configurable concurrency.

Evidence from customer must-gather

  • 1Gi pod, restartCount: 0, clean start — auth controller logged "Handling Integrated OAuth" at 16:00:09 then never logged again (1h48m silence)
  • All other controllers (RayCluster, networkpolicy, mTLS) continued working
  • 73 occurrences of "Failed to watch *v1.ConfigMap: stream error: INTERNAL_ERROR"
  • RayCluster controller continuously reported "Waiting for AuthenticationReady condition"

Test plan

  • Unit tests pass (go test ./controllers/ray/...)
  • Verify auth controller creates ConfigMaps with app.kubernetes.io/created-by=kuberay-operator label
  • Verify enableIngress enforcement works from the RayCluster controller on upgrade (pre-existing cluster with enableIngress: true or nil)
  • Deploy patched operator on customer cluster to confirm auth controller completes and head pods are created
  • E2E: create auth-enabled RayCluster, verify AuthenticationReady=True and head pod comes up

…ssure

When the kuberay operator experiences OOM kills or its internal webhook
becomes unresponsive, the authentication controller's single worker can
block indefinitely on API calls, preventing all RayClusters from having
their AuthenticationReady condition set — which blocks head pod creation.

This change adds three resilience improvements:

1. Context timeout (60s) on each reconcile — breaks indefinite blocks
   caused by unresponsive webhooks or slow API calls
2. Increase MaxConcurrentReconciles from 1 to 3 — prevents head-of-line
   blocking when one cluster's reconciliation is slow
3. Safety requeue (5min) — ensures clusters self-heal if watch events are
   missed due to predicate filtering or operator restarts

Additionally fixes:
- Re-fetches RayCluster after enforceEnableIngressFalse to avoid stale
  resourceVersion causing 409 Conflict errors
- Registers ReferenceGrant and HTTPRoute as owned resources for proper
  watch-based reconciliation

Verified: operator recovers from OOM (32Mi → 512Mi) and successfully
reconciles all pending RayClusters with these fixes applied.

Ref: RHOAIENG-61483

Signed-off-by: Laura Fitzgerald <lfitzger@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Reconcile context is wrapped with a 60s timeout and successful reconciles now return RequeueAfter: SafetyRequeueDelay (5 minutes). After enforcing enableIngress=false on OpenShift the controller re-fetches the RayCluster to obtain the updated resourceVersion. SetupWithManager sets MaxConcurrentReconciles to 3 and limits owner-watch wiring to in-namespace resources. The OIDC ConfigMap gains the KubernetesCreatedBy label. main.go adds corev1 import and registers ConfigMap in cacheSelectors.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Security & Code Quality Findings

  • Context cancellation / goroutine leak risk (range_ee585cfee37a): Reconcile uses context.WithTimeout(60s). If background goroutines or async calls ignore cancellation, they may continue running after timeout -> CWE-401 (Missing Release of Memory after Effective Lifetime). Action: ensure all spawned goroutines observe ctx.Done() and that external calls accept/propagate ctx.

  • Unchecked re-fetch error path (range_dc372f840953): Re-fetch after spec update may fail; current flow can leave spec change applied without sync to resourceVersion -> CWE-703 (Improper Check or Handling of Exceptional Conditions). Action: handle re-fetch error explicitly (retry or surface error) or document safe-to-continue semantics.

  • Mandatory periodic requeue may delay surfacing transient failures (range_79b3ed7893d3): Forcing a 5-minute sleep between successful reconciles can mask transient partial failures -> CWE-399 (Resource Management Errors) / operational risk. Action: ensure reconcile always validates desired state fully before returning success or reduce requeue interval for critical invariants.

  • Concurrent reconciles race risk (range_07766707cc06): MaxConcurrentReconciles=3 increases chance of concurrent operations against same cluster object -> CWE-362 (Concurrent Execution using Shared Resource with Improper Synchronization). Action: verify reconciliation is idempotent, use per-resource keyed queuing or optimistic conflict handling and test concurrent reconcile scenarios.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: preventing authentication controller deadlock during OOM/webhook pressure.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed PR description provides comprehensive problem statement, root cause analysis, detailed changes, customer evidence, and test plan—exceeds template requirements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@ray-operator/controllers/ray/authentication_controller.go`:
- Around line 1109-1110: The controller currently uses
Owns(&gatewayv1beta1.ReferenceGrant{}) and Owns(&gatewayv1beta1.HTTPRoute{})
which won't enqueue reconciles for these objects because ReferenceGrant avoids
OwnerReferences and HTTPRoute is often created in a different namespace; replace
these Owns() entries with Watches() using a custom EnqueueRequestsFromMapFunc
that looks up the owning RayCluster by labels (e.g. "ray.io/cluster-namespace"
and "ray.io/cluster-name") and enqueues the corresponding reconcile.Request;
implement the mapping function referenced from the Watches call and ensure the
Watch is registered for gatewayv1beta1.ReferenceGrant and
gatewayv1beta1.HTTPRoute instead of Owns to get watch-based triggers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: a0fd4ec3-c266-47d5-b2d2-4c27aaae955a

📥 Commits

Reviewing files that changed from the base of the PR and between a7f5517 and de4b8ed.

📒 Files selected for processing (1)
  • ray-operator/controllers/ray/authentication_controller.go

Comment on lines +1109 to +1110
Owns(&gatewayv1beta1.ReferenceGrant{}).
Owns(&gatewayv1beta1.HTTPRoute{}).

@coderabbitai coderabbitai Bot May 19, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify owner reference handling in ensureReferenceGrant and ensureHttpRoute
echo "=== ReferenceGrant owner reference handling ==="
rg -n -A5 'SetControllerReference.*referenceGrant' ray-operator/controllers/ray/authentication_controller.go || echo "No SetControllerReference for ReferenceGrant found"

echo ""
echo "=== HTTPRoute owner reference handling ==="
rg -n -B2 -A5 'SetControllerReference.*httpRoute' ray-operator/controllers/ray/authentication_controller.go

echo ""
echo "=== Comment about shared grant ==="
rg -n 'Do NOT set controller reference' ray-operator/controllers/ray/authentication_controller.go

Repository: opendatahub-io/kuberay

Length of output: 639


🏁 Script executed:

sed -n '1100,1115p' ray-operator/controllers/ray/authentication_controller.go

Repository: opendatahub-io/kuberay

Length of output: 699


Owns() watches are ineffective for resources without OwnerReferences.

Owns(&gatewayv1beta1.ReferenceGrant{}) and Owns(&gatewayv1beta1.HTTPRoute{}) only trigger reconciliation when these resources have an OwnerReference pointing to the parent RayCluster. However:

  • ReferenceGrant explicitly avoids setting owner reference (line 637: shared across clusters)
  • HTTPRoute only sets owner reference when platformNamespace == cluster.Namespace (lines 438-443), which is typically not the case for cross-namespace routes

These watches will not fire for the majority of cases. The controller will rely only on SafetyRequeueDelay for eventual consistency. If watch-based triggers are required for these resources, use Watches() with a custom EnqueueRequestsFromMapFunc that maps via labels (ray.io/cluster-namespace, ray.io/cluster-name) instead.

🤖 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 `@ray-operator/controllers/ray/authentication_controller.go` around lines 1109
- 1110, The controller currently uses Owns(&gatewayv1beta1.ReferenceGrant{}) and
Owns(&gatewayv1beta1.HTTPRoute{}) which won't enqueue reconciles for these
objects because ReferenceGrant avoids OwnerReferences and HTTPRoute is often
created in a different namespace; replace these Owns() entries with Watches()
using a custom EnqueueRequestsFromMapFunc that looks up the owning RayCluster by
labels (e.g. "ray.io/cluster-namespace" and "ray.io/cluster-name") and enqueues
the corresponding reconcile.Request; implement the mapping function referenced
from the Watches call and ensure the Watch is registered for
gatewayv1beta1.ReferenceGrant and gatewayv1beta1.HTTPRoute instead of Owns to
get watch-based triggers.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@laurafitzgerald changes look good for me. got this comment from cursor though (not sure if we want to address it here or not):

The Owns(&gatewayv1beta1.HTTPRoute{}) call is a no-op as a watch trigger because HTTPRoute is intentionally created in platformNamespace rather than cluster.Namespace, 
making cross-namespace owner references impossible (as the code itself acknowledges with // NOTE: Cannot use SetControllerReference for cross-namespace ownership). 
Similarly, Owns(&gatewayv1beta1.ReferenceGrant{}) won't enqueue reconciles because the ReferenceGrant intentionally has no owner reference set — cleanup is managed via reference counting instead. 
The SafetyRequeueDelay of 5 minutes effectively compensates for this gap, so it's low priority, but the comment "ensures proper watch-based reconciliation" is misleading.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

@pawelpaszki

Copy link
Copy Markdown

/run-kuberay-e2e

@rhods-ci-bot

Copy link
Copy Markdown

@laurafitzgerald: The following test has Succeeded:

OCI Artifact Browser URL

View in Artifact Browser

Inspecting Test Artifacts Manually

To inspect your test artifacts manually, follow these steps:

  1. Install ORAS (see the ORAS installation guide).
  2. Download artifacts with the following commands:
mkdir -p oras-artifacts
cd oras-artifacts
oras pull quay.io/opendatahub/odh-ci-artifacts:odh-pr-test-kuberay-hbzv6

laurafitzgerald and others added 3 commits May 19, 2026 15:04
HTTPRoute is created in platformNamespace (not the RayCluster namespace),
making cross-namespace owner references impossible. ReferenceGrant uses
reference-counting cleanup without owner references. In both cases,
Owns() never triggers reconciliation because no ownerRef is set.

SafetyRequeueDelay (5m) already compensates by periodically re-reconciling
to detect drift in these resources. Update comments to reflect this.

Part of RHOAIENG-61483.

Co-authored-by: Cursor <cursoragent@cursor.com>
The operator's informer cache watches all ConfigMaps cluster-wide when
no watchNamespace is set. On clusters with many namespaces and thousands
of ConfigMaps, the List response is large enough to trigger HTTP/2 stream
errors (INTERNAL_ERROR), which poison the shared connection pool and can
cause unrelated API calls (e.g. Update for auth finalizer) to hang
indefinitely.

Fix by applying the same label-selector filter already used for Jobs:
only cache ConfigMaps with app.kubernetes.io/created-by=kuberay-operator.
The auth controller now adds this label when creating OIDC ConfigMaps.

Part of RHOAIENG-61483.

Co-authored-by: Cursor <cursoragent@cursor.com>
…troller

The auth controller should not be updating RayCluster spec fields — that
r.Update() call triggers the mutating webhook and can block indefinitely
on a poisoned HTTP/2 connection, deadlocking the entire auth controller.

The RayCluster controller is the natural owner of spec enforcement:
- It already handles ingress reconciliation (reconcileIngress)
- It already does spec-level Updates (finalizers, status)
- It has configurable MaxConcurrentReconciles, so one blocked Update
  doesn't block all clusters

The enforcement runs early in rayClusterReconcile, before reconcileIngress,
and returns early with a requeue to let the next reconcile see the updated
spec. The webhook continues to handle new CREATE/UPDATE operations.

Part of RHOAIENG-61483.

Co-authored-by: Cursor <cursoragent@cursor.com>
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.

3 participants