Skip to content

Commit 4c9b66e

Browse files
authored
CSHARP-5798: Implement test to track if any UnobservedTaskExceptions were raised while test run (#1831)
1 parent e3b25ba commit 4c9b66e

File tree

12 files changed

+300
-100
lines changed

12 files changed

+300
-100
lines changed

src/MongoDB.Driver/Core/Clusters/Cluster.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,14 @@ private void ExitServerSelectionWaitQueue()
489489
{
490490
if (--_serverSelectionWaitQueueSize == 0)
491491
{
492-
_rapidHeartbeatTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
492+
try
493+
{
494+
_rapidHeartbeatTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
495+
}
496+
catch (ObjectDisposedException)
497+
{
498+
// Ignore ObjectDisposedException here, as ExitServerSelectionWaitQueue could be done after the WaitQueue was disposed.
499+
}
493500
}
494501
}
495502
}

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

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -165,42 +165,54 @@ private void ConfigureConnectedSocket(Socket socket)
165165

166166
private void Connect(Socket socket, EndPoint endPoint, CancellationToken cancellationToken)
167167
{
168-
IAsyncResult connectOperation;
169-
#if NET472
170-
if (endPoint is DnsEndPoint dnsEndPoint)
171-
{
172-
// mono doesn't support DnsEndPoint in its BeginConnect method.
173-
connectOperation = socket.BeginConnect(dnsEndPoint.Host, dnsEndPoint.Port, null, null);
174-
}
175-
else
168+
var isSocketDisposed = false;
169+
using var timeoutCancellationTokenSource = new CancellationTokenSource(_settings.ConnectTimeout);
170+
using var combinedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCancellationTokenSource.Token);
171+
using var cancellationSubscription = combinedCancellationTokenSource.Token.Register(DisposeSocket);
172+
173+
try
176174
{
177-
connectOperation = socket.BeginConnect(endPoint, null, null);
178-
}
175+
#if NET472
176+
if (endPoint is DnsEndPoint dnsEndPoint)
177+
{
178+
// mono doesn't support DnsEndPoint in its Connect method.
179+
socket.Connect(dnsEndPoint.Host, dnsEndPoint.Port);
180+
}
181+
else
182+
{
183+
socket.Connect(endPoint);
184+
}
179185
#else
180-
connectOperation = socket.BeginConnect(endPoint, null, null);
186+
socket.Connect(endPoint);
181187
#endif
182-
183-
WaitHandle.WaitAny([connectOperation.AsyncWaitHandle, cancellationToken.WaitHandle], _settings.ConnectTimeout);
184-
185-
if (!connectOperation.IsCompleted)
188+
}
189+
catch
186190
{
187-
try
191+
if (!isSocketDisposed)
188192
{
189-
socket.Dispose();
190-
} catch { }
193+
DisposeSocket();
194+
}
191195

192196
cancellationToken.ThrowIfCancellationRequested();
193-
throw new TimeoutException($"Timed out connecting to {endPoint}. Timeout was {_settings.ConnectTimeout}.");
194-
}
197+
if (timeoutCancellationTokenSource.IsCancellationRequested)
198+
{
199+
throw new TimeoutException($"Timed out connecting to {endPoint}. Timeout was {_settings.ConnectTimeout}.");
200+
}
195201

196-
try
197-
{
198-
socket.EndConnect(connectOperation);
202+
throw;
199203
}
200-
catch
204+
205+
void DisposeSocket()
201206
{
202-
try { socket.Dispose(); } catch { }
203-
throw;
207+
isSocketDisposed = true;
208+
try
209+
{
210+
socket.Dispose();
211+
}
212+
catch
213+
{
214+
// Ignore any exceptions.
215+
}
204216
}
205217
}
206218

