From 6b9eb5041687eba24d09bf2e37f37ee0c01a4333 Mon Sep 17 00:00:00 2001 From: Oleksandr Poliakov Date: Mon, 8 Dec 2025 13:37:16 -0800 Subject: [PATCH 1/5] CSHARP-5798: Implement test to track if any UnobservedTaskExceptions were raised while test run --- .../Core/Connections/TcpStreamFactory.cs | 46 +++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs b/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs index 78f7fcc09f1..10e602f102d 100644 --- a/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs +++ b/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs @@ -31,6 +31,8 @@ namespace MongoDB.Driver.Core.Connections /// internal sealed class TcpStreamFactory : IStreamFactory { + private static readonly byte[] __ensureConnectedBuffer = new byte[1]; + // fields private readonly TcpStreamSettings _settings; @@ -165,10 +167,18 @@ private void ConfigureConnectedSocket(Socket socket) private void Connect(Socket socket, EndPoint endPoint, CancellationToken cancellationToken) { - var isSocketDisposed = false; + var callbackState = new ConnectOperationState(socket); using var timeoutCancellationTokenSource = new CancellationTokenSource(_settings.ConnectTimeout); using var combinedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token); - using var cancellationSubscription = combinedCancellationTokenSource.Token.Register(DisposeSocket); + using var cancellationSubscription = combinedCancellationTokenSource.Token.Register(state => + { + var operationState = (ConnectOperationState)state; + if (operationState.IsSucceeded) + { + return; + } + DisposeSocket(operationState.Socket); + }, callbackState); try { @@ -185,13 +195,12 @@ private void Connect(Socket socket, EndPoint endPoint, CancellationToken cancell #else socket.Connect(endPoint); #endif + EnsureConnected(socket); + callbackState.IsSucceeded = true; } catch { - if (!isSocketDisposed) - { - DisposeSocket(); - } + DisposeSocket(socket); cancellationToken.ThrowIfCancellationRequested(); if (timeoutCancellationTokenSource.IsCancellationRequested) @@ -202,9 +211,8 @@ private void Connect(Socket socket, EndPoint endPoint, CancellationToken cancell throw; } - void DisposeSocket() + static void DisposeSocket(Socket socket) { - isSocketDisposed = true; try { socket.Dispose(); @@ -214,6 +222,23 @@ void DisposeSocket() // Ignore any exceptions. } } + + static void EnsureConnected(Socket socket) + { + bool originalBlockingState = socket.Blocking; + socket.Blocking = false; + + try + { + // Try to use the socket to ensure it's connected. On MacOS with net6.0 sometimes Connect is completed successfully even after the socket disposal. + socket.Send(__ensureConnectedBuffer, 0, 0); + } + finally + { + // Restore original blocking state + socket.Blocking = originalBlockingState; + } + } } private async Task ConnectAsync(Socket socket, EndPoint endPoint, CancellationToken cancellationToken) @@ -376,5 +401,10 @@ public int Compare(EndPoint x, EndPoint y) return 0; } } + + private sealed record ConnectOperationState(Socket Socket) + { + public bool IsSucceeded { get; set; } + } } } From 83e8b524c621bb05a45515fcbfc3b03d70399b78 Mon Sep 17 00:00:00 2001 From: Oleksandr Poliakov Date: Mon, 8 Dec 2025 14:13:58 -0800 Subject: [PATCH 2/5] pr --- src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs b/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs index 10e602f102d..247738df6ef 100644 --- a/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs +++ b/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs @@ -226,10 +226,10 @@ static void DisposeSocket(Socket socket) static void EnsureConnected(Socket socket) { bool originalBlockingState = socket.Blocking; - socket.Blocking = false; try { + socket.Blocking = false; // Try to use the socket to ensure it's connected. On MacOS with net6.0 sometimes Connect is completed successfully even after the socket disposal. socket.Send(__ensureConnectedBuffer, 0, 0); } From 1c3449e3511f650dc29fae8dc58f2d8157594aac Mon Sep 17 00:00:00 2001 From: Oleksandr Poliakov Date: Mon, 8 Dec 2025 16:19:18 -0800 Subject: [PATCH 3/5] CSHARP-5798: Implement test to track if any UnobservedTaskExceptions were raised while test run --- .../Core/Connections/TcpStreamFactory.cs | 37 ++++--------------- .../Core/Misc/OperationCallbackState.cs | 37 +++++++++++++++++++ .../Core/Misc/StreamExtensionMethods.cs | 33 ++++------------- 3 files changed, 53 insertions(+), 54 deletions(-) create mode 100644 src/MongoDB.Driver/Core/Misc/OperationCallbackState.cs diff --git a/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs b/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs index 247738df6ef..e4d68d84622 100644 --- a/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs +++ b/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs @@ -167,17 +167,16 @@ private void ConfigureConnectedSocket(Socket socket) private void Connect(Socket socket, EndPoint endPoint, CancellationToken cancellationToken) { - var callbackState = new ConnectOperationState(socket); + var callbackState = new OperationCallbackState(socket); using var timeoutCancellationTokenSource = new CancellationTokenSource(_settings.ConnectTimeout); using var combinedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token); using var cancellationSubscription = combinedCancellationTokenSource.Token.Register(state => { - var operationState = (ConnectOperationState)state; - if (operationState.IsSucceeded) + var operationState = (OperationCallbackState)state; + if (operationState.TryChangeStatusFromInProgress(OperationCallbackState.OperationStatus.Interrupted)) { - return; + DisposeSocket(operationState.Subject); } - DisposeSocket(operationState.Socket); }, callbackState); try @@ -195,8 +194,10 @@ private void Connect(Socket socket, EndPoint endPoint, CancellationToken cancell #else socket.Connect(endPoint); #endif - EnsureConnected(socket); - callbackState.IsSucceeded = true; + if (!callbackState.TryChangeStatusFromInProgress(OperationCallbackState.OperationStatus.Done)) + { + throw new ObjectDisposedException(nameof(Socket)); + } } catch { @@ -222,23 +223,6 @@ static void DisposeSocket(Socket socket) // Ignore any exceptions. } } - - static void EnsureConnected(Socket socket) - { - bool originalBlockingState = socket.Blocking; - - try - { - socket.Blocking = false; - // Try to use the socket to ensure it's connected. On MacOS with net6.0 sometimes Connect is completed successfully even after the socket disposal. - socket.Send(__ensureConnectedBuffer, 0, 0); - } - finally - { - // Restore original blocking state - socket.Blocking = originalBlockingState; - } - } } private async Task ConnectAsync(Socket socket, EndPoint endPoint, CancellationToken cancellationToken) @@ -401,10 +385,5 @@ public int Compare(EndPoint x, EndPoint y) return 0; } } - - private sealed record ConnectOperationState(Socket Socket) - { - public bool IsSucceeded { get; set; } - } } } diff --git a/src/MongoDB.Driver/Core/Misc/OperationCallbackState.cs b/src/MongoDB.Driver/Core/Misc/OperationCallbackState.cs new file mode 100644 index 00000000000..3b8059545df --- /dev/null +++ b/src/MongoDB.Driver/Core/Misc/OperationCallbackState.cs @@ -0,0 +1,37 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Threading; + +namespace MongoDB.Driver.Core.Misc; + +internal sealed class OperationCallbackState(T subject) +{ + private int _status = (int)OperationStatus.InProgress; + + public OperationStatus Status => (OperationStatus)_status; + public T Subject => subject; + public bool TryChangeStatusFromInProgress(OperationStatus newState) => + Interlocked.CompareExchange(ref _status, (int)newState, (int)OperationStatus.InProgress) == (int)OperationStatus.InProgress; + + public enum OperationStatus + { + InProgress = 0, + Done, + Interrupted, + } +} + + diff --git a/src/MongoDB.Driver/Core/Misc/StreamExtensionMethods.cs b/src/MongoDB.Driver/Core/Misc/StreamExtensionMethods.cs index 06e0d4a5ac2..11bc4edaed7 100644 --- a/src/MongoDB.Driver/Core/Misc/StreamExtensionMethods.cs +++ b/src/MongoDB.Driver/Core/Misc/StreamExtensionMethods.cs @@ -287,25 +287,25 @@ private static void ExecuteOperationWithTimeout(Stream stream, TState st throw new TimeoutException(); } - StreamDisposeCallbackState callbackState = null; + OperationCallbackState callbackState = null; Timer timer = null; CancellationTokenRegistration cancellationSubscription = default; if (timeoutMs > 0) { - callbackState = new StreamDisposeCallbackState(stream); + callbackState = new OperationCallbackState(stream); timer = new Timer(DisposeStreamCallback, callbackState, timeoutMs, Timeout.Infinite); } if (cancellationToken.CanBeCanceled) { - callbackState ??= new StreamDisposeCallbackState(stream); + callbackState ??= new OperationCallbackState(stream); cancellationSubscription = cancellationToken.Register(DisposeStreamCallback, callbackState); } try { operation(stream, state); - if (callbackState?.TryChangeStateFromInProgress(OperationState.Done) == false) + if (callbackState?.TryChangeStatusFromInProgress(OperationCallbackState.OperationStatus.Done) == false) { // If the state can't be changed - then the stream was/will be disposed, throw here throw new IOException(); @@ -313,7 +313,7 @@ private static void ExecuteOperationWithTimeout(Stream stream, TState st } catch (Exception ex) { - if (callbackState?.OperationState == OperationState.Interrupted) + if (callbackState?.Status == OperationCallbackState.OperationStatus.Interrupted) { cancellationToken.ThrowIfCancellationRequested(); throw new TimeoutException(); @@ -334,8 +334,8 @@ private static void ExecuteOperationWithTimeout(Stream stream, TState st static void DisposeStreamCallback(object state) { - var disposeCallbackState = (StreamDisposeCallbackState)state; - if (!disposeCallbackState.TryChangeStateFromInProgress(OperationState.Interrupted)) + var disposeCallbackState = (OperationCallbackState)state; + if (!disposeCallbackState.TryChangeStatusFromInProgress(OperationCallbackState.OperationStatus.Interrupted)) { // If the state can't be changed - then I/O had already succeeded return; @@ -343,7 +343,7 @@ static void DisposeStreamCallback(object state) try { - disposeCallbackState.Stream.Dispose(); + disposeCallbackState.Subject.Dispose(); } catch (Exception) { @@ -351,22 +351,5 @@ static void DisposeStreamCallback(object state) } } } - - private record StreamDisposeCallbackState(Stream Stream) - { - private int _operationState = (int)OperationState.InProgress; - - public OperationState OperationState => (OperationState)_operationState; - - public bool TryChangeStateFromInProgress(OperationState newState) => - Interlocked.CompareExchange(ref _operationState, (int)newState, (int)OperationState.InProgress) == (int)OperationState.InProgress; - } - - private enum OperationState - { - InProgress = 0, - Done, - Interrupted, - } } } From b8ab96383afe89b4402f35ebdae8f5dead9b12a1 Mon Sep 17 00:00:00 2001 From: Oleksandr Poliakov Date: Mon, 8 Dec 2025 16:22:52 -0800 Subject: [PATCH 4/5] PR --- src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs | 2 -- src/MongoDB.Driver/Core/Misc/OperationCallbackState.cs | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs b/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs index e4d68d84622..8229ba660b5 100644 --- a/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs +++ b/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs @@ -31,8 +31,6 @@ namespace MongoDB.Driver.Core.Connections /// internal sealed class TcpStreamFactory : IStreamFactory { - private static readonly byte[] __ensureConnectedBuffer = new byte[1]; - // fields private readonly TcpStreamSettings _settings; diff --git a/src/MongoDB.Driver/Core/Misc/OperationCallbackState.cs b/src/MongoDB.Driver/Core/Misc/OperationCallbackState.cs index 3b8059545df..f04794eccad 100644 --- a/src/MongoDB.Driver/Core/Misc/OperationCallbackState.cs +++ b/src/MongoDB.Driver/Core/Misc/OperationCallbackState.cs @@ -33,5 +33,3 @@ public enum OperationStatus Interrupted, } } - - From 823a0f79d836c560a973b84f7216d3c1925c5aaa Mon Sep 17 00:00:00 2001 From: Oleksandr Poliakov Date: Mon, 8 Dec 2025 18:12:17 -0800 Subject: [PATCH 5/5] pr --- .../UnobservedExceptionTestDiscoverer.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTestDiscoverer.cs b/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTestDiscoverer.cs index b4fdc098c48..d38c4d5ca81 100644 --- a/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTestDiscoverer.cs +++ b/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTestDiscoverer.cs @@ -42,13 +42,16 @@ public UnobservedExceptionTestDiscoverer(IMessageSink diagnosticsMessageSink) public IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) { - return [new XunitTestCase(_diagnosticsMessageSink, TestMethodDisplay.Method, TestMethodDisplayOptions.All, testMethod) + var testCase = new XunitTestCase(_diagnosticsMessageSink, TestMethodDisplay.Method, TestMethodDisplayOptions.All, testMethod); + if (!testCase.Traits.TryGetValue("Category", out var categories)) { - Traits = - { - { "Category", ["UnobservedExceptionTracking"] } - } - }]; + categories = new List(); + testCase.Traits.Add("Category", categories); + } + + categories.Add("UnobservedExceptionTracking"); + + return [testCase]; } void UnobservedTaskExceptionEventHandler(object sender, UnobservedTaskExceptionEventArgs unobservedException) =>