From 930bead12567d8cf6f780dc483c3456bee97342a Mon Sep 17 00:00:00 2001 From: Oleksandr Poliakov Date: Thu, 27 Nov 2025 15:40:56 -0800 Subject: [PATCH 1/7] CSHARP-5798: Implement test to track if any UnobservedTaskExceptions were raised while test run --- src/MongoDB.Driver/Core/Clusters/Cluster.cs | 7 +++ .../Core/Jira/CSharp3302Tests.cs | 7 ++- .../UnobservedTaskExceptionTracking.cs | 29 +++++++++++ ...TimeoutEnforcingXunitTestAssemblyRunner.cs | 39 +++++++++++++-- .../UnobservedExceptionTestDiscoverer.cs | 32 +++++++++++++ .../UnobservedExceptionTrackingTestCase.cs | 48 +++++++++++++++++++ 6 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 tests/MongoDB.Driver.Tests/UnobservedTaskExceptionTracking.cs create mode 100644 tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTestDiscoverer.cs create mode 100644 tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTrackingTestCase.cs diff --git a/src/MongoDB.Driver/Core/Clusters/Cluster.cs b/src/MongoDB.Driver/Core/Clusters/Cluster.cs index db14eed8b19..4a396a5d9f4 100644 --- a/src/MongoDB.Driver/Core/Clusters/Cluster.cs +++ b/src/MongoDB.Driver/Core/Clusters/Cluster.cs @@ -445,6 +445,7 @@ private sealed class ServerSelectionWaitQueue : IDisposable private readonly InterlockedInt32 _rapidHeartbeatTimerCallbackState; private int _serverSelectionWaitQueueSize; + private bool _disposed; public ServerSelectionWaitQueue(Cluster cluster) { @@ -455,6 +456,7 @@ public ServerSelectionWaitQueue(Cluster cluster) public void Dispose() { + _disposed = true; _rapidHeartbeatTimer.Dispose(); } @@ -489,6 +491,11 @@ private void ExitServerSelectionWaitQueue() { if (--_serverSelectionWaitQueueSize == 0) { + if (_disposed) + { + return; + } + _rapidHeartbeatTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); } } diff --git a/tests/MongoDB.Driver.Tests/Core/Jira/CSharp3302Tests.cs b/tests/MongoDB.Driver.Tests/Core/Jira/CSharp3302Tests.cs index 792344ad6de..fbd2a379525 100644 --- a/tests/MongoDB.Driver.Tests/Core/Jira/CSharp3302Tests.cs +++ b/tests/MongoDB.Driver.Tests/Core/Jira/CSharp3302Tests.cs @@ -96,10 +96,15 @@ public async Task RapidHeartbeatTimerCallback_should_ignore_reentrant_calls() cluster.Initialize(); // Trigger Cluster._rapidHeartbeatTimer - _ = cluster.SelectServerAsync(OperationContext.NoTimeout, CreateWritableServerAndEndPointSelector(__endPoint1)); + using var cancellationTokenSource = new CancellationTokenSource(); + var operationContext = new OperationContext(Timeout.InfiniteTimeSpan, cancellationTokenSource.Token); + cluster.SelectServerAsync(operationContext, CreateWritableServerAndEndPointSelector(__endPoint1)) + .IgnoreExceptions(); // Wait for all heartbeats to complete await Task.WhenAny(allHeartbeatsReceived.Task, Task.Delay(1000)); + + cancellationTokenSource.Cancel(); } allHeartbeatsReceived.Task.Status.Should().Be(TaskStatus.RanToCompletion); diff --git a/tests/MongoDB.Driver.Tests/UnobservedTaskExceptionTracking.cs b/tests/MongoDB.Driver.Tests/UnobservedTaskExceptionTracking.cs new file mode 100644 index 00000000000..ebb1076c428 --- /dev/null +++ b/tests/MongoDB.Driver.Tests/UnobservedTaskExceptionTracking.cs @@ -0,0 +1,29 @@ +/* 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 FluentAssertions; +using MongoDB.TestHelpers.XunitExtensions.TimeoutEnforcing; + +namespace MongoDB.Driver.Tests; + +public class UnobservedTaskExceptionTracking +{ + [UnobservedExceptionTrackingFact] + public void EnsureNoUnobservedTaskException() + { + UnobservedExceptionTrackingTestCase.__unobservedExceptions.Should().BeEmpty(); + } +} + diff --git a/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/TimeoutEnforcingXunitTestAssemblyRunner.cs b/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/TimeoutEnforcingXunitTestAssemblyRunner.cs index 7719fe7500e..971785f9b77 100644 --- a/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/TimeoutEnforcingXunitTestAssemblyRunner.cs +++ b/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/TimeoutEnforcingXunitTestAssemblyRunner.cs @@ -15,6 +15,7 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Xunit.Abstractions; @@ -25,20 +26,50 @@ namespace MongoDB.TestHelpers.XunitExtensions.TimeoutEnforcing [DebuggerStepThrough] internal sealed class TimeoutEnforcingXunitTestAssemblyRunner : XunitTestAssemblyRunner { + private readonly UnobservedExceptionTrackingTestCase _unobservedExceptionTrackingTestCase; + public TimeoutEnforcingXunitTestAssemblyRunner( ITestAssembly testAssembly, IEnumerable testCases, IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions) - : base(testAssembly, testCases, diagnosticMessageSink, executionMessageSink, executionOptions) - { } + : base(testAssembly, testCases.Where(t => t is not UnobservedExceptionTrackingTestCase), diagnosticMessageSink, executionMessageSink, executionOptions) + { + _unobservedExceptionTrackingTestCase = (UnobservedExceptionTrackingTestCase)testCases.SingleOrDefault(t => t is UnobservedExceptionTrackingTestCase); + } protected override Task RunTestCollectionAsync( IMessageBus messageBus, ITestCollection testCollection, IEnumerable testCases, - CancellationTokenSource cancellationTokenSource) => - new TimeoutEnforcingXunitTestCollectionRunner(testCollection, testCases,DiagnosticMessageSink, messageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), cancellationTokenSource).RunAsync(); + CancellationTokenSource cancellationTokenSource) + { + return new TimeoutEnforcingXunitTestCollectionRunner(testCollection, testCases, DiagnosticMessageSink, messageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), cancellationTokenSource).RunAsync(); + } + + protected override async Task RunTestCollectionsAsync(IMessageBus messageBus, CancellationTokenSource cancellationTokenSource) + { + var baseSummary = await base.RunTestCollectionsAsync(messageBus, cancellationTokenSource); + + if (_unobservedExceptionTrackingTestCase == null) + { + return baseSummary; + } + + var unobservedExceptionTestCaseRunSummary = await RunTestCollectionAsync( + messageBus, + _unobservedExceptionTrackingTestCase.TestMethod.TestClass.TestCollection, + [_unobservedExceptionTrackingTestCase], + cancellationTokenSource); + + return new RunSummary + { + Total = baseSummary.Total + unobservedExceptionTestCaseRunSummary.Total, + Failed = baseSummary.Failed + unobservedExceptionTestCaseRunSummary.Failed, + Skipped = baseSummary.Skipped + unobservedExceptionTestCaseRunSummary.Skipped, + Time = baseSummary.Time + unobservedExceptionTestCaseRunSummary.Time + }; + } } } diff --git a/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTestDiscoverer.cs b/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTestDiscoverer.cs new file mode 100644 index 00000000000..5d8d13bd991 --- /dev/null +++ b/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTestDiscoverer.cs @@ -0,0 +1,32 @@ +/* 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.Collections.Generic; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace MongoDB.TestHelpers.XunitExtensions.TimeoutEnforcing; + +[XunitTestCaseDiscoverer("MongoDB.TestHelpers.XunitExtensions.TimeoutEnforcing.UnobservedExceptionTestDiscoverer", "MongoDB.TestHelpers")] +public class UnobservedExceptionTrackingFactAttribute: FactAttribute +{} + +public class UnobservedExceptionTestDiscoverer(IMessageSink DiagnosticsMessageSink) : IXunitTestCaseDiscoverer +{ + public IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) => + [new UnobservedExceptionTrackingTestCase(DiagnosticsMessageSink, testMethod)]; +} + diff --git a/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTrackingTestCase.cs b/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTrackingTestCase.cs new file mode 100644 index 00000000000..69d07a55a6f --- /dev/null +++ b/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTrackingTestCase.cs @@ -0,0 +1,48 @@ +/* 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.Collections.Generic; +using System.Threading.Tasks; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace MongoDB.TestHelpers.XunitExtensions.TimeoutEnforcing; + +public sealed class UnobservedExceptionTrackingTestCase : XunitTestCase +{ + public static readonly List __unobservedExceptions = new(); + +#pragma warning disable CS0618 // Type or member is obsolete + public UnobservedExceptionTrackingTestCase() + { + } +#pragma warning restore CS0618 // Type or member is obsolete + + public UnobservedExceptionTrackingTestCase(IMessageSink diagnosticMessageSink, ITestMethod testMethod) + : base(diagnosticMessageSink, TestMethodDisplay.Method, TestMethodDisplayOptions.All, testMethod) + { + TaskScheduler.UnobservedTaskException += UnobservedTaskExceptionEventHandler; + } + + public override void Dispose() + { + base.Dispose(); + TaskScheduler.UnobservedTaskException -= UnobservedTaskExceptionEventHandler; + } + + void UnobservedTaskExceptionEventHandler(object sender, UnobservedTaskExceptionEventArgs unobservedException) => + __unobservedExceptions.Add(unobservedException.Exception.ToString()); +} + From 0620e0ca0fbe89a56f722d4d7714ff117af77b1d Mon Sep 17 00:00:00 2001 From: Oleksandr Poliakov Date: Tue, 2 Dec 2025 13:05:34 -0800 Subject: [PATCH 2/7] pr --- .../Core/Connections/TcpStreamFactory.cs | 68 ++++++++------ .../ClusterRegistryTests.cs | 3 - .../Core/Connections/TcpStreamFactoryTests.cs | 92 ++++++------------- .../UnobservedTaskExceptionTracking.cs | 6 +- .../MongoDB.TestHelpers.csproj | 4 + .../TimeoutEnforcingTestInvoker.cs | 25 +++++ ...TimeoutEnforcingXunitTestAssemblyRunner.cs | 14 ++- .../UnobservedExceptionTestDiscoverer.cs | 28 +++++- .../UnobservedExceptionTrackingTestCase.cs | 48 ---------- 9 files changed, 141 insertions(+), 147 deletions(-) delete mode 100644 tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTrackingTestCase.cs diff --git a/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs b/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs index db1e71df9ca..27785363a55 100644 --- a/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs +++ b/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs @@ -165,41 +165,57 @@ private void ConfigureConnectedSocket(Socket socket) private void Connect(Socket socket, EndPoint endPoint, CancellationToken cancellationToken) { - IAsyncResult connectOperation; -#if NET472 - if (endPoint is DnsEndPoint dnsEndPoint) - { - // mono doesn't support DnsEndPoint in its BeginConnect method. - connectOperation = socket.BeginConnect(dnsEndPoint.Host, dnsEndPoint.Port, null, null); - } - else - { - connectOperation = socket.BeginConnect(endPoint, null, null); - } -#else - connectOperation = socket.BeginConnect(endPoint, null, null); -#endif - - WaitHandle.WaitAny([connectOperation.AsyncWaitHandle, cancellationToken.WaitHandle], _settings.ConnectTimeout); - - if (!connectOperation.IsCompleted) + var wasCallbackExecuted = false; + using var timeoutCancellationTokenSource = new CancellationTokenSource(_settings.ConnectTimeout); + using var combinedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token); + using var cancellationSubscription = combinedCancellationTokenSource.Token.Register(() => { try { + wasCallbackExecuted = true; socket.Dispose(); - } catch { } - - cancellationToken.ThrowIfCancellationRequested(); - throw new TimeoutException($"Timed out connecting to {endPoint}. Timeout was {_settings.ConnectTimeout}."); - } + } + catch + { + // Ignore any exception here, as we should avoid throwing in callback. + } + }); try { - socket.EndConnect(connectOperation); +#if NET472 + if (endPoint is DnsEndPoint dnsEndPoint) + { + // mono doesn't support DnsEndPoint in its Connect method. + socket.Connect(dnsEndPoint.Host, dnsEndPoint.Port); + } + else + { + socket.Connect(endPoint); + } +#else + socket.Connect(endPoint); +#endif } - catch + catch (Exception) { - try { socket.Dispose(); } catch { } + if (!wasCallbackExecuted) + { + try + { + socket.Dispose(); + } + catch (Exception) + { + } + } + + cancellationToken.ThrowIfCancellationRequested(); + if (timeoutCancellationTokenSource.IsCancellationRequested) + { + throw new TimeoutException($"Timed out connecting to {endPoint}. Timeout was {_settings.ConnectTimeout}."); + } + throw; } } diff --git a/tests/MongoDB.Driver.Tests/ClusterRegistryTests.cs b/tests/MongoDB.Driver.Tests/ClusterRegistryTests.cs index 2a63791de2f..666f42ddd92 100644 --- a/tests/MongoDB.Driver.Tests/ClusterRegistryTests.cs +++ b/tests/MongoDB.Driver.Tests/ClusterRegistryTests.cs @@ -27,7 +27,6 @@ using MongoDB.Driver.Core.Misc; using MongoDB.Driver.Core.Servers; using MongoDB.Driver.Core.TestHelpers.Logging; -using MongoDB.Driver.TestHelpers; using Xunit; using Xunit.Abstractions; @@ -73,7 +72,6 @@ public void DisposingClusterSource_should_use_cluster_registry_and_return_cluste ClusterRegistry.Instance._registry().Keys.Should().NotContain(clusterKey); } -#if WINDOWS [Fact] public void Instance_should_return_the_same_instance_every_time() { @@ -221,7 +219,6 @@ public void UnregisterAndDisposeCluster_should_unregister_and_dispose_the_cluste subject._registry().Count.Should().Be(0); cluster._state().Should().Be(2); } -#endif } internal static class ClusterRegistryReflector diff --git a/tests/MongoDB.Driver.Tests/Core/Connections/TcpStreamFactoryTests.cs b/tests/MongoDB.Driver.Tests/Core/Connections/TcpStreamFactoryTests.cs index cfb8f8194c5..5a11faba3b1 100644 --- a/tests/MongoDB.Driver.Tests/Core/Connections/TcpStreamFactoryTests.cs +++ b/tests/MongoDB.Driver.Tests/Core/Connections/TcpStreamFactoryTests.cs @@ -34,7 +34,7 @@ public class TcpStreamFactoryTests { [Theory] [ParameterAttributeData] - public void Connect_should_dispose_socket_if_socket_fails([Values(false, true)] bool async) + public async Task Connect_should_dispose_socket_if_socket_fails([Values(false, true)] bool async) { RequireServer.Check(); @@ -43,20 +43,9 @@ public void Connect_should_dispose_socket_if_socket_fails([Values(false, true)] using (var testSocket = new TestSocket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) { - Exception exception; - if (async) - { - exception = Record.Exception( - () => - subject - .ConnectAsync(testSocket, endpoint, CancellationToken.None) - .GetAwaiter() - .GetResult()); - } - else - { - exception = Record.Exception(() => subject.Connect(testSocket, endpoint, CancellationToken.None)); - } + var exception = async ? + await Record.ExceptionAsync(() => subject.ConnectAsync(testSocket, endpoint, CancellationToken.None)) : + Record.Exception(() => subject.Connect(testSocket, endpoint, CancellationToken.None)); exception.Should().NotBeNull(); testSocket.DisposeAttempts.Should().Be(1); @@ -66,83 +55,64 @@ public void Connect_should_dispose_socket_if_socket_fails([Values(false, true)] [Fact] public void Constructor_should_throw_an_ArgumentNullException_when_tcpStreamSettings_is_null() { - Action act = () => new TcpStreamFactory(null); + var exception = Record.Exception(() => new TcpStreamFactory(null)); - act.ShouldThrow(); + exception.Should().BeOfType().Subject + .ParamName.Should().Be("settings"); } [Theory] [ParameterAttributeData] - public void CreateStream_should_throw_a_SocketException_when_the_endpoint_could_not_be_resolved( - [Values(false, true)] - bool async) + public async Task CreateStream_should_throw_a_SocketException_when_the_endpoint_could_not_be_resolved([Values(false, true)]bool async) { var subject = new TcpStreamFactory(); - Action act; - if (async) - { - act = () => subject.CreateStreamAsync(new DnsEndPoint("not-gonna-exist-i-hope", 27017), CancellationToken.None).GetAwaiter().GetResult(); - } - else - { - act = () => subject.CreateStream(new DnsEndPoint("not-gonna-exist-i-hope", 27017), CancellationToken.None); - } + var exception = async ? + await Record.ExceptionAsync(() => subject.CreateStreamAsync(new DnsEndPoint("not-gonna-exist-i-hope", 27017), CancellationToken.None)) : + Record.Exception(() => subject.CreateStream(new DnsEndPoint("not-gonna-exist-i-hope", 27017), CancellationToken.None)); - act.ShouldThrow(); + exception.Should().BeAssignableTo(); } [Theory] [ParameterAttributeData] - public void CreateStream_should_throw_when_cancellation_is_requested( - [Values(false, true)] - bool async) + public async Task CreateStream_should_throw_when_cancellation_is_requested([Values(false, true)]bool async) { var subject = new TcpStreamFactory(); var endPoint = new IPEndPoint(new IPAddress(0x01010101), 12345); // a non-existent host and port var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(20)); - Action action; + var exception = async ? + await Record.ExceptionAsync(() => subject.CreateStreamAsync(endPoint, cancellationTokenSource.Token)) : + Record.Exception(() => subject.CreateStream(endPoint, cancellationTokenSource.Token)); if (async) { - action = () => subject.CreateStreamAsync(endPoint, cancellationTokenSource.Token).GetAwaiter().GetResult(); + exception.Should().BeOfType(); } else { - action = () => subject.CreateStream(endPoint, cancellationTokenSource.Token); + exception.Should().BeOfType(); } - - action.ShouldThrow(); } [Theory] [ParameterAttributeData] - public void CreateStream_should_throw_when_connect_timeout_has_expired( - [Values(false, true)] - bool async) + public async Task CreateStream_should_throw_when_connect_timeout_has_expired([Values(false, true)]bool async) { var settings = new TcpStreamSettings(connectTimeout: TimeSpan.FromMilliseconds(20)); var subject = new TcpStreamFactory(settings); var endPoint = new IPEndPoint(new IPAddress(0x01010101), 12345); // a non-existent host and port - Action action; - if (async) - { - action = () => subject.CreateStreamAsync(endPoint, CancellationToken.None).GetAwaiter().GetResult(); ; - } - else - { - action = () => subject.CreateStream(endPoint, CancellationToken.None); - } + var exception = async ? + await Record.ExceptionAsync(() => subject.CreateStreamAsync(endPoint, CancellationToken.None)) : + Record.Exception(() => subject.CreateStream(endPoint, CancellationToken.None)); - action.ShouldThrow(); + exception.Should().BeOfType(); } [Theory] [ParameterAttributeData] - public void CreateStream_should_call_the_socketConfigurator( - [Values(false, true)] - bool async) + public async Task CreateStream_should_call_the_socketConfigurator([Values(false, true)]bool async) { RequireServer.Check(); var socketConfiguratorWasCalled = false; @@ -153,7 +123,7 @@ public void CreateStream_should_call_the_socketConfigurator( if (async) { - subject.CreateStreamAsync(endPoint, CancellationToken.None).GetAwaiter().GetResult(); + await subject.CreateStreamAsync(endPoint, CancellationToken.None); } else { @@ -165,9 +135,7 @@ public void CreateStream_should_call_the_socketConfigurator( [Theory] [ParameterAttributeData] - public void CreateStream_should_connect_to_a_running_server_and_return_a_non_null_stream( - [Values(false, true)] - bool async) + public async Task CreateStream_should_connect_to_a_running_server_and_return_a_non_null_stream([Values(false, true)]bool async) { RequireServer.Check(); var subject = new TcpStreamFactory(); @@ -176,7 +144,7 @@ public void CreateStream_should_connect_to_a_running_server_and_return_a_non_nul Stream stream; if (async) { - stream = subject.CreateStreamAsync(endPoint, CancellationToken.None).GetAwaiter().GetResult(); + stream = await subject.CreateStreamAsync(endPoint, CancellationToken.None); } else { @@ -188,9 +156,7 @@ public void CreateStream_should_connect_to_a_running_server_and_return_a_non_nul [Theory] [ParameterAttributeData] - public void SocketConfigurator_can_be_used_to_set_keepAlive( - [Values(false, true)] - bool async) + public async Task SocketConfigurator_can_be_used_to_set_keepAlive([Values(false, true)]bool async) { RequireServer.Check(); Action socketConfigurator = s => s.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); @@ -201,7 +167,7 @@ public void SocketConfigurator_can_be_used_to_set_keepAlive( Stream stream; if (async) { - stream = subject.CreateStreamAsync(endPoint, CancellationToken.None).GetAwaiter().GetResult(); + stream = await subject.CreateStreamAsync(endPoint, CancellationToken.None); } else { diff --git a/tests/MongoDB.Driver.Tests/UnobservedTaskExceptionTracking.cs b/tests/MongoDB.Driver.Tests/UnobservedTaskExceptionTracking.cs index ebb1076c428..02ab6896ea7 100644 --- a/tests/MongoDB.Driver.Tests/UnobservedTaskExceptionTracking.cs +++ b/tests/MongoDB.Driver.Tests/UnobservedTaskExceptionTracking.cs @@ -13,6 +13,7 @@ * limitations under the License. */ +using System; using FluentAssertions; using MongoDB.TestHelpers.XunitExtensions.TimeoutEnforcing; @@ -23,7 +24,10 @@ public class UnobservedTaskExceptionTracking [UnobservedExceptionTrackingFact] public void EnsureNoUnobservedTaskException() { - UnobservedExceptionTrackingTestCase.__unobservedExceptions.Should().BeEmpty(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + + UnobservedExceptionTestDiscoverer.UnobservedExceptions.Should().BeEmpty(); } } diff --git a/tests/MongoDB.TestHelpers/MongoDB.TestHelpers.csproj b/tests/MongoDB.TestHelpers/MongoDB.TestHelpers.csproj index f26db14e0fb..47cf0329cdc 100644 --- a/tests/MongoDB.TestHelpers/MongoDB.TestHelpers.csproj +++ b/tests/MongoDB.TestHelpers/MongoDB.TestHelpers.csproj @@ -12,4 +12,8 @@ Helper classes applicable to all test projects. + + $(DefineConstants);UNOBSERVED_TASK_EXCEPTION_DEBUGGING + + diff --git a/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/TimeoutEnforcingTestInvoker.cs b/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/TimeoutEnforcingTestInvoker.cs index 69c16fc24d8..a6c97a01e41 100644 --- a/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/TimeoutEnforcingTestInvoker.cs +++ b/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/TimeoutEnforcingTestInvoker.cs @@ -80,6 +80,9 @@ private async Task InvokeBaseOnTaskScheduler(object testClassInstance) protected override async Task InvokeTestMethodAsync(object testClassInstance) { +#if UNOBSERVED_TASK_EXCEPTION_DEBUGGING + Exception unobservedException = null; +#endif var xUnitTestCase = Test.TestCase as IXunitTestCase; var timeoutMS = xUnitTestCase?.Timeout ?? 0; var timeout = Debugger.IsAttached @@ -92,9 +95,24 @@ protected override async Task InvokeTestMethodAsync(object testClassIns decimal result; try { +#if UNOBSERVED_TASK_EXCEPTION_DEBUGGING + TaskScheduler.UnobservedTaskException += UnobservedTaskExceptionEventHandler; +#endif + var baseTask = InvokeBaseOnTaskScheduler(testClassInstance); var resultTask = await Task.WhenAny(baseTask, Task.Delay(timeout)); +#if UNOBSERVED_TASK_EXCEPTION_DEBUGGING + GC.Collect(); + GC.WaitForPendingFinalizers(); + TaskScheduler.UnobservedTaskException -= UnobservedTaskExceptionEventHandler; + + if (unobservedException != null) + { + throw unobservedException; + } +#endif + if (resultTask != baseTask) { throw new TestTimeoutException((int)timeout.TotalMilliseconds); @@ -120,6 +138,13 @@ protected override async Task InvokeTestMethodAsync(object testClassIns } return result; + +#if UNOBSERVED_TASK_EXCEPTION_DEBUGGING + void UnobservedTaskExceptionEventHandler(object sender, UnobservedTaskExceptionEventArgs unobservedExceptionArgs) + { + unobservedException = unobservedExceptionArgs.Exception; + } +#endif } } } diff --git a/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/TimeoutEnforcingXunitTestAssemblyRunner.cs b/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/TimeoutEnforcingXunitTestAssemblyRunner.cs index 971785f9b77..29072b20ab9 100644 --- a/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/TimeoutEnforcingXunitTestAssemblyRunner.cs +++ b/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/TimeoutEnforcingXunitTestAssemblyRunner.cs @@ -26,7 +26,7 @@ namespace MongoDB.TestHelpers.XunitExtensions.TimeoutEnforcing [DebuggerStepThrough] internal sealed class TimeoutEnforcingXunitTestAssemblyRunner : XunitTestAssemblyRunner { - private readonly UnobservedExceptionTrackingTestCase _unobservedExceptionTrackingTestCase; + private readonly IXunitTestCase _unobservedExceptionTrackingTestCase; public TimeoutEnforcingXunitTestAssemblyRunner( ITestAssembly testAssembly, @@ -34,9 +34,14 @@ public TimeoutEnforcingXunitTestAssemblyRunner( IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions) - : base(testAssembly, testCases.Where(t => t is not UnobservedExceptionTrackingTestCase), diagnosticMessageSink, executionMessageSink, executionOptions) + : base( + testAssembly, + testCases.Where(t => !IsUnobservedExceptionTrackingTestCase(t)), + diagnosticMessageSink, + executionMessageSink, + executionOptions) { - _unobservedExceptionTrackingTestCase = (UnobservedExceptionTrackingTestCase)testCases.SingleOrDefault(t => t is UnobservedExceptionTrackingTestCase); + _unobservedExceptionTrackingTestCase = testCases.SingleOrDefault(IsUnobservedExceptionTrackingTestCase); } protected override Task RunTestCollectionAsync( @@ -71,5 +76,8 @@ protected override async Task RunTestCollectionsAsync(IMessageBus me Time = baseSummary.Time + unobservedExceptionTestCaseRunSummary.Time }; } + + private static bool IsUnobservedExceptionTrackingTestCase(IXunitTestCase testCase) + => testCase.Traits.TryGetValue("Category", out var categories) && categories.Contains("UnobservedExceptionTracking"); } } diff --git a/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTestDiscoverer.cs b/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTestDiscoverer.cs index 5d8d13bd991..71d3611c15f 100644 --- a/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTestDiscoverer.cs +++ b/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTestDiscoverer.cs @@ -14,6 +14,7 @@ */ using System.Collections.Generic; +using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; using Xunit.Sdk; @@ -24,9 +25,30 @@ namespace MongoDB.TestHelpers.XunitExtensions.TimeoutEnforcing; public class UnobservedExceptionTrackingFactAttribute: FactAttribute {} -public class UnobservedExceptionTestDiscoverer(IMessageSink DiagnosticsMessageSink) : IXunitTestCaseDiscoverer +public class UnobservedExceptionTestDiscoverer : IXunitTestCaseDiscoverer { - public IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) => - [new UnobservedExceptionTrackingTestCase(DiagnosticsMessageSink, testMethod)]; + private readonly IMessageSink _diagnosticsMessageSink; + + public UnobservedExceptionTestDiscoverer(IMessageSink diagnosticsMessageSink) + { + _diagnosticsMessageSink = diagnosticsMessageSink; + TaskScheduler.UnobservedTaskException += UnobservedTaskExceptionEventHandler; + } + + public static readonly List UnobservedExceptions = new(); + + public IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) + { + return [new XunitTestCase(_diagnosticsMessageSink, TestMethodDisplay.Method, TestMethodDisplayOptions.All, testMethod) + { + Traits = + { + { "Category", ["UnobservedExceptionTracking"] } + } + }]; + } + + void UnobservedTaskExceptionEventHandler(object sender, UnobservedTaskExceptionEventArgs unobservedException) => + UnobservedExceptions.Add(unobservedException.Exception.ToString()); } diff --git a/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTrackingTestCase.cs b/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTrackingTestCase.cs deleted file mode 100644 index 69d07a55a6f..00000000000 --- a/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTrackingTestCase.cs +++ /dev/null @@ -1,48 +0,0 @@ -/* 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.Collections.Generic; -using System.Threading.Tasks; -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace MongoDB.TestHelpers.XunitExtensions.TimeoutEnforcing; - -public sealed class UnobservedExceptionTrackingTestCase : XunitTestCase -{ - public static readonly List __unobservedExceptions = new(); - -#pragma warning disable CS0618 // Type or member is obsolete - public UnobservedExceptionTrackingTestCase() - { - } -#pragma warning restore CS0618 // Type or member is obsolete - - public UnobservedExceptionTrackingTestCase(IMessageSink diagnosticMessageSink, ITestMethod testMethod) - : base(diagnosticMessageSink, TestMethodDisplay.Method, TestMethodDisplayOptions.All, testMethod) - { - TaskScheduler.UnobservedTaskException += UnobservedTaskExceptionEventHandler; - } - - public override void Dispose() - { - base.Dispose(); - TaskScheduler.UnobservedTaskException -= UnobservedTaskExceptionEventHandler; - } - - void UnobservedTaskExceptionEventHandler(object sender, UnobservedTaskExceptionEventArgs unobservedException) => - __unobservedExceptions.Add(unobservedException.Exception.ToString()); -} - From 39ecea77c373ddae4c9d4b0acedd71d4d9b5e19f Mon Sep 17 00:00:00 2001 From: Oleksandr Poliakov Date: Wed, 3 Dec 2025 11:00:57 -0800 Subject: [PATCH 3/7] PR --- src/MongoDB.Driver/Core/Clusters/Cluster.cs | 7 +++++-- .../TimeoutEnforcing/UnobservedExceptionTestDiscoverer.cs | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/MongoDB.Driver/Core/Clusters/Cluster.cs b/src/MongoDB.Driver/Core/Clusters/Cluster.cs index 4a396a5d9f4..0d412ffd803 100644 --- a/src/MongoDB.Driver/Core/Clusters/Cluster.cs +++ b/src/MongoDB.Driver/Core/Clusters/Cluster.cs @@ -456,8 +456,11 @@ public ServerSelectionWaitQueue(Cluster cluster) public void Dispose() { - _disposed = true; - _rapidHeartbeatTimer.Dispose(); + lock (_serverSelectionWaitQueueLock) + { + _disposed = true; + _rapidHeartbeatTimer.Dispose(); + } } public IDisposable Enter(OperationContext operationContext, IServerSelector selector, ClusterDescription clusterDescription, long? operationId) diff --git a/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTestDiscoverer.cs b/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTestDiscoverer.cs index 71d3611c15f..b4fdc098c48 100644 --- a/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTestDiscoverer.cs +++ b/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/UnobservedExceptionTestDiscoverer.cs @@ -13,6 +13,7 @@ * limitations under the License. */ +using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading.Tasks; using Xunit; @@ -27,6 +28,8 @@ public class UnobservedExceptionTrackingFactAttribute: FactAttribute public class UnobservedExceptionTestDiscoverer : IXunitTestCaseDiscoverer { + private static readonly ConcurrentBag __unobservedExceptions = new(); + private readonly IMessageSink _diagnosticsMessageSink; public UnobservedExceptionTestDiscoverer(IMessageSink diagnosticsMessageSink) @@ -35,7 +38,7 @@ public UnobservedExceptionTestDiscoverer(IMessageSink diagnosticsMessageSink) TaskScheduler.UnobservedTaskException += UnobservedTaskExceptionEventHandler; } - public static readonly List UnobservedExceptions = new(); + public static IReadOnlyCollection UnobservedExceptions => __unobservedExceptions; public IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) { @@ -49,6 +52,6 @@ public IEnumerable Discover(ITestFrameworkDiscoveryOptions disco } void UnobservedTaskExceptionEventHandler(object sender, UnobservedTaskExceptionEventArgs unobservedException) => - UnobservedExceptions.Add(unobservedException.Exception.ToString()); + __unobservedExceptions.Add(unobservedException.Exception.ToString()); } From 2d1a17526fb9c783d7a65ad3ccd4f0077139c712 Mon Sep 17 00:00:00 2001 From: Oleksandr Poliakov Date: Thu, 4 Dec 2025 15:30:32 -0800 Subject: [PATCH 4/7] PR --- src/MongoDB.Driver/Core/Clusters/Cluster.cs | 17 +++--- .../Core/Connections/TcpStreamFactory.cs | 11 ++-- .../UnobservedTaskExceptionTracking.cs | 33 +++++++++++ ...onnectionMonitoringAndPoolingTestRunner.cs | 1 + .../UnobservedTaskExceptionTracking.cs | 11 +++- .../MongoDB.TestHelpers.csproj | 1 + .../TimeoutEnforcingTestInvoker.cs | 55 +++++++++++-------- 7 files changed, 91 insertions(+), 38 deletions(-) create mode 100644 tests/MongoDB.Bson.Tests/UnobservedTaskExceptionTracking.cs diff --git a/src/MongoDB.Driver/Core/Clusters/Cluster.cs b/src/MongoDB.Driver/Core/Clusters/Cluster.cs index 0d412ffd803..fba95fb5d81 100644 --- a/src/MongoDB.Driver/Core/Clusters/Cluster.cs +++ b/src/MongoDB.Driver/Core/Clusters/Cluster.cs @@ -445,7 +445,6 @@ private sealed class ServerSelectionWaitQueue : IDisposable private readonly InterlockedInt32 _rapidHeartbeatTimerCallbackState; private int _serverSelectionWaitQueueSize; - private bool _disposed; public ServerSelectionWaitQueue(Cluster cluster) { @@ -456,11 +455,7 @@ public ServerSelectionWaitQueue(Cluster cluster) public void Dispose() { - lock (_serverSelectionWaitQueueLock) - { - _disposed = true; - _rapidHeartbeatTimer.Dispose(); - } + _rapidHeartbeatTimer.Dispose(); } public IDisposable Enter(OperationContext operationContext, IServerSelector selector, ClusterDescription clusterDescription, long? operationId) @@ -494,12 +489,14 @@ private void ExitServerSelectionWaitQueue() { if (--_serverSelectionWaitQueueSize == 0) { - if (_disposed) + try { - return; + _rapidHeartbeatTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); + } + catch (ObjectDisposedException) + { + // Ignore ObjectDisposedException here, as ExitServerSelectionWaitQueue could be done after the WaitQueue was disposed. } - - _rapidHeartbeatTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); } } } diff --git a/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs b/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs index 27785363a55..8630b6cbf09 100644 --- a/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs +++ b/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs @@ -165,14 +165,14 @@ private void ConfigureConnectedSocket(Socket socket) private void Connect(Socket socket, EndPoint endPoint, CancellationToken cancellationToken) { - var wasCallbackExecuted = false; + var cancelledOrTimedOut = false; using var timeoutCancellationTokenSource = new CancellationTokenSource(_settings.ConnectTimeout); using var combinedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token); using var cancellationSubscription = combinedCancellationTokenSource.Token.Register(() => { try { - wasCallbackExecuted = true; + cancelledOrTimedOut = true; socket.Dispose(); } catch @@ -199,14 +199,15 @@ private void Connect(Socket socket, EndPoint endPoint, CancellationToken cancell } catch (Exception) { - if (!wasCallbackExecuted) + if (!cancelledOrTimedOut) { try { socket.Dispose(); } - catch (Exception) + catch { + // Ignore any exceptions. Connection was failed, we do not need the socket anyway. } } @@ -244,8 +245,8 @@ private async Task ConnectAsync(Socket socket, EndPoint endPoint, CancellationTo { try { - socket.Dispose(); connectTask.IgnoreExceptions(); + socket.Dispose(); } catch { } diff --git a/tests/MongoDB.Bson.Tests/UnobservedTaskExceptionTracking.cs b/tests/MongoDB.Bson.Tests/UnobservedTaskExceptionTracking.cs new file mode 100644 index 00000000000..1d535659b3c --- /dev/null +++ b/tests/MongoDB.Bson.Tests/UnobservedTaskExceptionTracking.cs @@ -0,0 +1,33 @@ +/* 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; +using FluentAssertions; +using MongoDB.TestHelpers.XunitExtensions.TimeoutEnforcing; + +namespace MongoDB.Bson.Tests; + +public class UnobservedTaskExceptionTracking +{ + [UnobservedExceptionTrackingFact] + public void EnsureNoUnobservedTaskException() + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + + UnobservedExceptionTestDiscoverer.UnobservedExceptions.Should().BeEmpty(); + } +} + diff --git a/tests/MongoDB.Driver.Tests/Specifications/connection-monitoring-and-pooling/ConnectionMonitoringAndPoolingTestRunner.cs b/tests/MongoDB.Driver.Tests/Specifications/connection-monitoring-and-pooling/ConnectionMonitoringAndPoolingTestRunner.cs index 6da8fcb73af..36167447d7d 100644 --- a/tests/MongoDB.Driver.Tests/Specifications/connection-monitoring-and-pooling/ConnectionMonitoringAndPoolingTestRunner.cs +++ b/tests/MongoDB.Driver.Tests/Specifications/connection-monitoring-and-pooling/ConnectionMonitoringAndPoolingTestRunner.cs @@ -387,6 +387,7 @@ private void ExecuteCheckOut( else { tasks[target] = CreateTask(() => CheckOut(operation, connectionPool, map)); + tasks[target].IgnoreExceptions(); } } } diff --git a/tests/MongoDB.Driver.Tests/UnobservedTaskExceptionTracking.cs b/tests/MongoDB.Driver.Tests/UnobservedTaskExceptionTracking.cs index 02ab6896ea7..128930c0fbc 100644 --- a/tests/MongoDB.Driver.Tests/UnobservedTaskExceptionTracking.cs +++ b/tests/MongoDB.Driver.Tests/UnobservedTaskExceptionTracking.cs @@ -16,13 +16,22 @@ using System; using FluentAssertions; using MongoDB.TestHelpers.XunitExtensions.TimeoutEnforcing; +using Xunit; namespace MongoDB.Driver.Tests; public class UnobservedTaskExceptionTracking { [UnobservedExceptionTrackingFact] - public void EnsureNoUnobservedTaskException() + public void EnsureNoUnobservedTaskException() => + EnsureNoUnobservedTaskExceptionImpl(); + + [UnobservedExceptionTrackingFact] + [Trait("Category", "Integration")] + public void EnsureNoUnobservedTaskException_Integration() => + EnsureNoUnobservedTaskExceptionImpl(); + + private void EnsureNoUnobservedTaskExceptionImpl() { GC.Collect(); GC.WaitForPendingFinalizers(); diff --git a/tests/MongoDB.TestHelpers/MongoDB.TestHelpers.csproj b/tests/MongoDB.TestHelpers/MongoDB.TestHelpers.csproj index 47cf0329cdc..ff63cbae194 100644 --- a/tests/MongoDB.TestHelpers/MongoDB.TestHelpers.csproj +++ b/tests/MongoDB.TestHelpers/MongoDB.TestHelpers.csproj @@ -12,6 +12,7 @@ Helper classes applicable to all test projects. + $(DefineConstants);UNOBSERVED_TASK_EXCEPTION_DEBUGGING diff --git a/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/TimeoutEnforcingTestInvoker.cs b/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/TimeoutEnforcingTestInvoker.cs index a6c97a01e41..4e6a2c0e43b 100644 --- a/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/TimeoutEnforcingTestInvoker.cs +++ b/tests/MongoDB.TestHelpers/XunitExtensions/TimeoutEnforcing/TimeoutEnforcingTestInvoker.cs @@ -80,9 +80,6 @@ private async Task InvokeBaseOnTaskScheduler(object testClassInstance) protected override async Task InvokeTestMethodAsync(object testClassInstance) { -#if UNOBSERVED_TASK_EXCEPTION_DEBUGGING - Exception unobservedException = null; -#endif var xUnitTestCase = Test.TestCase as IXunitTestCase; var timeoutMS = xUnitTestCase?.Timeout ?? 0; var timeout = Debugger.IsAttached @@ -93,26 +90,11 @@ protected override async Task InvokeTestMethodAsync(object testClassIns var testExceptionHandler = testClassInstance as ITestExceptionHandler; decimal result; + using var unobservedExceptionDebugger = UnobservedExceptionDebugger.Create(); try { -#if UNOBSERVED_TASK_EXCEPTION_DEBUGGING - TaskScheduler.UnobservedTaskException += UnobservedTaskExceptionEventHandler; -#endif - var baseTask = InvokeBaseOnTaskScheduler(testClassInstance); var resultTask = await Task.WhenAny(baseTask, Task.Delay(timeout)); - -#if UNOBSERVED_TASK_EXCEPTION_DEBUGGING - GC.Collect(); - GC.WaitForPendingFinalizers(); - TaskScheduler.UnobservedTaskException -= UnobservedTaskExceptionEventHandler; - - if (unobservedException != null) - { - throw unobservedException; - } -#endif - if (resultTask != baseTask) { throw new TestTimeoutException((int)timeout.TotalMilliseconds); @@ -138,13 +120,42 @@ protected override async Task InvokeTestMethodAsync(object testClassIns } return result; + } -#if UNOBSERVED_TASK_EXCEPTION_DEBUGGING - void UnobservedTaskExceptionEventHandler(object sender, UnobservedTaskExceptionEventArgs unobservedExceptionArgs) + private class UnobservedExceptionDebugger : IDisposable + { + private Exception _unobservedException; + + private UnobservedExceptionDebugger() { - unobservedException = unobservedExceptionArgs.Exception; + TaskScheduler.UnobservedTaskException += UnobservedTaskExceptionEventHandler; } + + public static UnobservedExceptionDebugger Create() + { +#if UNOBSERVED_TASK_EXCEPTION_DEBUGGING + return new UnobservedExceptionDebugger(); +#else + return null; #endif + } + + public void Dispose() + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + TaskScheduler.UnobservedTaskException -= UnobservedTaskExceptionEventHandler; + + if (_unobservedException != null) + { + throw _unobservedException; + } + } + + private void UnobservedTaskExceptionEventHandler(object sender, UnobservedTaskExceptionEventArgs unobservedExceptionArgs) + { + _unobservedException = unobservedExceptionArgs.Exception; + } } } } From 4ab4a94b4cea113640cb428776025968542ff3b3 Mon Sep 17 00:00:00 2001 From: Oleksandr Poliakov Date: Thu, 4 Dec 2025 15:31:52 -0800 Subject: [PATCH 5/7] pr --- tests/MongoDB.TestHelpers/MongoDB.TestHelpers.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/MongoDB.TestHelpers/MongoDB.TestHelpers.csproj b/tests/MongoDB.TestHelpers/MongoDB.TestHelpers.csproj index ff63cbae194..67b38768be9 100644 --- a/tests/MongoDB.TestHelpers/MongoDB.TestHelpers.csproj +++ b/tests/MongoDB.TestHelpers/MongoDB.TestHelpers.csproj @@ -13,8 +13,8 @@ - - $(DefineConstants);UNOBSERVED_TASK_EXCEPTION_DEBUGGING - + + + From 21f9a946430395f11b1979e428b9f367d87ea4f7 Mon Sep 17 00:00:00 2001 From: Oleksandr Poliakov Date: Thu, 4 Dec 2025 15:39:03 -0800 Subject: [PATCH 6/7] pr --- .../Core/Connections/TcpStreamFactory.cs | 39 ++++++++----------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs b/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs index 8630b6cbf09..6db6d1f3d59 100644 --- a/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs +++ b/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs @@ -165,21 +165,10 @@ private void ConfigureConnectedSocket(Socket socket) private void Connect(Socket socket, EndPoint endPoint, CancellationToken cancellationToken) { - var cancelledOrTimedOut = false; + var isSocketDisposed = false; using var timeoutCancellationTokenSource = new CancellationTokenSource(_settings.ConnectTimeout); using var combinedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token); - using var cancellationSubscription = combinedCancellationTokenSource.Token.Register(() => - { - try - { - cancelledOrTimedOut = true; - socket.Dispose(); - } - catch - { - // Ignore any exception here, as we should avoid throwing in callback. - } - }); + using var cancellationSubscription = combinedCancellationTokenSource.Token.Register(DisposeSocket); try { @@ -199,16 +188,9 @@ private void Connect(Socket socket, EndPoint endPoint, CancellationToken cancell } catch (Exception) { - if (!cancelledOrTimedOut) + if (!isSocketDisposed) { - try - { - socket.Dispose(); - } - catch - { - // Ignore any exceptions. Connection was failed, we do not need the socket anyway. - } + DisposeSocket(); } cancellationToken.ThrowIfCancellationRequested(); @@ -219,6 +201,19 @@ private void Connect(Socket socket, EndPoint endPoint, CancellationToken cancell throw; } + + void DisposeSocket() + { + isSocketDisposed = true; + try + { + socket.Dispose(); + } + catch + { + // Ignore any exceptions. + } + } } private async Task ConnectAsync(Socket socket, EndPoint endPoint, CancellationToken cancellationToken) From 2d5af864bee79aacdf3209ea1acaaf07795a0e4f Mon Sep 17 00:00:00 2001 From: Oleksandr Poliakov Date: Thu, 4 Dec 2025 17:35:12 -0800 Subject: [PATCH 7/7] 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 6db6d1f3d59..78f7fcc09f1 100644 --- a/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs +++ b/src/MongoDB.Driver/Core/Connections/TcpStreamFactory.cs @@ -186,7 +186,7 @@ private void Connect(Socket socket, EndPoint endPoint, CancellationToken cancell socket.Connect(endPoint); #endif } - catch (Exception) + catch { if (!isSocketDisposed) {