@@ -228,8 +240,8 @@ private async Task ConnectAsync(Socket socket, EndPoint endPoint, CancellationTo
228240
{
229241
try
230242
{
231-
socket.Dispose();
232243
connectTask.IgnoreExceptions();
244+
socket.Dispose();
233245
}
234246
catch { }
235247

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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;
17+
using FluentAssertions;
18+
using MongoDB.TestHelpers.XunitExtensions.TimeoutEnforcing;
19+
20+
namespace MongoDB.Bson.Tests;
21+
22+
public class UnobservedTaskExceptionTracking
23+
{
24+
[UnobservedExceptionTrackingFact]
25+
public void EnsureNoUnobservedTaskException()
26+
{
27+
GC.Collect();
28+
GC.WaitForPendingFinalizers();
29+
30+
UnobservedExceptionTestDiscoverer.UnobservedExceptions.Should().BeEmpty();
31+
}
32+
}
33+

tests/MongoDB.Driver.Tests/ClusterRegistryTests.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
using MongoDB.Driver.Core.Misc;
2828
using MongoDB.Driver.Core.Servers;
2929
using MongoDB.Driver.Core.TestHelpers.Logging;
30-
using MongoDB.Driver.TestHelpers;
3130
using Xunit;
3231
using Xunit.Abstractions;
3332

@@ -73,7 +72,6 @@ public void DisposingClusterSource_should_use_cluster_registry_and_return_cluste
7372
ClusterRegistry.Instance._registry().Keys.Should().NotContain(clusterKey);
7473
}
7574

76-
#if WINDOWS
7775
[Fact]
7876
public void Instance_should_return_the_same_instance_every_time()
7977
{
@@ -221,7 +219,6 @@ public void UnregisterAndDisposeCluster_should_unregister_and_dispose_the_cluste
221219
subject._registry().Count.Should().Be(0);
222220
cluster._state().Should().Be(2);
223221
}
224-
#endif
225222
}
226223

227224
internal static class ClusterRegistryReflector

tests/MongoDB.Driver.Tests/Core/Connections/TcpStreamFactoryTests.cs

