diff --git a/BitFaster.Caching.UnitTests/Atomic/AsyncAtomicFactoryTests.cs b/BitFaster.Caching.UnitTests/Atomic/AsyncAtomicFactoryTests.cs index fdb8a6f8..b47f414e 100644 --- a/BitFaster.Caching.UnitTests/Atomic/AsyncAtomicFactoryTests.cs +++ b/BitFaster.Caching.UnitTests/Atomic/AsyncAtomicFactoryTests.cs @@ -114,6 +114,48 @@ public async Task WhenCallersRunConcurrentlyResultIsFromWinner() winnerCount.Should().Be(1); } + [Fact] + public async Task WhenCallersRunConcurrentlyWithFailureSameExceptionIsPropagated() + { + var enter = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var resume = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var atomicFactory = new AsyncAtomicFactory(); + + var first = atomicFactory.GetValueAsync(1, async k => + { + enter.SetResult(true); + await resume.Task; + + throw new ArithmeticException("1"); + }).AsTask(); + + var second = atomicFactory.GetValueAsync(1, async k => + { + enter.SetResult(true); + await resume.Task; + + throw new InvalidOperationException("2"); + }).AsTask(); + + await enter.Task; + resume.SetResult(true); + + // Both tasks will throw, but the first one to complete will propagate its exception + // Both exceptions should be the same. If they are not, there will be an aggregate exception. + try + { + await Task.WhenAll(first, second) + .TimeoutAfter(TimeSpan.FromSeconds(5), "Tasks did not complete within the expected time. Exceptions are not propagated between callers correctly."); + } + catch (ArithmeticException) + { + } + catch (InvalidOperationException) + { + } + } + [Fact] public void WhenValueNotCreatedHashCodeIsZero() { diff --git a/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheTests.cs b/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheTests.cs index 7459de31..00ff2a1c 100644 --- a/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheTests.cs +++ b/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheTests.cs @@ -92,7 +92,7 @@ public void WhenRemovedEventHandlerIsRegisteredItIsFired() } // backcompat: remove conditional compile -#if NETCOREAPP3_0_OR_GREATER +#if NET [Fact] public void WhenUpdatedEventHandlerIsRegisteredItIsFired() { @@ -259,7 +259,7 @@ public async Task WhenFactoryThrowsEmptyKeyIsNotEnumerable() } // backcompat: remove conditional compile -#if NETCOREAPP3_0_OR_GREATER +#if NET [Fact] public void WhenRemovedValueIsReturned() { diff --git a/BitFaster.Caching.UnitTests/Atomic/ScopedAsyncAtomicFactoryTests.cs b/BitFaster.Caching.UnitTests/Atomic/ScopedAsyncAtomicFactoryTests.cs index bc941309..c2705bc7 100644 --- a/BitFaster.Caching.UnitTests/Atomic/ScopedAsyncAtomicFactoryTests.cs +++ b/BitFaster.Caching.UnitTests/Atomic/ScopedAsyncAtomicFactoryTests.cs @@ -156,6 +156,49 @@ public async Task WhenCallersRunConcurrentlyResultIsFromWinner() winnerCount.Should().Be(1); } + + [Fact] + public async Task WhenCallersRunConcurrentlyWithFailureSameExceptionIsPropagated() + { + var enter = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var resume = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var atomicFactory = new ScopedAsyncAtomicFactory(); + + var first = atomicFactory.TryCreateLifetimeAsync(1, async k => + { + enter.SetResult(true); + await resume.Task; + + throw new ArithmeticException("1"); + }).AsTask(); ; + + var second = atomicFactory.TryCreateLifetimeAsync(1, async k => + { + enter.SetResult(true); + await resume.Task; + + throw new InvalidOperationException("2"); + }).AsTask(); + + await enter.Task; + resume.SetResult(true); + + // Both tasks will throw, but the first one to complete will propagate its exception + // Both exceptions should be the same. If they are not, there will be an aggregate exception. + try + { + await Task.WhenAll(first, second) + .TimeoutAfter(TimeSpan.FromSeconds(5), "Tasks did not complete within the expected time. Exceptions are not propagated between callers correctly."); + } + catch (ArithmeticException) + { + } + catch (InvalidOperationException) + { + } + } + [Fact] public async Task WhenDisposedWhileInitResultIsDisposed() {