Skip to content

Commit 1227101

Browse files
authored
SWIFT-1102 Support snapshot reads on secondaries (#689)
1 parent 4ae3b6d commit 1227101

15 files changed

+2755
-19
lines changed

Sources/MongoSwift/Operations/StartSessionOperation.swift

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@ import Foundation
33

44
/// Options to use when creating a `ClientSession`.
55
public struct ClientSessionOptions {
6-
/// Whether to enable causal consistency for this session. By default, causal consistency is enabled.
7-
///
8-
/// - SeeAlso: https://docs.mongodb.com/manual/core/read-isolation-consistency-recency/
6+
/**
7+
* Whether to enable causal consistency for this session. By default, if `snapshot` is not set to true, then
8+
* causal consistency is enabled. Note that setting this option to true is incompatible with setting `snapshot` to
9+
* true.
10+
*
11+
* - SeeAlso: https://docs.mongodb.com/manual/core/read-isolation-consistency-recency/
12+
*/
913
public var causalConsistency: Bool?
1014

1115
/// The default `TransactionOptions` to use for transactions started on this session.
@@ -16,10 +20,24 @@ public struct ClientSessionOptions {
1620
/// applicable (e.g. write concern).
1721
public var defaultTransactionOptions: TransactionOptions?
1822

23+
/**
24+
* If true, then all reads performed using this session will read from the same snapshot. This option defaults to
25+
* false.
26+
*
27+
* Note that setting this option to true is incompatible with setting `causalConsistency` to true.
28+
* - SeeAlso: https://docs.mongodb.com/manual/reference/read-concern-snapshot/
29+
*/
30+
public var snapshot: Bool?
31+
1932
/// Convenience initializer allowing any/all parameters to be omitted.
20-
public init(causalConsistency: Bool? = nil, defaultTransactionOptions: TransactionOptions? = nil) {
33+
public init(
34+
causalConsistency: Bool? = nil,
35+
defaultTransactionOptions: TransactionOptions? = nil,
36+
snapshot: Bool? = nil
37+
) {
2138
self.causalConsistency = causalConsistency
2239
self.defaultTransactionOptions = defaultTransactionOptions
40+
self.snapshot = snapshot
2341
}
2442
}
2543

@@ -28,11 +46,17 @@ public struct ClientSessionOptions {
2846
private func withSessionOpts<T>(
2947
wrapping options: ClientSessionOptions?,
3048
_ body: (OpaquePointer) throws -> T
31-
) rethrows -> T {
49+
) throws -> T {
3250
// swiftlint:disable:next force_unwrapping
3351
let opts = mongoc_session_opts_new()! // always returns a value
3452
defer { mongoc_session_opts_destroy(opts) }
3553

54+
if options?.causalConsistency == true && options?.snapshot == true {
55+
throw MongoError.InvalidArgumentError(
56+
message: "Only one of causalConsistency and snapshot can be set to true."
57+
)
58+
}
59+
3660
if let causalConsistency = options?.causalConsistency {
3761
mongoc_session_opts_set_causal_consistency(opts, causalConsistency)
3862
}
@@ -41,6 +65,10 @@ private func withSessionOpts<T>(
4165
mongoc_session_opts_set_default_transaction_opts(opts, $0)
4266
}
4367

68+
if let snapshot = options?.snapshot {
69+
mongoc_session_opts_set_snapshot(opts, snapshot)
70+
}
71+
4472
return try body(opts)
4573
}
4674

Tests/LinuxMain.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,8 @@ extension SyncClientSessionTests {
360360
("testCausalConsistency", testCausalConsistency),
361361
("testCausalConsistencyStandalone", testCausalConsistencyStandalone),
362362
("testCausalConsistencyAnyTopology", testCausalConsistencyAnyTopology),
363+
("testSessionsUnified", testSessionsUnified),
364+
("testSnapshotSessionsProse", testSnapshotSessionsProse),
363365
]
364366
}
365367

Tests/MongoSwiftSyncTests/ClientSessionTests.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,4 +524,26 @@ final class SyncClientSessionTests: MongoSwiftTestCase {
524524
expect(session.operationTime).to(beNil())
525525
}
526526
}
527+
528+
func testSessionsUnified() throws {
529+
let tests = try retrieveSpecTestFiles(
530+
specName: "sessions",
531+
subdirectory: "unified",
532+
asType: UnifiedTestFile.self
533+
)
534+
let runner = try UnifiedTestRunner()
535+
try runner.runFiles(tests.map { $0.1 })
536+
}
537+
538+
func testSnapshotSessionsProse() throws {
539+
/// 1. Setting both snapshot and causalConsistency to true is not allowed
540+
try self.withTestNamespace { client, _, _ in
541+
let invalidOpts = ClientSessionOptions(causalConsistency: true, snapshot: true)
542+
let session = client.startSession(options: invalidOpts)
543+
// We don't actually start the underlying libmongoc session until the ClientSession is used, so we need to
544+
// try to do an operation to generate an error.
545+
expect(try client.listDatabaseNames(session: session))
546+
.to(throwError(errorType: MongoError.InvalidArgumentError.self))
547+
}
548+
}
527549
}

Tests/MongoSwiftSyncTests/SpecTestRunner/CodableExtensions.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,20 @@ extension ClientSessionOptions: StrictDecodable {
6969

7070
let container = try decoder.container(keyedBy: CodingKeys.self)
7171
let causalConsistency = try container.decodeIfPresent(Bool.self, forKey: .causalConsistency)
72+
let snapshot = try container.decodeIfPresent(Bool.self, forKey: .snapshot)
7273
let defaultTransactionOptions = try container.decodeIfPresent(
7374
TransactionOptions.self,
7475
forKey: .defaultTransactionOptions
7576
)
76-
self.init(causalConsistency: causalConsistency, defaultTransactionOptions: defaultTransactionOptions)
77+
self.init(
78+
causalConsistency: causalConsistency,
79+
defaultTransactionOptions: defaultTransactionOptions,
80+
snapshot: snapshot
81+
)
7782
}
7883

7984
internal enum CodingKeys: String, CodingKey, CaseIterable {
80-
case causalConsistency, defaultTransactionOptions
85+
case causalConsistency, defaultTransactionOptions, snapshot
8186
}
8287
}
8388

