Skip to content

Fix KeyRingProvider thread pool starvation on forced refresh - #67986

Merged
DeagleGross merged 4 commits into
dotnet:mainfrom
DeagleGross:deaglegross-analyze-keyring-thread-starvation
Jul 24, 2026
Merged

DeagleGross merged 4 commits into
dotnet:mainfrom
DeagleGross:deaglegross-analyze-keyring-thread-starvation

Conversation

@DeagleGross

Copy link
Copy Markdown
Member

The cold-start starvation fix (#66683/#66737) left a second variant: when a valid key ring is cached and a caller forces a refresh (e.g. during the two-minute startup auto-refresh window an incoming cookie names a key not in the current ring), GetCurrentKeyRingCoreNew scheduled the refresh onto TaskScheduler.Default and blocked on Task.Wait(). With every thread-pool thread parked on that wait, the queued refresh could not run, causing thread pool starvation and a multi-second freeze. See #66380 (comment).

Unify the invariant that already protects cold start: any caller that must block for a value produces it inline on its own thread, so no caller ever blocks on TaskScheduler.Default work. Non-forced callers with a stale ring still dispatch the async refresh and return the stale ring without waiting, preserving the perf win from #54675.

Fixes #66380

DeagleGross and others added 2 commits July 23, 2026 14:39
The cold-start starvation fix (dotnet#66683/dotnet#66737) left a second variant: when a
valid key ring is cached and a caller forces a refresh (e.g. during the
two-minute startup auto-refresh window an incoming cookie names a key not in
the current ring), GetCurrentKeyRingCoreNew scheduled the refresh onto
TaskScheduler.Default and blocked on Task.Wait(). With every thread-pool
thread parked on that wait, the queued refresh could not run, causing thread
pool starvation and a multi-second freeze. See dotnet#66380.

Unify the invariant that already protects cold start: any caller that must
block for a value produces it inline on its own thread, so no caller ever
blocks on TaskScheduler.Default work. Non-forced callers with a stale ring
still dispatch the async refresh and return the stale ring without waiting,
preserving the perf win from dotnet#54675.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ec35c6be-ab9b-47d1-9563-d7a61490ebb2
@DeagleGross
DeagleGross requested a review from halter73 July 23, 2026 17:34
@DeagleGross DeagleGross self-assigned this Jul 23, 2026
@DeagleGross DeagleGross added bug This issue describes a behavior which is not expected - a bug. area-dataprotection Includes: DataProtection labels Jul 23, 2026
@DeagleGross
DeagleGross marked this pull request as ready for review July 23, 2026 17:41
Copilot AI review requested due to automatic review settings July 23, 2026 17:41

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 remaining thread-pool starvation scenario in KeyRingProvider.GetCurrentKeyRingCoreNew: when a key ring is already cached but a caller forces a refresh, the refresh work could be queued to TaskScheduler.Default while the caller blocks, potentially deadlocking under thread-pool saturation. The update enforces an invariant that any caller who must block for a refreshed ring performs the refresh inline on its own thread, while non-forced callers with a stale ring still trigger an async refresh and return the stale value.

Changes:

  • Run forced-refresh (and cold-start) key ring refresh inline rather than scheduling and blocking on thread-pool work.
  • Preserve the async-refresh performance optimization for non-forced callers when a stale ring is still available.
  • Add a regression test ensuring forced refresh with a cached ring performs the refresh on the calling thread.
Show a summary per file
File Description
src/DataProtection/DataProtection/src/KeyManagement/KeyRingProvider.cs Reworks forced-refresh + stale/cold-start handling to avoid thread-pool starvation by running blocking refresh work inline.
src/DataProtection/DataProtection/test/Microsoft.AspNetCore.DataProtection.Tests/KeyManagement/KeyRingProviderTests.cs Adds regression coverage asserting forced refresh with a cached ring refreshes on the calling thread.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 2

Addresses two issues raised in review of the forced-refresh starvation fix:

- On inline refresh failure, only extend the lifetime of a ring that is
  already stale. A forced refresh can fail while the cached ring is still
  valid; previously WithTemporaryExtendedLifetime would then cut that valid
  ring's expiration to 2 minutes and swap its expiration token for
  CancellationToken.None, causing key changes to be ignored until the
  shortened lifetime elapsed. This now mirrors the guard in
  GetKeyRingFromCompletedTaskUnsynchronized.

- After publishing an inline forced-refresh result, discard any async refresh
  task still in flight so its older result cannot later be consumed and
  clobber the fresher ring. The discarded task's exception is observed to
  avoid an unobserved TaskScheduler.UnobservedTaskException.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d900e559-5b83-4d0c-b179-e1cc2cda9792

@halter73 halter73 left a comment

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.

It might be worth adding regression tests for the issues Copilot pointed out earlier though.

@DeagleGross
DeagleGross merged commit a973708 into dotnet:main Jul 24, 2026
24 checks passed
@DeagleGross
DeagleGross deleted the deaglegross-analyze-keyring-thread-starvation branch July 24, 2026 12:51
@dotnet-milestone-bot dotnet-milestone-bot Bot added this to the 11.0-rc1 milestone Jul 26, 2026
wtgodbe pushed a commit that referenced this pull request Aug 11, 2026
…#68001)

* Fix KeyRingProvider thread pool starvation on forced refresh

The cold-start starvation fix (#66683/#66737) left a second variant: when a
valid key ring is cached and a caller forces a refresh (e.g. during the
two-minute startup auto-refresh window an incoming cookie names a key not in
the current ring), GetCurrentKeyRingCoreNew scheduled the refresh onto
TaskScheduler.Default and blocked on Task.Wait(). With every thread-pool
thread parked on that wait, the queued refresh could not run, causing thread
pool starvation and a multi-second freeze. See #66380.

Unify the invariant that already protects cold start: any caller that must
block for a value produces it inline on its own thread, so no caller ever
blocks on TaskScheduler.Default work. Non-forced callers with a stale ring
still dispatch the async refresh and return the stale ring without waiting,
preserving the perf win from #54675.




* add debug assert

* Harden forced-refresh path per PR review

Addresses two issues raised in review of the forced-refresh starvation fix:

- On inline refresh failure, only extend the lifetime of a ring that is
  already stale. A forced refresh can fail while the cached ring is still
  valid; previously WithTemporaryExtendedLifetime would then cut that valid
  ring's expiration to 2 minutes and swap its expiration token for
  CancellationToken.None, causing key changes to be ignored until the
  shortened lifetime elapsed. This now mirrors the guard in
  GetKeyRingFromCompletedTaskUnsynchronized.

- After publishing an inline forced-refresh result, discard any async refresh
  task still in flight so its older result cannot later be consumed and
  clobber the fresher ring. The discarded task's exception is observed to
  avoid an unobserved TaskScheduler.UnobservedTaskException.




* add regression tests

---------




(cherry picked from commit a973708)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ec35c6be-ab9b-47d1-9563-d7a61490ebb2
Copilot-Session: d900e559-5b83-4d0c-b179-e1cc2cda9792
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-dataprotection Includes: DataProtection bug This issue describes a behavior which is not expected - a bug.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

KeyRingProvider causes thread pool starvation on cold start in .NET 10

4 participants