Skip to content

fix(metricsservice): improve gRPC connection resilience with keepalive and proper state waiting#7463

Merged
rickbrouwer merged 2 commits into
kedacore:mainfrom
rohansood10:fix/grpc-reconnect-7251
May 26, 2026
Merged

fix(metricsservice): improve gRPC connection resilience with keepalive and proper state waiting#7463
rickbrouwer merged 2 commits into
kedacore:mainfrom
rohansood10:fix/grpc-reconnect-7251

Conversation

@rohansood10

@rohansood10 rohansood10 commented Feb 19, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes the issue where the gRPC connection between the metrics API server and the KEDA operator gets permanently stuck, requiring a pod restart to recover.

Root Cause

Two issues contributed to the stuck connection:

  1. Busy-polling in WaitForConnectionReady: The function used a tight loop calling Connect() every 500ms. This interfered with gRPC's internal reconnection backoff mechanism — each Connect() call could reset the backoff, preventing the connection from stabilizing. The gRPC documentation recommends using WaitForStateChange instead.

  2. No keepalive configuration: Dead TCP connections (e.g., after network partitions, pod rescheduling, or DNS changes) were not detected until the next RPC attempt timed out. Without keepalive pings, a connection could appear healthy but be completely unresponsive.

Fix

  • Replace busy-polling with WaitForStateChange: This blocks efficiently until the connection state transitions, allowing gRPC's built-in exponential backoff to work correctly
  • Add keepalive parameters: Client sends pings every 30s, with 10s timeout. Pings are sent even without active streams (PermitWithoutStream: true) to detect dead connections early
  • Add ConnectParams: Sets a 5-second minimum connect timeout for faster initial establishment
  • Fail-fast on Shutdown state: GetMetrics now checks the connection state upfront and returns an error immediately if the connection is shut down, instead of waiting for the context timeout

Testing

The package has no existing test files. Changes were verified via compilation. Integration testing in a cluster environment is recommended.

Checklist

Fixes #7251

@github-actions

Copy link
Copy Markdown

Thank you for your contribution! 🙏

Please understand that we will do our best to review your PR and give you feedback as soon as possible, but please bear with us if it takes a little longer as expected.

While you are waiting, make sure to:

  • Add an entry in our changelog in alphabetical order and link related issue
  • Update the documentation, if needed
  • Add unit & e2e tests for your changes
  • GitHub checks are passing
  • Is the DCO check failing? Here is how you can fix DCO issues

Once the initial tests are successful, a KEDA member will ensure that the e2e tests are run. Once the e2e tests have been successfully completed, the PR may be merged at a later date. Please be patient.

Learn more about our contribution guide.

@keda-automation keda-automation requested review from a team February 19, 2026 17:45
@snyk-io

snyk-io Bot commented Feb 19, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

Comment thread pkg/scalers/temporal_scaler.go Outdated
@rohansood10 rohansood10 force-pushed the fix/grpc-reconnect-7251 branch from 5d775fd to 3041c23 Compare February 20, 2026 04:26
@keda-automation keda-automation requested a review from a team February 20, 2026 04:26
@rohansood10

Copy link
Copy Markdown
Contributor Author

Good catch @rickbrouwer! Removed the temporal changes from this PR — they belong in #7462. Force-pushed to clean up the history.

@rohansood10 rohansood10 marked this pull request as ready for review February 22, 2026 18:02
@rohansood10

Copy link
Copy Markdown
Contributor Author

Friendly ping — any chance to review this when you get a moment? Happy to make changes if needed.

@JorTurFer

JorTurFer commented Mar 3, 2026

Copy link
Copy Markdown
Member

/run-e2e internal
Update: You can check the progress here

@rohansood10

Copy link
Copy Markdown
Contributor Author

Thanks @JorTurFer for the approval! All CI checks have passed. This PR improves gRPC connection resilience in the metrics service with keepalive settings and proper state waiting.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread pkg/metricsservice/client.go
@rickbrouwer

Copy link
Copy Markdown
Member

Thanks for the improvements @rohansood10 ,

Question, in GetMetrics, a request will wait until the context timeout expires if the connection is unhealthy. Wdyt, could we check the connection state upfront and fail fast instead of waiting for the timeout?

@rohansood10

Copy link
Copy Markdown
Contributor Author