Tests/MongoSwiftSyncTests/UnifiedTestRunner/UnifiedClientOperations.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,19 @@ struct AssertNumberConnectionsCheckedOut: UnifiedOperationProtocol {
2525
}
2626

2727
struct UnifiedListDatabases: UnifiedOperationProtocol {
28-
static var knownArguments: Set<String> { [] }
28+
/// Optional identifier for a session entity to use.
29+
let session: String?
30+
31+
static var knownArguments: Set<String> { ["session"] }
32+
33+
init() {
34+
self.session = nil
35+
}
2936

3037
func execute(on object: UnifiedOperation.Object, context: Context) throws -> UnifiedOperationResult {
3138
let testClient = try context.entities.getEntity(from: object).asTestClient()
32-
let dbSpecs = try testClient.client.listDatabases()
39+
let session = try context.entities.resolveSession(id: self.session)
40+
let dbSpecs = try testClient.client.listDatabases(session: session)
3341
let encoded = try BSONEncoder().encode(dbSpecs)
3442
return .bson(.array(encoded.map { .document($0) }))
3543
}

Tests/MongoSwiftSyncTests/UnifiedTestRunner/UnifiedCollectionOperations.swift

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -565,13 +565,17 @@ struct UnifiedCountDocuments: UnifiedOperationProtocol {
565565
/// Filter for the query.
566566
let filter: BSONDocument
567567

568+
/// Optional identifier for a session entity to use.
569+
let session: String?
570+
568571
static var knownArguments: Set<String> {
569-
["filter"]
572+
["filter", "session"]
570573
}
571574

572575
func execute(on object: UnifiedOperation.Object, context: Context) throws -> UnifiedOperationResult {
573576
let collection = try context.entities.getEntity(from: object).asCollection()
574-
let result = try collection.countDocuments(filter)
577+
let session = try context.entities.resolveSession(id: self.session)
578+
let result = try collection.countDocuments(filter, session: session)
575579
return .bson(BSON(result))
576580
}
577581
}
@@ -605,13 +609,17 @@ struct UnifiedDistinct: UnifiedOperationProtocol {
605609
/// Filter for the query.
606610
let filter: BSONDocument
607611

612+
/// Optional identifier for a session entity to use.
613+
let session: String?
614+
608615
static var knownArguments: Set<String> {
609-
["fieldName", "filter"]
616+
["fieldName", "filter", "session"]
610617
}
611618

612619
func execute(on object: UnifiedOperation.Object, context: Context) throws -> UnifiedOperationResult {
613620
let collection = try context.entities.getEntity(from: object).asCollection()
614-
let result = try collection.distinct(fieldName: self.fieldName, filter: filter)
621+
let session = try context.entities.resolveSession(id: self.session)
622+
let result = try collection.distinct(fieldName: self.fieldName, filter: filter, session: session)
615623
return .bson(.array(result))
616624
}
617625
}

Tests/MongoSwiftSyncTests/UnifiedTestRunner/UnifiedDatabaseOperations.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,11 @@ struct UnifiedRunCommand: UnifiedOperationProtocol {
6161
/// The command to run.
6262
let command: BSONDocument
6363

64+
/// Optional identifier for a session entity to use.
65+
let session: String?
66+
6467
static var knownArguments: Set<String> {
65-
["commandName", "command"]
68+
["commandName", "command", "session"]
6669
}
6770

6871
func execute(on object: UnifiedOperation.Object, context: Context) throws -> UnifiedOperationResult {
@@ -76,14 +79,15 @@ struct UnifiedRunCommand: UnifiedOperationProtocol {
7679
}
7780
}
7881
let db = try context.entities.getEntity(from: object).asDatabase()
79-
try db.runCommand(orderedCommand)
82+
let session = try context.entities.resolveSession(id: self.session)
83+
try db.runCommand(orderedCommand, session: session)
8084
return .none
8185
}
8286
}
8387

