Skip to content

fix: solve race in scalers cache rebuild that caused transient scaler errors#7737

Merged
zroubalik merged 8 commits into
kedacore:mainfrom
rickbrouwer:len-is-zero
May 26, 2026
Merged

fix: solve race in scalers cache rebuild that caused transient scaler errors#7737
zroubalik merged 8 commits into
kedacore:mainfrom
rickbrouwer:len-is-zero

Conversation

@rickbrouwer

Copy link
Copy Markdown
Member

When a ScaledJob or ScaledObject spec change rebuilds the scalers cache, a running scale loop could still read from the old cache while it was being torn down, surfacing errors like redis: client is closed or scaler with id N not found. Len = 0.

The cache now tracks active readers and waits for them to finish before closing the underlying scalers. Callers treat the new ErrCacheClosed sentinel as benign so a transient race no longer flips Ready to False or invalidates the fresh cache.

Checklist

  • I have verified that my change is according to the deprecations & breaking changes policy
  • Tests have been added (if applicable)
  • Ensure make generate-scalers-schema has been run to update any outdated generated files
  • Changelog has been updated and is aligned with our changelog requirements, only when the change impacts end users
  • Commits are signed with Developer Certificate of Origin (DCO - learn more)

Fixes #7574

@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 a review from a team May 13, 2026 19:26
@snyk-io

snyk-io Bot commented May 13, 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.

@keda-automation keda-automation requested a review from a team May 13, 2026 19:26
Comment thread pkg/scaling/scale_handler_test.go Outdated
Comment thread pkg/scaling/scale_handler_test.go Outdated
Signed-off-by: Rick Brouwer <[email protected]>
Signed-off-by: Rick Brouwer <[email protected]>
@rickbrouwer

rickbrouwer commented May 13, 2026

Copy link
Copy Markdown
Member Author

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

@rickbrouwer

rickbrouwer commented May 14, 2026

Copy link
Copy Markdown
Member Author

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

@rickbrouwer rickbrouwer marked this pull request as ready for review May 14, 2026 06:26
@rickbrouwer rickbrouwer added the nice-to-have:keda-v2.20 Not strictly necessary, but nice if you can bring it along label May 15, 2026
c.closed = true
c.mutex.Unlock()

c.activeReaders.Wait()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

just thinking out loud - can this possibly lead to prolonged ErrCacheClosed responses if one trigger just takes too long to finish the active metric call?

wondering if we may want to timebox this with some reasonable default (10 second?) or if that would be unnecessary overkill.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think that close is almost instant and closes current active connections. We could ship it as it is right now and in case of feedback about long refreshing, we could cut a hotfix. WDYT?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

agree with @JorTurFer

@wozniakjan wozniakjan May 26, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

but the wait is not waiting for close to finish, it's waiting for the metric call to finish, closes are below after this wait passes. So a prometheus trigger on a server that is taking 30s to respond, all other triggers will get ErrCacheClosed for the whole duration of that overloaded prometheus hanging call. That is the difference from the current behavior, right now nothing waits and rebuilds scalers under the active calls which has it's own set of issues.

@wozniakjan wozniakjan May 26, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

meanwhile the PR merged, I will spin up what I see could be a reasonable mitigation. We can continue the discussion on that PR and see if it has merit in merging or if we are ok with what is in main implemented through this PR

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I saw it. I will also take a look at the code this afternoon and your comment to check if a follow-up is needed.

@wozniakjan wozniakjan May 26, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

the promised PR - #7780, ptal

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.

Pull request overview

This PR addresses a concurrency race during scaler cache rebuilds where in-flight scale loops could observe partially-torn-down caches/scalers and surface transient errors (eg redis: client is closed, scaler with id N not found). It introduces reader-tracking in the scalers cache and treats a new ErrCacheClosed sentinel as benign in key call paths to avoid transient failures impacting readiness/behavior.

Changes:

  • Add ErrCacheClosed sentinel + reader tracking (activeReaders) to ScalersCache, and make Close() idempotent while waiting for in-flight readers before closing scalers.
  • Update scale handler paths (ScaledObject metrics/state + ScaledJob metrics) to ignore ErrCacheClosed as a transient cache-swap condition.
  • Add unit tests for cache close semantics and benign handling of ErrCacheClosed, plus a changelog entry.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pkg/scaling/cache/scalers_cache.go Introduces ErrCacheClosed, reader tracking, and a safer Close() that waits for active readers.
