Skip to content

Commit 2545db0

Browse files
SWIFT-752 Implement transactions spec test runner (#443)
1 parent 162ed98 commit 2545db0

22 files changed

+1118
-200
lines changed

Sources/MongoSwift/ClientSession.swift

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,80 @@ public final class ClientSession {
7070
/// started the libmongoc session.
7171
internal var id: Document?
7272

73+
/// The server ID of the mongos this session is pinned to. A server ID of 0 indicates that the session is unpinned.
74+
internal var serverId: UInt32? {
75+
switch self.state {
76+
case .notStarted, .ended:
77+
return nil
78+
case let .started(session, _):
79+
return mongoc_client_session_get_server_id(session)
80+
}
81+
}
82+
83+
/// Enum tracking the state of the transaction associated with this session.
84+
internal enum TransactionState: String, Decodable {
85+
/// There is no transaction in progress.
86+
case none
87+
/// A transaction has been started, but no operation has been sent to the server.
88+
case starting
89+
/// A transaction is in progress.
90+
case inProgress
91+
/// The transaction was committed.
92+
case committed
93+
/// The transaction was aborted.
94+
case aborted
95+
96+
fileprivate var mongocTransactionState: mongoc_transaction_state_t {
97+
switch self {
98+
case .none:
99+
return MONGOC_TRANSACTION_NONE
100+
case .starting:
101+
return MONGOC_TRANSACTION_STARTING
102+
case .inProgress:
103+
return MONGOC_TRANSACTION_IN_PROGRESS
104+
case .committed:
105+
return MONGOC_TRANSACTION_COMMITTED
106+
case .aborted:
107+
return MONGOC_TRANSACTION_ABORTED
108+
}
109+
}
110+
111+
fileprivate init(mongocTransactionState: mongoc_transaction_state_t) {
112+
switch mongocTransactionState {
113+
case MONGOC_TRANSACTION_NONE:
114+
self = .none
115+
case MONGOC_TRANSACTION_STARTING:
116+
self = .starting
117+
case MONGOC_TRANSACTION_IN_PROGRESS:
118+
self = .inProgress
119+
case MONGOC_TRANSACTION_COMMITTED:
120+
self = .committed
121+
case MONGOC_TRANSACTION_ABORTED:
122+
self = .aborted
123+
default:
124+
fatalError("Unexpected transaction state: \(mongocTransactionState)")
125+
}
126+
}
127+
}
128+
129+
/// The transaction state of this session.
130+
internal var transactionState: TransactionState? {
131+
switch self.state {
132+
case .notStarted, .ended:
133+
return nil
134+
case let .started(session, _):
135+
return TransactionState(mongocTransactionState: mongoc_client_session_get_transaction_state(session))
136+
}
137+
}
138+
139+
/// Indicates whether or not the session is in a transaction.
140+
internal var inTransaction: Bool {
141+
if let transactionState = self.transactionState {
142+
return transactionState != .none
143+
}
144+
return false
145+
}
146+
73147
/// The most recent cluster time seen by this session. This value will be nil if either of the following are true:
74148
/// - No operations have been executed using this session and `advanceClusterTime` has not been called.
75149
/// - This session has been ended.
@@ -243,7 +317,7 @@ public final class ClientSession {
243317
* - SeeAlso:
244318
* - https://docs.mongodb.com/manual/core/transactions/
245319
*/
246-
public func startTransaction(_ options: TransactionOptions?) -> EventLoopFuture<Void> {
320+
public func startTransaction(options: TransactionOptions? = nil) -> EventLoopFuture<Void> {
247321
switch self.state {
248322
case .notStarted, .started:
249323
let operation = StartTransactionOperation(options: options)

Sources/MongoSwift/MongoClient.swift

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,14 @@ import NIO
33
import NIOConcurrencyHelpers
44

55
/// Options to use when creating a `MongoClient`.
6-
public struct ClientOptions: CodingStrategyProvider, Decodable {
7-
// swiftlint:disable redundant_optional_initialization
8-
6+
public struct ClientOptions: CodingStrategyProvider {
97
/// Specifies the `DataCodingStrategy` to use for BSON encoding/decoding operations performed by this client and any
108
/// databases or collections that derive from it.
11-
public var dataCodingStrategy: DataCodingStrategy? = nil
9+
public var dataCodingStrategy: DataCodingStrategy?
1210

1311
/// Specifies the `DateCodingStrategy` to use for BSON encoding/decoding operations performed by this client and any
1412
/// databases or collections that derive from it.
15-
public var dateCodingStrategy: DateCodingStrategy? = nil
13+
public var dateCodingStrategy: DateCodingStrategy?
1614

1715
/// The maximum number of connections that may be associated with a connection pool created by this client at a
1816
/// given time. This includes in-use and available connections. Defaults to 100.
@@ -22,7 +20,7 @@ public struct ClientOptions: CodingStrategyProvider, Decodable {
2220
public var readConcern: ReadConcern?
2321

2422
/// Specifies a ReadPreference to use for the client.
25-
public var readPreference: ReadPreference? = nil
23+
public var readPreference: ReadPreference?
2624

2725
/// Determines whether the client should retry supported read operations (on by default).
2826
public var retryReads: Bool?
@@ -65,7 +63,7 @@ public struct ClientOptions: CodingStrategyProvider, Decodable {
6563

6664
/// Specifies the `UUIDCodingStrategy` to use for BSON encoding/decoding operations performed by this client and any
6765
/// databases or collections that derive from it.
68-
public var uuidCodingStrategy: UUIDCodingStrategy? = nil
66+
public var uuidCodingStrategy: UUIDCodingStrategy?
6967

7068
// swiftlint:enable redundant_optional_initialization
7169

Sources/MongoSwift/MongoCollection+BulkWrite.swift

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,13 @@ internal struct BulkWriteOperation<T: Codable>: Operation {
197197
let opts = try encodeOptions(options: options, session: session)
198198
var insertedIds: [Int: BSON] = [:]
199199

200+
if session?.inTransaction == true && self.options?.writeConcern != nil {
201+
throw InvalidArgumentError(
202+
message: "Cannot specify a write concern on an individual helper in a " +
203+
"transaction. Instead specify it when starting the transaction."
204+
)
205+
}
206+
200207
let (serverId, isAcknowledged): (UInt32, Bool) =
201208
try self.collection.withMongocCollection(from: connection) { collPtr in
202209
guard let bulk = mongoc_collection_create_bulk_operation_with_opts(collPtr, opts?._bson) else {
@@ -214,8 +221,22 @@ internal struct BulkWriteOperation<T: Codable>: Operation {
214221
mongoc_bulk_operation_execute(bulk, replyPtr, &error)
215222
}
216223

217-
let writeConcern = WriteConcern(from: mongoc_bulk_operation_get_write_concern(bulk))
218-
return (serverId, writeConcern.isAcknowledged)
224+
var writeConcernAcknowledged: Bool
225+
if session?.inTransaction == true {
226+
// Bulk write operations in transactions must get their write concern from the session, not from
227+
// the `BulkWriteOptions` passed to the `bulkWrite` helper. `libmongoc` surfaces this
228+
// implementation detail by nulling out the write concern stored on the bulk write. To sidestep
229+
// this, we can only call `mongoc_bulk_operation_get_write_concern` out of a transaction.
230+
//
231+
// In a transaction, default to writeConcernAcknowledged = true. This is acceptable because
232+
// transactions do not support unacknowledged writes.
233+
writeConcernAcknowledged = true
234+
} else {
235+
let writeConcern = WriteConcern(from: mongoc_bulk_operation_get_write_concern(bulk))
236+
writeConcernAcknowledged = writeConcern.isAcknowledged
237+
}
238+
239+
return (serverId, writeConcernAcknowledged)
219240
}
220241

221242
let result = try BulkWriteResult(reply: reply, insertedIds: insertedIds)

Sources/MongoSwift/MongoCollection+Read.swift

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -114,28 +114,24 @@ extension MongoCollection {
114114
}
115115

116116
/**
117-
* Gets an estimate of the count of documents in this collection using collection metadata.
117+
* Gets an estimate of the count of documents in this collection using collection metadata. This operation cannot
118+
* be used in a transaction.
118119
*
119120
* - Parameters:
120121
* - options: Optional `EstimatedDocumentCountOptions` to use when executing the command
121-
* - session: Optional `ClientSession` to use when executing this command
122122
*
123123
* - Returns:
124124
* An `EventLoopFuture<Int>`. On success, contains an estimate of the count of documents in this collection.
125125
*
126126
* If the future fails, the error is likely one of the following:
127127
* - `CommandError` if an error occurs that prevents the command from executing.
128128
* - `InvalidArgumentError` if the options passed in form an invalid combination.
129-
* - `LogicError` if the provided session is inactive.
130129
* - `LogicError` if this collection's parent client has already been closed.
131130
* - `EncodingError` if an error occurs while encoding the options to BSON.
132131
*/
133-
public func estimatedDocumentCount(
134-
options: EstimatedDocumentCountOptions? = nil,
135-
session: ClientSession? = nil
136-
) -> EventLoopFuture<Int> {
132+
public func estimatedDocumentCount(options: EstimatedDocumentCountOptions? = nil) -> EventLoopFuture<Int> {
137133
let operation = EstimatedDocumentCountOperation(collection: self, options: options)
138-
return self._client.operationExecutor.execute(operation, client: self._client, session: session)
134+
return self._client.operationExecutor.execute(operation, client: self._client, session: nil)
139135
}
140136

141137
/**

Sources/MongoSwift/MongoError.swift

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public struct InternalError: RuntimeError {
9696
/// An error thrown when encountering a connection or socket related error.
9797
/// May contain labels providing additional information on the nature of the error.
9898
public struct ConnectionError: RuntimeError, LabeledError {
99-
internal let message: String
99+
public let message: String
100100

101101
public let errorLabels: [String]?
102102

@@ -173,11 +173,36 @@ public struct WriteConcernFailure: Codable {
173173
/// A description of the error.
174174
public let message: String
175175

176+
/// Labels that may describe the context in which this error was thrown.
177+
public let errorLabels: [String]?
178+
176179
private enum CodingKeys: String, CodingKey {
177180
case code
178181
case codeName
179182
case details = "errInfo"
180183
case message = "errmsg"
184+
case errorLabels
185+
}
186+
187+
// TODO: can remove this once SERVER-36755 is resolved
188+
public init(from decoder: Decoder) throws {
189+
let container = try decoder.container(keyedBy: CodingKeys.self)
190+
self.code = try container.decode(ServerErrorCode.self, forKey: .code)
191+
self.message = try container.decode(String.self, forKey: .message)
192+
self.codeName = try container.decodeIfPresent(String.self, forKey: .codeName) ?? ""
193+
self.details = try container.decodeIfPresent(Document.self, forKey: .details)
194+
self.errorLabels = try container.decodeIfPresent([String].self, forKey: .errorLabels)
195+
}
196+
197+
// TODO: can remove this once SERVER-36755 is resolved
198+
internal init(
199+
code: ServerErrorCode, codeName: String, details: Document?, message: String, errorLabels: [String]? = nil
200+
) {
201+
self.code = code
202+
self.codeName = codeName
203+
self.message = message
204+
self.details = details
205+
self.errorLabels = errorLabels
181206
}
182207
}
183208

@@ -279,6 +304,7 @@ internal func extractMongoError(error bsonError: bson_error_t, reply: Document?
279304
// if the reply is nil or writeErrors or writeConcernErrors aren't present, then this is likely a commandError.
280305
guard let serverReply: Document = reply,
281306
!(serverReply["writeErrors"]?.arrayValue ?? []).isEmpty ||
307+
!(serverReply["writeConcernError"]?.documentValue?.keys ?? []).isEmpty ||
282308
!(serverReply["writeConcernErrors"]?.arrayValue ?? []).isEmpty else {
283309
return parseMongocError(bsonError, reply: reply)
284310
}
@@ -390,11 +416,14 @@ internal func extractBulkWriteError<T: Codable>(
390416

391417
/// Extracts a `WriteConcernError` from a server reply.
392418
private func extractWriteConcernError(from reply: Document) throws -> WriteConcernFailure? {
393-
guard let writeConcernErrors = reply["writeConcernErrors"]?.arrayValue?.compactMap({ $0.documentValue }),
394-
!writeConcernErrors.isEmpty else {
419+
if let writeConcernErrors = reply["writeConcernErrors"]?.arrayValue?.compactMap({ $0.documentValue }),
420+
!writeConcernErrors.isEmpty {
421+
return try BSONDecoder().decode(WriteConcernFailure.self, from: writeConcernErrors[0])
422+
} else if let writeConcernError = reply["writeConcernError"]?.documentValue {
423+
return try BSONDecoder().decode(WriteConcernFailure.self, from: writeConcernError)
424+
} else {
395425
return nil
396426
}
397-
return try BSONDecoder().decode(WriteConcernFailure.self, from: writeConcernErrors[0])
398427
}
399428

400429
/// Internal function used by write methods performing single writes that are implemented via the bulk API. If the
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
// Generated using Sourcery 0.16.1 — https://github.com/krzysztofzablocki/Sourcery
22
// DO NOT EDIT
33

4-
5-
// swiftlint:disable:previous vertical_whitespace
64
internal let MongoSwiftVersionString = "1.0.0-rc0"

Sources/MongoSwiftSync/ClientSession.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ public final class ClientSession {
115115
* - https://docs.mongodb.com/manual/core/transactions/
116116
*/
117117
public func startTransaction(options: TransactionOptions? = nil) throws {
118-
try self.asyncSession.startTransaction(options).wait()
118+
try self.asyncSession.startTransaction(options: options).wait()
119119
}
120120

121121
/**

Sources/MongoSwiftSync/MongoCollection+Read.swift

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,19 +94,16 @@ extension MongoCollection {
9494
}
9595

9696
/**
97-
* Gets an estimate of the count of documents in this collection using collection metadata.
97+
* Gets an estimate of the count of documents in this collection using collection metadata. This operation cannot
98+
* be used in a transaction.
9899
*
99100
* - Parameters:
100101
* - options: Optional `EstimatedDocumentCountOptions` to use when executing the command
101-
* - session: Optional `ClientSession` to use when executing this command
102102
*
103103
* - Returns: an estimate of the count of documents in this collection
104104
*/
105-
public func estimatedDocumentCount(
106-
options: EstimatedDocumentCountOptions? = nil,
107-
session: ClientSession? = nil
108-
) throws -> Int {
109-
try self.asyncColl.estimatedDocumentCount(options: options, session: session?.asyncSession).wait()
105+
public func estimatedDocumentCount(options: EstimatedDocumentCountOptions? = nil) throws -> Int {
106+
try self.asyncColl.estimatedDocumentCount(options: options).wait()
110107
}
111108

112109
/**

Tests/LinuxMain.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,12 @@ extension SyncMongoClientTests {
366366
]
367367
}
368368

369+
extension TransactionsTests {
370+
static var allTests = [
371+
("testTransactions", testTransactions),
372+
]
373+
}
374+
369375
extension WriteConcernTests {
370376
static var allTests = [
371377
("testWriteConcernType", testWriteConcernType),
@@ -408,5 +414,6 @@ XCTMain([
408414
testCase(SyncChangeStreamTests.allTests),
409415
testCase(SyncClientSessionTests.allTests),
410416
testCase(SyncMongoClientTests.allTests),
417+
testCase(TransactionsTests.allTests),
411418
testCase(WriteConcernTests.allTests),
412419
])

0 commit comments

Comments
 (0)