Lines changed: 29 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public class TcpStreamFactoryTests
3434
{
3535
[Theory]
3636
[ParameterAttributeData]
37-
public void Connect_should_dispose_socket_if_socket_fails([Values(false, true)] bool async)
37+
public async Task Connect_should_dispose_socket_if_socket_fails([Values(false, true)] bool async)
3838
{
3939
RequireServer.Check();
4040

@@ -43,20 +43,9 @@ public void Connect_should_dispose_socket_if_socket_fails([Values(false, true)]
4343

4444
using (var testSocket = new TestSocket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
4545
{
46-
Exception exception;
47-
if (async)
48-
{
49-
exception = Record.Exception(
50-
() =>
51-
subject
52-
.ConnectAsync(testSocket, endpoint, CancellationToken.None)
53-
.GetAwaiter()
54-
.GetResult());
55-
}
56-
else
57-
{
58-
exception = Record.Exception(() => subject.Connect(testSocket, endpoint, CancellationToken.None));
59-
}
46+
var exception = async ?
47+
await Record.ExceptionAsync(() => subject.ConnectAsync(testSocket, endpoint, CancellationToken.None)) :
48+
Record.Exception(() => subject.Connect(testSocket, endpoint, CancellationToken.None));
6049

6150
exception.Should().NotBeNull();
6251
testSocket.DisposeAttempts.Should().Be(1);
@@ -66,83 +55,64 @@ public void Connect_should_dispose_socket_if_socket_fails([Values(false, true)]
6655
[Fact]
6756
public void Constructor_should_throw_an_ArgumentNullException_when_tcpStreamSettings_is_null()
6857
{
69-
Action act = () => new TcpStreamFactory(null);
58+
var exception = Record.Exception(() => new TcpStreamFactory(null));
7059

71-
act.ShouldThrow<ArgumentNullException>();
60+
exception.Should().BeOfType<ArgumentNullException>().Subject
61+
.ParamName.Should().Be("settings");
7262
}
7363

7464
[Theory]
7565
[ParameterAttributeData]
76-
public void CreateStream_should_throw_a_SocketException_when_the_endpoint_could_not_be_resolved(
77-
[Values(false, true)]
78-
bool async)
66+
public async Task CreateStream_should_throw_a_SocketException_when_the_endpoint_could_not_be_resolved([Values(false, true)]bool async)
7967
{
8068
var subject = new TcpStreamFactory();
8169

82-
Action act;
83-
if (async)
84-
{
85-
act = () => subject.CreateStreamAsync(new DnsEndPoint("not-gonna-exist-i-hope", 27017), CancellationToken.None).GetAwaiter().GetResult();
86-
}
87-
else
88-
{
89-
act = () => subject.CreateStream(new DnsEndPoint("not-gonna-exist-i-hope", 27017), CancellationToken.None);
90-
}
70+
var exception = async ?
71+
await Record.ExceptionAsync(() => subject.CreateStreamAsync(new DnsEndPoint("not-gonna-exist-i-hope", 27017), CancellationToken.None)) :
72+
Record.Exception(() => subject.CreateStream(new DnsEndPoint("not-gonna-exist-i-hope", 27017), CancellationToken.None));
9173

92-
act.ShouldThrow<SocketException>();
74+
exception.Should().BeAssignableTo<SocketException>();
9375
}
9476

9577
[Theory]
9678
[ParameterAttributeData]
97-
public void CreateStream_should_throw_when_cancellation_is_requested(
98-
[Values(false, true)]
99-
bool async)
79+
public async Task CreateStream_should_throw_when_cancellation_is_requested([Values(false, true)]bool async)
10080
{
10181
var subject = new TcpStreamFactory();
10282
var endPoint = new IPEndPoint(new IPAddress(0x01010101), 12345); // a non-existent host and port
10383
var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(20));
10484

105-
Action action;
85+
var exception = async ?
86+
await Record.ExceptionAsync(() => subject.CreateStreamAsync(endPoint, cancellationTokenSource.Token)) :
87+
Record.Exception(() => subject.CreateStream(endPoint, cancellationTokenSource.Token));
10688
if (async)
10789
{
108-
action = () => subject.CreateStreamAsync(endPoint, cancellationTokenSource.Token).GetAwaiter().GetResult();
90+
exception.Should().BeOfType<TaskCanceledException>();
10991
}
11092
else
11193
{
112-
action = () => subject.CreateStream(endPoint, cancellationTokenSource.Token);
94+
exception.Should().BeOfType<OperationCanceledException>();
11395
}
114-
115-
action.ShouldThrow<OperationCanceledException>();
11696
}
11797

11898
[Theory]
11999
[ParameterAttributeData]
120-
public void CreateStream_should_throw_when_connect_timeout_has_expired(
121-
[Values(false, true)]
122-
bool async)
100+
public async Task CreateStream_should_throw_when_connect_timeout_has_expired([Values(false, true)]bool async)
123101
{
124102
var settings = new TcpStreamSettings(connectTimeout: TimeSpan.FromMilliseconds(20));
125103
var subject = new TcpStreamFactory(settings);
126104
var endPoint = new IPEndPoint(new IPAddress(0x01010101), 12345); // a non-existent host and port
127105

128-
Action action;
129-
if (async)
130-
{
131-
action = () => subject.CreateStreamAsync(endPoint, CancellationToken.None).GetAwaiter().GetResult(); ;
132-
}
133-
else
134-
{
135-
action = () => subject.CreateStream(endPoint, CancellationToken.None);
136-
}
106+
var exception = async ?
107+
await Record.ExceptionAsync(() => subject.CreateStreamAsync(endPoint, CancellationToken.None)) :
108+
Record.Exception(() => subject.CreateStream(endPoint, CancellationToken.None));
137109

138-
action.ShouldThrow<TimeoutException>();
110+
exception.Should().BeOfType<TimeoutException>();
139111
}
140112

141113
[Theory]
142114
[ParameterAttributeData]
143-
public void CreateStream_should_call_the_socketConfigurator(
144-
[Values(false, true)]
145-
bool async)
115+
public async Task CreateStream_should_call_the_socketConfigurator([Values(false, true)]bool async)
146116
{
147117
RequireServer.Check();
148118
var socketConfiguratorWasCalled = false;
@@ -153,7 +123,7 @@ public void CreateStream_should_call_the_socketConfigurator(
153123

154124
if (async)
155125
{
156-
subject.CreateStreamAsync(endPoint, CancellationToken.None).GetAwaiter().GetResult();
126+
await subject.CreateStreamAsync(endPoint, CancellationToken.None);
157127
}
158128
else
159129
{
@@ -165,9 +135,7 @@ public void CreateStream_should_call_the_socketConfigurator(
165135

166136
[Theory]
167137
[ParameterAttributeData]
168-
public void CreateStream_should_connect_to_a_running_server_and_return_a_non_null_stream(
169-
[Values(false, true)]
170-
bool async)
138+
public async Task CreateStream_should_connect_to_a_running_server_and_return_a_non_null_stream([Values(false, true)]bool async)
171139
{
172140
RequireServer.Check();
173141
var subject = new TcpStreamFactory();
@@ -176,7 +144,7 @@ public void CreateStream_should_connect_to_a_running_server_and_return_a_non_nul
176144
Stream stream;
177145
if (async)
178146
{
179-
stream = subject.CreateStreamAsync(endPoint, CancellationToken.None).GetAwaiter().GetResult();
147+
stream = await subject.CreateStreamAsync(endPoint, CancellationToken.None);
180148
}
181149
else
182150
{
@@ -188,9 +156,7 @@ public void CreateStream_should_connect_to_a_running_server_and_return_a_non_nul
188156

189157
[Theory]
190158
[ParameterAttributeData]
191-
public void SocketConfigurator_can_be_used_to_set_keepAlive(
192-
[Values(false, true)]
193-
bool async)
159+
public async Task SocketConfigurator_can_be_used_to_set_keepAlive([Values(false, true)]bool async)
194160
{
195161
RequireServer.Check();
196162
Action<Socket> socketConfigurator = s => s.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
@@ -201,7 +167,7 @@ public void SocketConfigurator_can_be_used_to_set_keepAlive(
201167
Stream stream;
202168
if (async)
203169
{
204-
stream = subject.CreateStreamAsync(endPoint, CancellationToken.None).GetAwaiter().GetResult();
170+
stream = await subject.CreateStreamAsync(endPoint, CancellationToken.None);
205171
}
206172
else
207173
{

tests/MongoDB.Driver.Tests/Core/Jira/CSharp3302Tests.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,15 @@ public async Task RapidHeartbeatTimerCallback_should_ignore_reentrant_calls()
9696
cluster.Initialize();
9797

9898
// Trigger Cluster._rapidHeartbeatTimer
99-
_ = cluster.SelectServerAsync(OperationContext.NoTimeout, CreateWritableServerAndEndPointSelector(__endPoint1));
99+
using var cancellationTokenSource = new CancellationTokenSource();
100+
var operationContext = new OperationContext(Timeout.InfiniteTimeSpan, cancellationTokenSource.Token);
101+
cluster.SelectServerAsync(operationContext, CreateWritableServerAndEndPointSelector(__endPoint1))
102+
.IgnoreExceptions();
100103

101104
// Wait for all heartbeats to complete
102105
await Task.WhenAny(allHeartbeatsReceived.Task, Task.Delay(1000));
106+
107+
cancellationTokenSource.Cancel();
103108
}
104109

105110
allHeartbeatsReceived.Task.Status.Should().Be(TaskStatus.RanToCompletion);

tests/MongoDB.Driver.Tests/Specifications/connection-monitoring-and-pooling/ConnectionMonitoringAndPoolingTestRunner.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ private void ExecuteCheckOut(
387387
else
388388
{
389389
tasks[target] = CreateTask(() => CheckOut(operation, connectionPool, map));
390+
tasks[target].IgnoreExceptions();
390391
}
391392
}
392393
}

0 commit comments

Comments
 (0)