Skip to content

Commit 29df829

Browse files
authored
SWIFT-1326 Run load balancer spec tests (#669)
1 parent a9e5592 commit 29df829

15 files changed

+230
-20
lines changed

Sources/MongoSwift/Operations/ListCollectionsOperation.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public struct CollectionSpecification: Codable {
7272
}
7373

7474
/// Options to use when executing a `listCollections` command on a `MongoDatabase`.
75-
public struct ListCollectionsOptions: Encodable {
75+
public struct ListCollectionsOptions: Codable {
7676
/// The batchSize for the returned cursor.
7777
public var batchSize: Int?
7878

Sources/TestsCommon/APMUtils.swift

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public class TestCommandMonitor: CommandEventHandler {
3131

3232
/// Retrieve all the events seen so far that match the optionally provided filters, clearing the event cache.
3333
public func events(
34-
withEventTypes typeFilter: [CommandEvent.EventType]? = nil,
34+
withEventTypes typeFilter: [EventType]? = nil,
3535
withNames nameFilter: [String]? = nil
3636
) -> [CommandEvent] {
3737
defer { self.events.removeAll() }
@@ -58,22 +58,22 @@ public class TestCommandMonitor: CommandEventHandler {
5858
}
5959
}
6060

61-
extension CommandEvent {
62-
public enum EventType: String, Decodable {
63-
case commandStarted = "commandStartedEvent"
64-
case commandSucceeded = "commandSucceededEvent"
65-
case commandFailed = "commandFailedEvent"
66-
}
61+
public enum EventType: String, Decodable {
62+
case commandStartedEvent, commandSucceededEvent, commandFailedEvent,
63+
connectionCreatedEvent, connectionReadyEvent, connectionClosedEvent,
64+
connectionCheckedInEvent, connectionCheckedOutEvent, connectionCheckOutFailedEvent,
65+
poolCreatedEvent, poolReadyEvent, poolClearedEvent, poolClosedEvent
66+
}
6767

68-
/// The "type" of this event. Used for filtering events by their type.
68+
extension CommandEvent {
6969
public var type: EventType {
7070
switch self {
7171
case .started:
72-
return .commandStarted
72+
return .commandStartedEvent
7373
case .failed:
74-
return .commandFailed
74+
return .commandFailedEvent
7575
case .succeeded:
76-
return .commandSucceeded
76+
return .commandSucceededEvent
7777
}
7878
}
7979

Tests/LinuxMain.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@ extension EventLoopBoundMongoClientTests {
111111
]
112112
}
113113

114+
extension LoadBalancerTests {
115+
static var allTests = [
116+
("testLoadBalancers", testLoadBalancers),
117+
]
118+
}
119+
114120
extension MongoClientTests {
115121
static var allTests = [
116122
("testUsingClosedClient", testUsingClosedClient),
@@ -397,6 +403,7 @@ XCTMain([
397403
testCase(CrudTests.allTests),
398404
testCase(DNSSeedlistTests.allTests),
399405
testCase(EventLoopBoundMongoClientTests.allTests),
406+
testCase(LoadBalancerTests.allTests),
400407
testCase(MongoClientTests.allTests),
401408
testCase(MongoCollectionTests.allTests),
402409
testCase(MongoCollection_BulkWriteTests.allTests),
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import MongoSwiftSync
2+
import Nimble
3+
import TestsCommon
4+
5+
final class LoadBalancerTests: MongoSwiftTestCase {
6+
let skipFiles: [String] = [
7+
// We don't support this option.
8+
"wait-queue-timeouts.json"
9+
]
10+
11+
func testLoadBalancers() throws {
12+
let tests = try retrieveSpecTestFiles(
13+
specName: "load-balancers",
14+
excludeFiles: skipFiles,
15+
asType: UnifiedTestFile.self
16+
).map { $0.1 }
17+
18+
let skipTests = [
19+
// The sessions spec requires that sessions can only be used with the MongoClient that created them.
20+
// Consequently, libmongoc enforces that a `mongoc_client_session_t` may only be used with the
21+
// `mongoc_client_t` that created it. In Swift, this translates to a requirement that we always pin
22+
// `Connection`s to `ClientSession`s from the time the session is first used until it is closed/
23+
// deinitialized. Since all of these tests use a session entity that is created before the tests are run
24+
// and closed after they complete, the session always has the connection pinned to it, and we never release
25+
// the connection as these tests expect.
26+
"transactions are correctly pinned to connections for load-balanced clusters": [
27+
"pinned connection is released after a non-transient abort error",
28+
"pinned connection is released after a transient non-network CRUD error",
29+
"pinned connection is released after a transient network CRUD error",
30+
"pinned connection is released after a transient non-network commit error",
31+
"pinned connection is released after a transient network commit error",
32+
"pinned connection is released after a transient non-network abort error",
33+
"pinned connection is released after a transient network abort error",
34+
"pinned connection is released on successful abort",
35+
"pinned connection is returned when a new transaction is started",
36+
"pinned connection is returned when a non-transaction operation uses the session",
37+
"a connection can be shared by a transaction and a cursor"
38+
],
39+
"cursors are correctly pinned to connections for load-balanced clusters": [
40+
// This test assumes that we release a cursor's pinned connection as soon as the cursor is exhausted
41+
// server-side, regardless of if it has been fully iterated. However, we do not release connections
42+
// until the first iteration attempt after the last document in the cursor.
43+
"no connection is pinned if all documents are returned in the initial batch",
44+
// This test assumes that we release a cursor's pinned connection after the last document is returned.
45+
// However, currently there is no way for us to reliably tell a libmongoc cursor is at its end without
46+
// attempting to iterate it first. To avoid having to implement some sort of caching mechanism we do
47+
// not close cursors until we attempt to iterate and get nil back, so this cursor is not closed/its
48+
// connection is not released after 3 iterations, as the test expects.
49+
"pinned connections are returned when the cursor is drained",
50+
// These tests assume we do not automatically close the cursor when we encounter such errors, however
51+
// we close cursors on all errors besides decoding errors, so the connections do get returned.
52+
"pinned connections are not returned after an network error during getMore",
53+
"pinned connections are not returned to the pool after a non-network error on getMore",
54+
// TODO: SWIFT-1322 Unskip.
55+
"listCollections pins the cursor to a connection",
56+
// Skipping as we do not support a batchSize for listIndexes. We closed SWIFT-1325 as "won't fix", but
57+
// should we ever decide to do it we could unskip this test then.
58+
"listIndexes pins the cursor to a connection"
59+
]
60+
]
61+
62+
let runner = try UnifiedTestRunner()
63+
try runner.runFiles(tests, skipTests: skipTests)
64+
}
65+
}

Tests/MongoSwiftSyncTests/SpecTestRunner/CodableExtensions.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ extension MongoClientOptions: StrictDecodable {
142142
internal typealias CodingKeysType = CodingKeys
143143

144144
internal enum CodingKeys: String, CodingKey, CaseIterable {
145-
case retryReads, retryWrites, w, readConcernLevel, readPreference, heartbeatFrequencyMS
145+
case retryReads, retryWrites, w, readConcernLevel, readPreference, heartbeatFrequencyMS, loadBalanced, appname
146146
}
147147

148148
public init(from decoder: Decoder) throws {
@@ -155,8 +155,12 @@ extension MongoClientOptions: StrictDecodable {
155155
let retryWrites = try container.decodeIfPresent(Bool.self, forKey: .retryWrites)
156156
let writeConcern = try? WriteConcern(w: container.decode(WriteConcern.W.self, forKey: .w))
157157
let heartbeatFrequencyMS = try container.decodeIfPresent(Int.self, forKey: .heartbeatFrequencyMS)
158+
let loadBalanced = try container.decodeIfPresent(Bool.self, forKey: .loadBalanced)
159+
let appName = try container.decodeIfPresent(String.self, forKey: .appname)
158160
self.init(
161+
appName: appName,
159162
heartbeatFrequencyMS: heartbeatFrequencyMS,
163+
loadBalanced: loadBalanced,
160164
readConcern: readConcern,
161165
readPreference: readPreference,
162166
retryReads: retryReads,

Tests/MongoSwiftSyncTests/SyncChangeStreamTests.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,10 @@ final class SyncChangeStreamTests: MongoSwiftTestCase {
443443
return
444444
}
445445

446-
let events = try captureCommandEvents(eventTypes: [.commandStarted], commandNames: ["aggregate"]) { client in
446+
let events = try captureCommandEvents(
447+
eventTypes: [.commandStartedEvent],
448+
commandNames: ["aggregate"]
449+
) { client in
447450
try withTestNamespace(client: client) { _, coll in
448451
let options = ChangeStreamOptions(
449452
batchSize: 123,

Tests/MongoSwiftSyncTests/SyncTestUtils.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ extension MongoClient {
141141
/// Captures any command monitoring events filtered by type and name that are emitted during the execution of the
142142
/// provided closure. A client pre-configured for command monitoring is passed into the closure.
143143
internal func captureCommandEvents(
144-
eventTypes: [CommandEvent.EventType]? = nil,
144+
eventTypes: [EventType]? = nil,
145145
commandNames: [String]? = nil,
146146
f: (MongoClient) throws -> Void
147147
) throws -> [CommandEvent] {

Tests/MongoSwiftSyncTests/UnifiedTestRunner/EntityDescription.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ enum EntityDescription: Decodable {
3232

3333
/// Types of events that can be observed for this client. Unspecified event types MUST be ignored by this
3434
/// client's event listeners.
35-
let observeEvents: [CommandEvent.EventType]?
35+
let observeEvents: [EventType]?
3636

3737
/// Command names for which the test runner MUST ignore any observed command monitoring events. The command(s)
3838
/// will be ignored in addition to configureFailPoint and any commands containing sensitive information (per
@@ -166,10 +166,10 @@ struct UnifiedTestClient {
166166
class UnifiedTestCommandMonitor: CommandEventHandler {
167167
private var monitoring: Bool
168168
var events: [CommandEvent]
169-
private let observeEvents: [CommandEvent.EventType]
169+
private let observeEvents: [EventType]
170170
private let ignoreEvents: [String]
171171

172-
public init(observeEvents: [CommandEvent.EventType]?, ignoreEvents: [String]?) {
172+
public init(observeEvents: [EventType]?, ignoreEvents: [String]?) {
173173
self.events = []
174174
self.monitoring = false
175175
self.observeEvents = observeEvents ?? []

Tests/MongoSwiftSyncTests/UnifiedTestRunner/ExpectedEventsForClient.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,35 @@ struct ExpectedEventsForClient: Decodable {
66
/// Client entity on which the events are expected to be observed.
77
let client: String
88

9+
enum EventType: String, Decodable {
10+
case command, cmap
11+
}
12+
13+
/// The type of events to be observed.
14+
let eventType: EventType
15+
916
/// List of events, which are expected to be observed (in this order) on the corresponding client while executing
1017
/// operations. If the array is empty, the test runner MUST assert that no events were observed on the client
1118
/// (excluding ignored events).
1219
let events: [ExpectedEvent]
20+
21+
enum CodingKeys: String, CodingKey {
22+
case client, eventType, events
23+
}
24+
25+
init(from decoder: Decoder) throws {
26+
let container = try decoder.container(keyedBy: CodingKeys.self)
27+
self.client = try container.decode(String.self, forKey: .client)
28+
// Defaults to command if omitted.
29+
self.eventType = try container.decodeIfPresent(EventType.self, forKey: .eventType) ?? .command
30+
switch self.eventType {
31+
case .command:
32+
self.events = try container.decode([ExpectedEvent].self, forKey: .events)
33+
case .cmap:
34+
// TODO: SWIFT-1321 actually parse these out.
35+
self.events = []
36+
}
37+
}
1338
}
1439

1540
/// Describes expected events.

Tests/MongoSwiftSyncTests/UnifiedTestRunner/HasListableProperties.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ extension UpdateModelOptions: HasListableProperties {}
2323
extension UpdateOptions: HasListableProperties {}
2424
extension ReplaceOneModelOptions: HasListableProperties {}
2525
extension ChangeStreamOptions: HasListableProperties {}
26+
extension ListCollectionsOptions: HasListableProperties {}

0 commit comments

Comments
 (0)