Skip to content

Commit e590d18

Browse files
authored
CSHARP-5798: Implement test to track if any UnobservedTaskExceptions were raised while test run (#1836)
1 parent fe6f12f commit e590d18

File tree

4 files changed

+67
-39
lines changed

4 files changed

+67
-39
lines changed

src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,17 @@ private void ConfigureConnectedSocket(Socket socket)
165165

166166
private void Connect(Socket socket, EndPoint endPoint, CancellationToken cancellationToken)
167167
{
168-
var isSocketDisposed = false;
168+
var callbackState = new OperationCallbackState<Socket>(socket);
169169
using var timeoutCancellationTokenSource = new CancellationTokenSource(_settings.ConnectTimeout);
170170
using var combinedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token);
171-
using var cancellationSubscription = combinedCancellationTokenSource.Token.Register(DisposeSocket);
171+
using var cancellationSubscription = combinedCancellationTokenSource.Token.Register(state =>
172+
{
173+
var operationState = (OperationCallbackState<Socket>)state;
174+
if (operationState.TryChangeStatusFromInProgress(OperationCallbackState<Socket>.OperationStatus.Interrupted))
175+
{
176+
DisposeSocket(operationState.Subject);
177+
}
178+
}, callbackState);
172179

173180
try
174181
{
@@ -185,13 +192,14 @@ private void Connect(Socket socket, EndPoint endPoint, CancellationToken cancell
185192
#else
186193
socket.Connect(endPoint);
187194
#endif
195+
if (!callbackState.TryChangeStatusFromInProgress(OperationCallbackState<Socket>.OperationStatus.Done))
196+
{
197+
throw new ObjectDisposedException(nameof(Socket));
198+
}
188199
}
189200
catch
190201
{
191-
if (!isSocketDisposed)
192-
{
193-
DisposeSocket();
194-
}
202+
DisposeSocket(socket);
195203

196204
cancellationToken.ThrowIfCancellationRequested();
197205
if (timeoutCancellationTokenSource.IsCancellationRequested)
@@ -202,9 +210,8 @@ private void Connect(Socket socket, EndPoint endPoint, CancellationToken cancell
202210
throw;
203211
}
204212

205-
void DisposeSocket()
213+
static void DisposeSocket(Socket socket)
206214
{
207-
isSocketDisposed = true;
208215
try
209216
{
210217
socket.Dispose();
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System.Threading;
17+
18+
namespace MongoDB.Driver.Core.Misc;
19+
20+
internal sealed class OperationCallbackState<T>(T subject)
21+
{
22+
private int _status = (int)OperationStatus.InProgress;
23+
24+
public OperationStatus Status => (OperationStatus)_status;
25+
public T Subject => subject;
26+
public bool TryChangeStatusFromInProgress(OperationStatus newState) =>
27+
Interlocked.CompareExchange(ref _status, (int)newState, (int)OperationStatus.InProgress) == (int)OperationStatus.InProgress;
28+
29+
public enum OperationStatus
30+
{
31+
InProgress = 0,
32+
Done,
33+
Interrupted,
34+
}
35+
}

src/MongoDB.Driver/Core/Misc/StreamExtensionMethods.cs

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -287,33 +287,33 @@ private static void ExecuteOperationWithTimeout<TState>(Stream stream, TState st
287287
throw new TimeoutException();
288288
}
289289

290-
StreamDisposeCallbackState callbackState = null;
290+
OperationCallbackState<Stream> callbackState = null;
291291
Timer timer = null;
292292
CancellationTokenRegistration cancellationSubscription = default;
293293
if (timeoutMs > 0)
294294
{
295-
callbackState = new StreamDisposeCallbackState(stream);
295+
callbackState = new OperationCallbackState<Stream>(stream);
296296
timer = new Timer(DisposeStreamCallback, callbackState, timeoutMs, Timeout.Infinite);
297297
}
298298

299299
if (cancellationToken.CanBeCanceled)
300300
{
301-
callbackState ??= new StreamDisposeCallbackState(stream);
301+
callbackState ??= new OperationCallbackState<Stream>(stream);
302302
cancellationSubscription = cancellationToken.Register(DisposeStreamCallback, callbackState);
303303
}
304304

305305
try
306306
{
307307
operation(stream, state);
308-
if (callbackState?.TryChangeStateFromInProgress(OperationState.Done) == false)
308+
if (callbackState?.TryChangeStatusFromInProgress(OperationCallbackState<Stream>.OperationStatus.Done) == false)
309309
{
310310
// If the state can't be changed - then the stream was/will be disposed, throw here
311311
throw new IOException();
312312
}
313313
}
314314
catch (Exception ex)
315315
{
316-
if (callbackState?.OperationState == OperationState.Interrupted)
316+
if (callbackState?.Status == OperationCallbackState<Stream>.OperationStatus.Interrupted)
317317
{
318318
cancellationToken.ThrowIfCancellationRequested();
319319
throw new TimeoutException();
@@ -334,39 +334,22 @@ private static void ExecuteOperationWithTimeout<TState>(Stream stream, TState st
334334

335335
static void DisposeStreamCallback(object state)
336336
{
337-
var disposeCallbackState = (StreamDisposeCallbackState)state;
338-
if (!disposeCallbackState.TryChangeStateFromInProgress(OperationState.Interrupted))
337+
var disposeCallbackState = (OperationCallbackState<Stream>)state;
338+
if (!disposeCallbackState.TryChangeStatusFromInProgress(OperationCallbackState<Stream>.OperationStatus.Interrupted))
339339
{
340340
// If the state can't be changed - then I/O had already succeeded
341341
return;
342342
}
343343

344344
try
345345
{
346-
disposeCallbackState.Stream.Dispose();
346+
disposeCallbackState.Subject.Dispose();
347347
}
348348
catch (Exception)
349349
{
350350
// callbacks should not fail, suppress any exceptions here
351351
}
352352
}
353353
}
354-
355-
private record StreamDisposeCallbackState(Stream Stream)
356-
{
357-
private int _operationState = (int)OperationState.InProgress;
358-
359-
public OperationState OperationState => (OperationState)_operationState;
360-
361-
public bool TryChangeStateFromInProgress(OperationState newState) =>
362-
Interlocked.CompareExchange(ref _operationState, (int)newState, (int)OperationState.InProgress) == (int)OperationState.InProgress;
363-
}
364-
365-
private enum OperationState
366-
{
367-
InProgress = 0,
368-
Done,
369-
Interrupted,
370-
}
371354
}
372355
}

tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTestDiscoverer.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,16 @@ public UnobservedExceptionTestDiscoverer(IMessageSink diagnosticsMessageSink)
4242

4343
public IEnumerable<IXunitTestCase> Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute)
4444
{
45-
return [new XunitTestCase(_diagnosticsMessageSink, TestMethodDisplay.Method, TestMethodDisplayOptions.All, testMethod)
45+
var testCase = new XunitTestCase(_diagnosticsMessageSink, TestMethodDisplay.Method, TestMethodDisplayOptions.All, testMethod);
46+
if (!testCase.Traits.TryGetValue("Category", out var categories))
4647
{
47-
Traits =
48-
{
49-
{ "Category", ["UnobservedExceptionTracking"] }
50-
}
51-
}];
48+
categories = new List<string>();
49+
testCase.Traits.Add("Category", categories);
50+
}
51+
52+
categories.Add("UnobservedExceptionTracking");
53+
54+
return [testCase];
5255
}
5356

5457
void UnobservedTaskExceptionEventHandler(object sender, UnobservedTaskExceptionEventArgs unobservedException) =>

0 commit comments

Comments
 (0)