From 843e8b97de34a79d3492823fa647db9e4834dfd5 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Fri, 13 Jun 2025 15:42:00 -0700 Subject: [PATCH 1/2] fix --- .../Atomic/AsyncAtomicFactoryTests.cs | 39 +++++++++++++++++ .../Atomic/ScopedAsyncAtomicFactoryTests.cs | 43 ++++++++++++++++++- .../Atomic/AsyncAtomicFactory.cs | 5 ++- .../Atomic/ScopedAsyncAtomicFactory.cs | 5 ++- 4 files changed, 89 insertions(+), 3 deletions(-) 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..445a6ba9 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 task 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..4431fc6a 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 task is waiting. + // this will re-throw the exception. + await tcs.Task.ConfigureAwait(false); } } From 828ed649a09c13c2eda487b49637f2583d17bcfa Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Fri, 13 Jun 2025 15:57:08 -0700 Subject: [PATCH 2/2] task => thread --- BitFaster.Caching/Atomic/AsyncAtomicFactory.cs | 2 +- BitFaster.Caching/Atomic/ScopedAsyncAtomicFactory.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/BitFaster.Caching/Atomic/AsyncAtomicFactory.cs b/BitFaster.Caching/Atomic/AsyncAtomicFactory.cs index 445a6ba9..ff48ce40 100644 --- a/BitFaster.Caching/Atomic/AsyncAtomicFactory.cs +++ b/BitFaster.Caching/Atomic/AsyncAtomicFactory.cs @@ -160,7 +160,7 @@ public async ValueTask CreateValueAsync(K key, TFactory valueFactor Volatile.Write(ref isInitialized, false); tcs.SetException(ex); - // always await the task to avoid unobserved task exceptions - normal case is that no other task is waiting. + // 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 4431fc6a..839b51a1 100644 --- a/BitFaster.Caching/Atomic/ScopedAsyncAtomicFactory.cs +++ b/BitFaster.Caching/Atomic/ScopedAsyncAtomicFactory.cs @@ -179,7 +179,7 @@ public async ValueTask> CreateScopeAsync(K key, TFactory val Volatile.Write(ref isTaskInitialized, false); tcs.SetException(ex); - // always await the task to avoid unobserved task exceptions - normal case is that no other task is waiting. + // 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); }