@@ -360,7 +360,44 @@ private IKeyRing GetCurrentKeyRingCoreNew(DateTime utcNow, bool forceRefresh)
360360 existingTask = _cacheableKeyRingTask ;
361361 if ( existingTask is null )
362362 {
363- // If there's no existing task, make one now
363+ // The forceRefresh path skipped reading _cacheableKeyRing above; read it now.
364+ existingCacheableKeyRing ??= Volatile . Read ( ref _cacheableKeyRing ) ;
365+
366+ if ( existingCacheableKeyRing is null )
367+ {
368+ // Cold start: run the refresh inline on this thread. Scheduling the work
369+ // onto TaskScheduler.Default would risk thread-pool starvation - if all
370+ // available pool threads are blocked waiting on the result, the queued
371+ // work item can't be picked up and the app hangs until the runtime
372+ // injects more threads. See https://github.com/dotnet/aspnetcore/issues/66380.
373+ //
374+ // Other concurrent callers are parked on _cacheableKeyRingLockObj while we
375+ // run; once we publish _cacheableKeyRing and release the lock, they wake up.
376+ // Non-force-refresh callers can then re-check the cache and return the
377+ // freshly populated ring, while forceRefresh callers observe that a cached
378+ // ring now exists and continue through the stale-cache refresh path below.
379+ // This matches the pre-#54675 behavior (https://github.com/dotnet/aspnetcore/pull/54675).
380+ CacheableKeyRing newCacheableKeyRing ;
381+ try
382+ {
383+ newCacheableKeyRing = CacheableKeyRingProvider . GetCacheableKeyRing ( utcNow ) ;
384+ }
385+ catch ( Exception ex )
386+ {
387+ // Cold-start branch: always a "reading" rather than "refreshing" failure, and
388+ // there's no stale ring to extend via WithTemporaryExtendedLifetime.
389+ // The async refresh path handles both concerns. We leave _cacheableKeyRing
390+ // and _cacheableKeyRingTask null, so the next caller retries from scratch.
391+ _logger . ErrorOccurredWhileReadingKeyRing ( ex ) ;
392+ throw ;
393+ }
394+
395+ Volatile . Write ( ref _cacheableKeyRing , newCacheableKeyRing ) ;
396+ return newCacheableKeyRing . KeyRing ;
397+ }
398+
399+ // Stale ring exists: dispatch the refresh asynchronously and let every other
400+ // caller return the stale ring without waiting. This is the perf win from #54675.
364401 // PERF: Closing over utcNow substantially slows down the fast case (valid cache) in micro-benchmarks
365402 // (closing over `this` for CacheableKeyRingProvider doesn't seem impactful)
366403 existingTask = Task . Factory . StartNew (
@@ -390,9 +427,8 @@ private IKeyRing GetCurrentKeyRingCoreNew(DateTime utcNow, bool forceRefresh)
390427 }
391428
392429 // Prefer a stale cached key ring to blocking
393- if ( existingCacheableKeyRing is not null )
430+ if ( ! forceRefresh && existingCacheableKeyRing is not null )
394431 {
395- Debug . Assert ( ! forceRefresh , "Consumed cached key ring even though forceRefresh is true" ) ;
396432 Debug . Assert ( ! CacheableKeyRing . IsValid ( existingCacheableKeyRing , utcNow ) , "Should have returned a valid cached key ring above" ) ;
397433 return existingCacheableKeyRing . KeyRing ;
398434 }
0 commit comments