Skip to content

Commit ca167f2

Browse files
authored
SWIFT-742 Implement unified replica set discovery behavior (#490)
1 parent 24de514 commit ca167f2

File tree

7 files changed

+119
-37
lines changed

7 files changed

+119
-37
lines changed

Sources/MongoSwift/ConnectionString.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ internal class ConnectionString {
3333
mongoc_uri_set_option_as_bool(self._uri, MONGOC_URI_RETRYREADS, rr)
3434
}
3535

36+
// Per SDAM spec: If the ``directConnection`` option is not specified, newly developed drivers MUST behave as
37+
// if it was specified with the false value.
38+
if let dc = options?.directConnection {
39+
mongoc_uri_set_option_as_bool(self._uri, MONGOC_URI_DIRECTCONNECTION, dc)
40+
} else if !self.hasOption("directConnection") {
41+
mongoc_uri_set_option_as_bool(self._uri, MONGOC_URI_DIRECTCONNECTION, false)
42+
}
43+
3644
if let credential = options?.credential {
3745
try self.setMongoCredential(credential)
3846
}
@@ -195,6 +203,10 @@ internal class ConnectionString {
195203
return hosts
196204
}
197205

206+
private func hasOption(_ option: String) -> Bool {
207+
mongoc_uri_has_option(self._uri, option)
208+
}
209+
198210
/// Executes the provided closure using a pointer to the underlying `mongoc_uri_t`.
199211
internal func withMongocURI<T>(_ body: (OpaquePointer) throws -> T) rethrows -> T {
200212
try body(self._uri)

Sources/MongoSwift/MongoClient.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ public struct MongoClientOptions: CodingStrategyProvider {
1616
/// databases or collections that derive from it.
1717
public var dateCodingStrategy: DateCodingStrategy?
1818

19+
/// When true, the client will connect directly to a single host. When false, the client will attempt to
20+
/// automatically discover all replica set members if a replica set name is provided. Defaults to false.
21+
/// It is an error to set this option to `true` when used with a mongodb+srv connection string or when multiple
22+
/// hosts are specified in the connection string.
23+
public var directConnection: Bool?
24+
1925
/// The maximum number of connections that may be associated with a connection pool created by this client at a
2026
/// given time. This includes in-use and available connections. Defaults to 100.
2127
public var maxPoolSize: Int?
@@ -83,6 +89,7 @@ public struct MongoClientOptions: CodingStrategyProvider {
8389
credential: MongoCredential? = nil,
8490
dataCodingStrategy: DataCodingStrategy? = nil,
8591
dateCodingStrategy: DateCodingStrategy? = nil,
92+
directConnection: Bool? = nil,
8693
maxPoolSize: Int? = nil,
8794
readConcern: ReadConcern? = nil,
8895
readPreference: ReadPreference? = nil,
@@ -101,6 +108,7 @@ public struct MongoClientOptions: CodingStrategyProvider {
101108
self.credential = credential
102109
self.dataCodingStrategy = dataCodingStrategy
103110
self.dateCodingStrategy = dateCodingStrategy
111+
self.directConnection = directConnection
104112
self.maxPoolSize = maxPoolSize
105113
self.readConcern = readConcern
106114
self.readPreference = readPreference

Tests/BSONTests/DocumentTests.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,8 +374,6 @@ final class DocumentTests: MongoSwiftTestCase {
374374
// save a reference to original bson_t so we can verify it doesn't change
375375
let pointer = doc.pointerAddress
376376

377-
print("pointer is: \(pointer)")
378-
379377
// overwrite int32 with int32
380378
doc["int32"] = .int32(15)
381379
expect(doc["int32"]).to(equal(.int32(15)))

Tests/LinuxMain.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ extension RetryableWritesTests {
319319
extension SDAMTests {
320320
static var allTests = [
321321
("testMonitoring", testMonitoring),
322+
("testInitialReplicaSetDiscovery", testInitialReplicaSetDiscovery),
322323
]
323324
}
324325

Tests/MongoSwiftSyncTests/SDAMMonitoringTests.swift renamed to Tests/MongoSwiftSyncTests/SDAMTests.swift

Lines changed: 77 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,6 @@ final class SDAMTests: MongoSwiftTestCase {
1616
expect(desc.passives).to(haveCount(0))
1717
}
1818

19-
func checkUnknownServerType(_ desc: ServerDescription) {
20-
expect(desc.type).to(equal(ServerDescription.ServerType.unknown))
21-
}
22-
2319
// Basic test based on the "standalone" spec test for SDAM monitoring:
2420
// swiftlint:disable line_length
2521
// https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/tests/monitoring/standalone.json
@@ -51,54 +47,100 @@ final class SDAMTests: MongoSwiftTestCase {
5147
}
5248
let hostAddress = try ServerAddress(host)
5349

54-
// check event count and that events are of the expected types
55-
expect(receivedEvents.count).to(beGreaterThanOrEqualTo(5))
50+
expect(receivedEvents.count).to(equal(4))
5651
expect(receivedEvents[0].topologyOpeningValue).toNot(beNil())
57-
expect(receivedEvents[1].topologyDescriptionChangedValue).toNot(beNil())
58-
expect(receivedEvents[2].serverOpeningValue).toNot(beNil())
59-
expect(receivedEvents[3].serverDescriptionChangedValue).toNot(beNil())
60-
expect(receivedEvents[4].topologyDescriptionChangedValue).toNot(beNil())
52+
expect(receivedEvents[1].serverOpeningValue).toNot(beNil())
53+
expect(receivedEvents[2].serverDescriptionChangedValue).toNot(beNil())
54+
expect(receivedEvents[3].topologyDescriptionChangedValue).toNot(beNil())
6155

62-
// verify that data in ServerDescription and TopologyDescription looks reasonable
6356
let event0 = receivedEvents[0].topologyOpeningValue!
64-
expect(event0.topologyID).toNot(beNil())
6557

66-
let event1 = receivedEvents[1].topologyDescriptionChangedValue!
58+
let event1 = receivedEvents[1].serverOpeningValue!
6759
expect(event1.topologyID).to(equal(event0.topologyID))
68-
expect(event1.previousDescription.type).to(equal(TopologyDescription.TopologyType.unknown))
69-
expect(event1.newDescription.type).to(equal(TopologyDescription.TopologyType.single))
70-
// This is a bit of a deviation from the SDAM spec tests linked above. However, this is how mongoc responds so
71-
// there is no other way to get around this.
72-
expect(event1.newDescription.servers).to(beEmpty())
60+
expect(event1.serverAddress).to(equal(hostAddress))
7361

74-
let event2 = receivedEvents[2].serverOpeningValue!
62+
let event2 = receivedEvents[2].serverDescriptionChangedValue!
7563
expect(event2.topologyID).to(equal(event1.topologyID))
76-
expect(event2.serverAddress).to(equal(hostAddress))
7764

78-
let event3 = receivedEvents[3].serverDescriptionChangedValue!
79-
expect(event3.topologyID).to(equal(event2.topologyID))
80-
let prevServer = event3.previousDescription
81-
expect(prevServer.address).to(equal(hostAddress))
82-
self.checkEmptyLists(prevServer)
83-
self.checkUnknownServerType(prevServer)
65+
let prevServer = event2.previousDescription
66+
let newServer = event2.newDescription
8467

85-
let newServer = event3.newDescription
68+
expect(prevServer.address).to(equal(hostAddress))
8669
expect(newServer.address).to(equal(hostAddress))
70+
71+
self.checkEmptyLists(prevServer)
8772
self.checkEmptyLists(newServer)
88-
expect(newServer.type).to(equal(ServerDescription.ServerType.standalone))
8973

90-
let event4 = receivedEvents[4].topologyDescriptionChangedValue!
91-
expect(event4.topologyID).to(equal(event3.topologyID))
92-
let prevTopology = event4.previousDescription
93-
expect(prevTopology.type).to(equal(TopologyDescription.TopologyType.single))
74+
expect(prevServer.type).to(equal(.unknown))
75+
expect(newServer.type).to(equal(.standalone))
76+
77+
let event3 = receivedEvents[3].topologyDescriptionChangedValue!
78+
expect(event3.topologyID).to(equal(event2.topologyID))
79+
80+
let prevTopology = event3.previousDescription
81+
let newTopology = event3.newDescription
82+
83+
expect(prevTopology.type).to(equal(.unknown))
84+
expect(newTopology.type).to(equal(.single))
85+
9486
expect(prevTopology.servers).to(beEmpty())
87+
expect(newTopology.servers).to(haveCount(1))
9588

96-
let newTopology = event4.newDescription
97-
expect(newTopology.type).to(equal(TopologyDescription.TopologyType.single))
9889
expect(newTopology.servers[0].address).to(equal(hostAddress))
99-
expect(newTopology.servers[0].type).to(equal(ServerDescription.ServerType.standalone))
90+
expect(newTopology.servers[0].type).to(equal(.standalone))
91+
10092
self.checkEmptyLists(newTopology.servers[0])
10193
}
94+
95+
func testInitialReplicaSetDiscovery() throws {
96+
guard MongoSwiftTestCase.topologyType == .replicaSetWithPrimary else {
97+
print(unsupportedTopologyMessage(testName: self.name))
98+
return
99+
}
100+
101+
let hostURIs = Self.getConnectionStringPerHost()
102+
103+
let optsFalse = MongoClientOptions(directConnection: false)
104+
let optsTrue = MongoClientOptions(directConnection: true)
105+
106+
// We should succeed in discovering the primary in all of these cases:
107+
let testClientsShouldSucceed = try
108+
hostURIs.map { try MongoClient.makeTestClient($0) } + // option unspecified
109+
hostURIs.map { try MongoClient.makeTestClient("\($0)&directConnection=false") } + // false in URI
110+
hostURIs.map { try MongoClient.makeTestClient($0, options: optsFalse) } // false in options struct
111+
112+
// separately connect to each host and verify we are able to perform a write, meaning
113+
// that the primary is successfully discovered no matter which host we start with
114+
for client in testClientsShouldSucceed {
115+
try withTestNamespace(client: client) { _, collection in
116+
expect(try collection.insertOne(["x": 1])).toNot(throwError())
117+
}
118+
}
119+
120+
let testClientsShouldMostlyFail = try
121+
hostURIs.map { try MongoClient.makeTestClient("\($0)&directConnection=true") } + // true in URI
122+
hostURIs.map { try MongoClient.makeTestClient($0, options: optsTrue) } // true in options struct
123+
124+
// 4 of 6 attempts to perform writes should fail assuming these are 3-node replica sets, since in 2 cases we
125+
// will directly connect to the primary, and in the other 4 we will directly connect to a secondary.
126+
127+
var failures = 0
128+
for client in testClientsShouldMostlyFail {
129+
do {
130+
_ = try withTestNamespace(client: client) { _, collection in
131+
try collection.insertOne(["x": 1])
132+
}
133+
} catch {
134+
expect(error).to(beAnInstanceOf(MongoError.CommandError.self))
135+
failures += 1
136+
}
137+
}
138+
139+
expect(failures).to(
140+
equal(4),
141+
description: "Writes should fail when connecting to secondaries with directConnection=true"
142+
)
143+
}
102144
}
103145

104146
/// SDAM monitoring event handler that behaves similarly to the `TestCommandMonitor`
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"uri": "mongodb+srv://test3.test.build.10gen.cc/?directConnection=false",
3+
"seeds": [
4+
"localhost.test.build.10gen.cc:27017"
5+
],
6+
"hosts": [
7+
"localhost:27017",
8+
"localhost:27018",
9+
"localhost:27019"
10+
],
11+
"options": {
12+
"ssl": true
13+
}
14+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"uri": "mongodb+srv://test3.test.build.10gen.cc/?directConnection=true",
3+
"seeds": [],
4+
"hosts": [],
5+
"error": true,
6+
"comment": "Should fail because directConnection=true is incompatible with SRV URIs."
7+
}

0 commit comments

Comments
 (0)