Skip to content

Commit 79c9683

Browse files
authored
SWIFT-1447 Add filtering by tag sets to finding suitable servers (#727)
1 parent 8f5e116 commit 79c9683

File tree

4 files changed

+122
-33
lines changed

4 files changed

+122
-33
lines changed

Sources/MongoSwift/MongoConnectionString.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public struct MongoConnectionString: Codable, LosslessStringConvertible {
2020
/// Characters that do not need to be percent-encoded when reconstructing URI options.
2121
fileprivate static let allowedForOptionEncoding = CharacterSet(charactersIn: "=&,:").inverted
2222

23-
fileprivate enum OptionName: String {
23+
internal enum OptionName: String {
2424
case appName = "appname"
2525
case authSource = "authsource"
2626
case authMechanism = "authmechanism"

Sources/MongoSwift/ReadPreference.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,16 @@ public struct ReadPreference: Equatable {
202202
)
203203
}
204204
}
205+
if let tagSets = tagSets {
206+
if mode == .primary {
207+
guard tagSets == [BSONDocument()] else {
208+
throw MongoError.InvalidArgumentError(
209+
message: "Invalid \(MongoConnectionString.OptionName.readPreferenceTags) \(tagSets): " +
210+
"when mode is primary, tag_sets must be empty"
211+
)
212+
}
213+
}
214+
}
205215
self.mode = mode
206216
self.tagSets = tagSets
207217
self.maxStalenessSeconds = maxStalenessSeconds

Sources/MongoSwift/SDAM.swift

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ public struct ServerDescription {
225225
}
226226

227227
// For testing purposes
228-
internal init(type: ServerType) {
228+
internal init(type: ServerType, tags: [String: String]? = nil) {
229229
self.type = type
230230
self.address = ServerAddress(host: "fake", port: 80)
231231
self.serverId = 0
@@ -243,7 +243,7 @@ public struct ServerDescription {
243243
self.hosts = []
244244
self.passives = []
245245
self.arbiters = []
246-
self.tags = [:]
246+
self.tags = tags ?? [:]
247247
}
248248
}
249249

@@ -417,7 +417,7 @@ extension TopologyDescription {
417417
}
418418
let secondaries = self.servers.filter { $0.type == .rsSecondary }
419419
return self.filterReplicaSetServers(readPreference: readPreference, servers: secondaries)
420-
default:
420+
default: // or .primary
421421
// the default mode is 'primary'.
422422
return self.servers.filter { $0.type == .rsPrimary }
423423
}
@@ -427,12 +427,22 @@ extension TopologyDescription {
427427
}
428428

429429
internal func filterReplicaSetServers(
430-
readPreference _: ReadPreference?,
430+
readPreference: ReadPreference?,
431431
servers: [ServerDescription]
432432
) -> [ServerDescription] {
433433
// TODO: Filter out servers staler than maxStalenessSeconds
434-
// TODO: Select servers matching the tag_sets
435-
// While waiting for the above to be implemented, this helper just returns the servers it was passed
436-
servers
434+
435+
// Filter by tag_sets
436+
let tagSets = readPreference?.tagSets ?? []
437+
if tagSets.isEmpty {
438+
return servers
439+
}
440+
for tagSet in tagSets {
441+
let matches = servers.filter { server in tagSet.allSatisfy { server.tags[$0.key] == $0.value.stringValue } }
442+
if !matches.isEmpty {
443+
return matches
444+
}
445+
}
446+
return []
437447
}
438448
}

Tests/MongoSwiftTests/ServerSelectionTests.swift

Lines changed: 94 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,56 +6,70 @@ import TestsCommon
66
import XCTest
77

88
final class ServerSelectionTests: MongoSwiftTestCase {
9-
func testServerSelection() throws {
10-
let standaloneServer = ServerDescription(type: .standalone)
11-
let rsPrimaryServer = ServerDescription(type: .rsPrimary)
12-
let rsSecondaryServer1 = ServerDescription(type: .rsSecondary)
13-
let rsSecondaryServer2 = ServerDescription(type: .rsSecondary)
14-
let mongosServer = ServerDescription(type: .mongos)
15-
16-
// unknown
9+
// Servers
10+
let standaloneServer = ServerDescription(type: .standalone)
11+
let rsPrimaryServer = ServerDescription(type: .rsPrimary)
12+
let rsSecondaryServer1 = ServerDescription(type: .rsSecondary, tags: ["dc": "ny", "rack": "2", "size": "large"])
13+
let rsSecondaryServer2 = ServerDescription(type: .rsSecondary)
14+
let rsSecondaryServer3 = ServerDescription(type: .rsSecondary, tags: ["dc": "ny", "rack": "3", "size": "small"])
15+
let mongosServer = ServerDescription(type: .mongos)
16+
17+
// Read Preferences
18+
let primaryReadPreference = ReadPreference(.primary)
19+
let primaryPrefReadPreferemce = ReadPreference(.primaryPreferred)
20+
21+
// Tag Sets
22+
let tagSet: BSONDocument = ["dc": "ny", "rack": "2"]
23+
let tagSet2: BSONDocument = ["dc": "ny"]
24+
let tagSet3: BSONDocument = ["size": "small"]
25+
26+
func testUnknownTopology() {
1727
let unkownTopology = TopologyDescription(type: .unknown, servers: [standaloneServer])
1828
expect(unkownTopology.findSuitableServers()).to(haveCount(0))
29+
}
1930

20-
// single
31+
func testSingleTopology() {
2132
let singleTopology = TopologyDescription(type: .single, servers: [standaloneServer])
2233
expect(singleTopology.findSuitableServers()[0].type).to(equal(.standalone))
34+
}
2335

24-
// replica set with primary
36+
func testReplicaSetWithPrimaryTopology() {
2537
let replicaSetTopology = TopologyDescription(type: .replicaSetWithPrimary, servers: [
2638
rsPrimaryServer,
2739
rsSecondaryServer1,
2840
rsSecondaryServer2
2941
])
30-
let primaryReadPreference = ReadPreference(.primary)
31-
let replicaSetSuitableServers = replicaSetTopology.findSuitableServers(readPreference: primaryReadPreference)
42+
let replicaSetSuitableServers = replicaSetTopology
43+
.findSuitableServers(readPreference: self.primaryReadPreference)
3244
expect(replicaSetSuitableServers[0].type).to(equal(.rsPrimary))
3345
expect(replicaSetSuitableServers).to(haveCount(1))
3446

35-
let primaryPrefReadPreferemce = ReadPreference(.primaryPreferred)
3647
let replicaSetSuitableServers2 = replicaSetTopology
37-
.findSuitableServers(readPreference: primaryPrefReadPreferemce)
48+
.findSuitableServers(readPreference: self.primaryPrefReadPreferemce)
3849
expect(replicaSetSuitableServers2[0].type).to(equal(.rsPrimary))
3950
expect(replicaSetSuitableServers2).to(haveCount(1))
51+
}
4052

41-
// replica set without primary
53+
func testReplicaSetNoPrimaryTopology() {
4254
let replicaSetNoPrimaryTopology = TopologyDescription(type: .replicaSetNoPrimary, servers: [
4355
rsSecondaryServer1,
4456
rsSecondaryServer2
4557
])
46-
let replicaSetNoPrimarySuitableServers = replicaSetNoPrimaryTopology
47-
.findSuitableServers(readPreference: primaryReadPreference)
48-
expect(replicaSetNoPrimarySuitableServers).to(haveCount(0))
58+
let suitable1 = replicaSetNoPrimaryTopology
59+
.findSuitableServers(readPreference: self.primaryReadPreference)
60+
expect(suitable1).to(haveCount(0))
4961

50-
let replicaSetNoPrimarySuitableServer2 = replicaSetNoPrimaryTopology.findSuitableServers(readPreference: nil)
51-
expect(replicaSetNoPrimarySuitableServer2).to(haveCount(0))
62+
let suitable2 = replicaSetNoPrimaryTopology
63+
.findSuitableServers(readPreference: nil)
64+
expect(suitable2).to(haveCount(0))
5265

53-
let replicaSetNoPrimarySuitableServer3 = replicaSetNoPrimaryTopology
54-
.findSuitableServers(readPreference: primaryPrefReadPreferemce)
55-
expect(replicaSetNoPrimarySuitableServer3[0].type).to(equal(.rsSecondary))
56-
expect(replicaSetNoPrimarySuitableServer3).to(haveCount(2))
66+
let suitable3 = replicaSetNoPrimaryTopology
67+
.findSuitableServers(readPreference: self.primaryPrefReadPreferemce)
68+
expect(suitable3[0].type).to(equal(.rsSecondary))
69+
expect(suitable3).to(haveCount(2))
70+
}
5771

58-
// sharded
72+
func testShardedTopology() {
5973
let shardedTopology = TopologyDescription(type: .sharded, servers: [
6074
mongosServer
6175
])
@@ -64,4 +78,59 @@ final class ServerSelectionTests: MongoSwiftTestCase {
6478
.to(equal(.mongos))
6579
expect(shardedSuitableServers).to(haveCount(1))
6680
}
81+
82+
func testTagSets() throws {
83+
// tag set 1
84+
let topology = TopologyDescription(type: .replicaSetNoPrimary, servers: [
85+
rsSecondaryServer1,
86+
rsSecondaryServer2,
87+
rsSecondaryServer3
88+
])
89+
let secondaryReadPreferenceWithTagSet = try ReadPreference(
90+
.secondaryPreferred,
91+
tagSets: [tagSet, tagSet3], // tagSet3 should be ignored, because tagSet matches some servers
92+
maxStalenessSeconds: nil
93+
)
94+
95+
let suitable = topology.findSuitableServers(readPreference: secondaryReadPreferenceWithTagSet)
96+
expect(suitable[0].type).to(equal(.rsSecondary))
97+
expect(suitable).to(haveCount(1))
98+
99+
// tag set 2
100+
let secondaryReadPreferenceWithTagSet2 = try ReadPreference(
101+
.secondaryPreferred,
102+
tagSets: [tagSet2],
103+
maxStalenessSeconds: nil
104+
)
105+
106+
let suitable2 = topology.findSuitableServers(readPreference: secondaryReadPreferenceWithTagSet2)
107+
expect(suitable2[0].type).to(equal(.rsSecondary))
108+
expect(suitable2).to(haveCount(2))
109+
110+
// invalid tag set passing
111+
expect(try ReadPreference(
112+
.primary,
113+
tagSets: [self.tagSet],
114+
maxStalenessSeconds: nil
115+
)).to(throwError(errorType: MongoError.InvalidArgumentError.self))
116+
117+
// valid tag set passing
118+
let replicaSetTopology = TopologyDescription(type: .replicaSetWithPrimary, servers: [
119+
rsPrimaryServer,
120+
rsSecondaryServer1,
121+
rsSecondaryServer2
122+
])
123+
124+
let emptyTagSet: BSONDocument = [:]
125+
126+
let primaryReadPreferenceWithEmptyTagSet = try ReadPreference(
127+
.primary,
128+
tagSets: [emptyTagSet],
129+
maxStalenessSeconds: nil
130+
)
131+
let replicaSetSuitableServers = replicaSetTopology
132+
.findSuitableServers(readPreference: primaryReadPreferenceWithEmptyTagSet)
133+
expect(replicaSetSuitableServers[0].type).to(equal(.rsPrimary))
134+
expect(replicaSetSuitableServers).to(haveCount(1))
135+
}
67136
}

0 commit comments

Comments
 (0)