Skip to content

Commit 3419231

Browse files
CSHARP-2433: Connections survive primary stepdown.
1 parent a0db633 commit 3419231

File tree

3 files changed

+205
-7
lines changed

3 files changed

+205
-7
lines changed

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,13 @@ public class Feature
5050
private static readonly Feature __eval = new Feature("Eval", new SemanticVersion(0, 0, 0), new SemanticVersion(4, 1, 0, ""));
5151
private static readonly Feature __explainCommand = new Feature("ExplainCommand", new SemanticVersion(3, 0, 0));
5252
private static readonly Feature __failPoints = new Feature("FailPoints", new SemanticVersion(2, 4, 0));
53+
private static readonly Feature __failPointsFailCommand = new Feature("FailPointsFailCommand", new SemanticVersion(4, 0, 0));
5354
private static readonly Feature __findAndModifyWriteConcern = new Feature("FindAndModifyWriteConcern", new SemanticVersion(3, 2, 0));
5455
private static readonly Feature __findCommand = new Feature("FindCommand", new SemanticVersion(3, 2, 0));
5556
private static readonly Feature __geoNearCommand = new Feature("GeoNearCommand", new SemanticVersion(1, 0, 0), new SemanticVersion(4, 1, 0, ""));
5657
private static readonly Feature __groupCommand = new Feature("GroupCommand", new SemanticVersion(1, 0, 0), new SemanticVersion(4, 0, 0, "rc1"));
58+
private static readonly Feature __keepConnectionPoolWhenNotMasterConnectionException = new Feature("KeepConnectionPoolWhenNotMasterConnectionException", new SemanticVersion(4, 1, 10));
59+
private static readonly Feature __keepConnectionPoolWhenReplSetStepDown = new Feature("KeepConnectionPoolWhenReplSetStepDown", new SemanticVersion(4, 1, 10));
5760
private static readonly Feature __killCursorsCommand = new Feature("KillCursorsCommand", new SemanticVersion(3, 2, 0));
5861
private static readonly Feature __listCollectionsCommand = new Feature("ListCollectionsCommand", new SemanticVersion(3, 0, 0));
5962
private static readonly Feature __listDatabasesFilter = new Feature("ListDatabasesFilter", new SemanticVersion(3, 4, 2));
@@ -211,6 +214,11 @@ public class Feature
211214
/// </summary>
212215
public static Feature FailPoints => __failPoints;
213216

217+
/// <summary>
218+
/// Gets the fail points fail command feature.
219+
/// </summary>
220+
public static Feature FailPointsFailCommand => __failPointsFailCommand;
221+
214222
/// <summary>
215223
/// Gets the find and modify write concern feature.
216224
/// </summary>
@@ -231,6 +239,16 @@ public class Feature
231239
/// </summary>
232240
public static Feature GroupCommand => __groupCommand;
233241

242+
/// <summary>
243+
/// Gets the keep connection pool when NotMaster connection exception feature.
244+
/// </summary>
245+
public static Feature KeepConnectionPoolWhenNotMasterConnectionException => __keepConnectionPoolWhenNotMasterConnectionException;
246+
247+
/// <summary>
248+
/// Gets the keep connection pool when replSetStepDown feature.
249+
/// </summary>
250+
public static Feature KeepConnectionPoolWhenReplSetStepDown => __keepConnectionPoolWhenReplSetStepDown;
251+
234252
/// <summary>
235253
/// Get the killCursors command feature.
236254
/// </summary>

src/MongoDB.Driver.Core/Core/Servers/Server.cs

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,10 @@ public IChannelHandle GetChannel(CancellationToken cancellationToken)
156156
_connectionPool.Clear();
157157
}
158158
catch
159-
{
159+
{
160160
// ignore exceptions
161161
}
162-
162+
163163
}
164164
connection.Dispose();
165165
throw;
@@ -190,7 +190,7 @@ public async Task<IChannelHandle> GetChannelAsync(CancellationToken cancellation
190190
_connectionPool.Clear();
191191
}
192192
catch
193-
{
193+
{
194194
// ignore exceptions
195195
}
196196
}
@@ -224,8 +224,7 @@ public void Initialize()
224224
public void Invalidate()
225225
{
226226
ThrowIfNotOpen();
227-
_connectionPool.Clear();
228-
_monitor.Invalidate();
227+
Invalidate(clearConnectionPool: true);
229228
}
230229

