Skip to content

Commit 6546dfa

Browse files
SWIFT-1457 Implement server selection logic test runner (#730)
1 parent efe1cf5 commit 6546dfa

File tree

145 files changed

+6619
-169
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

145 files changed

+6619
-169
lines changed

Sources/MongoSwift/ReadPreference.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ public struct ReadPreference: Equatable {
193193
self.maxStalenessSeconds = nil
194194
}
195195

196-
internal init(_ mode: Mode, tagSets: [BSONDocument]?, maxStalenessSeconds: Int?) throws {
196+
internal init(_ mode: Mode, tagSets: [BSONDocument]? = nil, maxStalenessSeconds: Int? = nil) throws {
197197
if let maxStaleness = maxStalenessSeconds {
198198
guard maxStaleness >= MONGOC_SMALLEST_MAX_STALENESS_SECONDS else {
199199
throw MongoError.InvalidArgumentError(

Sources/MongoSwift/SDAM.swift

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ private struct HelloResponse: Decodable {
6767
/// A struct describing a mongod or mongos process.
6868
public struct ServerDescription {
6969
/// The possible types for a server.
70-
public struct ServerType: RawRepresentable, Equatable {
70+
public struct ServerType: RawRepresentable, Equatable, Decodable {
7171
/// A standalone mongod server.
7272
public static let standalone = ServerType(.standalone)
7373

@@ -225,9 +225,12 @@ public struct ServerDescription {
225225
}
226226

227227
// For testing purposes
228-
internal init(type: ServerType, tags: [String: String]? = nil) {
228+
internal init(address: ServerAddress, type: ServerType, tags: [String: String]?) {
229+
self.address = address
229230
self.type = type
230-
self.address = ServerAddress(host: "fake", port: 80)
231+
self.tags = tags ?? [:]
232+
233+
// these fields are not used by the server selection tests
231234
self.serverId = 0
232235
self.roundTripTime = 0
233236
self.lastUpdateTime = Date()
@@ -243,7 +246,6 @@ public struct ServerDescription {
243246
self.hosts = []
244247
self.passives = []
245248
self.arbiters = []
246-
self.tags = tags ?? [:]
247249
}
248250
}
249251

@@ -271,7 +273,7 @@ extension ServerDescription: Equatable {
271273
/// which servers are up, what type of servers they are, which is primary, and so on.
272274
public struct TopologyDescription: Equatable {
273275
/// The possible types for a topology.
274-
public struct TopologyType: RawRepresentable, Equatable, CustomStringConvertible {
276+
public struct TopologyType: RawRepresentable, Equatable, CustomStringConvertible, Decodable {
275277
/// A single mongod server.
276278
public static let single = TopologyType(.single)
277279

@@ -402,7 +404,7 @@ extension TopologyDescription {
402404
return self.filterReplicaSetServers(readPreference: readPreference, servers: secondariesAndPrimary)
403405
case .secondaryPreferred:
404406
// If mode is 'secondaryPreferred', attempt the selection algorithm with mode 'secondary' and the
405-
// user's maxStalenessSeconds and tag_sets.If no server matches, select the primary.
407+
// user's maxStalenessSeconds and tag_sets. If no server matches, select the primary.
406408
let secondaries = self.servers.filter { $0.type == .rsSecondary }
407409
let primaries = self.servers.filter { $0.type == .rsPrimary }
408410
let matches = self.filterReplicaSetServers(readPreference: readPreference, servers: secondaries)
@@ -433,8 +435,7 @@ extension TopologyDescription {
433435
// TODO: Filter out servers staler than maxStalenessSeconds
434436

435437
// Filter by tag_sets
436-
let tagSets = readPreference?.tagSets ?? []
437-
if tagSets.isEmpty {
438+
guard let tagSets = readPreference?.tagSets else {
438439
return servers
439440
}
440441
for tagSet in tagSets {

Tests/MongoSwiftSyncTests/SpecTestRunner/CodableExtensions.swift renamed to Sources/TestsCommon/CodableExtensions.swift

Lines changed: 76 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
@testable import struct MongoSwift.ReadPreference
2-
import MongoSwiftSync
3-
import TestsCommon
1+
@testable import MongoSwift
42

53
/// Allows a type to specify a set of known keys and check whether any unknown top-level keys are found in a decoder.
64
internal protocol StrictDecodable: Decodable {
@@ -110,39 +108,6 @@ extension TransactionOptions: StrictDecodable {
110108
}
111109
}
112110

113-
extension ReadPreference.Mode: Decodable {
114-
public init(from decoder: Decoder) throws {
115-
let container = try decoder.singleValueContainer()
116-
var string = try container.decode(String.self)
117-
// spec tests capitalize first letter of mode, so need to account for that.
118-
string = string.prefix(1).lowercased() + string.dropFirst()
119-
guard let mode = Self(rawValue: string) else {
120-
throw DecodingError.dataCorruptedError(
121-
in: container,
122-
debugDescription: "can't parse ReadPreference mode from \(string)"
123-
)
124-
}
125-
self = mode
126-
}
127-
}
128-
129-
extension ReadPreference: Decodable {
130-
private enum CodingKeys: String, CodingKey {
131-
case mode
132-
}
133-
134-
public init(from decoder: Decoder) throws {
135-
if let container = try? decoder.container(keyedBy: CodingKeys.self) {
136-
let mode = try container.decode(Mode.self, forKey: .mode)
137-
self.init(mode)
138-
} else { // sometimes the spec tests only specify the mode as a string
139-
let container = try decoder.singleValueContainer()
140-
let mode = try container.decode(ReadPreference.Mode.self)
141-
self.init(mode)
142-
}
143-
}
144-
}
145-
146111
extension MongoClientOptions: StrictDecodable {
147112
internal typealias CodingKeysType = CodingKeys
148113

@@ -174,3 +139,78 @@ extension MongoClientOptions: StrictDecodable {
174139
)
175140
}
176141
}
142+
143+
extension ReadPreference: StrictDecodable {
144+
internal typealias CodingKeysType = CodingKeys
145+
146+
public init(from decoder: Decoder) throws {
147+
if let container = try? decoder.container(keyedBy: CodingKeys.self) {
148+
try Self.checkKeys(using: decoder)
149+
let mode = try container.decode(Mode.self, forKey: .mode)
150+
let tagSets = try container.decodeIfPresent([BSONDocument].self, forKey: .tagSets)
151+
try self.init(mode, tagSets: tagSets)
152+
} else { // sometimes the spec tests only specify the mode as a string
153+
let container = try decoder.singleValueContainer()
154+
let mode = try container.decode(ReadPreference.Mode.self)
155+
self.init(mode)
156+
}
157+
}
158+
159+
internal enum CodingKeys: String, CodingKey, CaseIterable {
160+
case mode, tagSets = "tag_sets"
161+
}
162+
}
163+
164+
extension ReadPreference.Mode: Decodable {
165+
public init(from decoder: Decoder) throws {
166+
let container = try decoder.singleValueContainer()
167+
var string = try container.decode(String.self)
168+
// spec tests capitalize first letter of mode, so need to account for that.
169+
string = string.prefix(1).lowercased() + string.dropFirst()
170+
guard let mode = Self(rawValue: string) else {
171+
throw DecodingError.dataCorruptedError(
172+
in: container,
173+
debugDescription: "can't parse ReadPreference mode from \(string)"
174+
)
175+
}
176+
self = mode
177+
}
178+
}
179+
180+
extension TopologyDescription: StrictDecodable {
181+
internal typealias CodingKeysType = CodingKeys
182+
183+
public init(from decoder: Decoder) throws {
184+
try Self.checkKeys(using: decoder)
185+
186+
let values = try decoder.container(keyedBy: CodingKeys.self)
187+
let type = try values.decode(TopologyDescription.TopologyType.self, forKey: .type)
188+
let servers = try values.decode([ServerDescription].self, forKey: .servers)
189+
190+
self.init(type: type, servers: servers)
191+
}
192+
193+
internal enum CodingKeys: String, CodingKey, CaseIterable {
194+
case type, servers
195+
}
196+
}
197+
198+
extension ServerDescription: StrictDecodable {
199+
internal typealias CodingKeysType = CodingKeys
200+
201+
public init(from decoder: Decoder) throws {
202+
try Self.checkKeys(using: decoder)
203+
204+
let values = try decoder.container(keyedBy: CodingKeys.self)
205+
let address = try ServerAddress(try values.decode(String.self, forKey: .address))
206+
let type = try values.decode(ServerType.self, forKey: .type)
207+
let tags = try values.decodeIfPresent([String: String].self, forKey: .tags) ?? [:]
208+
// TODO: SWIFT-1461: decode and set averageRoundTripTimeMS
209+
210+
self.init(address: address, type: type, tags: tags)
211+
}
212+
213+
internal enum CodingKeys: String, CodingKey, CaseIterable {
214+
case address, type, tags, averageRoundTripTimeMS = "avg_rtt_ms"
215+
}
216+
}

Sources/TestsCommon/SpecTestUtils.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public func retrieveSpecTestFiles<T: Decodable>(
2929
path += "/\(sd)"
3030
}
3131
return try FileManager.default
32-
.contentsOfDirectory(atPath: path)
32+
.subpathsOfDirectory(atPath: path)
3333
.filter { $0.hasSuffix(".json") }
3434
.compactMap { filename in
3535
guard !excludeFiles.contains(filename) else {

Tests/MongoSwiftTests/ServerSelectionTests.swift

Lines changed: 28 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -5,132 +5,37 @@ import NIO
55
import TestsCommon
66
import XCTest
77

8-
final class ServerSelectionTests: MongoSwiftTestCase {
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() {
27-
let unkownTopology = TopologyDescription(type: .unknown, servers: [standaloneServer])
28-
expect(unkownTopology.findSuitableServers()).to(haveCount(0))
29-
}
30-
31-
func testSingleTopology() {
32-
let singleTopology = TopologyDescription(type: .single, servers: [standaloneServer])
33-
expect(singleTopology.findSuitableServers()[0].type).to(equal(.standalone))
34-
}
35-
36-
func testReplicaSetWithPrimaryTopology() {
37-
let replicaSetTopology = TopologyDescription(type: .replicaSetWithPrimary, servers: [
38-
rsPrimaryServer,
39-
rsSecondaryServer1,
40-
rsSecondaryServer2
41-
])
42-
let replicaSetSuitableServers = replicaSetTopology
43-
.findSuitableServers(readPreference: self.primaryReadPreference)
44-
expect(replicaSetSuitableServers[0].type).to(equal(.rsPrimary))
45-
expect(replicaSetSuitableServers).to(haveCount(1))
46-
47-
let replicaSetSuitableServers2 = replicaSetTopology
48-
.findSuitableServers(readPreference: self.primaryPrefReadPreferemce)
49-
expect(replicaSetSuitableServers2[0].type).to(equal(.rsPrimary))
50-
expect(replicaSetSuitableServers2).to(haveCount(1))
51-
}
52-
53-
func testReplicaSetNoPrimaryTopology() {
54-
let replicaSetNoPrimaryTopology = TopologyDescription(type: .replicaSetNoPrimary, servers: [
55-
rsSecondaryServer1,
56-
rsSecondaryServer2
57-
])
58-
let suitable1 = replicaSetNoPrimaryTopology
59-
.findSuitableServers(readPreference: self.primaryReadPreference)
60-
expect(suitable1).to(haveCount(0))
61-
62-
let suitable2 = replicaSetNoPrimaryTopology
63-
.findSuitableServers(readPreference: nil)
64-
expect(suitable2).to(haveCount(0))
65-
66-
let suitable3 = replicaSetNoPrimaryTopology
67-
.findSuitableServers(readPreference: self.primaryPrefReadPreferemce)
68-
expect(suitable3[0].type).to(equal(.rsSecondary))
69-
expect(suitable3).to(haveCount(2))
8+
private struct ServerSelectionLogicTestFile: Decodable {
9+
let topologyDescription: TopologyDescription
10+
let operation: OperationType
11+
let readPreference: ReadPreference
12+
let suitableServers: [ServerDescription]
13+
let inLatencyWindow: [ServerDescription]
14+
15+
enum CodingKeys: String, CodingKey {
16+
case topologyDescription = "topology_description", operation, readPreference = "read_preference",
17+
suitableServers = "suitable_servers", inLatencyWindow = "in_latency_window"
7018
}
19+
}
7120

72-
func testShardedTopology() {
73-
let shardedTopology = TopologyDescription(type: .sharded, servers: [
74-
mongosServer
75-
])
76-
let shardedSuitableServers = shardedTopology.findSuitableServers()
77-
expect(shardedSuitableServers[0].type)
78-
.to(equal(.mongos))
79-
expect(shardedSuitableServers).to(haveCount(1))
80-
}
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 = [:]
21+
private enum OperationType: String, Decodable {
22+
case read, write
23+
}
12524

126-
let primaryReadPreferenceWithEmptyTagSet = try ReadPreference(
127-
.primary,
128-
tagSets: [emptyTagSet],
129-
maxStalenessSeconds: nil
25+
final class ServerSelectionTests: MongoSwiftTestCase {
26+
func testServerSelectionLogic() throws {
27+
let tests = try retrieveSpecTestFiles(
28+
specName: "server-selection",
29+
subdirectory: "server_selection",
30+
asType: ServerSelectionLogicTestFile.self
13031
)
131-
let replicaSetSuitableServers = replicaSetTopology
132-
.findSuitableServers(readPreference: primaryReadPreferenceWithEmptyTagSet)
133-
expect(replicaSetSuitableServers[0].type).to(equal(.rsPrimary))
134-
expect(replicaSetSuitableServers).to(haveCount(1))
32+
for (filename, test) in tests {
33+
print("Running test from \(filename)...")
34+
// Server selection assumes that no read preference is passed for write operations.
35+
let readPreference = test.operation == .read ? test.readPreference : nil
36+
let selectedServers = test.topologyDescription.findSuitableServers(readPreference: readPreference)
37+
expect(selectedServers.count).to(equal(test.suitableServers.count))
38+
expect(selectedServers).to(contain(test.suitableServers))
39+
}
13540
}
13641
}

0 commit comments

Comments
 (0)