diff --git a/BitFaster.Caching.UnitTests/Atomic/AsyncAtomicFactoryTests.cs b/BitFaster.Caching.UnitTests/Atomic/AsyncAtomicFactoryTests.cs index b47f414e..7723cc43 100644 --- a/BitFaster.Caching.UnitTests/Atomic/AsyncAtomicFactoryTests.cs +++ b/BitFaster.Caching.UnitTests/Atomic/AsyncAtomicFactoryTests.cs @@ -4,11 +4,19 @@ using BitFaster.Caching.Atomic; using FluentAssertions; using Xunit; +using Xunit.Abstractions; namespace BitFaster.Caching.UnitTests.Atomic { public class AsyncAtomicFactoryTests { + private readonly ITestOutputHelper outputHelper; + + public AsyncAtomicFactoryTests(ITestOutputHelper outputHelper) + { + this.outputHelper = outputHelper; + } + [Fact] public void DefaultCtorValueIsNotCreated() { @@ -156,6 +164,46 @@ 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) + { + outputHelper.WriteLine($"Unobserved task exception {e.Exception}"); + 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..7805b5d4 100644 --- a/BitFaster.Caching.UnitTests/Atomic/ScopedAsyncAtomicFactoryTests.cs +++ b/BitFaster.Caching.UnitTests/Atomic/ScopedAsyncAtomicFactoryTests.cs @@ -4,11 +4,19 @@ using BitFaster.Caching.Atomic; using FluentAssertions; using Xunit; +using Xunit.Abstractions; namespace BitFaster.Caching.UnitTests.Atomic { public class ScopedAsyncAtomicFactoryTests { + private readonly ITestOutputHelper outputHelper; + + public ScopedAsyncAtomicFactoryTests(ITestOutputHelper outputHelper) + { + this.outputHelper = outputHelper; + } + [Fact] public void WhenScopeIsNotCreatedScopeIfCreatedReturnsNull() { @@ -105,7 +113,7 @@ public void WhenValueIsCreatedDisposeDisposesValue() { var holder = new IntHolder() { actualNumber = 2 }; var atomicFactory = new ScopedAsyncAtomicFactory(holder); - + atomicFactory.Dispose(); holder.disposed.Should().BeTrue(); @@ -152,11 +160,10 @@ public async Task WhenCallersRunConcurrentlyResultIsFromWinner() result1.l.Value.actualNumber.Should().Be(winningNumber); result2.l.Value.actualNumber.Should().Be(winningNumber); - + winnerCount.Should().Be(1); } - [Fact] public async Task WhenCallersRunConcurrentlyWithFailureSameExceptionIsPropagated() { @@ -199,6 +206,49 @@ await Task.WhenAll(first, second) } } + [Fact] + public async Task WhenCreateFromFactoryLifetimeThrowsDoesNotCauseUnobservedTaskException() + { + bool unobservedExceptionThrown = false; + TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; + + try + { + await ScopedAsyncAtomicFactoryTryCreateLifetimeAsync(); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + finally + { + TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException; + } + + unobservedExceptionThrown.Should().BeFalse(); + + void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) + { + outputHelper.WriteLine($"Unobserved task exception {e.Exception}"); + unobservedExceptionThrown = true; + e.SetObserved(); + } + + static async Task ScopedAsyncAtomicFactoryTryCreateLifetimeAsync() + { + 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); } }