diff --git a/BitFaster.Caching.UnitTests/Atomic/AsyncAtomicFactoryTests.cs b/BitFaster.Caching.UnitTests/Atomic/AsyncAtomicFactoryTests.cs index b47f414e..44ffef70 100644 --- a/BitFaster.Caching.UnitTests/Atomic/AsyncAtomicFactoryTests.cs +++ b/BitFaster.Caching.UnitTests/Atomic/AsyncAtomicFactoryTests.cs @@ -156,6 +156,45 @@ await Task.WhenAll(first, second) } } + [Fact] + public async Task WhenValueCreateThrowsDoesNotCauseUnobservedTaskException() + { + bool unobservedExceptionThrown = false; + TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; + + try + { + await AsyncAtomicFactoryGetValueAsync(); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + finally + { + TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException; + } + + unobservedExceptionThrown.Should().BeFalse(); + + void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) + { + unobservedExceptionThrown = true; + e.SetObserved(); + } + + static async Task AsyncAtomicFactoryGetValueAsync() + { + var a = new AsyncAtomicFactory(); + try + { + _ = await a.GetValueAsync(12, i => throw new ArithmeticException()); + } + catch (ArithmeticException) + { + } + } + } + [Fact] public void WhenValueNotCreatedHashCodeIsZero() { diff --git a/BitFaster.Caching.UnitTests/Atomic/ScopedAsyncAtomicFactoryTests.cs b/BitFaster.Caching.UnitTests/Atomic/ScopedAsyncAtomicFactoryTests.cs index c2705bc7..cc6f7244 100644 --- a/BitFaster.Caching.UnitTests/Atomic/ScopedAsyncAtomicFactoryTests.cs +++ b/BitFaster.Caching.UnitTests/Atomic/ScopedAsyncAtomicFactoryTests.cs @@ -156,7 +156,6 @@ public async Task WhenCallersRunConcurrentlyResultIsFromWinner() winnerCount.Should().Be(1); } - [Fact] public async Task WhenCallersRunConcurrentlyWithFailureSameExceptionIsPropagated() { @@ -199,6 +198,48 @@ await Task.WhenAll(first, second) } } + [Fact] + public async Task WhenValueCreateThrowsDoesNotCauseUnobservedTaskException() + { + bool unobservedExceptionThrown = false; + TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; + + try + { + await AsyncAtomicFactoryGetValueAsync(); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + finally + { + TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException; + } + + unobservedExceptionThrown.Should().BeFalse(); + + void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) + { + unobservedExceptionThrown = true; + e.SetObserved(); + } + + static async Task AsyncAtomicFactoryGetValueAsync() + { + var a = new ScopedAsyncAtomicFactory(); + try + { + _ = await a.TryCreateLifetimeAsync(1, k => + { + throw new ArithmeticException(); + }); + } + catch (ArithmeticException) + { + } + } + } + [Fact] public async Task WhenDisposedWhileInitResultIsDisposed() { diff --git a/BitFaster.Caching/Atomic/AsyncAtomicFactory.cs b/BitFaster.Caching/Atomic/AsyncAtomicFactory.cs index 0588d0a9..ff48ce40 100644 --- a/BitFaster.Caching/Atomic/AsyncAtomicFactory.cs +++ b/BitFaster.Caching/Atomic/AsyncAtomicFactory.cs @@ -159,7 +159,10 @@ public async ValueTask CreateValueAsync(K key, TFactory valueFactor { Volatile.Write(ref isInitialized, false); tcs.SetException(ex); - throw; + + // always await the task to avoid unobserved task exceptions - normal case is that no other thread is waiting. + // this will re-throw the exception. + await tcs.Task.ConfigureAwait(false); } } diff --git a/BitFaster.Caching/Atomic/ScopedAsyncAtomicFactory.cs b/BitFaster.Caching/Atomic/ScopedAsyncAtomicFactory.cs index 594a609b..839b51a1 100644 --- a/BitFaster.Caching/Atomic/ScopedAsyncAtomicFactory.cs +++ b/BitFaster.Caching/Atomic/ScopedAsyncAtomicFactory.cs @@ -178,7 +178,10 @@ public async ValueTask> CreateScopeAsync(K key, TFactory val { Volatile.Write(ref isTaskInitialized, false); tcs.SetException(ex); - throw; + + // always await the task to avoid unobserved task exceptions - normal case is that no other thread is waiting. + // this will re-throw the exception. + await tcs.Task.ConfigureAwait(false); } }