231230
public void RequestHeartbeat()
@@ -279,14 +278,24 @@ private void HandleChannelException(IConnection connection, Exception ex)
279278

280279
if (ShouldInvalidateServer(ex))
281280
{
282-
Invalidate();
281+
var shouldClearConnectionPool = ShouldClearConnectionPoolForChannelException(ex, connection.Description.ServerVersion);
282+
Invalidate(shouldClearConnectionPool);
283283
}
284284
else
285285
{
286286
RequestHeartbeat();
287287
}
288288
}
289289

290+
private void Invalidate(bool clearConnectionPool)
291+
{
292+
if (clearConnectionPool)
293+
{
294+
_connectionPool.Clear();
295+
}
296+
_monitor.Invalidate();
297+
}
298+
290299
private bool IsNotMaster(ServerErrorCode code, string message)
291300
{
292301
switch (code)
@@ -336,12 +345,22 @@ private bool IsRecovering(ServerErrorCode code, string message)
336345

337346
return false;
338347
}
339-
348+
340349
private bool ShouldClearConnectionPool(Exception ex)
341350
{
342351
return ex is MongoAuthenticationException;
343352
}
344353

354+
private bool ShouldClearConnectionPoolForChannelException(Exception ex, SemanticVersion serverVersion)
355+
{
356+
if (ex is MongoNotPrimaryException mongoNotPrimaryException && mongoNotPrimaryException.Code == (int)ServerErrorCode.NotMaster)
357+
{
358+
return !Feature.KeepConnectionPoolWhenNotMasterConnectionException.IsSupported(serverVersion);
359+
}
360+
361+
return true;
362+
}
363+
345364
private bool ShouldInvalidateServer(Exception exception)
346365
{
347366
if (__invalidatingExceptions.Contains(exception.GetType()))
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/* Copyright 2019-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 FluentAssertions;
17+
using MongoDB.Bson;
18+
using MongoDB.Bson.TestHelpers.XunitExtensions;
19+
using MongoDB.Driver.Core;
20+
using MongoDB.Driver.Core.Bindings;
21+
using MongoDB.Driver.Core.Clusters;
22+
using MongoDB.Driver.Core.Events;
23+
using MongoDB.Driver.Core.Misc;
24+
using MongoDB.Driver.Core.TestHelpers;
25+
using MongoDB.Driver.Core.TestHelpers.XunitExtensions;
26+
using MongoDB.Driver.TestHelpers;
27+
using Xunit;
28+
29+
namespace MongoDB.Driver.Tests
30+
{
31+
public class ConnectionsSurvivePrimaryStepDownTests
32+
{
33+
private readonly string _collectionName = "step-down";
34+
private readonly string _databaseName = "step-down";
35+
36+
[SkippableTheory]
37+
[ParameterAttributeData]
38+
public void Connection_pool_should_be_cleared_when_Shutdown_exceptions(
39+
[Values(
40+
ServerErrorCode.ShutdownInProgress, // 91
41+
ServerErrorCode.InterruptedAtShutdown)] // 11600
42+
int errorCode)
43+
{
44+
RequireServer.Check().Supports(Feature.FailPointsFailCommand).ClusterType(ClusterType.ReplicaSet);
45+
46+
var eventCapturer = new EventCapturer().Capture<ConnectionPoolRemovedConnectionEvent>();
47+
using (var client = CreateDisposableClient(eventCapturer))
48+
{
49+
var database = client.GetDatabase(_databaseName, new MongoDatabaseSettings { WriteConcern = WriteConcern.WMajority });
50+
database.DropCollection(_databaseName);
51+
var collection = database.GetCollection<BsonDocument>(_collectionName, new MongoCollectionSettings { WriteConcern = WriteConcern.WMajority });
52+
eventCapturer.Clear();
53+
54+
using (ConfigureFailPoint(client, errorCode))
55+
{
56+
var exception = Record.Exception(() => { collection.InsertOne(new BsonDocument("test", 1)); });
57+
58+
var e = exception.Should().BeOfType<MongoNodeIsRecoveringException>().Subject;
59+
e.Code.Should().Be(errorCode);
60+
}
61+
eventCapturer.Events.Count.Should().Be(1);
62+
}
63+
}
64+
65+
[SkippableTheory]
66+
[ParameterAttributeData]
67+
public void Connection_pool_should_not_be_cleared_when_replSetStepDown_and_GetMore([Values(false, true)] bool async)
68+
{
69+
RequireServer.Check().Supports(Feature.KeepConnectionPoolWhenReplSetStepDown).ClusterType(ClusterType.ReplicaSet);
70+
71+
var eventCapturer = new EventCapturer().Capture<ConnectionPoolRemovedConnectionEvent>();
72+
using (var client = CreateDisposableClient(eventCapturer))
73+
{
74+
var database = client.GetDatabase(_databaseName, new MongoDatabaseSettings { WriteConcern = WriteConcern.WMajority });
75+
database.DropCollection(_databaseName);
76+
var collection = database.GetCollection<BsonDocument>(_collectionName, new MongoCollectionSettings { WriteConcern = WriteConcern.WMajority });
77+
var adminDatabase = client.GetDatabase("admin").WithWriteConcern(WriteConcern.W1);
78+
79+
collection.InsertMany(
80+
new[]
81+
{
82+
new BsonDocument("x", 1),
83+
new BsonDocument("x", 2),
84+
new BsonDocument("x", 3),
85+
new BsonDocument("x", 4),
86+
new BsonDocument("x", 5),
87+
});
88+
eventCapturer.Clear();
89+
90+
var cursor = collection.FindSync(FilterDefinition<BsonDocument>.Empty, new FindOptions<BsonDocument> { BatchSize = 2 });
91+
cursor.MoveNext();
92+
93+
BsonDocument replSetStepDownResult;
94+
if (async)
95+
{
96+
replSetStepDownResult = adminDatabase.RunCommandAsync<BsonDocument>("{ replSetStepDown : 5, force : true }").GetAwaiter().GetResult();
97+
}
98+
else
99+
{
100+
replSetStepDownResult = adminDatabase.RunCommand<BsonDocument>("{ replSetStepDown : 5, force : true }");
101+
}
102+
103+
replSetStepDownResult.GetValue("ok", false).ToBoolean().Should().BeTrue();
104+
105+
cursor.MoveNext();
106+
107+
eventCapturer.Events.Should().BeEmpty();
108+
}
109+
}
110+
111+
[SkippableFact]
112+
public void Connection_pool_should_work_as_expected_when_NonMaster_exception()
113+
{
114+
RequireServer.Check().Supports(Feature.FailPointsFailCommand).ClusterType(ClusterType.ReplicaSet);
115+
116+
var shouldConnectionPoolBeCleared = !Feature.KeepConnectionPoolWhenNotMasterConnectionException.IsSupported(CoreTestConfiguration.ServerVersion);
117+
118+
var eventCapturer = new EventCapturer().Capture<ConnectionPoolRemovedConnectionEvent>();
119+
using (var client = CreateDisposableClient(eventCapturer))
120+
{
121+
var database = client.GetDatabase(_databaseName, new MongoDatabaseSettings { WriteConcern = WriteConcern.WMajority });
122+
database.DropCollection(_databaseName);
123+
var collection = database.GetCollection<BsonDocument>(_collectionName, new MongoCollectionSettings { WriteConcern = WriteConcern.WMajority });
124+
eventCapturer.Clear();
125+
126+
using (ConfigureFailPoint(client, 10107))
127+
{
128+
var document = new BsonDocument("test", 1);
129+
var exception = Record.Exception(() => { collection.InsertOne(document); });
130+
131+
var e = exception.Should().BeOfType<MongoNotPrimaryException>().Subject;
132+
e.Code.Should().Be(10107);
133+
134+
if (!shouldConnectionPoolBeCleared)
135+
{
136+
collection.InsertOne(document);
137+
}
138+
}
139+
eventCapturer.Events.Count.Should().Be(shouldConnectionPoolBeCleared ? 1 : 0);
140+
}
141+
}
142+
143+
private FailPoint ConfigureFailPoint(IMongoClient client, int errorCode)
144+
{
145+
var session = NoCoreSession.NewHandle();
146+
147+
var args = BsonDocument.Parse($"{{ mode : {{ times : 1 }}, data : {{ failCommands : [\"insert\"], errorCode : {errorCode} }} }}");
148+
return FailPoint.Configure(client.Cluster, session, "failCommand", args);
149+
}
150+
151+
private DisposableMongoClient CreateDisposableClient(EventCapturer capturedEvents)
152+
{
153+
return DriverTestConfiguration.CreateDisposableClient(
154+
settings =>
155+
{
156+
settings.RetryWrites = false;
157+
settings.ClusterConfigurator = c => { c.Subscribe(capturedEvents); };
158+
});
159+
}
160+
}
161+
}

0 commit comments

Comments
 (0)