@@ -321,137 +321,152 @@ private IKeyRing GetCurrentKeyRingCoreNew(DateTime utcNow, bool forceRefresh)
321
321
// key ring is valid. We do what we can to avoid unnecessary overhead (locking,
322
322
// context switching, etc) on this path.
323
323
324
- CacheableKeyRing ? existingCacheableKeyRing = null ;
325
-
326
324
// Can we return the cached keyring to the caller?
327
325
if ( ! forceRefresh )
328
326
{
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 ) )
331
329
{
332
- return existingCacheableKeyRing . KeyRing ;
330
+ return cached . KeyRing ;
333
331
}
334
332
}
335
333
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 )
344
338
{
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 )
347
342
{
348
- return taskKeyRing ;
343
+ existingCacheableKeyRing = Volatile . Read ( ref _cacheableKeyRing ) ;
344
+ if ( CacheableKeyRing . IsValid ( existingCacheableKeyRing , utcNow ) )
345
+ {
346
+ return existingCacheableKeyRing . KeyRing ;
347
+ }
349
348
}
350
- }
351
349
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 ;
360
351
if ( existingTask is null )
361
352
{
362
353
// If there's no existing task, make one now
363
354
// PERF: Closing over utcNow substantially slows down the fast case (valid cache) in micro-benchmarks
364
355
// (closing over `this` for CacheableKeyRingProvider doesn't seem impactful)
365
356
existingTask = Task . Factory . StartNew (
366
- utcNow => CacheableKeyRingProvider . GetCacheableKeyRing ( ( DateTime ) utcNow ! ) ,
357
+ utcNowState => CacheableKeyRingProvider . GetCacheableKeyRing ( ( DateTime ) utcNowState ! ) ,
367
358
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
369
360
TaskCreationOptions . DenyChildAttach ,
370
361
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 ;
372
379
}
373
380
}
374
381
382
+ // Prefer a stale cached key ring to blocking
375
383
if ( existingCacheableKeyRing is not null )
376
384
{
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 " ) ;
379
387
return existingCacheableKeyRing . KeyRing ;
380
388
}
381
389
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.
385
398
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 )
387
404
. Wait ( ) ;
388
405
389
- var newKeyRing = GetKeyRingFromCompletedTask ( existingTask , utcNow ) ; // Throws if the task failed (winning thread only)
390
- if ( newKeyRing is null )
406
+ lock ( _cacheableKeyRingLockObj )
391
407
{
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 )
395
410
{
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 ;
398
420
}
399
421
400
- newKeyRing = newCacheableKeyRing . KeyRing ;
422
+ return newKeyRing ;
401
423
}
402
-
403
- return newKeyRing ;
404
424
}
405
425
406
426
/// <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.
411
430
/// </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 )
413
435
{
414
436
Debug . Assert ( task . IsCompleted ) ;
437
+ Debug . Assert ( ! task . IsCanceled , "How did a task with no cancellation token get canceled?" ) ;
415
438
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 ) )
417
441
{
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
+ }
435
444
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 ;
439
446
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
+ {
440
455
var existingCacheableKeyRing = Volatile . Read ( ref _cacheableKeyRing ) ;
441
456
if ( existingCacheableKeyRing is not null && ! CacheableKeyRing . IsValid ( existingCacheableKeyRing , utcNow ) )
442
457
{
443
458
// If reading failed, we probably don't want to try again for a little bit, so slightly extend the
444
459
// lifetime of the current cache entry
445
460
Volatile . Write ( ref _cacheableKeyRing , existingCacheableKeyRing . WithTemporaryExtendedLifetime ( utcNow ) ) ;
446
461
447
- _logger . ErrorOccurredWhileRefreshingKeyRing ( exception ) ; // This one mentions the no-retry window
462
+ _logger . ErrorOccurredWhileRefreshingKeyRing ( e ) ; // This one mentions the no-retry window
448
463
}
449
464
else
450
465
{
451
- _logger . ErrorOccurredWhileReadingKeyRing ( exception ) ;
466
+ _logger . ErrorOccurredWhileReadingKeyRing ( e ) ;
452
467
}
453
468
454
- throw exception . InnerExceptions . Count == 1 ? exception . InnerExceptions [ 0 ] : exception ;
469
+ throw ;
455
470
}
456
471
}
457
472
0 commit comments