Skip to content

Commit 2e420f0

Browse files
authored
Avoid unobserved task exception in AsyncAtomicFactory (#686)
* AsyncAtomicFactory.Initializer: do not call SetException on TCS to avoid unobserved task exception when garbage collecting TCS * Consider other exception type on build server * Remove second GetValueAsync call * ScopedAsyncAtomicFactory.Initializer: avoid unobserved task exception as well Remove test in AsyncAtomicFactoryTests again * fix * task => thread * Rename test in ScopedAsyncAtomicFactoryTests and log unobserved task exception to test output. ---------
1 parent aefdf90 commit 2e420f0

File tree

4 files changed

+109
-5
lines changed

4 files changed

+109
-5
lines changed

BitFaster.Caching.UnitTests/Atomic/AsyncAtomicFactoryTests.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@
44
using BitFaster.Caching.Atomic;
55
using FluentAssertions;
66
using Xunit;
7+
using Xunit.Abstractions;
78

89
namespace BitFaster.Caching.UnitTests.Atomic
910
{
1011
public class AsyncAtomicFactoryTests
1112
{
13+
private readonly ITestOutputHelper outputHelper;
14+
15+
public AsyncAtomicFactoryTests(ITestOutputHelper outputHelper)
16+
{
17+
this.outputHelper = outputHelper;
18+
}
19+
1220
[Fact]
1321
public void DefaultCtorValueIsNotCreated()
1422
{
@@ -156,6 +164,46 @@ await Task.WhenAll(first, second)
156164
}
157165
}
158166

167+
[Fact]
168+
public async Task WhenValueCreateThrowsDoesNotCauseUnobservedTaskException()
169+
{
170+
bool unobservedExceptionThrown = false;
171+
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
172+
173+
try
174+
{
175+
await AsyncAtomicFactoryGetValueAsync();
176+
177+
GC.Collect();
178+
GC.WaitForPendingFinalizers();
179+
}
180+
finally
181+
{
182+
TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException;
183+
}
184+
185+
unobservedExceptionThrown.Should().BeFalse();
186+
187+
void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
188+
{
189+
outputHelper.WriteLine($"Unobserved task exception {e.Exception}");
190+
unobservedExceptionThrown = true;
191+
e.SetObserved();
192+
}
193+
194+
static async Task AsyncAtomicFactoryGetValueAsync()
195+
{
196+
var a = new AsyncAtomicFactory<int, int>();
197+
try
198+
{
199+
_ = await a.GetValueAsync(12, i => throw new ArithmeticException());
200+
}
201+
catch (ArithmeticException)
202+
{
203+
}
204+
}
205+
}
206+
159207
[Fact]
160208
public void WhenValueNotCreatedHashCodeIsZero()
161209
{

BitFaster.Caching.UnitTests/Atomic/ScopedAsyncAtomicFactoryTests.cs

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@
44
using BitFaster.Caching.Atomic;
55
using FluentAssertions;
66
using Xunit;
7+
using Xunit.Abstractions;
78

89
namespace BitFaster.Caching.UnitTests.Atomic
910
{
1011
public class ScopedAsyncAtomicFactoryTests
1112
{
13+
private readonly ITestOutputHelper outputHelper;
14+
15+
public ScopedAsyncAtomicFactoryTests(ITestOutputHelper outputHelper)
16+
{
17+
this.outputHelper = outputHelper;
18+
}
19+
1220
[Fact]
1321
public void WhenScopeIsNotCreatedScopeIfCreatedReturnsNull()
1422
{
@@ -105,7 +113,7 @@ public void WhenValueIsCreatedDisposeDisposesValue()
105113
{
106114
var holder = new IntHolder() { actualNumber = 2 };
107115
var atomicFactory = new ScopedAsyncAtomicFactory<int, IntHolder>(holder);
108-
116+
109117
atomicFactory.Dispose();
110118

111119
holder.disposed.Should().BeTrue();
@@ -152,11 +160,10 @@ public async Task WhenCallersRunConcurrentlyResultIsFromWinner()
152160

153161
result1.l.Value.actualNumber.Should().Be(winningNumber);
154162
result2.l.Value.actualNumber.Should().Be(winningNumber);
155-
163+
156164
winnerCount.Should().Be(1);
157165
}
158166

159-
160167
[Fact]
161168
public async Task WhenCallersRunConcurrentlyWithFailureSameExceptionIsPropagated()
162169
{
@@ -199,6 +206,49 @@ await Task.WhenAll(first, second)
199206
}
200207
}
201208

209+
[Fact]
210+
public async Task WhenCreateFromFactoryLifetimeThrowsDoesNotCauseUnobservedTaskException()
211+
{
212+
bool unobservedExceptionThrown = false;
213+
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
214+
215+
try
216+
{
217+
await ScopedAsyncAtomicFactoryTryCreateLifetimeAsync();
218+
219+
GC.Collect();
220+
GC.WaitForPendingFinalizers();
221+
}
222+
finally
223+
{
224+
TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException;
225+
}
226+
227+
unobservedExceptionThrown.Should().BeFalse();
228+
229+
void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
230+
{
231+
outputHelper.WriteLine($"Unobserved task exception {e.Exception}");
232+
unobservedExceptionThrown = true;
233+
e.SetObserved();
234+
}
235+
236+
static async Task ScopedAsyncAtomicFactoryTryCreateLifetimeAsync()
237+
{
238+
var a = new ScopedAsyncAtomicFactory<int, IntHolder>();
239+
try
240+
{
241+
_ = await a.TryCreateLifetimeAsync(1, k =>
242+
{
243+
throw new ArithmeticException();
244+
});
245+
}
246+
catch (ArithmeticException)
247+
{
248+
}
249+
}
250+
}
251+
202252
[Fact]
203253
public async Task WhenDisposedWhileInitResultIsDisposed()
204254
{

BitFaster.Caching/Atomic/AsyncAtomicFactory.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,10 @@ public async ValueTask<V> CreateValueAsync<TFactory>(K key, TFactory valueFactor
159159
{
160160
Volatile.Write(ref isInitialized, false);
161161
tcs.SetException(ex);
162-
throw;
162+
163+
// always await the task to avoid unobserved task exceptions - normal case is that no other thread is waiting.
164+
// this will re-throw the exception.
165+
await tcs.Task.ConfigureAwait(false);
163166
}
164167
}
165168

BitFaster.Caching/Atomic/ScopedAsyncAtomicFactory.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,10 @@ public async ValueTask<Scoped<V>> CreateScopeAsync<TFactory>(K key, TFactory val
178178
{
179179
Volatile.Write(ref isTaskInitialized, false);
180180
tcs.SetException(ex);
181-
throw;
181+
182+
// always await the task to avoid unobserved task exceptions - normal case is that no other thread is waiting.
183+
// this will re-throw the exception.
184+
await tcs.Task.ConfigureAwait(false);
182185
}
183186
}
184187

0 commit comments

Comments
 (0)