@@ -321,137 +321,152 @@ private IKeyRing GetCurrentKeyRingCoreNew(DateTime utcNow, bool forceRefresh)
321321 // key ring is valid. We do what we can to avoid unnecessary overhead (locking,
322322 // context switching, etc) on this path.
323323
324- CacheableKeyRing ? existingCacheableKeyRing = null ;
325-
326324 // Can we return the cached keyring to the caller?
327325 if ( ! forceRefresh )
328326 {
329- existingCacheableKeyRing = Volatile . Read ( ref _cacheableKeyRing ) ;
330- if ( CacheableKeyRing . IsValid ( existingCacheableKeyRing , utcNow ) )
327+ var cached = Volatile . Read ( ref _cacheableKeyRing ) ;
328+ if ( CacheableKeyRing . IsValid ( cached , utcNow ) )
331329 {
332- return existingCacheableKeyRing . KeyRing ;
330+ return cached . KeyRing ;
333331 }
334332 }
335333
336- // If work kicked off by a previous caller has completed, we should use those results
337- // We check this outside the lock to reduce contention in the common case (no task).
338- // Logically, it would probably make more sense to check this before checking whether
339- // the cache is valid - there could be a newer value available - but keeping that path
340- // fast is more important. The next forced refresh or cache expiration will cause the
341- // new value to be picked up.
342- var existingTask = Volatile . Read ( ref _cacheableKeyRingTask ) ;
343- if ( existingTask is not null && existingTask . IsCompleted )
334+ CacheableKeyRing ? existingCacheableKeyRing = null ;
335+ Task < CacheableKeyRing > ? existingTask = null ;
336+
337+ lock ( _cacheableKeyRingLockObj )
344338 {
345- var taskKeyRing = GetKeyRingFromCompletedTask ( existingTask , utcNow ) ; // Throws if the task failed
346- if ( taskKeyRing is not null )
339+ // Did another thread acquire the lock first and populate the cache?
340+ // This could have happened if there was a completed in-flight task for the other thread to process.
341+ if ( ! forceRefresh )
347342 {
348- return taskKeyRing ;
343+ existingCacheableKeyRing = Volatile . Read ( ref _cacheableKeyRing ) ;
344+ if ( CacheableKeyRing . IsValid ( existingCacheableKeyRing , utcNow ) )
345+ {
346+ return existingCacheableKeyRing . KeyRing ;
347+ }
349348 }
350- }
351349
352- // The cached keyring hasn't been created or must be refreshed. We'll allow one thread to
353- // create a task to update the keyring, and all threads will continue to use the existing cached
354- // keyring while the first thread performs the update. There is an exception: if there
355- // is no usable existing cached keyring, all callers must block until the keyring exists.
356- lock ( _cacheableKeyRingLockObj )
357- {
358- // Update existingTask, in case we're not the first to acquire the lock
359- existingTask = Volatile . Read ( ref _cacheableKeyRingTask ) ;
350+ existingTask = _cacheableKeyRingTask ;
360351 if ( existingTask is null )
361352 {
362353 // If there's no existing task, make one now
363354 // PERF: Closing over utcNow substantially slows down the fast case (valid cache) in micro-benchmarks
364355 // (closing over `this` for CacheableKeyRingProvider doesn't seem impactful)
365356 existingTask = Task . Factory . StartNew (
366- utcNow => CacheableKeyRingProvider . GetCacheableKeyRing ( ( DateTime ) utcNow ! ) ,
357+ utcNowState => CacheableKeyRingProvider . GetCacheableKeyRing ( ( DateTime ) utcNowState ! ) ,
367358 utcNow ,
368- CancellationToken . None , // GetKeyRingFromCompletedTask will need to react if this becomes cancellable
359+ CancellationToken . None , // GetKeyRingFromCompletedTaskUnsynchronized will need to react if this becomes cancellable
369360 TaskCreationOptions . DenyChildAttach ,
370361 TaskScheduler . Default ) ;
371- Volatile . Write ( ref _cacheableKeyRingTask , existingTask ) ;
362+ _cacheableKeyRingTask = existingTask ;
363+ }
364+
365+ // This is mostly for the case where existingTask already set, but no harm in checking a fresh one
366+ if ( existingTask . IsCompleted )
367+ {
368+ // If work kicked off by a previous caller has completed, we should use those results.
369+ // Logically, it would probably make more sense to check this before checking whether
370+ // the cache is valid - there could be a newer value available - but keeping that path
371+ // fast is more important. The next forced refresh or cache expiration will cause the
372+ // new value to be picked up.
373+
374+ // An unconsumed task result is considered to satisfy forceRefresh. One could quibble that this isn't really
375+ // a forced refresh, but we'll still return a key ring newer than the one the caller was dissatisfied with.
376+ var taskKeyRing = GetKeyRingFromCompletedTaskUnsynchronized ( existingTask , utcNow ) ; // Throws if the task failed
377+ Debug . Assert ( taskKeyRing is not null , "How did _cacheableKeyRingTask change while we were holding the lock?" ) ;
378+ return taskKeyRing ;
372379 }
373380 }
374381
382+ // Prefer a stale cached key ring to blocking
375383 if ( existingCacheableKeyRing is not null )
376384 {
377- Debug . Assert ( ! forceRefresh , "Read cached key ring even though forceRefresh is true" ) ;
378- Debug . Assert ( ! CacheableKeyRing . IsValid ( existingCacheableKeyRing , utcNow ) , "Should already have returned a valid cached key ring" ) ;
385+ Debug . Assert ( ! forceRefresh , "Consumed cached key ring even though forceRefresh is true" ) ;
386+ Debug . Assert ( ! CacheableKeyRing . IsValid ( existingCacheableKeyRing , utcNow ) , "Should have returned a valid cached key ring above " ) ;
379387 return existingCacheableKeyRing . KeyRing ;
380388 }
381389
382- // Since there's no cached key ring we can return, we have to wait. It's not ideal to wait for a task we
383- // just scheduled, but it makes the code a lot simpler (compared to having a separate, synchronous code path).
384- // Cleverness: swallow any exceptions - they'll be surfaced by GetKeyRingFromCompletedTask, if appropriate.
390+ // If there's not even a stale cached key ring we can use, we have to wait.
391+ // It's not ideal to wait for a task that was just scheduled, but it makes the code a lot simpler
392+ // (compared to having a separate, synchronous code path).
393+
394+ // The reason we yield the lock and wait for the task instead is to allow racing forceRefresh threads
395+ // to wait for the same task, rather than being sequentialized (and each doing its own refresh).
396+
397+ // Cleverness: swallow any exceptions - they'll be surfaced by GetKeyRingFromCompletedTaskUnsynchronized, if appropriate.
385398 existingTask
386- . ContinueWith ( static _ => { } , TaskScheduler . Default )
399+ . ContinueWith (
400+ static t => _ = t . Exception , // Still observe the exception - just don't throw it
401+ CancellationToken . None ,
402+ TaskContinuationOptions . ExecuteSynchronously ,
403+ TaskScheduler . Default )
387404 . Wait ( ) ;
388405
389- var newKeyRing = GetKeyRingFromCompletedTask ( existingTask , utcNow ) ; // Throws if the task failed (winning thread only)
390- if ( newKeyRing is null )
406+ lock ( _cacheableKeyRingLockObj )
391407 {
392- // Another thread won - check whether it cached a new key ring
393- var newCacheableKeyRing = Volatile . Read ( ref _cacheableKeyRing ) ;
394- if ( newCacheableKeyRing is null )
408+ var newKeyRing = GetKeyRingFromCompletedTaskUnsynchronized ( existingTask , utcNow ) ; // Throws if the task failed (winning thread only)
409+ if ( newKeyRing is null )
395410 {
396- // There will have been a better exception from the winning thread
397- throw Error . KeyRingProvider_RefreshFailedOnOtherThread ( ) ;
411+ // Another thread won - check whether it cached a new key ring
412+ var newCacheableKeyRing = Volatile . Read ( ref _cacheableKeyRing ) ;
413+ if ( newCacheableKeyRing is null )
414+ {
415+ // There will have been a better exception from the winning thread
416+ throw Error . KeyRingProvider_RefreshFailedOnOtherThread ( existingTask . Exception ) ;
417+ }
418+
419+ newKeyRing = newCacheableKeyRing . KeyRing ;
398420 }
399421
400- newKeyRing = newCacheableKeyRing . KeyRing ;
422+ return newKeyRing ;
401423 }
402-
403- return newKeyRing ;
404424 }
405425
406426 /// <summary>
407- /// Returns null if another thread already processed the completed task.
408- /// Otherwise, if the given completed task completed successfully, clears the task
409- /// and either caches and returns the resulting key ring or throws, according to the
410- /// successfulness of the task.
427+ /// If the given completed task completed successfully, clears the task and either
428+ /// caches and returns the resulting key ring or throws, according to the successfulness
429+ /// of the task.
411430 /// </summary>
412- private IKeyRing ? GetKeyRingFromCompletedTask ( Task < CacheableKeyRing > task , DateTime utcNow )
431+ /// <remarks>
432+ /// Must be called under <see cref="_cacheableKeyRingLockObj"/>.
433+ /// </remarks>
434+ private IKeyRing ? GetKeyRingFromCompletedTaskUnsynchronized ( Task < CacheableKeyRing > task , DateTime utcNow )
413435 {
414436 Debug . Assert ( task . IsCompleted ) ;
437+ Debug . Assert ( ! task . IsCanceled , "How did a task with no cancellation token get canceled?" ) ;
415438
416- lock ( _cacheableKeyRingLockObj )
439+ // If the parameter doesn't match the field, another thread has already consumed the task (and it's reflected in _cacheableKeyRing)
440+ if ( ! ReferenceEquals ( task , _cacheableKeyRingTask ) )
417441 {
418- // If the parameter doesn't match the field, another thread has already consumed the task (and it's reflected in _cacheableKeyRing)
419- if ( ! ReferenceEquals ( task , Volatile . Read ( ref _cacheableKeyRingTask ) ) )
420- {
421- return null ;
422- }
423-
424- Volatile . Write ( ref _cacheableKeyRingTask , null ) ;
425-
426- if ( task . Status == TaskStatus . RanToCompletion )
427- {
428- var newCacheableKeyRing = task . Result ;
429- Volatile . Write ( ref _cacheableKeyRing , newCacheableKeyRing ) ;
430-
431- // An unconsumed task result is considered to satisfy forceRefresh. One could quibble that this isn't really
432- // a forced refresh, but we'll still return a key ring newer than the one the caller was dissatisfied with.
433- return newCacheableKeyRing . KeyRing ;
434- }
442+ return null ;
443+ }
435444
436- Debug . Assert ( ! task . IsCanceled , "How did a task with no cancellation token get canceled?" ) ;
437- Debug . Assert ( task . Exception is not null , "Task should have either completed successfully or with an exception" ) ;
438- var exception = task . Exception ;
445+ _cacheableKeyRingTask = null ;
439446
447+ try
448+ {
449+ var newCacheableKeyRing = task . GetAwaiter ( ) . GetResult ( ) ; // Call GetResult to throw on failure
450+ Volatile . Write ( ref _cacheableKeyRing , newCacheableKeyRing ) ;
451+ return newCacheableKeyRing . KeyRing ;
452+ }
453+ catch ( Exception e )
454+ {
440455 var existingCacheableKeyRing = Volatile . Read ( ref _cacheableKeyRing ) ;
441456 if ( existingCacheableKeyRing is not null && ! CacheableKeyRing . IsValid ( existingCacheableKeyRing , utcNow ) )
442457 {
443458 // If reading failed, we probably don't want to try again for a little bit, so slightly extend the
444459 // lifetime of the current cache entry
445460 Volatile . Write ( ref _cacheableKeyRing , existingCacheableKeyRing . WithTemporaryExtendedLifetime ( utcNow ) ) ;
446461
447- _logger . ErrorOccurredWhileRefreshingKeyRing ( exception ) ; // This one mentions the no-retry window
462+ _logger . ErrorOccurredWhileRefreshingKeyRing ( e ) ; // This one mentions the no-retry window
448463 }
449464 else
450465 {
451- _logger . ErrorOccurredWhileReadingKeyRing ( exception ) ;
466+ _logger . ErrorOccurredWhileReadingKeyRing ( e ) ;
452467 }
453468
454- throw exception . InnerExceptions . Count == 1 ? exception . InnerExceptions [ 0 ] : exception ;
469+ throw ;
455470 }
456471 }
457472
0 commit comments