8488
struct UnifiedListCollections: UnifiedOperationProtocol {
8589
/// Filter to use for the command.
86-
let filter: BSONDocument
90+
let filter: BSONDocument?
8791

8892
/// Optional identifier for a session entity to use.
8993
let session: String?
@@ -98,7 +102,7 @@ struct UnifiedListCollections: UnifiedOperationProtocol {
98102
self.options = try decoder.singleValueContainer().decode(ListCollectionsOptions.self)
99103
let container = try decoder.container(keyedBy: CodingKeys.self)
100104
self.session = try container.decodeIfPresent(String.self, forKey: .session)
101-
self.filter = try container.decode(BSONDocument.self, forKey: .filter)
105+
self.filter = try container.decodeIfPresent(BSONDocument.self, forKey: .filter)
102106
}
103107

104108
static var knownArguments: Set<String> {

Tests/MongoSwiftSyncTests/UnifiedTestRunner/UnifiedOperation.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,11 @@ struct UnifiedOperation: Decodable {
219219
case "listCollections":
220220
self.operation = try container.decode(UnifiedListCollections.self, forKey: .arguments)
221221
case "listDatabases":
222-
self.operation = UnifiedListDatabases()
222+
if container.allKeys.contains(.arguments) {
223+
self.operation = try container.decode(UnifiedListDatabases.self, forKey: .arguments)
224+
} else {
225+
self.operation = UnifiedListDatabases()
226+
}
223227
case "listIndexes":
224228
self.operation = try container.decode(UnifiedListIndexes.self, forKey: .arguments)
225229
case "replaceOne":

0 commit comments

Comments
 (0)