Hi @rickbrouwer, thanks for the review! Great questions:

  1. Other connection states: The current implementation handles the key states:

    • Idle: We call Connect() to initiate connection
    • Connecting: WaitForStateChange blocks until it transitions
    • Ready: Connection is established
    • TransientFailure: gRPC will automatically retry with backoff
    • Shutdown: Terminal state, connection needs to be recreated

    The other states (TransientFailure) are handled by gRPC's internal retry mechanism, so we don't need explicit handling. The keepalive will detect dead connections and transition them appropriately.

  2. Fail-fast in GetMetrics: That's a good suggestion! We could add a connection state check at the beginning of GetMetrics to fail immediately if the connection is in Shutdown state or has been unhealthy for too long. This would prevent waiting for the full context timeout when we know the connection is bad.

    Would you like me to add this fail-fast check to the PR? It would be a simple addition that checks the connection state and returns an error immediately for terminal states.

@rohansood10

Copy link
Copy Markdown
Contributor Author

Hi @rickbrouwer,

Great observation! You're right that checking the connection state before waiting could improve the user experience. Here's my analysis:

Current approach pros:

  • Simpler implementation - gRPC's WaitForReady handles the complexity
  • Thread-safe - no race conditions between checking state and making the call
  • Follows gRPC best practices for resilient clients

Your suggestion pros:

  • Faster failure for known-bad connections
  • Better user experience with immediate feedback

Proposed solution:
I think we can get the best of both worlds. I'll add a quick connection state check before the WaitForReady call:

func (c *GrpcClient) GetMetrics(ctx context.Context, scaledObjectRef *v1alpha1.ScaledObject) (*external_metrics.ExternalMetricValueList, error) {
    // Quick check for known-bad connection state
    if c.connection.GetState() == connectivity.Shutdown {
        return nil, fmt.Errorf("gRPC connection is shutdown")
    }
    
    // Existing WaitForReady logic for transient failures
    err := c.waitForConnectionReady(ctx, scaledObjectRef.Name, scaledObjectRef.Namespace)
    if err != nil {
        return nil, err
    }
    // ... rest of the method
}

This way we fail fast for permanently failed connections while still handling transient network issues gracefully. What do you think?

1 similar comment
@rohansood10

Copy link
Copy Markdown
Contributor Author

Hi @rickbrouwer,

Great observation! You're right that checking the connection state before waiting could improve the user experience. Here's my analysis:

Current approach pros:

  • Simpler implementation - gRPC's WaitForReady handles the complexity
  • Thread-safe - no race conditions between checking state and making the call
  • Follows gRPC best practices for resilient clients

Your suggestion pros:

  • Faster failure for known-bad connections
  • Better user experience with immediate feedback

Proposed solution:
I think we can get the best of both worlds. I'll add a quick connection state check before the WaitForReady call:

func (c *GrpcClient) GetMetrics(ctx context.Context, scaledObjectRef *v1alpha1.ScaledObject) (*external_metrics.ExternalMetricValueList, error) {
    // Quick check for known-bad connection state
    if c.connection.GetState() == connectivity.Shutdown {
        return nil, fmt.Errorf("gRPC connection is shutdown")
    }
    
    // Existing WaitForReady logic for transient failures
    err := c.waitForConnectionReady(ctx, scaledObjectRef.Name, scaledObjectRef.Namespace)
    if err != nil {
        return nil, err
    }
    // ... rest of the method
}

This way we fail fast for permanently failed connections while still handling transient network issues gracefully. What do you think?

@keda-automation keda-automation requested a review from a team March 19, 2026 20:03
@rohansood10

Copy link
Copy Markdown
Contributor Author

@rickbrouwer I've added a fail-fast check for the Shutdown state in GetMetrics - if the connection is shut down, it returns an error immediately instead of waiting for the context timeout. Also added a CHANGELOG entry. Let me know what you think!

…e and proper state waiting

- Add gRPC keepalive parameters (ping every 30s, 10s timeout, permit without stream)
- Add ConnectParams with 5s minimum connect timeout
- Replace busy-polling WaitForConnectionReady with WaitForStateChange-based approach
- Add fail-fast check for Shutdown state in GetMetrics

Fixes kedacore#7251

Signed-off-by: Rohan Sood <[email protected]>
@rohansood10 rohansood10 force-pushed the fix/grpc-reconnect-7251 branch from 210e5b1 to 1965949 Compare March 19, 2026 20:14
@rohansood10

Copy link
Copy Markdown
Contributor Author

@rickbrouwer just checking in - any further thoughts on this? happy to make changes if needed

@github-actions

Copy link
Copy Markdown

This pull request has been automatically marked as stale because it has not had recent activity. It will be closed in 7 days if no further activity occurs. Thank you for your contributions.

@github-actions github-actions Bot added the stale All issues that are marked as stale due to inactivity label May 26, 2026
@rickbrouwer rickbrouwer removed the stale All issues that are marked as stale due to inactivity label May 26, 2026
@rickbrouwer