pkg/scaling/scale_handler.go Handles ErrCacheClosed in scaler metric/spec reads to avoid treating cache-swap as a scaler failure.
pkg/scaling/cache/scalers_cache_test.go Adds concurrency/idempotency tests for the new cache close + reader-wait behavior.
pkg/scaling/scale_handler_test.go Adds tests asserting ErrCacheClosed is benign for ScaledJob metrics and ScaledObject state paths.
CHANGELOG.md Documents the user-visible fix for transient scaler errors during cache rebuild.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/scaling/cache/scalers_cache_test.go
Comment thread pkg/scaling/scale_handler_test.go Outdated
Comment thread pkg/scaling/scale_handler_test.go Outdated
Signed-off-by: Rick Brouwer <[email protected]>
@keda-automation keda-automation requested a review from a team May 25, 2026 13:38
@rickbrouwer rickbrouwer added the Awaiting/2nd-approval This PR needs one more approval review label May 25, 2026
Signed-off-by: Rick Brouwer <[email protected]>
@rickbrouwer

rickbrouwer commented May 25, 2026

Copy link
Copy Markdown
Member Author

/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/min_replica_sj/min_replica_sj_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/restore_original/restore_original_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_job_validation/scaled_job_validation_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/subresource_scale/subresource_scale_test.go, has passed after "one" attempts
Execution of tests/internals/eventemitter/azureeventgridtopic/azureeventgridtopic_test.go, has passed after "two" attempts
Execution of tests/internals/events/events_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/custom_hpa_name/custom_hpa_name_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/pause_scale_in_restore/pause_scale_in_restore_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/value_metric_type/value_metric_type_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/global_custom_ca/global_custom_ca_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/replica_update_so/replica_update_so_test.go, has passed after "one" attempts
Execution of tests/internals/idle_replicas/idle_replicas_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/initial_delay_cooldownperiod/initial_delay_cooldownperiod_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/cloudevent_source/cloudevent_source_test.go, has passed after "one" attempts
Execution of tests/internals/pause_scale_in/pause_scale_in_test.go, has passed after "two" attempts
Execution of tests/internals/pause_scaledjob/pause_scaledjob_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/polling_cooldown_so/polling_cooldown_so_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/deployments/fallback_test.go, has passed after "one" attempts
Execution of tests/internals/fallback/rollouts/fallback_test.go, has passed after "one" attempts
failed tests: 0

Signed-off-by: Rick Brouwer <[email protected]>
@rickbrouwer

rickbrouwer commented May 25, 2026

Copy link
Copy Markdown
Member Author

/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/min_replica_sj/min_replica_sj_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/file_based_auth/file_based_auth_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/replicaset_scale/replicaset_scale_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/scaling_strategies/eager_scaling_strategy_test.go, has passed after "one" attempts
Execution of tests/internals/pause_scale_in_restore/pause_scale_in_restore_test.go, has passed after "one" attempts
Execution of tests/internals/idle_replicas/idle_replicas_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/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/scaled_job_conditions/scaled_job_conditions_test.go, has passed after "one" attempts
Execution of tests/internals/pause_scale_in/pause_scale_in_test.go, has passed after "two" attempts
Execution of tests/internals/trigger_update_so/trigger_update_so_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/pause_scale_out/pause_scale_out_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/scaled_object_validation/scaled_object_validation_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/initial_delay_cooldownperiod/initial_delay_cooldownperiod_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/force_activation/force_activation_test.go, has passed after "two" attempts
Execution of tests/internals/cache_metrics/cache_metrics_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/pause_scaledjob/pause_scaledjob_test.go, has passed after "one" attempts
Execution of tests/internals/value_metric_type/value_metric_type_test.go, has passed after "two" 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/deployments/fallback_test.go, has passed after "one" attempts
Execution of tests/internals/fallback/rollouts/fallback_test.go, has passed after "one" attempts
failed tests: 0

@zroubalik zroubalik merged commit b9a8df0 into kedacore:main May 26, 2026
17 of 18 checks passed
@keda-automation keda-automation requested a review from a team May 26, 2026 08:16
shcherbak pushed a commit to shcherbak/keda that referenced this pull request Jun 3, 2026
… errors (kedacore#7737)

* fix: solve race in scalers cache rebuild that caused transient scaler errors

Signed-off-by: Rick Brouwer <[email protected]>

* fix

Signed-off-by: Rick Brouwer <[email protected]>

* fix

Signed-off-by: Rick Brouwer <[email protected]>

* adjust tests

Signed-off-by: Rick Brouwer <[email protected]>

* update

Signed-off-by: Rick Brouwer <[email protected]>

* update

Signed-off-by: Rick Brouwer <[email protected]>

---------

Signed-off-by: Rick Brouwer <[email protected]>
Co-authored-by: Zbynek Roubalik <[email protected]>
Signed-off-by: Yurii Shcherbak <[email protected]>
@rickbrouwer rickbrouwer removed the Awaiting/2nd-approval This PR needs one more approval review label Jun 5, 2026
@rickbrouwer rickbrouwer deleted the len-is-zero branch June 5, 2026 14:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

nice-to-have:keda-v2.20 Not strictly necessary, but nice if you can bring it along

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ScaledJob: transient "redis: client is closed" error during scaler cache rebuild sets Ready=False

5 participants