Skip to content

Commit 9f39ed4

Browse files
authored
SWIFT-1277, SWIFT-1263, SWIFT-1507 Sync latest initial DNS seedlist discovery tests (#748)
1 parent 2fdf64d commit 9f39ed4

24 files changed

+409
-47
lines changed

Sources/MongoSwift/MongoClient.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ public struct MongoClientOptions: CodingStrategyProvider {
7777
/// seconds (30000 ms).
7878
public var serverSelectionTimeoutMS: Int?
7979

80+
/// The maximum number of SRV results to randomly select when initially populating the seedlist or, during SRV
81+
/// polling, adding new hosts to the topology.
82+
public var srvMaxHosts: Int?
83+
84+
/// The service name to use for SRV lookup. Must be a valid SRV service name according to RFC 6335.
85+
/// -SeeAlso: https://datatracker.ietf.org/doc/html/rfc6335#section-5.1
86+
public var srvServiceName: String?
87+
8088
/**
8189
* `MongoSwift.MongoClient` provides an asynchronous API by running all blocking operations off of their
8290
* originating threads in a thread pool. `MongoSwiftSync.MongoClient` is implemented as a wrapper of the async
@@ -158,6 +166,8 @@ public struct MongoClientOptions: CodingStrategyProvider {
158166
retryWrites: Bool? = nil,
159167
serverAPI: MongoServerAPI? = nil,
160168
serverSelectionTimeoutMS: Int? = nil,
169+
srvMaxHosts: Int? = nil,
170+
srvServiceName: String? = nil,
161171
threadPoolSize: Int? = nil,
162172
tls: Bool? = nil,
163173
tlsAllowInvalidCertificates: Bool? = nil,
@@ -187,6 +197,8 @@ public struct MongoClientOptions: CodingStrategyProvider {
187197
self.retryWrites = retryWrites
188198
self.retryReads = retryReads
189199
self.serverAPI = serverAPI
200+
self.srvMaxHosts = srvMaxHosts
201+
self.srvServiceName = srvServiceName
190202
self.serverSelectionTimeoutMS = serverSelectionTimeoutMS
191203
self.threadPoolSize = threadPoolSize
192204
self.tls = tls

Sources/MongoSwift/MongoConnectionString.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,12 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
604604
if let serverSelectionTimeoutMS = options.serverSelectionTimeoutMS {
605605
self.serverSelectionTimeoutMS = serverSelectionTimeoutMS
606606
}
607+
if let srvMaxHosts = options.srvMaxHosts {
608+
self.srvMaxHosts = srvMaxHosts
609+
}
610+
if let srvServiceName = options.srvServiceName {
611+
self.srvServiceName = srvServiceName
612+
}
607613
if let tls = options.tls {
608614
self.tls = tls
609615
}

Tests/MongoSwiftTests/DNSSeedlistTests.swift

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ struct DNSSeedlistTestCase: Decodable {
1010
/// A mongodb+srv connection string.
1111
let uri: String
1212
/// The expected set of initial seeds discovered from the SRV record.
13-
let seeds: [String]
13+
let seeds: [String]?
1414
/// The discovered topology's list of hosts once SDAM completes a scan.
15-
let hosts: [ServerAddress]
15+
let hosts: [ServerAddress]?
1616
/// The parsed connection string options as discovered from URI and TXT records.
1717
let options: BSONDocument?
1818
/// Additional options present in the connection string URI such as Userinfo (as user and password), and Auth
@@ -22,9 +22,13 @@ struct DNSSeedlistTestCase: Decodable {
2222
let error: Bool?
2323
/// A comment to indicate why a test would fail.
2424
let comment: String?
25+
/// The expected number of initial seeds discovered from the SRV record.
26+
let numSeeds: Int?
27+
/// The expected number of hosts discovered once SDAM completes a scan.
28+
let numHosts: Int?
2529

2630
private enum CodingKeys: String, CodingKey {
27-
case uri, seeds, hosts, options, parsedOptions = "parsed_options", error, comment
31+
case uri, seeds, hosts, options, parsedOptions = "parsed_options", error, comment, numSeeds, numHosts
2832
}
2933
}
3034

@@ -88,13 +92,23 @@ final class DNSSeedlistTests: MongoSwiftTestCase {
8892
try runDNSSeedlistTests(tests)
8993
}
9094

95+
func testInitialDNSSeedlistDiscoverySharded() throws {
96+
guard MongoSwiftTestCase.topologyType == .sharded else {
97+
print("Skipping test because of unsupported topology type \(MongoSwiftTestCase.topologyType)")
98+
return
99+
}
100+
101+
let tests = try retrieveSpecTestFiles(
102+
specName: "initial-dns-seedlist-discovery",
103+
subdirectory: "sharded",
104+
asType: DNSSeedlistTestCase.self
105+
)
106+
107+
try runDNSSeedlistTests(tests)
108+
}
109+
91110
func runDNSSeedlistTests(_ tests: [(String, DNSSeedlistTestCase)]) throws {
92111
for (fileName, testCase) in tests {
93-
// TODO: SWIFT-1455: unskip these test
94-
if fileName == "loadBalanced-no-results.json" || fileName == "loadBalanced-true-multiple-hosts.json" {
95-
continue
96-
}
97-
98112
// this particular test case requires SSL is disabled. see DRIVERS-1324.
99113
let requiresTLS = fileName != "txt-record-with-overridden-ssl-option.json"
100114

@@ -118,8 +132,7 @@ final class DNSSeedlistTests: MongoSwiftTestCase {
118132
try self.withTestClient(testCase.uri, options: opts) { client in
119133
client.addSDAMEventHandler(topologyWatcher)
120134

121-
// try selecting a server to trigger SDAM
122-
_ = try client.connectionPool.selectServer(forWrites: false)
135+
_ = try client.db("admin").runCommand(["ping": 1]).wait()
123136

124137
guard testCase.error != true else {
125138
XCTFail("Expected error for test case \(testCase.comment ?? ""), got none")
@@ -130,8 +143,13 @@ final class DNSSeedlistTests: MongoSwiftTestCase {
130143
// eventually matches the list of hosts."
131144
// This needs to be done before the client leaves scope to ensure the SDAM machinery
132145
// keeps running.
133-
expect(topologyWatcher.getLastDescription()?.servers.map { $0.address })
134-
.toEventually(equal(testCase.hosts), timeout: 5)
146+
if let expectedHosts = testCase.hosts {
147+
expect(topologyWatcher.getLastDescription()?.servers.map { $0.address })
148+
.toEventually(equal(expectedHosts), timeout: 5)
149+
} else if let expectedNumHosts = testCase.numHosts {
150+
expect(topologyWatcher.getLastDescription()?.servers)
151+
.toEventually(haveCount(expectedNumHosts), timeout: 5)
152+
}
135153

136154
// "You MUST verify that each of the values of the Connection String Options under options match the
137155
// Client's parsed value for that option."

Tests/MongoSwiftTests/MongoConnectionStringTests.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,4 +661,42 @@ final class ConnectionStringTests: MongoSwiftTestCase {
661661
connString7.credential?.source = nil
662662
expect(connString7.description).toNot(contain("authsource"))
663663
}
664+
665+
func testSrvMaxHosts() throws {
666+
var connString1 = try MongoConnectionString(string: "mongodb+srv://test1.test.build.10gen.cc/?srvMaxHosts=1")
667+
expect(connString1.srvMaxHosts).to(equal(1))
668+
669+
// applyOptions should properly set and override original value
670+
let opts1 = MongoClientOptions(srvMaxHosts: 2)
671+
try connString1.applyOptions(opts1)
672+
expect(connString1.srvMaxHosts).to(equal(2))
673+
674+
// invalid value of option
675+
let opts2 = MongoClientOptions(srvMaxHosts: -1)
676+
expect(try connString1.applyOptions(opts2)).to(throwError(errorType: MongoError.InvalidArgumentError.self))
677+
678+
var connString2 = try MongoConnectionString(string: "mongodb://localhost:27017")
679+
// option is only valid with SRV URIs
680+
expect(try connString2.applyOptions(opts1)).to(throwError(errorType: MongoError.InvalidArgumentError.self))
681+
}
682+
683+
func testSrvServiceName() throws {
684+
var connString1 = try MongoConnectionString(
685+
string: "mongodb+srv://test1.test.build.10gen.cc/?srvServiceName=customname"
686+
)
687+
expect(connString1.srvServiceName).to(equal("customname"))
688+
689+
// applyOptions should properly set and override original value
690+
let opts1 = MongoClientOptions(srvServiceName: "newcustomname")
691+
try connString1.applyOptions(opts1)
692+
expect(connString1.srvServiceName).to(equal("newcustomname"))
693+
694+
// invalid value of option
695+
let opts2 = MongoClientOptions(srvServiceName: "-invalid-")
696+
expect(try connString1.applyOptions(opts2)).to(throwError(errorType: MongoError.InvalidArgumentError.self))
697+
698+
var connString2 = try MongoConnectionString(string: "mongodb://localhost:27017")
699+
// option is only valid with SRV URIs
700+
expect(try connString2.applyOptions(opts1)).to(throwError(errorType: MongoError.InvalidArgumentError.self))
701+
}
664702
}

Tests/Specs/initial-dns-seedlist-discovery/initial-dns-seedlist-discovery.rst

Lines changed: 109 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ Initial DNS Seedlist Discovery
1010
:Authors: Derick Rethans
1111
:Status: Draft
1212
:Type: Standards
13-
:Last Modified: 2019-03-07
14-
:Version: 1.3.2
13+
:Last Modified: 2021-10-14
14+
:Version: 1.6.0
1515
:Spec Lead: Matt Broadstone
1616
:Advisory Group: \A. Jesse Jiryu Davis
1717
:Approver(s): Bernie Hackett, David Golden, Jeff Yemin, Matt Broadstone, A. Jesse Jiryu Davis
@@ -48,6 +48,9 @@ interpreted as described in `RFC 2119 <https://www.ietf.org/rfc/rfc2119.txt>`_.
4848
Specification
4949
=============
5050

51+
Connection String Format
52+
------------------------
53+
5154
The connection string parser in the driver is extended with a new protocol
5255
``mongodb+srv`` as a logical pre-processing step before it considers the
5356
connection string and SDAM specifications. In this protocol, the comma
@@ -61,33 +64,59 @@ format is::
6164
specification following the ``Host Information``. This includes the ``Auth
6265
database`` and ``Connection Options``.
6366

64-
Seedlist Discovery
65-
------------------
6667

67-
In this preprocessing step, the driver will query the DNS server for SRV
68-
records on ``{hostname}.{domainname}``, prefixed with ``_mongodb._tcp.``:
69-
``_mongodb._tcp.{hostname}.{domainname}``. This DNS query is expected to
70-
respond with one or more SRV records. From the DNS result, the driver now MUST
71-
behave the same as if an ``mongodb://`` URI was provided with all the host
72-
names and port numbers that were returned as part of the DNS SRV query result.
68+
MongoClient Configuration
69+
-------------------------
7370

74-
The priority and weight fields in returned SRV records MUST be ignored.
71+
srvMaxHosts
72+
~~~~~~~~~~~
7573

76-
If ``mongodb+srv`` is used, a driver MUST implicitly also enable TLS. Clients
77-
can turn this off by passing ``ssl=false`` in either the Connection String,
78-
or options passed in as parameters in code to the MongoClient constructor (or
79-
equivalent API for each driver), but not through a TXT record (discussed in
80-
the next section).
74+
This option is used to limit the number of mongos connections that may be
75+
created for sharded topologies. This option limits the number of SRV records
76+
used to populate the seedlist during initial discovery, as well as the number of
77+
additional hosts that may be added during
78+
`SRV polling <../polling-srv-records-for-mongos-discovery/polling-srv-records-for-mongos-discovery.rst>`_.
79+
This option requires a non-negative integer and defaults to zero (i.e. no
80+
limit). This option MUST only be configurable at the level of a ``MongoClient``.
8181

82-
A driver MUST verify that in addition to the ``{hostname}``, the
83-
``{domainname}`` consists of at least two parts: the domain name, and a TLD.
84-
Drivers MUST raise an error and MUST NOT contact the DNS server to obtain SRV
85-
(or TXT records) if the full URI does not consists of at least three parts.
8682

87-
A driver MUST verify that the host names returned through SRV records have the
88-
same parent ``{domainname}``. Drivers MUST raise an error and MUST NOT
89-
initiate a connection to any returned host name which does not share the same
90-
``{domainname}``.
83+
srvServiceName
84+
~~~~~~~~~~~~~~
85+
86+
This option specifies a valid SRV service name according to
87+
`RFC 6335 <https://datatracker.ietf.org/doc/html/rfc6335#section-5.1>`_, with
88+
the exception that it may exceed 15 characters as long as the 63rd (62nd with
89+
prepended underscore) character DNS query limit is not surpassed. This option
90+
requires a string value and defaults to "mongodb". This option MUST only be
91+
configurable at the level of a ``MongoClient``.
92+
93+
94+
URI Validation
95+
~~~~~~~~~~~~~~
96+
97+
The driver MUST report an error if either the ``srvServiceName`` or
98+
``srvMaxHosts`` URI options are specified with a non-SRV URI (i.e. scheme other
99+
than ``mongodb+srv``). The driver MUST allow specifying the ``srvServiceName``
100+
and ``srvMaxHosts`` URI options with an SRV URI (i.e. ``mongodb+srv`` scheme).
101+
102+
If ``srvMaxHosts`` is a positive integer, the driver MUST throw an error in the
103+
following cases:
104+
105+
- The connection string contains a ``replicaSet`` option.
106+
- The connection string contains a ``loadBalanced`` option with a value of
107+
``true``.
108+
109+
When validating URI options, the driver MUST first do the SRV and TXT lookup and
110+
then perform the validation. For drivers that do SRV lookup asynchronously this
111+
may result in a ``MongoClient`` being instantiated but erroring later during
112+
operation execution.
113+
114+
115+
Seedlist Discovery
116+
------------------
117+
118+
Validation Before Querying DNS
119+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
91120

92121
It is an error to specify a port in a connection string with the
93122
``mongodb+srv`` protocol, and the driver MUST raise a parse error and MUST NOT
@@ -97,14 +126,54 @@ It is an error to specify more than one host name in a connection string with
97126
the ``mongodb+srv`` protocol, and the driver MUST raise a parse error and MUST
98127
NOT do DNS resolution or contact hosts.
99128

100-
The driver MUST NOT attempt to connect to any hosts until the DNS query has
101-
returned its results.
129+
A driver MUST verify that in addition to the ``{hostname}``, the
130+
``{domainname}`` consists of at least two parts: the domain name, and a TLD.
131+
Drivers MUST raise an error and MUST NOT contact the DNS server to obtain SRV
132+
(or TXT records) if the full URI does not consist of at least three parts.
133+
134+
If ``mongodb+srv`` is used, a driver MUST implicitly also enable TLS. Clients
135+
can turn this off by passing ``tls=false`` in either the Connection String,
136+
or options passed in as parameters in code to the MongoClient constructor (or
137+
equivalent API for each driver), but not through a TXT record (discussed in a
138+
later section).
139+
140+
141+
Querying DNS
142+
~~~~~~~~~~~~
143+
144+
In this preprocessing step, the driver will query the DNS server for SRV records
145+
on ``{hostname}.{domainname}``, prefixed with the SRV service name and protocol.
146+
The SRV service name is provided in the ``srvServiceName`` URI option and
147+
defaults to ``mongodb``. The protocol is always ``tcp``. After prefixing, the
148+
URI should look like: ``_{srvServiceName}._tcp.{hostname}.{domainname}``. This
149+
DNS query is expected to respond with one or more SRV records.
150+
151+
The priority and weight fields in returned SRV records MUST be ignored.
102152

103153
If the DNS result returns no SRV records, or no records at all, or a DNS error
104154
happens, an error MUST be raised indicating that the URI could not be used to
105155
find hostnames. The error SHALL include the reason why they could not be
106156
found.
107157

158+
A driver MUST verify that the host names returned through SRV records have the
159+
same parent ``{domainname}``. Drivers MUST raise an error and MUST NOT
160+
initiate a connection to any returned host name which does not share the same
161+
``{domainname}``.
162+
163+
The driver MUST NOT attempt to connect to any hosts until the DNS query has
164+
returned its results.
165+
166+
If ``srvMaxHosts`` is zero or greater than or equal to the number of hosts in
167+
the DNS result, the driver MUST populate the seedlist with all hosts.
168+
169+
If ``srvMaxHosts`` is greater than zero and less than the number of hosts in the
170+
DNS result, the driver MUST randomly select that many hosts and use them to
171+
populate the seedlist. Drivers SHOULD use the `Fisher-Yates shuffle`_ for
172+
randomization.
173+
174+
.. _`Fisher-Yates shuffle`: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm
175+
176+
108177
Default Connection String Options
109178
---------------------------------
110179

@@ -122,8 +191,8 @@ raise an error when multiple TXT records are encountered.
122191
Information returned within a TXT record is a simple URI string, just like
123192
the ``{options}`` in a connection string.
124193

125-
A Client MUST only support the ``authSource`` and ``replicaSet`` options
126-
through a TXT record, and MUST raise an error if any other option is
194+
A Client MUST only support the ``authSource``, ``replicaSet``, and ``loadBalanced``
195+
options through a TXT record, and MUST raise an error if any other option is
127196
encountered. Although using ``mongodb+srv://`` implicitly enables TLS, a
128197
Client MUST NOT allow the ``ssl`` option to be set through a TXT record
129198
option.
@@ -274,6 +343,18 @@ SRV records.
274343
ChangeLog
275344
=========
276345

346+
2021-10-14 - 1.6.0
347+
Add ``srvMaxHosts`` MongoClient option and restructure Seedlist Discovery
348+
section. Improve documentation for the ``srvServiceName`` MongoClient
349+
option and add a new URI Validation section.
350+
351+
2021-09-15 - 1.5.0
352+
Clarify that service name only defaults to ``mongodb``, and should be
353+
defined by the ``srvServiceName`` URI option.
354+
355+
2021-04-15 - 1.4.0
356+
Adding in behaviour for load balancer mode.
357+
277358
2019-03-07 - 1.3.2
278359
Clarify that CNAME is not supported
279360

Tests/Specs/initial-dns-seedlist-discovery/tests/load-balanced/loadBalanced-directConnection.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
2-
"uri": "mongodb+srv://test20.test.build.10gen.cc/?directConnection=false",
2+
"uri": "mongodb+srv://test24.test.build.10gen.cc/?directConnection=false",
33
"seeds": [
4-
"localhost.test.build.10gen.cc:27017"
4+
"localhost.test.build.10gen.cc:8000"
55
],
66
"hosts": [
7-
"localhost.test.build.10gen.cc:27017"
7+
"localhost.test.build.10gen.cc:8000"
88
],
99
"options": {
1010
"loadBalanced": true,

Tests/Specs/initial-dns-seedlist-discovery/tests/load-balanced/loadBalanced-replicaSet-errors.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"uri": "mongodb+srv://test20.test.build.10gen.cc/?replicaSet=replset",
2+
"uri": "mongodb+srv://test24.test.build.10gen.cc/?replicaSet=replset",
33
"seeds": [],
44
"hosts": [],
55
"error": true,

Tests/Specs/initial-dns-seedlist-discovery/tests/load-balanced/loadBalanced-true-txt.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
2-
"uri": "mongodb+srv://test20.test.build.10gen.cc/",
2+
"uri": "mongodb+srv://test24.test.build.10gen.cc/",
33
"seeds": [
4-
"localhost.test.build.10gen.cc:27017"
4+
"localhost.test.build.10gen.cc:8000"
55
],
66
"hosts": [
7-
"localhost.test.build.10gen.cc:27017"
7+
"localhost.test.build.10gen.cc:8000"
88
],
99
"options": {
1010
"loadBalanced": true,

0 commit comments

Comments
 (0)