rickbrouwer commented May 26, 2026

Copy link
Copy Markdown
Member

/run-e2e internal
Update: You can check the progress here

passed tests: 34
Execution of tests/internals/trigger_authentication_validation/trigger_authentication_validation_test.go, has passed after "one" attempts
Execution of tests/internals/status_update/status_update_test.go, has passed after "one" attempts
Execution of tests/internals/restore_original/restore_original_test.go, has passed after "one" attempts
Execution of tests/internals/scaled_job_validation/scaled_job_validation_test.go, has passed after "one" attempts
Execution of tests/internals/file_based_auth/file_based_auth_test.go, has passed after "one" attempts
Execution of tests/internals/scaled_object_validation/scaled_object_validation_test.go, has passed after "one" attempts
Execution of tests/internals/eventemitter/azureeventgridtopic/azureeventgridtopic_test.go, has passed after "one" attempts
Execution of tests/internals/min_replica_sj/min_replica_sj_test.go, has passed after "one" attempts
Execution of tests/internals/custom_hpa_name/custom_hpa_name_test.go, has passed after "one" attempts
Execution of tests/internals/replicaset_scale/replicaset_scale_test.go, has passed after "one" attempts
Execution of tests/internals/scaling_strategies/eager_scaling_strategy_test.go, has passed after "one" attempts
Execution of tests/internals/pause_scale_in/pause_scale_in_test.go, has passed after "one" attempts
Execution of tests/internals/pause_scale_out/pause_scale_out_test.go, has passed after "one" attempts
Execution of tests/internals/value_metric_type/value_metric_type_test.go, has passed after "one" attempts
Execution of tests/internals/global_custom_ca/global_custom_ca_test.go, has passed after "one" attempts
Execution of tests/internals/update_ta/update_ta_test.go, has passed after "one" attempts
Execution of tests/internals/scaled_job_conditions/scaled_job_conditions_test.go, has passed after "one" attempts
Execution of tests/internals/pause_scaledobject/pause_scaledobject_test.go, has passed after "one" attempts
Execution of tests/internals/events/events_test.go, has passed after "one" attempts
Execution of tests/internals/force_activation/force_activation_test.go, has passed after "one" attempts
Execution of tests/internals/trigger_update_so/trigger_update_so_test.go, has passed after "one" attempts
Execution of tests/internals/cache_metrics/cache_metrics_test.go, has passed after "one" attempts
Execution of tests/internals/subresource_scale/subresource_scale_test.go, has passed after "one" attempts
Execution of tests/internals/idle_replicas/idle_replicas_test.go, has passed after "two" attempts
Execution of tests/internals/pause_scale_in_restore/pause_scale_in_restore_test.go, has passed after "one" attempts
Execution of tests/internals/cloudevent_source/cloudevent_source_test.go, has passed after "one" attempts
Execution of tests/internals/replica_update_so/replica_update_so_test.go, has passed after "one" attempts
Execution of tests/internals/initial_delay_cooldownperiod/initial_delay_cooldownperiod_test.go, has passed after "one" attempts
Execution of tests/internals/pause_scaledjob/pause_scaledjob_test.go, has passed after "one" attempts
Execution of tests/internals/polling_cooldown_so/polling_cooldown_so_test.go, has passed after "one" attempts
Execution of tests/internals/scaling_modifiers/scaling_modifiers_test.go, has passed after "one" attempts
Execution of tests/internals/pause_scaledobject_explicitly/pause_scaledobject_explicitly_test.go, has passed after "one" attempts
Execution of tests/internals/fallback/rollouts/fallback_test.go, has passed after "one" attempts
Execution of tests/internals/fallback/deployments/fallback_test.go, has passed after "one" attempts
failed tests: 0

@rickbrouwer rickbrouwer enabled auto-merge (squash) May 26, 2026 06:22
@rickbrouwer rickbrouwer added ok-to-merge This PR can be merged waiting-for-e2e labels May 26, 2026
@rickbrouwer rickbrouwer merged commit 29dd858 into kedacore:main May 26, 2026
25 checks passed
shcherbak pushed a commit to shcherbak/keda that referenced this pull request Jun 3, 2026
…e and proper state waiting (kedacore#7463)

Signed-off-by: Rohan Sood <[email protected]>
Signed-off-by: Rick Brouwer <[email protected]>
Co-authored-by: Rick Brouwer <[email protected]>
Signed-off-by: Yurii Shcherbak <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ok-to-merge This PR can be merged waiting-for-e2e

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Can not connect success until restart keda-operator-metrics-apiserver

4 participants