From 150d6906d1e50de55024821c8ea26d6942c010ee Mon Sep 17 00:00:00 2001 From: James Heppenstall Date: Mon, 6 Apr 2020 16:59:10 -0400 Subject: [PATCH 1/3] SWIFT-423 Provide transactions guide for docs --- Guides/Transactions.md | 111 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 Guides/Transactions.md diff --git a/Guides/Transactions.md b/Guides/Transactions.md new file mode 100644 index 000000000..8d062a945 --- /dev/null +++ b/Guides/Transactions.md @@ -0,0 +1,111 @@ +# Swift Driver Transactions Guide + +`MongoSwift` 1.0.0 added support for [transactions](https://docs.mongodb.com/manual/core/transactions/), which allow applications to use multi-statement transactions that guarantee the atomicity of reads and writes to multiple documents (in a single or multiple collections). Applications can use transactions instead of implementing complex and error-prone ACID-compliant logic themselves, simplifying development and allowing developers to focus on the new features that really matter. + +**Note**: Transactions only work with MongoDB replica sets (v4.0+) and sharded clusters (v4.2+). + +## Examples + +### Transaction that Atomically Moves a `Document` from One `MongoCollection` to Another +```swift +let elg = MultiThreadedEventLoopGroup(numberOfThreads: 4) + +defer { + // free driver resources + client.syncShutdown() + cleanupMongoSwift() + + // shut down EventLoopGroup + try? elg.syncShutdownGracefully() +} + +let client = try MongoClient(using: elg) +let session = client.startSession() + +let db = client.db("test") +let srcColl = db.collection("src") +let destColl = db.collection("coll") +let docToMove: Document = ["hello": "world"] + +session.startTransaction().flatMap { _ in + srcColl.deleteOne(docToMove, session: session) +}.flatMap { _ in + destColl.insertOne(docToMove, session: session) +}.flatMap { _ in + session.commitTransaction() +}.whenFailure { error in + session.abortTransaction() + // handle error +} +``` + +### Transaction with Custom Transaction Options +```swift +let elg = MultiThreadedEventLoopGroup(numberOfThreads: 4) + +defer { + // free driver resources + client.syncShutdown() + cleanupMongoSwift() + + // shut down EventLoopGroup + try? elg.syncShutdownGracefully() +} + +let client = try MongoClient(using: elg) +let session = client.startSession() + +let txnOpts = TransactionOptions( + maxCommitTimeMS: 30, + readConcern: ReadConcern(.local), + readPreference: ReadPreference.primaryPreferred, + writeConcern: try WriteConcern(w: .majority) +) + +session.startTransaction(options: txnOpts).flatMap { _ in + // do something +}.flatMap { _ in + session.commitTransaction() +}.whenFailure { error in + session.abortTransaction() + // handle error +} +``` + +### Transaction with Default Transaction Options +```swift +let elg = MultiThreadedEventLoopGroup(numberOfThreads: 4) + +defer { + // free driver resources + client.syncShutdown() + cleanupMongoSwift() + + // shut down EventLoopGroup + try? elg.syncShutdownGracefully() +} + +let txnOpts = TransactionOptions( + maxCommitTimeMS: 30, + readConcern: ReadConcern(.local), + readPreference: ReadPreference.primaryPreferred, + writeConcern: try WriteConcern(w: .majority) +) + +let client = try MongoClient(using: elg) +let session = client.startSession(options: ClientSessionOptions(defaultTransactionOptions: txnOpts)) + +session.startTransaction().flatMap { _ in + // do something +}.flatMap { _ in + session.commitTransaction() +}.whenFailure { error in + session.abortTransaction() + // handle error +} +``` + +Note: Any transaction options provided directly to `startTransaction()` override the default transaction options for the session. More so, the default transaction options for the session override any options inherited from the client. + +## See Also +- [MongoDB Transactions documentation](https://docs.mongodb.com/manual/core/transactions/) \ No newline at end of file From 93ddbf4f5a75001ec82268373e2b55ae1b0cf455 Mon Sep 17 00:00:00 2001 From: James Heppenstall Date: Fri, 10 Apr 2020 11:45:21 -0400 Subject: [PATCH 2/3] Responded to comments --- Examples/Docs/Package.swift | 2 +- .../Docs/Sources/AsyncExamples/main.swift | 77 +++++++++++++++++++ Guides/Transactions.md | 68 ++++++---------- 3 files changed, 100 insertions(+), 47 deletions(-) diff --git a/Examples/Docs/Package.swift b/Examples/Docs/Package.swift index b7423ae26..53ba04627 100644 --- a/Examples/Docs/Package.swift +++ b/Examples/Docs/Package.swift @@ -4,7 +4,7 @@ import PackageDescription let package = Package( name: "DocsExamples", dependencies: [ - .package(url: "https://github.com/mongodb/mongo-swift-driver", .upToNextMajor(from: "1.0.0-rc0")), + .package(url: "https://github.com/mongodb/mongo-swift-driver", .branch("master")), .package(url: "https://github.com/apple/swift-nio", .upToNextMajor(from: "2.0.0")) ], targets: [ diff --git a/Examples/Docs/Sources/AsyncExamples/main.swift b/Examples/Docs/Sources/AsyncExamples/main.swift index edf07eb7a..429a24082 100644 --- a/Examples/Docs/Sources/AsyncExamples/main.swift +++ b/Examples/Docs/Sources/AsyncExamples/main.swift @@ -147,3 +147,80 @@ private func changeStreams() throws { // End Changestream Example 4 } } + +/// Examples used for the MongoDB documentation on Transactions. +/// - SeeAlso: https://docs.mongodb.com/manual/core/transactions/ +private func transactions() throws { + let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) + let client = try MongoClient(using: elg) + let session = client.startSession() + + defer { + client.syncShutdown() + cleanupMongoSwift() + try? elg.syncShutdownGracefully() + } + + do { + // Start Transactions Example 1 + let db = client.db("test") + let srcColl = db.collection("src") + let destColl = db.collection("coll") + let docToMove: Document = ["hello": "world"] + + session.startTransaction().flatMap { _ in + srcColl.deleteOne(docToMove, session: session) + }.flatMap { _ in + destColl.insertOne(docToMove, session: session) + }.flatMap { _ in + session.commitTransaction() + }.whenFailure { error in + session.abortTransaction() + // handle error + } + // End Transactions Example 1 + } + + do { + // Start Transactions Example 2 + let txnOpts = TransactionOptions( + maxCommitTimeMS: 30, + readConcern: ReadConcern(.local), + readPreference: .primaryPreferred, + writeConcern: try WriteConcern(w: .majority) + ) + + session.startTransaction(options: txnOpts).flatMap { _ in + // do something + }.flatMap { _ in + session.commitTransaction() + }.whenFailure { error in + session.abortTransaction() + // handle error + } + // End Transactions Example 2 + } + + do { + // Start Transactions Example 3 + let txnOpts = TransactionOptions( + maxCommitTimeMS: 30, + readConcern: ReadConcern(.local), + readPreference: .primaryPreferred, + writeConcern: try WriteConcern(w: .majority) + ) + + let client = try MongoClient(using: elg) + let session = client.startSession(options: ClientSessionOptions(defaultTransactionOptions: txnOpts)) + + session.startTransaction().flatMap { _ in + // do something + }.flatMap { _ in + session.commitTransaction() + }.whenFailure { error in + session.abortTransaction() + // handle error + } + // End Transactions Example 3 + } +} diff --git a/Guides/Transactions.md b/Guides/Transactions.md index 8d062a945..25bfebafa 100644 --- a/Guides/Transactions.md +++ b/Guides/Transactions.md @@ -1,30 +1,27 @@ # Swift Driver Transactions Guide -`MongoSwift` 1.0.0 added support for [transactions](https://docs.mongodb.com/manual/core/transactions/), which allow applications to use multi-statement transactions that guarantee the atomicity of reads and writes to multiple documents (in a single or multiple collections). Applications can use transactions instead of implementing complex and error-prone ACID-compliant logic themselves, simplifying development and allowing developers to focus on the new features that really matter. +`MongoSwift` 1.0.0 added support for [transactions](https://docs.mongodb.com/manual/core/transactions/), which allow applications to use to execute multiple read and write operations atomically across multiple documents and/or collections. Transactions reduce the need for complicated application logic when operating on several different documents simultaneously; however, because operations on single documents are always atomic, transactions are often not necessary. + +Transactions in the driver must be started on a `ClientSession` using `startTransaction()`. The session must then be passed to each operation in the transaction. If the session is not passed to an operation, said operation will be executed outside the context of the transaction. Transactions must be committed or aborted using `commitTransaction()` or `abortTransaction()`, respectively. Ending a session *aborts* all in-progress transactions. **Note**: Transactions only work with MongoDB replica sets (v4.0+) and sharded clusters (v4.2+). ## Examples ### Transaction that Atomically Moves a `Document` from One `MongoCollection` to Another -```swift -let elg = MultiThreadedEventLoopGroup(numberOfThreads: 4) -defer { - // free driver resources - client.syncShutdown() - cleanupMongoSwift() - - // shut down EventLoopGroup - try? elg.syncShutdownGracefully() -} +The transaction below atomically deletes the document `{ "hello": "world" }` from the collection `test.src` and inserts the document in the collection `test.dest`. This ensures that the document exists in either `test.src` or `test.dest`, but not both or neither. Exectuting the delete and insert non-atomically raises the following issues: +- A race between `deleteOne()` and `insertOne()` where the document does not exist in either collection. +- If `deleteOne()` fails and `insertOne()` succeeds, the document exists in both collections. +- If `deleteOne()` succeeds and `insertOne()` fails, the document does not exist in either collection. +```swift let client = try MongoClient(using: elg) let session = client.startSession() let db = client.db("test") let srcColl = db.collection("src") -let destColl = db.collection("coll") +let destColl = db.collection("dest") let docToMove: Document = ["hello": "world"] session.startTransaction().flatMap { _ in @@ -39,30 +36,19 @@ session.startTransaction().flatMap { _ in } ``` -### Transaction with Custom Transaction Options +### Transaction with Default Transaction Options ```swift -let elg = MultiThreadedEventLoopGroup(numberOfThreads: 4) - -defer { - // free driver resources - client.syncShutdown() - cleanupMongoSwift() - - // shut down EventLoopGroup - try? elg.syncShutdownGracefully() -} - -let client = try MongoClient(using: elg) -let session = client.startSession() - let txnOpts = TransactionOptions( maxCommitTimeMS: 30, readConcern: ReadConcern(.local), - readPreference: ReadPreference.primaryPreferred, + readPreference: .primaryPreferred, writeConcern: try WriteConcern(w: .majority) ) -session.startTransaction(options: txnOpts).flatMap { _ in +let client = try MongoClient(using: elg) +let session = client.startSession(options: ClientSessionOptions(defaultTransactionOptions: txnOpts)) + +session.startTransaction().flatMap { _ in // do something }.flatMap { _ in session.commitTransaction() @@ -72,30 +58,22 @@ session.startTransaction(options: txnOpts).flatMap { _ in } ``` -### Transaction with Default Transaction Options -```swift -let elg = MultiThreadedEventLoopGroup(numberOfThreads: 4) +### Transaction with Custom Transaction Options -defer { - // free driver resources - client.syncShutdown() - cleanupMongoSwift() +**Note**:: Any transaction options provided directly to `startTransaction()` override the default transaction options for the session. More so, the default transaction options for the session override any options inherited from the client. - // shut down EventLoopGroup - try? elg.syncShutdownGracefully() -} +```swift +let client = try MongoClient(using: elg) +let session = client.startSession() let txnOpts = TransactionOptions( maxCommitTimeMS: 30, readConcern: ReadConcern(.local), - readPreference: ReadPreference.primaryPreferred, + readPreference: .primaryPreferred, writeConcern: try WriteConcern(w: .majority) ) -let client = try MongoClient(using: elg) -let session = client.startSession(options: ClientSessionOptions(defaultTransactionOptions: txnOpts)) - -session.startTransaction().flatMap { _ in +session.startTransaction(options: txnOpts).flatMap { _ in // do something }.flatMap { _ in session.commitTransaction() @@ -105,7 +83,5 @@ session.startTransaction().flatMap { _ in } ``` -Note: Any transaction options provided directly to `startTransaction()` override the default transaction options for the session. More so, the default transaction options for the session override any options inherited from the client. - ## See Also - [MongoDB Transactions documentation](https://docs.mongodb.com/manual/core/transactions/) \ No newline at end of file From 81617390e09906bea2aa470b4c3e58cfbabf9518 Mon Sep 17 00:00:00 2001 From: James Heppenstall Date: Fri, 10 Apr 2020 16:59:38 -0400 Subject: [PATCH 3/3] Responded to comments 2 --- Examples/Docs/Sources/AsyncExamples/main.swift | 6 +++--- Guides/Transactions.md | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Examples/Docs/Sources/AsyncExamples/main.swift b/Examples/Docs/Sources/AsyncExamples/main.swift index 429a24082..a57907aab 100644 --- a/Examples/Docs/Sources/AsyncExamples/main.swift +++ b/Examples/Docs/Sources/AsyncExamples/main.swift @@ -174,7 +174,7 @@ private func transactions() throws { destColl.insertOne(docToMove, session: session) }.flatMap { _ in session.commitTransaction() - }.whenFailure { error in + }.whenFailure { _ in session.abortTransaction() // handle error } @@ -194,7 +194,7 @@ private func transactions() throws { // do something }.flatMap { _ in session.commitTransaction() - }.whenFailure { error in + }.whenFailure { _ in session.abortTransaction() // handle error } @@ -217,7 +217,7 @@ private func transactions() throws { // do something }.flatMap { _ in session.commitTransaction() - }.whenFailure { error in + }.whenFailure { _ in session.abortTransaction() // handle error } diff --git a/Guides/Transactions.md b/Guides/Transactions.md index 25bfebafa..dd12469d7 100644 --- a/Guides/Transactions.md +++ b/Guides/Transactions.md @@ -10,7 +10,7 @@ Transactions in the driver must be started on a `ClientSession` using `startTran ### Transaction that Atomically Moves a `Document` from One `MongoCollection` to Another -The transaction below atomically deletes the document `{ "hello": "world" }` from the collection `test.src` and inserts the document in the collection `test.dest`. This ensures that the document exists in either `test.src` or `test.dest`, but not both or neither. Exectuting the delete and insert non-atomically raises the following issues: +The transaction below atomically deletes the document `{ "hello": "world" }` from the collection `test.src` and inserts the document in the collection `test.dest`. This ensures that the document exists in either `test.src` or `test.dest`, but not both or neither. Executing the delete and insert non-atomically raises the following issues: - A race between `deleteOne()` and `insertOne()` where the document does not exist in either collection. - If `deleteOne()` fails and `insertOne()` succeeds, the document exists in both collections. - If `deleteOne()` succeeds and `insertOne()` fails, the document does not exist in either collection. @@ -37,6 +37,9 @@ session.startTransaction().flatMap { _ in ``` ### Transaction with Default Transaction Options + +The default transaction options specified below apply to any transaction started on the session. + ```swift let txnOpts = TransactionOptions( maxCommitTimeMS: 30,