From b2c8df1c66a4dff56c81a5920835f6c3e3d3fd55 Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Mon, 18 Jun 2018 17:21:12 +0100 Subject: [PATCH 01/38] PERL-918 updated retryable-writes test specs --- t/data/retryable-writes/README.rst | 22 +- .../bulkWrite-serverErrors.json | 182 +++ .../bulkWrite-serverErrors.yml | 90 ++ t/data/retryable-writes/bulkWrite.json | 167 ++- t/data/retryable-writes/bulkWrite.yml | 100 +- .../deleteOne-serverErrors.json | 96 ++ .../deleteOne-serverErrors.yml | 50 + t/data/retryable-writes/deleteOne.json | 12 + t/data/retryable-writes/deleteOne.yml | 9 + .../findOneAndDelete-serverErrors.json | 108 ++ .../findOneAndDelete-serverErrors.yml | 50 + t/data/retryable-writes/findOneAndDelete.json | 12 + t/data/retryable-writes/findOneAndDelete.yml | 9 + .../findOneAndReplace-serverErrors.json | 116 ++ .../findOneAndReplace-serverErrors.yml | 54 + .../retryable-writes/findOneAndReplace.json | 12 + t/data/retryable-writes/findOneAndReplace.yml | 9 + .../findOneAndUpdate-serverErrors.json | 118 ++ .../findOneAndUpdate-serverErrors.yml | 54 + t/data/retryable-writes/findOneAndUpdate.json | 12 + t/data/retryable-writes/findOneAndUpdate.yml | 9 + .../insertMany-serverErrors.json | 134 +++ .../insertMany-serverErrors.yml | 59 + t/data/retryable-writes/insertMany.json | 12 + t/data/retryable-writes/insertMany.yml | 9 + .../insertOne-serverErrors.json | 1000 +++++++++++++++++ .../insertOne-serverErrors.yml | 471 ++++++++ t/data/retryable-writes/insertOne.json | 12 + t/data/retryable-writes/insertOne.yml | 9 + .../replaceOne-serverErrors.json | 116 ++ .../replaceOne-serverErrors.yml | 58 + t/data/retryable-writes/replaceOne.json | 12 + t/data/retryable-writes/replaceOne.yml | 9 + .../updateOne-serverErrors.json | 118 ++ .../updateOne-serverErrors.yml | 58 + t/data/retryable-writes/updateOne.json | 24 + t/data/retryable-writes/updateOne.yml | 18 + 37 files changed, 3398 insertions(+), 12 deletions(-) create mode 100644 t/data/retryable-writes/bulkWrite-serverErrors.json create mode 100644 t/data/retryable-writes/bulkWrite-serverErrors.yml create mode 100644 t/data/retryable-writes/deleteOne-serverErrors.json create mode 100644 t/data/retryable-writes/deleteOne-serverErrors.yml create mode 100644 t/data/retryable-writes/findOneAndDelete-serverErrors.json create mode 100644 t/data/retryable-writes/findOneAndDelete-serverErrors.yml create mode 100644 t/data/retryable-writes/findOneAndReplace-serverErrors.json create mode 100644 t/data/retryable-writes/findOneAndReplace-serverErrors.yml create mode 100644 t/data/retryable-writes/findOneAndUpdate-serverErrors.json create mode 100644 t/data/retryable-writes/findOneAndUpdate-serverErrors.yml create mode 100644 t/data/retryable-writes/insertMany-serverErrors.json create mode 100644 t/data/retryable-writes/insertMany-serverErrors.yml create mode 100644 t/data/retryable-writes/insertOne-serverErrors.json create mode 100644 t/data/retryable-writes/insertOne-serverErrors.yml create mode 100644 t/data/retryable-writes/replaceOne-serverErrors.json create mode 100644 t/data/retryable-writes/replaceOne-serverErrors.yml create mode 100644 t/data/retryable-writes/updateOne-serverErrors.json create mode 100644 t/data/retryable-writes/updateOne-serverErrors.yml diff --git a/t/data/retryable-writes/README.rst b/t/data/retryable-writes/README.rst index b9f1008c..606ba93b 100644 --- a/t/data/retryable-writes/README.rst +++ b/t/data/retryable-writes/README.rst @@ -15,10 +15,11 @@ that drivers can use to prove their conformance to the Retryable Writes spec. Several prose tests, which are not easily expressed in YAML, are also presented in this file. Those tests will need to be manually implemented by each driver. -Tests will require a MongoClient with ``retryWrites`` enabled. Integration tests -will require a running MongoDB cluster with server versions 3.6.0 or later. The -``{setFeatureCompatibilityVersion: 3.6}`` admin command will also need to have -been executed to enable support for retryable writes on the cluster. +Tests will require a MongoClient created with options defined in the tests. +Integration tests will require a running MongoDB cluster with server versions +3.6.0 or later. The ``{setFeatureCompatibilityVersion: 3.6}`` admin command +will also need to have been executed to enable support for retryable writes on +the cluster. Server Fail Point ================= @@ -137,15 +138,16 @@ Each YAML file has the following keys: - ``description``: The name of the test. - - ``failPoint``: Document describing options for configuring the - ``onPrimaryTransactionalWrite`` fail point on the primary server. This - document should be merged with the - ``{ configureFailPoint: "onPrimaryTransactionalWrite" }`` command document. + - ``clientOptions``: Parameters to pass to MongoClient(). + + - ``failPoint``: The ``configureFailPoint`` command document to run to + configure a fail point on the primary server. Drivers must ensure that + ``configureFailPoint`` is the first field in the command. - ``operation``: Document describing the operation to be executed. The operation should be executed through a collection object derived from a - client that has been created with the ``retryWrites=true`` option. - This will have some or all of the following fields: + client that has been created with ``clientOptions``. The operation will have + some or all of the following fields: - ``name``: The name of the operation as defined in the CRUD specification. diff --git a/t/data/retryable-writes/bulkWrite-serverErrors.json b/t/data/retryable-writes/bulkWrite-serverErrors.json new file mode 100644 index 00000000..cd81fc61 --- /dev/null +++ b/t/data/retryable-writes/bulkWrite-serverErrors.json @@ -0,0 +1,182 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "minServerVersion": "3.99", + "tests": [ + { + "description": "BulkWrite succeeds after PrimarySteppedDown", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "update" + ], + "errorCode": 189 + } + }, + "operation": { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 1 + } + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + { + "name": "updateOne", + "arguments": { + "filter": { + "_id": 2 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + } + ], + "options": { + "ordered": true + } + } + }, + "outcome": { + "result": { + "deletedCount": 1, + "insertedIds": { + "1": 3 + }, + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0, + "upsertedIds": {} + }, + "collection": { + "data": [ + { + "_id": 2, + "x": 23 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "BulkWrite succeeds after WriteConcernError ShutdownInProgress", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 1 + } + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + { + "name": "updateOne", + "arguments": { + "filter": { + "_id": 2 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + } + ], + "options": { + "ordered": true + } + } + }, + "outcome": { + "result": { + "deletedCount": 1, + "insertedIds": { + "1": 3 + }, + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0, + "upsertedIds": {} + }, + "collection": { + "data": [ + { + "_id": 2, + "x": 23 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + } + ] +} diff --git a/t/data/retryable-writes/bulkWrite-serverErrors.yml b/t/data/retryable-writes/bulkWrite-serverErrors.yml new file mode 100644 index 00000000..de9cff5e --- /dev/null +++ b/t/data/retryable-writes/bulkWrite-serverErrors.yml @@ -0,0 +1,90 @@ +data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + +# TODO: this should change to 4.0 once 4.0.0 is released. +minServerVersion: '3.99' + +tests: + - + description: "BulkWrite succeeds after PrimarySteppedDown" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["update"] + errorCode: 189 + operation: + name: "bulkWrite" + arguments: + requests: + - + name: "deleteOne" + arguments: + filter: { _id: 1 } + - + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + - + name: "updateOne" + arguments: + filter: { _id: 2 } + update: { $inc: { x : 1 }} + options: { ordered: true } + outcome: + result: + deletedCount: 1 + insertedIds: { 1: 3 } + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + upsertedIds: { } + collection: + data: + - { _id: 2, x: 23 } + - { _id: 3, x: 33 } + - + description: "BulkWrite succeeds after WriteConcernError ShutdownInProgress" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + operation: + name: "bulkWrite" + arguments: + requests: + - + name: "deleteOne" + arguments: + filter: { _id: 1 } + - + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + - + name: "updateOne" + arguments: + filter: { _id: 2 } + update: { $inc: { x : 1 }} + options: { ordered: true } + outcome: + result: + deletedCount: 1 + insertedIds: { 1: 3 } + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + upsertedIds: { } + collection: + data: + - { _id: 2, x: 23 } + - { _id: 3, x: 33 } diff --git a/t/data/retryable-writes/bulkWrite.json b/t/data/retryable-writes/bulkWrite.json index 7b88ffb3..6029aece 100644 --- a/t/data/retryable-writes/bulkWrite.json +++ b/t/data/retryable-writes/bulkWrite.json @@ -9,7 +9,11 @@ "tests": [ { "description": "First command is retried", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 } @@ -77,7 +81,11 @@ }, { "description": "All commands are retried", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 7 } @@ -206,7 +214,11 @@ }, { "description": "Both commands are retried after their first statement fails", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 2 } @@ -283,7 +295,11 @@ }, { "description": "Second command is retried after its second statement fails", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "skip": 2 } @@ -360,7 +376,11 @@ }, { "description": "BulkWrite with unordered execution", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 } @@ -425,7 +445,11 @@ }, { "description": "First insertOne is never committed", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 2 }, @@ -495,7 +519,11 @@ }, { "description": "Second updateOne is never committed", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "skip": 1 }, @@ -571,7 +599,11 @@ }, { "description": "Third updateOne is never committed", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "skip": 2 }, @@ -629,7 +661,140 @@ "result": { "deletedCount": 0, "insertedIds": { - "0": 2 + "1": 2 + }, + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0, + "upsertedIds": {} + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 12 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "Single-document write following deleteMany is retried", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", + "mode": { + "times": 1 + }, + "data": { + "failBeforeCommitExceptionCode": 1 + } + }, + "operation": { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "deleteMany", + "arguments": { + "filter": { + "x": 11 + } + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 2, + "x": 22 + } + } + } + ], + "options": { + "ordered": true + } + } + }, + "outcome": { + "result": { + "deletedCount": 1, + "insertedIds": { + "1": 2 + }, + "matchedCount": 0, + "modifiedCount": 0, + "upsertedCount": 0, + "upsertedIds": {} + }, + "collection": { + "data": [ + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "Single-document write following updateMany is retried", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", + "mode": { + "times": 1 + }, + "data": { + "failBeforeCommitExceptionCode": 1 + } + }, + "operation": { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "updateMany", + "arguments": { + "filter": { + "x": 11 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 2, + "x": 22 + } + } + } + ], + "options": { + "ordered": true + } + } + }, + "outcome": { + "result": { + "deletedCount": 0, + "insertedIds": { + "1": 2 }, "matchedCount": 1, "modifiedCount": 1, diff --git a/t/data/retryable-writes/bulkWrite.yml b/t/data/retryable-writes/bulkWrite.yml index 167f8943..408aba7e 100644 --- a/t/data/retryable-writes/bulkWrite.yml +++ b/t/data/retryable-writes/bulkWrite.yml @@ -6,7 +6,10 @@ minServerVersion: '3.6' tests: - description: "First command is retried" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } operation: name: "bulkWrite" @@ -42,7 +45,10 @@ tests: # that each write command consists of a single statement, which will # fail on the first attempt and succeed on the second, retry attempt. description: "All commands are retried" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 7 } operation: name: "bulkWrite" @@ -97,7 +103,10 @@ tests: - { _id: 5, x: 55 } - description: "Both commands are retried after their first statement fails" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 2 } operation: name: "bulkWrite" @@ -132,7 +141,10 @@ tests: - { _id: 2, x: 23 } - description: "Second command is retried after its second statement fails" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { skip: 2 } operation: name: "bulkWrite" @@ -167,7 +179,10 @@ tests: - { _id: 2, x: 23 } - description: "BulkWrite with unordered execution" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } operation: name: "bulkWrite" @@ -197,7 +212,10 @@ tests: - { _id: 3, x: 33 } - description: "First insertOne is never committed" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 2 } data: { failBeforeCommitExceptionCode: 1 } operation: @@ -232,7 +250,10 @@ tests: - { _id: 1, x: 11 } - description: "Second updateOne is never committed" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { skip: 1 } data: { failBeforeCommitExceptionCode: 1 } operation: @@ -268,7 +289,10 @@ tests: - { _id: 2, x: 22 } - description: "Third updateOne is never committed" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { skip: 2 } data: { failBeforeCommitExceptionCode: 1 } operation: @@ -294,7 +318,81 @@ tests: error: true result: deletedCount: 0 - insertedIds: { 0: 2 } + insertedIds: { 1: 2 } + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + upsertedIds: { } + collection: + data: + - { _id: 1, x: 12 } + - { _id: 2, x: 22 } + - + # The onPrimaryTransactionalWrite fail point only triggers for write + # operations that include a transaction ID. Therefore, it will not + # affect the initial deleteMany and will trigger once (and only once) + # for the first insertOne attempt. + description: "Single-document write following deleteMany is retried" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: onPrimaryTransactionalWrite + mode: { times: 1 } + data: { failBeforeCommitExceptionCode: 1 } + operation: + name: "bulkWrite" + arguments: + requests: + - + name: "deleteMany" + arguments: + filter: { x: 11 } + - + name: "insertOne" + arguments: + document: { _id: 2, x: 22 } + options: { ordered: true } + outcome: + result: + deletedCount: 1 + insertedIds: { 1: 2 } + matchedCount: 0 + modifiedCount: 0 + upsertedCount: 0 + upsertedIds: { } + collection: + data: + - { _id: 2, x: 22 } + - + # The onPrimaryTransactionalWrite fail point only triggers for write + # operations that include a transaction ID. Therefore, it will not + # affect the initial updateMany and will trigger once (and only once) + # for the first insertOne attempt. + description: "Single-document write following updateMany is retried" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: onPrimaryTransactionalWrite + mode: { times: 1 } + data: { failBeforeCommitExceptionCode: 1 } + operation: + name: "bulkWrite" + arguments: + requests: + - + name: "updateMany" + arguments: + filter: { x: 11 } + update: { $inc: { x : 1 }} + - + name: "insertOne" + arguments: + document: { _id: 2, x: 22 } + options: { ordered: true } + outcome: + result: + deletedCount: 0 + insertedIds: { 1: 2 } matchedCount: 1 modifiedCount: 1 upsertedCount: 0 diff --git a/t/data/retryable-writes/deleteOne-serverErrors.json b/t/data/retryable-writes/deleteOne-serverErrors.json new file mode 100644 index 00000000..606fc66f --- /dev/null +++ b/t/data/retryable-writes/deleteOne-serverErrors.json @@ -0,0 +1,96 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "minServerVersion": "3.99", + "tests": [ + { + "description": "DeleteOne succeeds after PrimarySteppedDown", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "delete" + ], + "errorCode": 189 + } + }, + "operation": { + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 1 + } + } + }, + "outcome": { + "result": { + "deletedCount": 1 + }, + "collection": { + "data": [ + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "DeleteOne succeeds after WriteConcernError ShutdownInProgress", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "delete" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 1 + } + } + }, + "outcome": { + "result": { + "deletedCount": 1 + }, + "collection": { + "data": [ + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/t/data/retryable-writes/deleteOne-serverErrors.yml b/t/data/retryable-writes/deleteOne-serverErrors.yml new file mode 100644 index 00000000..006f1513 --- /dev/null +++ b/t/data/retryable-writes/deleteOne-serverErrors.yml @@ -0,0 +1,50 @@ +data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + +# TODO: this should change to 4.0 once 4.0.0 is released. +minServerVersion: '3.99' + +tests: + - + description: "DeleteOne succeeds after PrimarySteppedDown" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["delete"] + errorCode: 189 + operation: + name: "deleteOne" + arguments: + filter: { _id: 1 } + outcome: + result: + deletedCount: 1 + collection: + data: + - { _id: 2, x: 22 } + - + description: "DeleteOne succeeds after WriteConcernError ShutdownInProgress" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["delete"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + operation: + name: "deleteOne" + arguments: + filter: { _id: 1 } + outcome: + result: + deletedCount: 1 + collection: + data: + - { _id: 2, x: 22 } diff --git a/t/data/retryable-writes/deleteOne.json b/t/data/retryable-writes/deleteOne.json index a552b893..8e42e9c4 100644 --- a/t/data/retryable-writes/deleteOne.json +++ b/t/data/retryable-writes/deleteOne.json @@ -13,7 +13,11 @@ "tests": [ { "description": "DeleteOne is committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 } @@ -42,7 +46,11 @@ }, { "description": "DeleteOne is not committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 }, @@ -74,7 +82,11 @@ }, { "description": "DeleteOne is never committed", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 2 }, diff --git a/t/data/retryable-writes/deleteOne.yml b/t/data/retryable-writes/deleteOne.yml index 20d61966..d82bfc05 100644 --- a/t/data/retryable-writes/deleteOne.yml +++ b/t/data/retryable-writes/deleteOne.yml @@ -7,7 +7,10 @@ minServerVersion: '3.6' tests: - description: "DeleteOne is committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } operation: name: "deleteOne" @@ -21,7 +24,10 @@ tests: - { _id: 2, x: 22 } - description: "DeleteOne is not committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } data: { failBeforeCommitExceptionCode: 1 } operation: @@ -36,7 +42,10 @@ tests: - { _id: 2, x: 22 } - description: "DeleteOne is never committed" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 2 } data: { failBeforeCommitExceptionCode: 1 } operation: diff --git a/t/data/retryable-writes/findOneAndDelete-serverErrors.json b/t/data/retryable-writes/findOneAndDelete-serverErrors.json new file mode 100644 index 00000000..306b1423 --- /dev/null +++ b/t/data/retryable-writes/findOneAndDelete-serverErrors.json @@ -0,0 +1,108 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "minServerVersion": "3.99", + "tests": [ + { + "description": "FindOneAndDelete succeeds after PrimarySteppedDown", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "findAndModify" + ], + "errorCode": 189 + } + }, + "operation": { + "name": "findOneAndDelete", + "arguments": { + "filter": { + "x": { + "$gte": 11 + } + }, + "sort": { + "x": 1 + } + } + }, + "outcome": { + "result": { + "_id": 1, + "x": 11 + }, + "collection": { + "data": [ + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "FindOneAndDelete succeeds after WriteConcernError ShutdownInProgress", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "findAndModify" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "findOneAndDelete", + "arguments": { + "filter": { + "x": { + "$gte": 11 + } + }, + "sort": { + "x": 1 + } + } + }, + "outcome": { + "result": { + "_id": 1, + "x": 11 + }, + "collection": { + "data": [ + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/t/data/retryable-writes/findOneAndDelete-serverErrors.yml b/t/data/retryable-writes/findOneAndDelete-serverErrors.yml new file mode 100644 index 00000000..75071a36 --- /dev/null +++ b/t/data/retryable-writes/findOneAndDelete-serverErrors.yml @@ -0,0 +1,50 @@ +data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + +# TODO: this should change to 4.0 once 4.0.0 is released. +minServerVersion: '3.99' + +tests: + - + description: "FindOneAndDelete succeeds after PrimarySteppedDown" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["findAndModify"] + errorCode: 189 + operation: + name: "findOneAndDelete" + arguments: + filter: { x: { $gte: 11 }} + sort: { x: 1 } + outcome: + result: { _id: 1, x: 11 } + collection: + data: + - { _id: 2, x: 22 } + - + description: "FindOneAndDelete succeeds after WriteConcernError ShutdownInProgress" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["findAndModify"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + operation: + name: "findOneAndDelete" + arguments: + filter: { x: { $gte: 11 }} + sort: { x: 1 } + outcome: + result: { _id: 1, x: 11 } + collection: + data: + - { _id: 2, x: 22 } diff --git a/t/data/retryable-writes/findOneAndDelete.json b/t/data/retryable-writes/findOneAndDelete.json index d8f6c8fa..5963c432 100644 --- a/t/data/retryable-writes/findOneAndDelete.json +++ b/t/data/retryable-writes/findOneAndDelete.json @@ -13,7 +13,11 @@ "tests": [ { "description": "FindOneAndDelete is committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 } @@ -48,7 +52,11 @@ }, { "description": "FindOneAndDelete is not committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 }, @@ -86,7 +94,11 @@ }, { "description": "FindOneAndDelete is never committed", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 2 }, diff --git a/t/data/retryable-writes/findOneAndDelete.yml b/t/data/retryable-writes/findOneAndDelete.yml index d945bc4d..4f5d71ca 100644 --- a/t/data/retryable-writes/findOneAndDelete.yml +++ b/t/data/retryable-writes/findOneAndDelete.yml @@ -7,7 +7,10 @@ minServerVersion: '3.6' tests: - description: "FindOneAndDelete is committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } operation: name: "findOneAndDelete" @@ -21,7 +24,10 @@ tests: - { _id: 2, x: 22 } - description: "FindOneAndDelete is not committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } data: { failBeforeCommitExceptionCode: 1 } operation: @@ -36,7 +42,10 @@ tests: - { _id: 2, x: 22 } - description: "FindOneAndDelete is never committed" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 2 } data: { failBeforeCommitExceptionCode: 1 } operation: diff --git a/t/data/retryable-writes/findOneAndReplace-serverErrors.json b/t/data/retryable-writes/findOneAndReplace-serverErrors.json new file mode 100644 index 00000000..ab209b8d --- /dev/null +++ b/t/data/retryable-writes/findOneAndReplace-serverErrors.json @@ -0,0 +1,116 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "minServerVersion": "3.99", + "tests": [ + { + "description": "FindOneAndReplace succeeds after PrimarySteppedDown", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "findAndModify" + ], + "errorCode": 189 + } + }, + "operation": { + "name": "findOneAndReplace", + "arguments": { + "filter": { + "_id": 1 + }, + "replacement": { + "_id": 1, + "x": 111 + }, + "returnDocument": "Before" + } + }, + "outcome": { + "result": { + "_id": 1, + "x": 11 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 111 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "FindOneAndReplace succeeds after WriteConcernError ShutdownInProgress", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "findAndModify" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "findOneAndReplace", + "arguments": { + "filter": { + "_id": 1 + }, + "replacement": { + "_id": 1, + "x": 111 + }, + "returnDocument": "Before" + } + }, + "outcome": { + "result": { + "_id": 1, + "x": 11 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 111 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/t/data/retryable-writes/findOneAndReplace-serverErrors.yml b/t/data/retryable-writes/findOneAndReplace-serverErrors.yml new file mode 100644 index 00000000..8b6a2012 --- /dev/null +++ b/t/data/retryable-writes/findOneAndReplace-serverErrors.yml @@ -0,0 +1,54 @@ +data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + +# TODO: this should change to 4.0 once 4.0.0 is released. +minServerVersion: '3.99' + +tests: + - + description: "FindOneAndReplace succeeds after PrimarySteppedDown" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["findAndModify"] + errorCode: 189 + operation: + name: "findOneAndReplace" + arguments: + filter: { _id: 1 } + replacement: { _id: 1, x: 111 } + returnDocument: "Before" + outcome: + result: { _id: 1, x: 11 } + collection: + data: + - { _id: 1, x: 111 } + - { _id: 2, x: 22 } + - + description: "FindOneAndReplace succeeds after WriteConcernError ShutdownInProgress" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["findAndModify"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + operation: + name: "findOneAndReplace" + arguments: + filter: { _id: 1 } + replacement: { _id: 1, x: 111 } + returnDocument: "Before" + outcome: + result: { _id: 1, x: 11 } + collection: + data: + - { _id: 1, x: 111 } + - { _id: 2, x: 22 } diff --git a/t/data/retryable-writes/findOneAndReplace.json b/t/data/retryable-writes/findOneAndReplace.json index 22c03d74..20b9e0bc 100644 --- a/t/data/retryable-writes/findOneAndReplace.json +++ b/t/data/retryable-writes/findOneAndReplace.json @@ -13,7 +13,11 @@ "tests": [ { "description": "FindOneAndReplace is committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 } @@ -52,7 +56,11 @@ }, { "description": "FindOneAndReplace is not committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 }, @@ -94,7 +102,11 @@ }, { "description": "FindOneAndReplace is never committed", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 2 }, diff --git a/t/data/retryable-writes/findOneAndReplace.yml b/t/data/retryable-writes/findOneAndReplace.yml index f2dfc572..6e26d7cf 100644 --- a/t/data/retryable-writes/findOneAndReplace.yml +++ b/t/data/retryable-writes/findOneAndReplace.yml @@ -7,7 +7,10 @@ minServerVersion: '3.6' tests: - description: "FindOneAndReplace is committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } operation: name: "findOneAndReplace" @@ -23,7 +26,10 @@ tests: - { _id: 2, x: 22 } - description: "FindOneAndReplace is not committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } data: { failBeforeCommitExceptionCode: 1 } operation: @@ -40,7 +46,10 @@ tests: - { _id: 2, x: 22 } - description: "FindOneAndReplace is never committed" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 2 } data: { failBeforeCommitExceptionCode: 1 } operation: diff --git a/t/data/retryable-writes/findOneAndUpdate-serverErrors.json b/t/data/retryable-writes/findOneAndUpdate-serverErrors.json new file mode 100644 index 00000000..92f09ce9 --- /dev/null +++ b/t/data/retryable-writes/findOneAndUpdate-serverErrors.json @@ -0,0 +1,118 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "minServerVersion": "3.99", + "tests": [ + { + "description": "FindOneAndUpdate succeeds after PrimarySteppedDown", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "findAndModify" + ], + "errorCode": 189 + } + }, + "operation": { + "name": "findOneAndUpdate", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "Before" + } + }, + "outcome": { + "result": { + "_id": 1, + "x": 11 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 12 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "FindOneAndUpdate succeeds after WriteConcernError ShutdownInProgress", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "findAndModify" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "findOneAndUpdate", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "Before" + } + }, + "outcome": { + "result": { + "_id": 1, + "x": 11 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 12 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/t/data/retryable-writes/findOneAndUpdate-serverErrors.yml b/t/data/retryable-writes/findOneAndUpdate-serverErrors.yml new file mode 100644 index 00000000..7eddcdc0 --- /dev/null +++ b/t/data/retryable-writes/findOneAndUpdate-serverErrors.yml @@ -0,0 +1,54 @@ +data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + +# TODO: this should change to 4.0 once 4.0.0 is released. +minServerVersion: '3.99' + +tests: + - + description: "FindOneAndUpdate succeeds after PrimarySteppedDown" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["findAndModify"] + errorCode: 189 + operation: + name: "findOneAndUpdate" + arguments: + filter: { _id: 1 } + update: { $inc: { x : 1 }} + returnDocument: "Before" + outcome: + result: { _id: 1, x: 11 } + collection: + data: + - { _id: 1, x: 12 } + - { _id: 2, x: 22 } + - + description: "FindOneAndUpdate succeeds after WriteConcernError ShutdownInProgress" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["findAndModify"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + operation: + name: "findOneAndUpdate" + arguments: + filter: { _id: 1 } + update: { $inc: { x : 1 }} + returnDocument: "Before" + outcome: + result: { _id: 1, x: 11 } + collection: + data: + - { _id: 1, x: 12 } + - { _id: 2, x: 22 } diff --git a/t/data/retryable-writes/findOneAndUpdate.json b/t/data/retryable-writes/findOneAndUpdate.json index 11a76ab1..92d4f54b 100644 --- a/t/data/retryable-writes/findOneAndUpdate.json +++ b/t/data/retryable-writes/findOneAndUpdate.json @@ -13,7 +13,11 @@ "tests": [ { "description": "FindOneAndUpdate is committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 } @@ -53,7 +57,11 @@ }, { "description": "FindOneAndUpdate is not committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 }, @@ -96,7 +104,11 @@ }, { "description": "FindOneAndUpdate is never committed", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 2 }, diff --git a/t/data/retryable-writes/findOneAndUpdate.yml b/t/data/retryable-writes/findOneAndUpdate.yml index c9fc7672..a82bd034 100644 --- a/t/data/retryable-writes/findOneAndUpdate.yml +++ b/t/data/retryable-writes/findOneAndUpdate.yml @@ -7,7 +7,10 @@ minServerVersion: '3.6' tests: - description: "FindOneAndUpdate is committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } operation: name: "findOneAndUpdate" @@ -23,7 +26,10 @@ tests: - { _id: 2, x: 22 } - description: "FindOneAndUpdate is not committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } data: { failBeforeCommitExceptionCode: 1 } operation: @@ -40,7 +46,10 @@ tests: - { _id: 2, x: 22 } - description: "FindOneAndUpdate is never committed" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 2 } data: { failBeforeCommitExceptionCode: 1 } operation: diff --git a/t/data/retryable-writes/insertMany-serverErrors.json b/t/data/retryable-writes/insertMany-serverErrors.json new file mode 100644 index 00000000..17ba9b14 --- /dev/null +++ b/t/data/retryable-writes/insertMany-serverErrors.json @@ -0,0 +1,134 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + } + ], + "minServerVersion": "3.99", + "tests": [ + { + "description": "InsertMany succeeds after PrimarySteppedDown", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 189 + } + }, + "operation": { + "name": "insertMany", + "arguments": { + "documents": [ + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ], + "options": { + "ordered": true + } + } + }, + "outcome": { + "result": { + "insertedIds": { + "0": 2, + "1": 3 + } + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertMany succeeds after WriteConcernError ShutdownInProgress", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "insertMany", + "arguments": { + "documents": [ + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ], + "options": { + "ordered": true + } + } + }, + "outcome": { + "result": { + "insertedIds": { + "0": 2, + "1": 3 + } + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + } + ] +} diff --git a/t/data/retryable-writes/insertMany-serverErrors.yml b/t/data/retryable-writes/insertMany-serverErrors.yml new file mode 100644 index 00000000..aafaee81 --- /dev/null +++ b/t/data/retryable-writes/insertMany-serverErrors.yml @@ -0,0 +1,59 @@ +data: + - { _id: 1, x: 11 } + +# TODO: this should change to 4.0 once 4.0.0 is released. +minServerVersion: '3.99' + +tests: + - + description: "InsertMany succeeds after PrimarySteppedDown" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 189 + operation: + name: "insertMany" + arguments: + documents: + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + options: { ordered: true } + outcome: + result: + insertedIds: { 0: 2, 1: 3 } + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertMany succeeds after WriteConcernError ShutdownInProgress" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + operation: + name: "insertMany" + arguments: + documents: + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + options: { ordered: true } + outcome: + result: + insertedIds: { 0: 2, 1: 3 } + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } diff --git a/t/data/retryable-writes/insertMany.json b/t/data/retryable-writes/insertMany.json index 2d71cb91..74dd4a7a 100644 --- a/t/data/retryable-writes/insertMany.json +++ b/t/data/retryable-writes/insertMany.json @@ -9,7 +9,11 @@ "tests": [ { "description": "InsertMany succeeds after one network error", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 } @@ -59,7 +63,11 @@ }, { "description": "InsertMany with unordered execution", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 } @@ -109,7 +117,11 @@ }, { "description": "InsertMany fails after multiple network errors", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": "alwaysOn", "data": { "failBeforeCommitExceptionCode": 1 diff --git a/t/data/retryable-writes/insertMany.yml b/t/data/retryable-writes/insertMany.yml index f3ff5d2f..7559e340 100644 --- a/t/data/retryable-writes/insertMany.yml +++ b/t/data/retryable-writes/insertMany.yml @@ -6,7 +6,10 @@ minServerVersion: '3.6' tests: - description: "InsertMany succeeds after one network error" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } operation: name: "insertMany" @@ -25,7 +28,10 @@ tests: - { _id: 3, x: 33 } - description: "InsertMany with unordered execution" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } operation: name: "insertMany" @@ -44,6 +50,8 @@ tests: - { _id: 3, x: 33 } - description: "InsertMany fails after multiple network errors" + clientOptions: + retryWrites: true failPoint: # Normally, a mongod will insert the documents as a batch with a # single commit. If this fails, mongod may try to insert each @@ -51,6 +59,7 @@ tests: # single insert command may trigger the failpoint twice on each # driver attempt. This test permanently enables the fail point to # ensure the retry attempt always fails. + configureFailPoint: onPrimaryTransactionalWrite mode: "alwaysOn" data: { failBeforeCommitExceptionCode: 1 } operation: diff --git a/t/data/retryable-writes/insertOne-serverErrors.json b/t/data/retryable-writes/insertOne-serverErrors.json new file mode 100644 index 00000000..9ef84370 --- /dev/null +++ b/t/data/retryable-writes/insertOne-serverErrors.json @@ -0,0 +1,1000 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "minServerVersion": "3.99", + "tests": [ + { + "description": "InsertOne succeeds after connection failure", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "closeConnection": true + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after NotMaster", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 10107, + "closeConnection": false + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after NotMasterOrSecondary", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 13436, + "closeConnection": false + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after NotMasterNoSlaveOk", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 13435, + "closeConnection": false + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after InterruptedDueToReplStateChange", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 11602, + "closeConnection": false + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after InterruptedAtShutdown", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 11600, + "closeConnection": false + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after PrimarySteppedDown", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 189, + "closeConnection": false + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after ShutdownInProgress", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 91, + "closeConnection": false + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after HostNotFound", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 7, + "closeConnection": false + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after HostUnreachable", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 6, + "closeConnection": false + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after SocketException", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 9001, + "closeConnection": false + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after NetworkTimeout", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 89, + "closeConnection": false + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne fails after Interrupted", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 11601, + "closeConnection": false + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "error": true, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after WriteConcernError InterruptedAtShutdown", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "writeConcernError": { + "code": 11600, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after WriteConcernError InterruptedDueToReplStateChange", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "writeConcernError": { + "code": 11602, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after WriteConcernError PrimarySteppedDown", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "writeConcernError": { + "code": 189, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne succeeds after WriteConcernError ShutdownInProgress", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "result": { + "insertedId": 3 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne fails after multiple retryable writeConcernErrors", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "insert" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "error": true, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne fails after WriteConcernError Interrupted", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "writeConcernError": { + "code": 11601, + "errmsg": "operation was interrupted" + } + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "error": true, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + }, + { + "description": "InsertOne fails after WriteConcernError WriteConcernFailed", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "writeConcernError": { + "code": 64, + "codeName": "WriteConcernFailed", + "errmsg": "waiting for replication timed out", + "errInfo": { + "wtimeout": true + } + } + } + }, + "operation": { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + "outcome": { + "error": true, + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + } + } + ] +} diff --git a/t/data/retryable-writes/insertOne-serverErrors.yml b/t/data/retryable-writes/insertOne-serverErrors.yml new file mode 100644 index 00000000..69173b9c --- /dev/null +++ b/t/data/retryable-writes/insertOne-serverErrors.yml @@ -0,0 +1,471 @@ +data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + +# TODO: this should change to 4.0 once 4.0.0 is released. +minServerVersion: '3.99' + +tests: + - + description: "InsertOne succeeds after connection failure" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + closeConnection: true + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after NotMaster" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 10107 + closeConnection: false + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after NotMasterOrSecondary" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 13436 + closeConnection: false + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after NotMasterNoSlaveOk" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 13435 + closeConnection: false + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after InterruptedDueToReplStateChange" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 11602 + closeConnection: false + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after InterruptedAtShutdown" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 11600 + closeConnection: false + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after PrimarySteppedDown" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 189 + closeConnection: false + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after ShutdownInProgress" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 91 + closeConnection: false + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after HostNotFound" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 7 + closeConnection: false + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after HostUnreachable" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 6 + closeConnection: false + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after SocketException" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 9001 + closeConnection: false + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after NetworkTimeout" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 89 + closeConnection: false + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne fails after Interrupted" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 11601 + closeConnection: false + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + error: true + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - + description: "InsertOne succeeds after WriteConcernError InterruptedAtShutdown" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + writeConcernError: + code: 11600 + errmsg: Replication is being shut down + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after WriteConcernError InterruptedDueToReplStateChange" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + writeConcernError: + code: 11602 + errmsg: Replication is being shut down + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after WriteConcernError PrimarySteppedDown" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + writeConcernError: + code: 189 + errmsg: Replication is being shut down + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne succeeds after WriteConcernError ShutdownInProgress" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + result: + insertedId: 3 + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - + description: "InsertOne fails after multiple retryable writeConcernErrors" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 2 } + data: + failCommands: ["insert"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + error: true + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } # The write was still applied. + - + description: "InsertOne fails after WriteConcernError Interrupted" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + writeConcernError: + code: 11601 + errmsg: operation was interrupted + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + error: true + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } # The write was still applied. + - + description: "InsertOne fails after WriteConcernError WriteConcernFailed" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + writeConcernError: + code: 64 + codeName: WriteConcernFailed + errmsg: waiting for replication timed out + errInfo: {wtimeout: True} + operation: + name: "insertOne" + arguments: + document: { _id: 3, x: 33 } + outcome: + error: true + collection: + data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } # The write was still applied. diff --git a/t/data/retryable-writes/insertOne.json b/t/data/retryable-writes/insertOne.json index 58ad6cc6..123d51ed 100644 --- a/t/data/retryable-writes/insertOne.json +++ b/t/data/retryable-writes/insertOne.json @@ -13,7 +13,11 @@ "tests": [ { "description": "InsertOne is committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 } @@ -51,7 +55,11 @@ }, { "description": "InsertOne is not committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 }, @@ -92,7 +100,11 @@ }, { "description": "InsertOne is never committed", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 2 }, diff --git a/t/data/retryable-writes/insertOne.yml b/t/data/retryable-writes/insertOne.yml index 5a303ddc..0e535ddb 100644 --- a/t/data/retryable-writes/insertOne.yml +++ b/t/data/retryable-writes/insertOne.yml @@ -7,7 +7,10 @@ minServerVersion: '3.6' tests: - description: "InsertOne is committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } operation: name: "insertOne" @@ -23,7 +26,10 @@ tests: - { _id: 3, x: 33 } - description: "InsertOne is not committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } data: { failBeforeCommitExceptionCode: 1 } operation: @@ -40,7 +46,10 @@ tests: - { _id: 3, x: 33 } - description: "InsertOne is never committed" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 2 } data: { failBeforeCommitExceptionCode: 1 } operation: diff --git a/t/data/retryable-writes/replaceOne-serverErrors.json b/t/data/retryable-writes/replaceOne-serverErrors.json new file mode 100644 index 00000000..b9c449a8 --- /dev/null +++ b/t/data/retryable-writes/replaceOne-serverErrors.json @@ -0,0 +1,116 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "minServerVersion": "3.99", + "tests": [ + { + "description": "ReplaceOne succeeds after PrimarySteppedDown", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "update" + ], + "errorCode": 189 + } + }, + "operation": { + "name": "replaceOne", + "arguments": { + "filter": { + "_id": 1 + }, + "replacement": { + "_id": 1, + "x": 111 + } + } + }, + "outcome": { + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 111 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "ReplaceOne succeeds after WriteConcernError ShutdownInProgress", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "update" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "replaceOne", + "arguments": { + "filter": { + "_id": 1 + }, + "replacement": { + "_id": 1, + "x": 111 + } + } + }, + "outcome": { + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 111 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/t/data/retryable-writes/replaceOne-serverErrors.yml b/t/data/retryable-writes/replaceOne-serverErrors.yml new file mode 100644 index 00000000..bd2bb0f3 --- /dev/null +++ b/t/data/retryable-writes/replaceOne-serverErrors.yml @@ -0,0 +1,58 @@ +data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + +# TODO: this should change to 4.0 once 4.0.0 is released. +minServerVersion: '3.99' + +tests: + - + description: "ReplaceOne succeeds after PrimarySteppedDown" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["update"] + errorCode: 189 + operation: + name: "replaceOne" + arguments: + filter: { _id: 1 } + replacement: { _id: 1, x: 111 } + outcome: + result: + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + collection: + data: + - { _id: 1, x: 111 } + - { _id: 2, x: 22 } + - + description: "ReplaceOne succeeds after WriteConcernError ShutdownInProgress" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["update"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + operation: + name: "replaceOne" + arguments: + filter: { _id: 1 } + replacement: { _id: 1, x: 111 } + outcome: + result: + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + collection: + data: + - { _id: 1, x: 111 } + - { _id: 2, x: 22 } diff --git a/t/data/retryable-writes/replaceOne.json b/t/data/retryable-writes/replaceOne.json index f31966c3..6e9107b7 100644 --- a/t/data/retryable-writes/replaceOne.json +++ b/t/data/retryable-writes/replaceOne.json @@ -13,7 +13,11 @@ "tests": [ { "description": "ReplaceOne is committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 } @@ -52,7 +56,11 @@ }, { "description": "ReplaceOne is not committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 }, @@ -94,7 +102,11 @@ }, { "description": "ReplaceOne is never committed", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 2 }, diff --git a/t/data/retryable-writes/replaceOne.yml b/t/data/retryable-writes/replaceOne.yml index 08fbda9a..1eed95ed 100644 --- a/t/data/retryable-writes/replaceOne.yml +++ b/t/data/retryable-writes/replaceOne.yml @@ -7,7 +7,10 @@ minServerVersion: '3.6' tests: - description: "ReplaceOne is committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } operation: name: "replaceOne" @@ -25,7 +28,10 @@ tests: - { _id: 2, x: 22 } - description: "ReplaceOne is not committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } data: { failBeforeCommitExceptionCode: 1 } operation: @@ -44,7 +50,10 @@ tests: - { _id: 2, x: 22 } - description: "ReplaceOne is never committed" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 2 } data: { failBeforeCommitExceptionCode: 1 } operation: diff --git a/t/data/retryable-writes/updateOne-serverErrors.json b/t/data/retryable-writes/updateOne-serverErrors.json new file mode 100644 index 00000000..84ebf877 --- /dev/null +++ b/t/data/retryable-writes/updateOne-serverErrors.json @@ -0,0 +1,118 @@ +{ + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "minServerVersion": "3.99", + "tests": [ + { + "description": "UpdateOne succeeds after PrimarySteppedDown", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "update" + ], + "errorCode": 189 + } + }, + "operation": { + "name": "updateOne", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + "outcome": { + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 12 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "UpdateOne succeeds after WriteConcernError ShutdownInProgress", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "update" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operation": { + "name": "updateOne", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + "outcome": { + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + }, + "collection": { + "data": [ + { + "_id": 1, + "x": 12 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/t/data/retryable-writes/updateOne-serverErrors.yml b/t/data/retryable-writes/updateOne-serverErrors.yml new file mode 100644 index 00000000..1d468861 --- /dev/null +++ b/t/data/retryable-writes/updateOne-serverErrors.yml @@ -0,0 +1,58 @@ +data: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + +# TODO: this should change to 4.0 once 4.0.0 is released. +minServerVersion: '3.99' + +tests: + - + description: "UpdateOne succeeds after PrimarySteppedDown" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["update"] + errorCode: 189 + operation: + name: "updateOne" + arguments: + filter: { _id: 1 } + update: { $inc: { x : 1 }} + outcome: + result: + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + collection: + data: + - { _id: 1, x: 12 } + - { _id: 2, x: 22 } + - + description: "UpdateOne succeeds after WriteConcernError ShutdownInProgress" + clientOptions: + retryWrites: true + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["update"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + operation: + name: "updateOne" + arguments: + filter: { _id: 1 } + update: { $inc: { x : 1 }} + outcome: + result: + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + collection: + data: + - { _id: 1, x: 12 } + - { _id: 2, x: 22 } diff --git a/t/data/retryable-writes/updateOne.json b/t/data/retryable-writes/updateOne.json index 96aa2bde..c342c562 100644 --- a/t/data/retryable-writes/updateOne.json +++ b/t/data/retryable-writes/updateOne.json @@ -13,7 +13,11 @@ "tests": [ { "description": "UpdateOne is committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 } @@ -53,7 +57,11 @@ }, { "description": "UpdateOne is not committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 }, @@ -96,7 +104,11 @@ }, { "description": "UpdateOne is never committed", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 2 }, @@ -135,7 +147,11 @@ }, { "description": "UpdateOne with upsert is committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 } @@ -182,7 +198,11 @@ }, { "description": "UpdateOne with upsert is not committed on first attempt", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 1 }, @@ -232,7 +252,11 @@ }, { "description": "UpdateOne with upsert is never committed", + "clientOptions": { + "retryWrites": true + }, "failPoint": { + "configureFailPoint": "onPrimaryTransactionalWrite", "mode": { "times": 2 }, diff --git a/t/data/retryable-writes/updateOne.yml b/t/data/retryable-writes/updateOne.yml index 810e49f2..900d4e7f 100644 --- a/t/data/retryable-writes/updateOne.yml +++ b/t/data/retryable-writes/updateOne.yml @@ -7,7 +7,10 @@ minServerVersion: '3.6' tests: - description: "UpdateOne is committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } operation: name: "updateOne" @@ -25,7 +28,10 @@ tests: - { _id: 2, x: 22 } - description: "UpdateOne is not committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } data: { failBeforeCommitExceptionCode: 1 } operation: @@ -44,7 +50,10 @@ tests: - { _id: 2, x: 22 } - description: "UpdateOne is never committed" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 2 } data: { failBeforeCommitExceptionCode: 1 } operation: @@ -60,7 +69,10 @@ tests: - { _id: 2, x: 22 } - description: "UpdateOne with upsert is committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } operation: name: "updateOne" @@ -81,7 +93,10 @@ tests: - { _id: 3, x: 34 } - description: "UpdateOne with upsert is not committed on first attempt" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 1 } data: { failBeforeCommitExceptionCode: 1 } operation: @@ -103,7 +118,10 @@ tests: - { _id: 3, x: 34 } - description: "UpdateOne with upsert is never committed" + clientOptions: + retryWrites: true failPoint: + configureFailPoint: onPrimaryTransactionalWrite mode: { times: 2 } data: { failBeforeCommitExceptionCode: 1 } operation: From 52c9ac53e3cc872be49c62b85142198233a183f5 Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Mon, 18 Jun 2018 17:43:31 +0100 Subject: [PATCH 02/38] minor: Added Callback class to factor out basic monitoring callback work --- t/lib/MongoDBTest/Callback.pm | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 t/lib/MongoDBTest/Callback.pm diff --git a/t/lib/MongoDBTest/Callback.pm b/t/lib/MongoDBTest/Callback.pm new file mode 100644 index 00000000..e87980d0 --- /dev/null +++ b/t/lib/MongoDBTest/Callback.pm @@ -0,0 +1,22 @@ +package MongoDBTest::Callback; + +use Moo; +use Storable qw/ dclone /; + +has events => ( + is => 'lazy', + default => sub { [] }, + clearer => 1, +); + +sub callback { + my $self = shift; + return sub { push @{ $self->events }, dclone $_[0] }; +} + +sub count { + my $self = shift; + return scalar( @{ $self->events } ); +} + +1; From 2fea40ff60d89ab6504fac4b232e230b850798bf Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Mon, 18 Jun 2018 17:44:31 +0100 Subject: [PATCH 03/38] PERL-918 - refactored spec tests for retryable writes --- t/lib/MongoDBTest.pm | 18 ++++++++ t/retryable-writes-spec.t | 91 +++++++++++++++++---------------------- 2 files changed, 57 insertions(+), 52 deletions(-) diff --git a/t/lib/MongoDBTest.pm b/t/lib/MongoDBTest.pm index f9cd0871..83ca55a4 100644 --- a/t/lib/MongoDBTest.pm +++ b/t/lib/MongoDBTest.pm @@ -36,6 +36,8 @@ our @EXPORT_OK = qw( skip_unless_mongod skip_unless_failpoints_available skip_unless_sessions + to_snake_case + remap_hashref_to_snake_case uri_escape get_unique_collection get_features @@ -271,6 +273,22 @@ sub uri_escape { return $str; } +sub to_snake_case { + my $t = shift; + $t =~ s{([A-Z])}{_\L$1}g; + return $t; +} + +sub remap_hashref_to_snake_case { + my $hash = shift; + return { + map { + my $k = to_snake_case( $_ ); + $k => $hash->{ $_ } + } keys %$hash + } +} + sub uuid_to_string { my $uuid = shift; return join "-", unpack( "H8H4H4H4H12", $uuid ); diff --git a/t/retryable-writes-spec.t b/t/retryable-writes-spec.t index 44d87c62..549c0e32 100644 --- a/t/retryable-writes-spec.t +++ b/t/retryable-writes-spec.t @@ -17,7 +17,8 @@ use warnings; use JSON::MaybeXS; use Path::Tiny 0.054; # basename with suffix use Test::More 0.88; -use Test::Fatal; +use Test::Deep ':v1'; +use Safe::Isa; use lib "t/lib"; @@ -32,6 +33,8 @@ use MongoDBTest qw/ skip_unless_mongod skip_unless_sessions skip_unless_failpoints_available + to_snake_case + remap_hashref_to_snake_case get_features /; @@ -46,7 +49,7 @@ my $server_type = server_type($conn); sub run_test { my ( $coll, $test ) = @_; - enable_failpoint( $test->{failPoint} ) if exists $test->{failPoint}; + enable_failpoint( $test->{failPoint} ); my $op = $test->{operation}; my $method = $op->{name}; @@ -63,33 +66,13 @@ sub run_test { if ( !exists $test->{outcome}{error} && exists $test->{outcome}->{result} ) { - #Dwarn $ret; - #Dwarn $test->{outcome}; - for my $res_key ( keys %{ $test->{outcome}->{result} } ) { - next if $res_key eq 'upsertedCount' && ! $ret->can('upserted_count'); # Driver does not parse this value on all things? - # next if $res_key eq 'upsertedId' && ! defined $ret->upserted_id; # upserted id is always present - my $res = $test->{outcome}->{result}->{$res_key}; - - if ( $res_key eq 'insertedIds' ) { - my $ret_parsed = {}; - for my $item ( @{ $ret->inserted } ) { - $ret_parsed->{$item->{index}} = $item->{_id}; - } - is_deeply $ret_parsed, $test->{outcome}->{result}->{insertedIds}, 'insertedIds correct in result'; - next; - } - if ( $res_key eq 'upsertedIds' ) { - my $ret_parsed = {}; - for my $item ( @{ $ret->upserted } ) { - $ret_parsed->{$item->{index}} = $item->{_id}; - } - is_deeply $ret_parsed, $test->{outcome}->{result}->{upsertedIds}, 'upsertedIds correct in result'; - next; - } - my $ret_key = $res_key; - $ret_key =~ s{([A-Z])}{_\L$1}g; + my $expected = remap_hashref_to_snake_case( $test->{outcome}->{result} ); + # not all commands return an upserted count + delete $expected->{upserted_count} unless $ret->$_can('upserted_count'); - is $ret->{$ret_key}, $res, "$res_key correct in result"; + for my $key ( keys %$expected ) { + my $got = ref $ret eq 'HASH' ? $ret->{$key} : $ret->$key; + cmp_deeply $got, $expected->{$key}, "$key result as expected"; } } @@ -97,7 +80,7 @@ sub run_test { my $coll_expected = $test->{outcome}->{collection}->{data}; is_deeply \@coll_outcome, $coll_expected, 'Collection has correct outcome'; - disable_failpoint() if exists $test->{failPoint}; + disable_failpoint( $test->{failPoint} ); } sub do_delete_one { @@ -152,6 +135,15 @@ sub do_find_one_and_delete { return $coll->find_one_and_delete( $filter, $options ); } +my %bulk_remap = ( + insert_one => [qw( document )], + update_one => [qw( filter update )], + update_many => [qw( filter update )], + replace_one => [qw( filter replacement )], + delete_one => [qw( filter )], + delete_many => [qw( filter )], +); + sub do_bulk_write { my ( $self, $coll, $args ) = @_; my $options = { @@ -164,24 +156,13 @@ sub do_bulk_write { my @arguments; for my $request ( @{ $args->{requests} } ) { - if ( $request->{name} eq 'insertOne' ) { - push @arguments, { insert_one => [ $request->{arguments}->{document} ] }; - } elsif ( $request->{name} eq 'updateOne' ) { - push @arguments, { update_one => [ - $request->{arguments}->{filter}, - $request->{arguments}->{update}, - ( defined $request->{arguments}->{upsert} - ? ( { upsert => $request->{arguments}->{upsert} ? 1 : 0 } ) - : () ) - ] }; - } elsif ( $request->{name} eq 'deleteOne' ) { - push @arguments, { delete_one => [ $request->{arguments}->{filter} ] }; - } elsif ( $request->{name} eq 'replaceOne' ) { - push @arguments, { replace_one => [ - $request->{arguments}->{filter}, - $request->{arguments}->{replacement} - ] }; - } + my $req_name = to_snake_case( $request->{name} ); + my @req_fields = @{ $bulk_remap{ $req_name } }; + my @arg = map { + delete $request->{arguments}->{ $_ } + } @req_fields; + push @arg, $request->{arguments} if keys %{ $request->{arguments} }; + push @arguments, { $req_name => \@arg }; } return $coll->bulk_write( \@arguments, $options ); } @@ -230,6 +211,7 @@ while ( my $path = $iterator->() ) { my $coll = get_unique_collection( $testdb, 'retry_write' ); my $ret = $coll->insert_many( $plan->{data} ); my $description = $test->{description}; + subtest $description => sub { run_test( $coll, $test ); } @@ -238,17 +220,22 @@ while ( my $path = $iterator->() ) { } sub enable_failpoint { - my $doc = shift; + my $failpoint = shift; + return unless defined $failpoint; $conn->send_admin_command([ - configureFailPoint => 'onPrimaryTransactionalWrite', - %$doc, + configureFailPoint => $failpoint->{configureFailPoint}, + mode => $failpoint->{mode}, + defined $failpoint->{data} + ? ( data => $failpoint->{data} ) + : (), ]); } sub disable_failpoint { - my $doc = shift; + my $failpoint = shift; + return unless defined $failpoint; $conn->send_admin_command([ - configureFailPoint => 'onPrimaryTransactionalWrite', + configureFailPoint => $failpoint->{configureFailPoint}, mode => 'off', ]); } From 4d2d53adbde9671e2953305a5a688ead833fb126 Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Mon, 18 Jun 2018 18:01:25 +0100 Subject: [PATCH 04/38] PERL-918 Add new error names and codes --- lib/MongoDB/Error.pm | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/MongoDB/Error.pm b/lib/MongoDB/Error.pm index 4e76cd8d..c9399f76 100644 --- a/lib/MongoDB/Error.pm +++ b/lib/MongoDB/Error.pm @@ -40,6 +40,8 @@ my $ERROR_CODES; BEGIN { $ERROR_CODES = { BAD_VALUE => 2, + HOST_UNREACHABLE => 6, + HOST_NOT_FOUND => 7, UNKNOWN_ERROR => 8, USER_NOT_FOUND => 11, NAMESPACE_NOT_FOUND => 26, @@ -48,9 +50,15 @@ BEGIN { EXCEEDED_TIME_LIMIT => 50, COMMAND_NOT_FOUND => 59, WRITE_CONCERN_ERROR => 64, + NETWORK_TIMEOUT => 89, + SHUTDOWN_IN_PROGRESS => 91, + PRIMARY_STEPPED_DOWN => 189, + SOCKET_EXCEPTION => 9001, NOT_MASTER => 10107, DUPLICATE_KEY => 11000, DUPLICATE_KEY_UPDATE => 11001, # legacy before 2.6 + INTERRUPTED_AT_SHUTDOWN => 11600, + INTERRUPTED_DUE_TO_REPL_STATE_CHANGE => 11602, DUPLICATE_KEY_CAPPED => 12582, # legacy before 2.6 UNRECOGNIZED_COMMAND => 13390, # mongos error before 2.4 NOT_MASTER_NO_SLAVE_OK => 13435, From ce08190a7362d869448490b54fa907899b720de9 Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Tue, 19 Jun 2018 18:28:09 +0100 Subject: [PATCH 05/38] PERL-918 Finish updating tests for retryable-writes --- lib/MongoDB/Error.pm | 60 ++++++++++++++++++++++++ lib/MongoDB/MongoClient.pm | 3 +- lib/MongoDB/Op/_Command.pm | 2 + lib/MongoDB/Role/_SessionSupport.pm | 14 ++++++ lib/MongoDB/Role/_SingleBatchDocWrite.pm | 4 +- 5 files changed, 81 insertions(+), 2 deletions(-) diff --git a/lib/MongoDB/Error.pm b/lib/MongoDB/Error.pm index c9399f76..e6c77409 100644 --- a/lib/MongoDB/Error.pm +++ b/lib/MongoDB/Error.pm @@ -32,6 +32,7 @@ use MongoDB::_Types qw( ); use Scalar::Util (); use Sub::Quote (); +use Safe::Isa; use Exporter 5.57 qw/import/; use namespace::clean -except => ['import']; @@ -121,6 +122,62 @@ sub throw { # an error occurs. sub _is_resumable { 1 } +# internal flag for if this error type specifically can be retried regardless +# of other state. See _is_retryable which contains the full retryable error +# logic. +sub __is_retryable_error { 0 } + +sub _check_is_retryable_code { + my $code = $_[-1]; + + my @retryable_codes = ( + MongoDB::Error::HOST_NOT_FOUND(), + MongoDB::Error::HOST_UNREACHABLE(), + MongoDB::Error::NETWORK_TIMEOUT(), + MongoDB::Error::SHUTDOWN_IN_PROGRESS(), + MongoDB::Error::PRIMARY_STEPPED_DOWN(), + MongoDB::Error::SOCKET_EXCEPTION(), + MongoDB::Error::NOT_MASTER(), + MongoDB::Error::INTERRUPTED_AT_SHUTDOWN(), + MongoDB::Error::INTERRUPTED_DUE_TO_REPL_STATE_CHANGE(), + MongoDB::Error::NOT_MASTER_NO_SLAVE_OK(), + MongoDB::Error::NOT_MASTER_OR_SECONDARY(), + ); + + return 1 if grep { $code == $_ } @retryable_codes; + return 0; +} + +sub _check_is_retryable_message { + my $message = $_[-1]; + + return 1 if $message =~ /(not master|node is recovering)/i; + return 0; +} + +# indicates if this error can be retried under retryable writes +sub _is_retryable { + my $self = shift; + + if ( $self->$_can( 'result' ) ) { + return 1 if _check_is_retryable_code( $self->result->last_code ); + } + + if ( $self->$_can( 'code' ) ) { + return 1 if _check_is_retryable_code( $self->code ); + } + + return 1 if _check_is_retryable_message( $self->message ); + + if ( $self->$_isa( 'MongoDB::WriteConcernError' ) ) { + return 1 if _check_is_retryable_code( $self->result->output->{writeConcernError}{code} ); + return 1 if _check_is_retryable_message( $self->result->output->{writeConcernError}{message} ); + } + + # Defaults to 0 unless its a network exception + return $self->__is_retryable_error; +} + #--------------------------------------------------------------------------# # Subclasses with attributes included inline below #--------------------------------------------------------------------------# @@ -208,6 +265,7 @@ use namespace::clean; extends 'MongoDB::Error'; sub _is_resumable { 1 } +sub __is_retryable_error { 1 } package MongoDB::HandshakeError; use Moo; @@ -225,6 +283,8 @@ use Moo; use namespace::clean; extends 'MongoDB::Error'; +sub __is_retryable_error { 1 } + package MongoDB::ExecutionTimeout; use Moo; use namespace::clean; diff --git a/lib/MongoDB/MongoClient.pm b/lib/MongoDB/MongoClient.pm index 0476b02f..2a6913d4 100644 --- a/lib/MongoDB/MongoClient.pm +++ b/lib/MongoDB/MongoClient.pm @@ -1606,7 +1606,8 @@ sub send_retryable_write_op { my $retry_link = $self->{_topology}->get_writable_link; # Rare chance that the new link is not retryable - unless ( $retry_link->supports_retryWrites ) { + unless ( $retry_link->supports_retryWrites + && $err->$_call_if_can('_is_retryable') ) { WITH_ASSERTS ? ( confess $err ) : ( die $err ); } diff --git a/lib/MongoDB/Op/_Command.pm b/lib/MongoDB/Op/_Command.pm index eed6e74b..fd3d9955 100644 --- a/lib/MongoDB/Op/_Command.pm +++ b/lib/MongoDB/Op/_Command.pm @@ -130,6 +130,8 @@ sub execute { $self->_update_session_and_cluster_time($res); + $self->_assert_session_errors($res); + return $res; } diff --git a/lib/MongoDB/Role/_SessionSupport.pm b/lib/MongoDB/Role/_SessionSupport.pm index 8e76a2cb..72f33c85 100644 --- a/lib/MongoDB/Role/_SessionSupport.pm +++ b/lib/MongoDB/Role/_SessionSupport.pm @@ -92,6 +92,20 @@ sub _update_operation_time { return; } +# Certain errors have to happen as soon as possible, such as write concern +# errors in a retryable write. This has to be seperate to the other functions +# due to not all result objects having the base response inside, so cannot be +# used to parse operationTime or $clusterTime +sub _assert_session_errors { + my ( $self, $response ) = @_; + + if ( $self->retryable_write ) { + $response->assert_no_write_concern_error; + } + + return; +} + sub __extract_from { my ( $self, $response, $key ) = @_; diff --git a/lib/MongoDB/Role/_SingleBatchDocWrite.pm b/lib/MongoDB/Role/_SingleBatchDocWrite.pm index 70430f22..abbc470a 100644 --- a/lib/MongoDB/Role/_SingleBatchDocWrite.pm +++ b/lib/MongoDB/Role/_SingleBatchDocWrite.pm @@ -222,12 +222,14 @@ sub _send_write_command { # otherwise, construct the desired result object, calling back # on class-specific parser to generate additional attributes - return $result_class->_new( + my $built_result = $result_class->_new( write_errors => ( $res->{writeErrors} ? $res->{writeErrors} : [] ), write_concern_errors => ( $res->{writeConcernError} ? [ $res->{writeConcernError} ] : [] ), $self->_parse_cmd($res), ); + $self->_assert_session_errors( $built_result ); + return $built_result; } else { return MongoDB::UnacknowledgedResult->_new( From 1dc7d81109d19fd05bf851e7eff5de12995e509b Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Thu, 21 Jun 2018 16:30:31 +0100 Subject: [PATCH 06/38] PERL-918 re-arrange retry error logic and add clearer comments --- lib/MongoDB/MongoClient.pm | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/MongoDB/MongoClient.pm b/lib/MongoDB/MongoClient.pm index 2a6913d4..a98559c1 100644 --- a/lib/MongoDB/MongoClient.pm +++ b/lib/MongoDB/MongoClient.pm @@ -1603,11 +1603,18 @@ sub send_retryable_write_op { # attempt the op the first time eval { ($result) = $self->_try_write_op_for_link( $link, $op ); 1 } or do { my $err = length($@) ? $@ : "caught error, but it was lost in eval unwind"; + + # If the error is not retryable, then drop out + unless ( $err->$_call_if_can('_is_retryable') ) { + WITH_ASSERTS ? ( confess $err ) : ( die $err ); + } + + # Must check if error is retryable before getting the link, in case we + # get a 'no writable servers' error my $retry_link = $self->{_topology}->get_writable_link; # Rare chance that the new link is not retryable - unless ( $retry_link->supports_retryWrites - && $err->$_call_if_can('_is_retryable') ) { + unless ( $retry_link->supports_retryWrites ) { WITH_ASSERTS ? ( confess $err ) : ( die $err ); } From c08d09d6212419fc21875c3e95498a71c761f7a5 Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Thu, 31 May 2018 15:41:23 +0100 Subject: [PATCH 07/38] PERL-875 first pass of transactions in sessions --- devel/config/replicaset-multi-4.0.yml | 11 ++ lib/MongoDB/ClientSession.pm | 148 +++++++++++++++++++++++++- lib/MongoDB/Error.pm | 8 ++ lib/MongoDB/Role/_SessionSupport.pm | 14 ++- lib/MongoDB/_Topology.pm | 8 ++ lib/MongoDB/_Types.pm | 3 + 6 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 devel/config/replicaset-multi-4.0.yml diff --git a/devel/config/replicaset-multi-4.0.yml b/devel/config/replicaset-multi-4.0.yml new file mode 100644 index 00000000..aec05d85 --- /dev/null +++ b/devel/config/replicaset-multi-4.0.yml @@ -0,0 +1,11 @@ +--- +type: replica +setName: foo +default_args: -v --noprealloc --smallfiles --bind_ip 0.0.0.0 --nssize 6 --quiet +default_version: 4.0 +mongod: + - name: host1 + - name: host2 + - name: host3 + +# vim: ts=4 sts=4 sw=4 et: diff --git a/lib/MongoDB/ClientSession.pm b/lib/MongoDB/ClientSession.pm index c25ef3c7..8de4cf48 100644 --- a/lib/MongoDB/ClientSession.pm +++ b/lib/MongoDB/ClientSession.pm @@ -24,9 +24,11 @@ our $VERSION = 'v1.999.1'; use MongoDB::Error; use Moo; +use MongoDB::ReadConcern; use MongoDB::_Types qw( Document BSONTimestamp + TransactionState ); use Types::Standard qw( Maybe @@ -72,7 +74,11 @@ Options provided for this particular session. Available options include: Consistency|https://docs.mongodb.com/manual/core/read-isolation-consistency-recency/#causal-consistency>. Note that causalConsistency does not apply for unacknowledged writes. Defaults to true. - +* C - Options to use by default for transactions + created with this session. If when creating a transaction, none or only some of + the transaction options are defined, these options will be used as a fallback. + Defaults to inheriting from the parent client. See L for + available options. =cut @@ -85,7 +91,11 @@ has options => ( coerce => sub { $_[0] = { causalConsistency => 1, - %{ $_[0] } + %{ $_[0] }, + # applied after to not override the clone with the original + defaultTransactionOptions => { + %{ $_[0]->{defaultTransactionOptions} }, + }, }; }, ); @@ -98,6 +108,20 @@ has _server_session => ( clearer => '__clear_server_session', ); +has _current_transaction_settings => ( + is => 'rwp', + isa => HashRef, + init_arg => undef, + default => undef, + clearer => '_clear_current_transaction_settings', +); + +has _transaction_state => ( + is => 'rwp', + isa => TransactionState, + default => 'none', +); + =attr operation_time The last operation time. This is updated when an operation is performed during @@ -215,6 +239,122 @@ sub advance_operation_time { return; } +=method in_transaction_state + + do { ... } if $session->in_transaction_state( 'starting', 'in_progress', ... ); + +Returns 1 if the session is in one of the specified transaction states. +Returns a false value if not in any of the states defined as an argument. + +=cut + +sub in_transaction_state { + my ( $self, @states ) = @_; + return 1 if scalar ( grep { $_ eq $self->_transaction_state } @states ); + return; +} + +=method start_transaction + +Start a transaction in this session. Takes a hashref of options which can contain the following options: + +=for :list +* C - The read concern to use for the first command in this + transaction. If not defined here or in the C in + L, will inherit from the parent client. +* C - The write concern to use for committing or aborting this + transaction. As per C, if not defined here then the value defined + in C will be used, or the parent client if not + defined. +* C - The read preference to use for all read operations in + this transaction. If not defined, then will inherit from + C or from the parent client. This value will + override all other read preferences set in any subsequent commands inside this + transaction. + +=cut + +sub start_transaction { + my ( $self, $opts ) = @_; + + MongoDB::TransactionError->throw("Transaction already in progress") + if $self->in_transaction_state( 'starting', 'in_progress' ); + + MongoDB::ConfigurationError->throw("Transactions are unsupported on this deployment") + unless $self->_client->_topology->_supports_transactions; + + $opts ||= {}; + $opts = { %{ $self->options->{defaultTransactionOptions} }, %$opts }; + + $self->_set_current_transaction_settings( $opts ); + + $self->_set_transaction_state('starting'); + + return; +} + +=method commit_transaction + +Commit the current transaction. This will use the writeConcern set on this transaction. + +=cut + +sub commit_transaction { + my $self = shift; + + MongoDB::TransactionError->throw("No transaction started") + if $self->_transaction_state eq 'none'; + + # Error message tweaked to use our function names + MongoDB::TransactionError->throw("Cannot call commit_transaction after calling abort_transaction") + if $self->_transaction_state eq 'aborted'; + + # TODO Actually commit the transaction - including retrying even if not enabled. + + $self->_set_transaction_state('committed'); + + return; +} + +=method abort_transaction + +Abort the current transaction. This will use the writeConcern set on this transaction. + +=cut + +sub abort_transaction { + my $self = shift; + + MongoDB::TransactionError->throw("No transaction started") + if $self->in_transaction_state( 'none' ); + + # Error message tweaked to use our function names + MongoDB::TransactionError->throw("Cannot call abort_transaction after calling commit_transaction") + if $self->in_transaction_state( 'committed' ); + + # Error message tweaked to use our function names + MongoDB::TransactionError->throw("Cannot call abort_transaction twice") + if $self->in_transaction_state( 'aborted' ); + + # TODO Actually abort the transaction, ignoring any errors as theres nothing that can be done. + + $self->_set_transaction_state('aborted'); + + return; +} + +sub _get_transaction_read_concern { + my $self = shift; + + # readConcern is merged during start_transaction + if ( defined $self->_current_transaction_settings->{readConcern} ) { + return MongoDB::ReadConcern->new( $self->_current_transaction_settings->{readConcern} ); + } + + # Default to the clients read concern + return $self->_client->read_concern; +} + =method end_session $session->end_session; @@ -227,6 +367,10 @@ recycling. Has no effect after calling for the first time. sub end_session { my ( $self ) = @_; + if ( $self->_transaction_state eq 'in_progress' ) { + # Ignore all errors + eval { $self->abort_transaction }; + } if ( defined $self->_server_session ) { $self->client->_server_session_pool->retire_server_session( $self->_server_session ); $self->__clear_server_session; diff --git a/lib/MongoDB/Error.pm b/lib/MongoDB/Error.pm index e6c77409..413f6430 100644 --- a/lib/MongoDB/Error.pm +++ b/lib/MongoDB/Error.pm @@ -295,6 +295,12 @@ use Moo; use namespace::clean; extends 'MongoDB::TimeoutError'; +#Transaction errors +package MongoDB::TransactionError; +use Moo; +use namespace::clean; +extends 'MongoDB::Error'; + # Database errors package MongoDB::DuplicateKeyError; use Moo; @@ -474,6 +480,8 @@ To retry failures automatically, consider using L. | | | |->MongoDB::NetworkTimeout | + |->MongoDB::TransactionError + | |->MongoDB::UsageError All classes inherit from C. diff --git a/lib/MongoDB/Role/_SessionSupport.pm b/lib/MongoDB/Role/_SessionSupport.pm index 72f33c85..feff7eee 100644 --- a/lib/MongoDB/Role/_SessionSupport.pm +++ b/lib/MongoDB/Role/_SessionSupport.pm @@ -24,6 +24,7 @@ our $VERSION = 'v1.999.1'; use Moo::Role; use MongoDB::_Types -types, 'to_IxHash'; use Safe::Isa; +use boolean; use namespace::clean; requires qw/ session retryable_write /; @@ -43,17 +44,26 @@ sub _apply_session_and_cluster_time { $$query_ref = to_IxHash( $$query_ref ); ($$query_ref)->Push( 'lsid' => $self->session->session_id ); - if ( $self->retryable_write ) { + if ( $self->retryable_write || ! $self->session->in_transaction_state( 'none' ) ) { ($$query_ref)->Push( 'txnNumber' => $self->session->_server_session->transaction_id ); } + # TODO are these the only states that need autocommit? + if ( $self->session->in_transaction_state( qw/ starting in_progress / ) ) { + ($$query_ref)->Push( 'autocommit' => false ); + } + + if ( $self->session->in_transaction_state( 'starting' ) ) { + ($$query_ref)->Push( 'startTransaction' => true ); + ($$query_ref)->Push( 'readConcern' => $self->session->_get_transaction_read_concern->as_args( $self->session ) ); + } + $self->session->_server_session->update_last_use; my $cluster_time = $self->session->get_latest_cluster_time; if ( defined $cluster_time && $link->supports_clusterTime ) { # Gossip the clusterTime - $$query_ref = to_IxHash($$query_ref); ($$query_ref)->Push( '$clusterTime' => $cluster_time ); } diff --git a/lib/MongoDB/_Topology.pm b/lib/MongoDB/_Topology.pm index 52413a6e..ab4a3ebd 100644 --- a/lib/MongoDB/_Topology.pm +++ b/lib/MongoDB/_Topology.pm @@ -657,6 +657,14 @@ sub _supports_retry_writes { return; } +sub _supports_transactions { + my ( $self ) = @_; + + return unless $self->_supports_sessions; + return if $self->wire_version_ceil < 7; + return if $self->type eq 'Sharded'; + return 1; +} sub _check_staleness_compatibility { my ($self, $read_pref) = @_; diff --git a/lib/MongoDB/_Types.pm b/lib/MongoDB/_Types.pm index c6947193..47dda33a 100644 --- a/lib/MongoDB/_Types.pm +++ b/lib/MongoDB/_Types.pm @@ -195,6 +195,9 @@ declare SingleKeyHash, as HashRef, where { 1 == scalar keys %$_ }; enum TopologyType, [qw/Single ReplicaSetNoPrimary ReplicaSetWithPrimary Sharded Direct Unknown/]; +enum TransactionState, + [ qw/ none starting committed in_progress aborted / ]; + class_type WriteConcern, { class => 'MongoDB::WriteConcern' }; # after SingleKeyHash, PairArrayRef and IxHash From 0feeebb8b79049a793275371de240b8462d6eb7d Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Fri, 1 Jun 2018 16:26:54 +0100 Subject: [PATCH 08/38] PERL-875 fix minor issues introduced in ClientSession and Types for transactions --- lib/MongoDB/ClientSession.pm | 7 +++++-- lib/MongoDB/_Types.pm | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/MongoDB/ClientSession.pm b/lib/MongoDB/ClientSession.pm index 8de4cf48..b7dc172c 100644 --- a/lib/MongoDB/ClientSession.pm +++ b/lib/MongoDB/ClientSession.pm @@ -94,7 +94,11 @@ has options => ( %{ $_[0] }, # applied after to not override the clone with the original defaultTransactionOptions => { - %{ $_[0]->{defaultTransactionOptions} }, + defined( $_[0] ) + && ref( $_[0] ) eq 'HASH' + && defined( $_[0]->{defaultTransactionOptions} ) + ? ( %{ $_[0]->{defaultTransactionOptions} } ) + : (), }, }; }, @@ -112,7 +116,6 @@ has _current_transaction_settings => ( is => 'rwp', isa => HashRef, init_arg => undef, - default => undef, clearer => '_clear_current_transaction_settings', ); diff --git a/lib/MongoDB/_Types.pm b/lib/MongoDB/_Types.pm index 47dda33a..17c482cc 100644 --- a/lib/MongoDB/_Types.pm +++ b/lib/MongoDB/_Types.pm @@ -67,6 +67,7 @@ use Type::Library SingleKeyHash Stringish TopologyType + TransactionState WriteConcern ); From 3423d029fec9b32ec19119d8c3f34fb087bfdbe3 Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Fri, 1 Jun 2018 16:55:27 +0100 Subject: [PATCH 09/38] PERL-875 Added boilerplate for PERL-875 spec tests --- devel/t-dynamic/PERL-875-transactions-spec.t | 63 ++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 devel/t-dynamic/PERL-875-transactions-spec.t diff --git a/devel/t-dynamic/PERL-875-transactions-spec.t b/devel/t-dynamic/PERL-875-transactions-spec.t new file mode 100644 index 00000000..ae1a9c77 --- /dev/null +++ b/devel/t-dynamic/PERL-875-transactions-spec.t @@ -0,0 +1,63 @@ +# +# Copyright 2015 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +use strict; +use warnings; +use Test::More 0.96; +use Test::Fatal; +use Test::Deep qw/!blessed/; + +use utf8; +use Tie::IxHash; + +use MongoDB; +use MongoDB::Error; + +use lib "t/lib"; +use lib "devel/lib"; + +use if $ENV{MONGOVERBOSE}, qw/Log::Any::Adapter Stderr/; + +use MongoDBTest::Orchestrator; + +use MongoDBTest qw/ + build_client + get_test_db + server_version + server_type + clear_testdbs + get_unique_collection +/; + +my $orc = +MongoDBTest::Orchestrator->new( + config_file => "devel/config/replicaset-multi-4.0.yml" ); +$orc->start; + +$ENV{MONGOD} = $orc->as_uri; + +print $ENV{MONGOD}; + +my $conn = build_client(); +my $testdb = get_test_db($conn); +my $server_version = server_version($conn); +my $server_type = server_type($conn); + +ok 1; + +clear_testdbs; + +done_testing; From aa9fe53edd30903dd1a65539ace5c9c9cb634099 Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Tue, 5 Jun 2018 15:49:04 +0100 Subject: [PATCH 10/38] PERL-875 Move transaction spec test boilerplate to normal t directory --- .../transactions-spec.t | 12 ------------ 1 file changed, 12 deletions(-) rename devel/t-dynamic/PERL-875-transactions-spec.t => t/transactions-spec.t (83%) diff --git a/devel/t-dynamic/PERL-875-transactions-spec.t b/t/transactions-spec.t similarity index 83% rename from devel/t-dynamic/PERL-875-transactions-spec.t rename to t/transactions-spec.t index ae1a9c77..454c1df1 100644 --- a/devel/t-dynamic/PERL-875-transactions-spec.t +++ b/t/transactions-spec.t @@ -27,12 +27,9 @@ use MongoDB; use MongoDB::Error; use lib "t/lib"; -use lib "devel/lib"; use if $ENV{MONGOVERBOSE}, qw/Log::Any::Adapter Stderr/; -use MongoDBTest::Orchestrator; - use MongoDBTest qw/ build_client get_test_db @@ -42,15 +39,6 @@ use MongoDBTest qw/ get_unique_collection /; -my $orc = -MongoDBTest::Orchestrator->new( - config_file => "devel/config/replicaset-multi-4.0.yml" ); -$orc->start; - -$ENV{MONGOD} = $orc->as_uri; - -print $ENV{MONGOD}; - my $conn = build_client(); my $testdb = get_test_db($conn); my $server_version = server_version($conn); From f7ad672726fd8a4f2ce86dbbab56256ce1c38c15 Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Wed, 6 Jun 2018 16:53:46 +0100 Subject: [PATCH 11/38] PERL-875 iterate over test files --- t/transactions-spec.t | 100 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 95 insertions(+), 5 deletions(-) diff --git a/t/transactions-spec.t b/t/transactions-spec.t index 454c1df1..ad5f02e5 100644 --- a/t/transactions-spec.t +++ b/t/transactions-spec.t @@ -16,12 +16,11 @@ use strict; use warnings; +use JSON::MaybeXS; +use Path::Tiny 0.054; # basename with suffix use Test::More 0.96; -use Test::Fatal; -use Test::Deep qw/!blessed/; use utf8; -use Tie::IxHash; use MongoDB; use MongoDB::Error; @@ -39,12 +38,103 @@ use MongoDBTest qw/ get_unique_collection /; +my @events; + +sub clear_events { @events = () } +sub event_count { scalar @events } +sub event_cb { push @events, $_[0] } + my $conn = build_client(); -my $testdb = get_test_db($conn); my $server_version = server_version($conn); my $server_type = server_type($conn); -ok 1; +plan skip_all => "Requires MongoDB 4.0" + if $server_version < v4.0.0; + +plan skip_all => "deployment does not support transactions" + unless $conn->_topology->_supports_transactions; + +my $dir = path("t/data/transactions"); +my $iterator = $dir->iterator; +while ( my $path = $iterator->() ) { + next unless $path =~ /\.json$/; + my $plan = eval { decode_json( $path->slurp_utf8 ) }; + if ($@) { + die "Error decoding $path: $@"; + } + my $test_db_name = $plan->{database_name}; + my $test_coll_name = $plan->{collection_name}; + + subtest $path => sub { + + for my $test ( @{ $plan->{tests} } ) { + my $description = $test->{description}; + subtest $description => sub { + my $client = build_client(); + + # Kills its own session as well + eval { $client->send_admin_command([ killAllSessions => [] ]) }; + my $test_db = $client->get_database( $test_db_name ); + $test_db + ->get_collection( $test_coll_name, { write_concern => { w => 'majority' } } ) + ->drop; + + my $test_coll = $test_db->get_collection( $test_coll_name, { write_concern => { w => 'majority' } } ); +use Carp::Always; + if ( scalar @{ $plan->{data} } > 0 ) { + $test_coll->insert_many( $plan->{data} ); + } + + run_test( $test_db_name, $test_coll_name, $test ); + }; + } + }; +} + +sub to_snake_case { + my $t = shift; + $t =~ s{([A-Z])}{_\L$1}g; + return $t; +} + +sub run_test { + my ( $test_db_name, $test_coll_name, $test ) = @_; + + my $client_options = $test->{clientOptions} // {}; + # Remap camel case to snake case + $client_options = { + map { + my $k = to_snake_case( $_ ); + $k => $client_options->{ $_ } + } keys %$client_options + }; + + use Devel::Dwarn; Dwarn $client_options; + + ok 1; + return; + + my $client = build_client( monitoring_callback => \&event_cb, %$client_options ); + + my $session0 = $client->start_session; + my $lsid0 = $session0->session_id; + my $session1 = $client->start_session; + my $lsid1 = $session1->session_id; + + for my $operation ( @{ $test->{operations} } ) { + eval { + my $test_db = $client->get_database( $test_db_name ); + my $test_coll = $client->get_collection( $test_coll_name ); + my $cmd = to_snake_case( $operation->{name} ); + + } + } + + $session0->end_session; + $session1->end_session; + + ok 1; +} clear_testdbs; From 4c20e5bdc2386c232a22e6fba47a5cf1d73df455 Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Fri, 8 Jun 2018 16:34:47 +0100 Subject: [PATCH 12/38] PERL-875 more implementation work for Transactions and errors --- lib/MongoDB/ClientSession.pm | 87 ++++++++++++++++------- lib/MongoDB/Error.pm | 30 ++++++++ lib/MongoDB/MongoClient.pm | 18 +++-- lib/MongoDB/Role/_DatabaseErrorThrower.pm | 1 + lib/MongoDB/Role/_SessionSupport.pm | 14 ++-- lib/MongoDB/_ServerSession.pm | 5 -- 6 files changed, 115 insertions(+), 40 deletions(-) diff --git a/lib/MongoDB/ClientSession.pm b/lib/MongoDB/ClientSession.pm index b7dc172c..3c74d96b 100644 --- a/lib/MongoDB/ClientSession.pm +++ b/lib/MongoDB/ClientSession.pm @@ -242,16 +242,9 @@ sub advance_operation_time { return; } -=method in_transaction_state - - do { ... } if $session->in_transaction_state( 'starting', 'in_progress', ... ); - -Returns 1 if the session is in one of the specified transaction states. -Returns a false value if not in any of the states defined as an argument. - -=cut - -sub in_transaction_state { +# Returns 1 if the session is in one of the specified transaction states. +# Returns a false value if not in any of the states defined as an argument. +sub _in_transaction_state { my ( $self, @states ) = @_; return 1 if scalar ( grep { $_ eq $self->_transaction_state } @states ); return; @@ -281,21 +274,30 @@ sub start_transaction { my ( $self, $opts ) = @_; MongoDB::TransactionError->throw("Transaction already in progress") - if $self->in_transaction_state( 'starting', 'in_progress' ); + if $self->_in_transaction_state( 'starting', 'in_progress' ); MongoDB::ConfigurationError->throw("Transactions are unsupported on this deployment") - unless $self->_client->_topology->_supports_transactions; + unless $self->client->_topology->_supports_transactions; $opts ||= {}; $opts = { %{ $self->options->{defaultTransactionOptions} }, %$opts }; - $self->_set_current_transaction_settings( $opts ); + $self->_set__current_transaction_settings( $opts ); + + $self->_set__transaction_state('starting'); - $self->_set_transaction_state('starting'); + $self->_increment_transaction_id; return; } +sub _increment_transaction_id { + my $self = shift; + return if $self->_in_transaction_state( qw/ in_progress committed aborted / ); + + $self->_server_session->transaction_id->binc(); +} + =method commit_transaction Commit the current transaction. This will use the writeConcern set on this transaction. @@ -312,9 +314,7 @@ sub commit_transaction { MongoDB::TransactionError->throw("Cannot call commit_transaction after calling abort_transaction") if $self->_transaction_state eq 'aborted'; - # TODO Actually commit the transaction - including retrying even if not enabled. - - $self->_set_transaction_state('committed'); + $self->_send_end_transaction_command( 'committed', [ commitTransaction => 1 ] ); return; } @@ -329,33 +329,70 @@ sub abort_transaction { my $self = shift; MongoDB::TransactionError->throw("No transaction started") - if $self->in_transaction_state( 'none' ); + if $self->_in_transaction_state( 'none' ); # Error message tweaked to use our function names MongoDB::TransactionError->throw("Cannot call abort_transaction after calling commit_transaction") - if $self->in_transaction_state( 'committed' ); + if $self->_in_transaction_state( 'committed' ); # Error message tweaked to use our function names MongoDB::TransactionError->throw("Cannot call abort_transaction twice") - if $self->in_transaction_state( 'aborted' ); - - # TODO Actually abort the transaction, ignoring any errors as theres nothing that can be done. + if $self->_in_transaction_state( 'aborted' ); - $self->_set_transaction_state('aborted'); + $self->_send_end_transaction_command( 'aborted', [ abortTransaction => 1 ] ); return; } +sub _send_end_transaction_command { + my ( $self, $end_state, $command ) = @_; + + # Only need to send commit command if the transaction actually sent anything + if ( ! $self->_in_transaction_state( qw/ starting / ) ) { + + # Must set state before running the op as otherwise it wont be retried + $self->_set__transaction_state( $end_state ); + + my $op = MongoDB::Op::_Command->_new( + db_name => 'admin', + query => $command, + query_flags => {}, + bson_codec => $self->client->bson_codec, + session => $self, + monitoring_callback => $self->client->monitoring_callback, + ); + + $self->client->send_retryable_write_op( $op, 'force' ); + } + + $self->_set__transaction_state( $end_state ); +} + sub _get_transaction_read_concern { my $self = shift; - # readConcern is merged during start_transaction if ( defined $self->_current_transaction_settings->{readConcern} ) { return MongoDB::ReadConcern->new( $self->_current_transaction_settings->{readConcern} ); } # Default to the clients read concern - return $self->_client->read_concern; + return $self->client->read_concern; +} + +# TODO TBSliver REMOVE ME ON RELEASE +sub _debug { + my $self = shift; + return { + state => $self->_transaction_state, + client => defined $self->client ? 'defined' : '', + session => defined $self->_server_session ? 'defined' : '', + session_id => $self->session_id, + transaction_id => defined $self->_server_session ? $self->_server_session->transaction_id : '', + cluster_time => $self->cluster_time, + options => $self->options, + transaction_settings => $self->_current_transaction_settings, + operation_time => $self->operation_time, + }; } =method end_session diff --git a/lib/MongoDB/Error.pm b/lib/MongoDB/Error.pm index 413f6430..bd1cb1e2 100644 --- a/lib/MongoDB/Error.pm +++ b/lib/MongoDB/Error.pm @@ -30,6 +30,10 @@ use Carp; use MongoDB::_Types qw( ErrorStr ); +use Types::Standard qw( + ArrayRef + Str +); use Scalar::Util (); use Sub::Quote (); use Safe::Isa; @@ -103,6 +107,18 @@ has 'previous_exception' => ( >), ); +has error_labels => ( + is => 'ro', + isa => ArrayRef[Str], +); + +sub has_error_label { + my ( $self, $expected ) = @_; + + return unless defined $self->error_labels; + return grep { $_ eq $expected } @{ $self->error_labels }; +} + sub throw { my ($inv) = shift; @@ -641,6 +657,20 @@ B: * The database uses multiple write concern error codes. The driver maps them all to WRITE_CONCERN_ERROR for consistency and convenience. +=head1 ERROR LABELS + +From MongoDB 4.0 onwards, errors may contain an error labels field. This field +is populated for extra information from either the server or the driver, +depending on the error. + +Known error labels include (but are not limited to): + +=for :list +* C - added when network errors are encountered + inside a transaction. +* C - added when a transaction commit may not + have been able to satisfy the provided write concern. + =cut # vim: ts=4 sts=4 sw=4 et: diff --git a/lib/MongoDB/MongoClient.pm b/lib/MongoDB/MongoClient.pm index a98559c1..3d777016 100644 --- a/lib/MongoDB/MongoClient.pm +++ b/lib/MongoDB/MongoClient.pm @@ -1009,7 +1009,9 @@ delete_many operations. =cut has retry_writes => ( - is => 'lazy', + # need rwp to allow for retryable writes inside transactions + is => 'rwp', + lazy => '1', isa => Boolish, builder => '_build_retry_writes', ); @@ -1577,15 +1579,21 @@ BEGIN { } sub send_retryable_write_op { - my ( $self, $op ) = @_; + my ( $self, $op, $force ) = @_; - return $self->send_write_op( $op ) unless $self->retry_writes; + # force is used specifically for retrying writes in transactions + # TODO untangle the send_write_op and _try_write_op_for_link duplication + return $self->send_write_op( $op ) unless $self->retry_writes || ( defined $force && $force eq 'force' ); my $result; my $link = $self->{_topology}->get_writable_link; + # Not sent to send_write_op to use the link we just got # If server doesnt support retryable writes, pretend its not enabled - unless ( $link->supports_retryWrites ) { + # active transactions also dont support retryable writes + # except in abort and commit state + unless ( $link->supports_retryWrites + && !$op->session->_in_transaction_state( qw/ starting in_progress / ) ) { eval { ($result) = $self->_try_write_op_for_link( $link, $op ); 1 } or do { my $err = length($@) ? $@ : "caught error, but it was lost in eval unwind"; WITH_ASSERTS ? ( confess $err ) : ( die $err ); @@ -1597,7 +1605,7 @@ sub send_retryable_write_op { # wrong, so probably not worth worrying about. # # increment transaction id before write, but otherwise is the same for both attempts - $op->session->_server_session->_increment_transaction_id; + $op->session->_increment_transaction_id; $op->retryable_write( 1 ); # attempt the op the first time diff --git a/lib/MongoDB/Role/_DatabaseErrorThrower.pm b/lib/MongoDB/Role/_DatabaseErrorThrower.pm index a31be93e..f4e31255 100644 --- a/lib/MongoDB/Role/_DatabaseErrorThrower.pm +++ b/lib/MongoDB/Role/_DatabaseErrorThrower.pm @@ -57,6 +57,7 @@ sub _throw_database_error { $error_class->throw( result => $self, code => $code || UNKNOWN_ERROR, + error_labels => $self->output->{errorLabels} || [], ( length($err) ? ( message => $err ) : () ), ); diff --git a/lib/MongoDB/Role/_SessionSupport.pm b/lib/MongoDB/Role/_SessionSupport.pm index feff7eee..93a7d657 100644 --- a/lib/MongoDB/Role/_SessionSupport.pm +++ b/lib/MongoDB/Role/_SessionSupport.pm @@ -44,18 +44,17 @@ sub _apply_session_and_cluster_time { $$query_ref = to_IxHash( $$query_ref ); ($$query_ref)->Push( 'lsid' => $self->session->session_id ); - if ( $self->retryable_write || ! $self->session->in_transaction_state( 'none' ) ) { + if ( $self->retryable_write || ! $self->session->_in_transaction_state( 'none' ) ) { ($$query_ref)->Push( 'txnNumber' => $self->session->_server_session->transaction_id ); } - # TODO are these the only states that need autocommit? - if ( $self->session->in_transaction_state( qw/ starting in_progress / ) ) { + if ( ! $self->session->_in_transaction_state( qw/ none / ) ) { ($$query_ref)->Push( 'autocommit' => false ); } - if ( $self->session->in_transaction_state( 'starting' ) ) { + if ( $self->session->_in_transaction_state( 'starting' ) ) { ($$query_ref)->Push( 'startTransaction' => true ); - ($$query_ref)->Push( 'readConcern' => $self->session->_get_transaction_read_concern->as_args( $self->session ) ); + ($$query_ref)->Push( @{ $self->session->_get_transaction_read_concern->as_args( $self->session ) } ); } $self->session->_server_session->update_last_use; @@ -91,11 +90,16 @@ sub _update_session_and_cluster_time { return; } +# All items here happen before the response is checked for success sub _update_operation_time { my ( $self, $response ) = @_; return unless defined $self->session; + if ( $self->session->_in_transaction_state( 'starting' ) ) { + $self->session->_set__transaction_state( 'in_progress' ); + } + my $operation_time = $self->__extract_from( $response, 'operationTime' ); $self->session->advance_operation_time( $operation_time ) if defined $operation_time; diff --git a/lib/MongoDB/_ServerSession.pm b/lib/MongoDB/_ServerSession.pm index 15e00e33..45a753f8 100644 --- a/lib/MongoDB/_ServerSession.pm +++ b/lib/MongoDB/_ServerSession.pm @@ -123,11 +123,6 @@ sub _is_expiring { return; } -sub _increment_transaction_id { - my $self = shift; - $self->transaction_id->binc(); -} - 1; __END__ From 129f406264429428a514be70154489672ebd3504 Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Fri, 8 Jun 2018 16:35:05 +0100 Subject: [PATCH 13/38] PERL-875 Added transaction test spec files --- t/data/transactions/README.rst | 273 +++ t/data/transactions/abort.json | 607 +++++ t/data/transactions/abort.yml | 403 ++++ t/data/transactions/bulk.json | 524 +++++ t/data/transactions/bulk.yml | 267 +++ t/data/transactions/causal-consistency.json | 294 +++ t/data/transactions/causal-consistency.yml | 173 ++ t/data/transactions/commit.json | 899 ++++++++ t/data/transactions/commit.yml | 593 +++++ t/data/transactions/delete.json | 313 +++ t/data/transactions/delete.yml | 184 ++ t/data/transactions/error-labels.json | 951 ++++++++ t/data/transactions/error-labels.yml | 600 +++++ t/data/transactions/errors.json | 208 ++ t/data/transactions/errors.yml | 125 ++ t/data/transactions/findOneAndDelete.json | 207 ++ t/data/transactions/findOneAndDelete.yml | 126 ++ t/data/transactions/findOneAndReplace.json | 241 ++ t/data/transactions/findOneAndReplace.yml | 140 ++ t/data/transactions/findOneAndUpdate.json | 399 ++++ t/data/transactions/findOneAndUpdate.yml | 228 ++ t/data/transactions/insert.json | 441 ++++ t/data/transactions/insert.yml | 264 +++ t/data/transactions/isolation.json | 211 ++ t/data/transactions/isolation.yml | 125 ++ t/data/transactions/read-pref.json | 706 ++++++ t/data/transactions/read-pref.yml | 340 +++ t/data/transactions/reads.json | 611 ++++++ t/data/transactions/reads.yml | 297 +++ t/data/transactions/retryable-abort.json | 1958 +++++++++++++++++ t/data/transactions/retryable-abort.yml | 1292 +++++++++++ t/data/transactions/retryable-commit.json | 2069 ++++++++++++++++++ t/data/transactions/retryable-commit.yml | 1332 +++++++++++ t/data/transactions/retryable-writes.json | 329 +++ t/data/transactions/retryable-writes.yml | 208 ++ t/data/transactions/run-command.json | 292 +++ t/data/transactions/run-command.yml | 189 ++ t/data/transactions/transaction-options.json | 1534 +++++++++++++ t/data/transactions/transaction-options.yml | 877 ++++++++ t/data/transactions/update.json | 436 ++++ t/data/transactions/update.yml | 246 +++ t/data/transactions/write-concern.json | 355 +++ t/data/transactions/write-concern.yml | 236 ++ 43 files changed, 22103 insertions(+) create mode 100644 t/data/transactions/README.rst create mode 100644 t/data/transactions/abort.json create mode 100644 t/data/transactions/abort.yml create mode 100644 t/data/transactions/bulk.json create mode 100644 t/data/transactions/bulk.yml create mode 100644 t/data/transactions/causal-consistency.json create mode 100644 t/data/transactions/causal-consistency.yml create mode 100644 t/data/transactions/commit.json create mode 100644 t/data/transactions/commit.yml create mode 100644 t/data/transactions/delete.json create mode 100644 t/data/transactions/delete.yml create mode 100644 t/data/transactions/error-labels.json create mode 100644 t/data/transactions/error-labels.yml create mode 100644 t/data/transactions/errors.json create mode 100644 t/data/transactions/errors.yml create mode 100644 t/data/transactions/findOneAndDelete.json create mode 100644 t/data/transactions/findOneAndDelete.yml create mode 100644 t/data/transactions/findOneAndReplace.json create mode 100644 t/data/transactions/findOneAndReplace.yml create mode 100644 t/data/transactions/findOneAndUpdate.json create mode 100644 t/data/transactions/findOneAndUpdate.yml create mode 100644 t/data/transactions/insert.json create mode 100644 t/data/transactions/insert.yml create mode 100644 t/data/transactions/isolation.json create mode 100644 t/data/transactions/isolation.yml create mode 100644 t/data/transactions/read-pref.json create mode 100644 t/data/transactions/read-pref.yml create mode 100644 t/data/transactions/reads.json create mode 100644 t/data/transactions/reads.yml create mode 100644 t/data/transactions/retryable-abort.json create mode 100644 t/data/transactions/retryable-abort.yml create mode 100644 t/data/transactions/retryable-commit.json create mode 100644 t/data/transactions/retryable-commit.yml create mode 100644 t/data/transactions/retryable-writes.json create mode 100644 t/data/transactions/retryable-writes.yml create mode 100644 t/data/transactions/run-command.json create mode 100644 t/data/transactions/run-command.yml create mode 100644 t/data/transactions/transaction-options.json create mode 100644 t/data/transactions/transaction-options.yml create mode 100644 t/data/transactions/update.json create mode 100644 t/data/transactions/update.yml create mode 100644 t/data/transactions/write-concern.json create mode 100644 t/data/transactions/write-concern.yml diff --git a/t/data/transactions/README.rst b/t/data/transactions/README.rst new file mode 100644 index 00000000..9b1273b9 --- /dev/null +++ b/t/data/transactions/README.rst @@ -0,0 +1,273 @@ +================== +Transactions Tests +================== + +.. contents:: + +---- + +Introduction +============ + +The YAML and JSON files in this directory are platform-independent tests that +drivers can use to prove their conformance to the Transactions Spec. They are +designed with the intention of sharing some test-runner code with the CRUD Spec +tests and the Command Monitoring Spec tests. + +Several prose tests, which are not easily expressed in YAML, are also presented +in this file. Those tests will need to be manually implemented by each driver. + +Server Fail Point +================= + +Some tests depend on a server fail point, expressed in the ``failPoint`` field. +For example the ``failCommand`` fail point allows the client to force the +server to return an error. Keep in mind that the fail point only triggers for +commands listed in the "failCommands" field. See `SERVER-35004`_ and +`SERVER-35083`_ for more information. + +.. _SERVER-35004: https://jira.mongodb.org/browse/SERVER-35004 +.. _SERVER-35083: https://jira.mongodb.org/browse/SERVER-35083 + +The ``failCommand`` fail point may be configured like so:: + + db.adminCommand({ + configureFailPoint: "failCommand", + mode: , + data: { + failCommands: ["commandName", "commandName2"], + closeConnection: , + errorCode: , + writeConcernError: + } + }); + +``mode`` is a generic fail point option and may be assigned a string or document +value. The string values ``"alwaysOn"`` and ``"off"`` may be used to enable or +disable the fail point, respectively. A document may be used to specify either +``times`` or ``skip``, which are mutually exclusive: + +- ``{ times: }`` may be used to limit the number of times the fail + point may trigger before transitioning to ``"off"``. +- ``{ skip: }`` may be used to defer the first trigger of a fail + point, after which it will transition to ``"alwaysOn"``. + +The ``data`` option is a document that may be used to specify options that +control the fail point's behavior. ``failCommand`` supports the following +``data`` options, which may be combined if desired: + +- ``failCommands``: Required, the list of command names to fail. +- ``closeConnection``: Boolean option, which defaults to ``false``. If + ``true``, the connection on which the command is executed will be closed + and the client will see a network error. +- ``errorCode``: Integer option, which is unset by default. If set, the + specified command error code will be returned as a command error. +- ``writeConcernError``: A document, which is unset by default. If set, the + server will return this document in the "writeConcernError" field. This + failure response only applies to commands that support write concern and + happens *after* the command finishes (regardless of success or failure). + +Test Format +=========== + +Each YAML file has the following keys: + +- ``database_name`` and ``collection_name``: The database and collection to use + for testing. + +- ``data``: The data that should exist in the collection under test before each + test run. + +- ``tests``: An array of tests that are to be run independently of each other. + Each test will have some or all of the following fields: + + - ``description``: The name of the test. + + - ``clientOptions``: Optional, parameters to pass to MongoClient(). + + - ``failPoint``: Optional, a server failpoint to enable expressed as the + configureFailPoint command to run on the admin database. + + - ``sessionOptions``: Optional, parameters to pass to + MongoClient.startSession(). + + - ``operations``: Array of documents, each describing an operation to be + executed. Each document has the following fields: + + - ``name``: The name of the operation on ``object``. + + - ``object``: The name of the object to perform the operation on. Can be + "database", "collection", "session0", or "session1". + + - ``collectionOptions``: Optional, parameters to pass to the Collection() + used for this operation. + + - ``command_name``: Present only when ``name`` is "runCommand". The name + of the command to run. Required for languages that are unable preserve + the order keys in the "command" argument when parsing JSON/YAML. + + - ``arguments``: Optional, the names and values of arguments. + + - ``result``: The return value from the operation, if any. If the + operation is expected to return an error, the ``result`` has one or more + of the following fields: + + - ``errorContains``: A substring of the expected error message. + + - ``errorCodeName``: The expected "codeName" field in the server + error response. + + - ``errorLabelsContain``: A list of error label strings that the + error is expected to have. + + - ``errorLabelsOmit``: A list of error label strings that the + error is expected not to have. + + - ``expectations``: Optional list of command-started events. + + - ``outcome``: Document describing the return value and/or expected state of + the collection after the operation is executed. Contains the following + fields: + + - ``collection``: + + - ``data``: The data that should exist in the collection after the + operations have run. + +Use as integration tests +======================== + +Run a MongoDB replica set with a primary, a secondary, and an arbiter, +server version 4.0 or later. (Including a secondary ensures that server +selection in a transaction works properly. Including an arbiter helps ensure +that no new bugs have been introduced related to arbiters.) + +Load each YAML (or JSON) file using a Canonical Extended JSON parser. + +Then for each element in ``tests``: + +#. Create a MongoClient and call + ``client.admin.runCommand({killAllSessions: []})`` to clean up any open + transactions from previous test failures. The command will fail with message + "operation was interrupted", because it kills its own implicit session. Catch + the exception and continue. +#. Create a collection object from the MongoClient, using the ``database_name`` + and ``collection_name`` fields of the YAML file. +#. Drop the test collection, using writeConcern "majority". +#. Execute the "create" command to recreate the collection, using writeConcern + "majority". (Creating the collection inside a transaction is prohibited, so + create it explicitly.) +#. If the YAML file contains a ``data`` array, insert the documents in ``data`` + into the test collection, using writeConcern "majority". +#. If ``failPoint`` is specified, its value is a configureFailPoint command. + Run the command on the admin database to enable the fail point. +#. Create a **new** MongoClient ``client``, with Command Monitoring listeners + enabled. (Using a new MongoClient for each test ensures a fresh session pool + that hasn't executed any transactions previously, so the tests can assert + actual txnNumbers, starting from 1.) Pass this test's ``clientOptions`` if + present. +#. Call ``client.startSession`` twice to create ClientSession objects + ``session0`` and ``session1``, using the test's "sessionOptions" if they + are present. Save their lsids so they are available after calling + ``endSession``, see `Logical Session Id`. +#. For each element in ``operations``: + + - Enter a "try" block or your programming language's closest equivalent. + - Create a Database object from the MongoClient, using the ``database_name`` + field at the top level of the test file. + - Create a Collection object from the Database, using the + ``collection_name`` field at the top level of the test file. + If ``collectionOptions`` is present create the Collection object with the + provided options. Otherwise create the object with the default options. + - Execute the named method on the provided ``object``, passing the + arguments listed. Pass ``session0`` or ``session1`` to the method, + depending on which session's name is in the arguments list. + If ``arguments`` contains no "session", pass no explicit session to the + method. + - If the driver throws an exception / returns an error while executing this + series of operations, store the error message and server error code. + - If the result document has an "errorContains" field, verify that the + method threw an exception or returned an error, and that the value of the + "errorContains" field matches the error string. "errorContains" is a + substring (case-insensitive) of the actual error message. + + If the result document has an "errorCodeName" field, verify that the + method threw a command failed exception or returned an error, and that + the value of the "errorCodeName" field matches the "codeName" in the + server error response. + + If the result document has an "errorLabelsContain" field, verify that the + method threw an exception or returned an error. Verify that all of the + error labels in "errorLabelsContain" are present in the error or exception + using the ``hasErrorLabel`` method. + + If the result document has an "errorLabelsOmit" field, verify that the + method threw an exception or returned an error. Verify that none of the + error labels in "errorLabelsOmit" are present in the error or exception + using the ``hasErrorLabel`` method. + - If the operation returns a raw command response, eg from ``runCommand``, + then compare only the fields present in the expected result document. + Otherwise, compare the method's return value to ``result`` using the same + logic as the CRUD Spec Tests runner. + +#. Call ``session0.endSession()`` and ``session1.endSession``. +#. If the test includes a list of command-started events in ``expectations``, + compare them to the actual command-started events using the + same logic as the Command Monitoring Spec Tests runner, plus the rules in + the Command-Started Events instructions below. +#. If ``failPoint`` is specified, disable the fail point to avoid spurious + failures in subsequent tests. The fail point may be disabled like so:: + + db.adminCommand({ + configureFailPoint: , + mode: "off" + }); + +#. For each element in ``outcome``: + + - If ``name`` is "collection", verify that the test collection contains + exactly the documents in the ``data`` array. Ensure this find uses + Primary read preference even when the MongoClient is configured with + another read preference. + +TODO: + +- drivers MUST retry commit/abort, needs to use failpoint. +- test writeConcernErrors + +Command-Started Events +`````````````````````` + +The event listener used for these tests MUST ignore the security commands +listed in the Command Monitoring Spec. + +Logical Session Id +~~~~~~~~~~~~~~~~~~ + +Each command-started event in ``expectations`` includes an ``lsid`` with the +value "session0" or "session1". Tests MUST assert that the command's actual +``lsid`` matches the id of the correct ClientSession named ``session0`` or +``session1``. + +Null Values +~~~~~~~~~~~ + +Some command-started events in ``expectations`` include ``null`` values for +fields such as ``txnNumber``, ``autocommit``, and ``writeConcern``. +Tests MUST assert that the actual command **omits** any field that has a +``null`` value in the expected command. + +Cursor Id +^^^^^^^^^ + +A ``getMore`` value of ``"42"`` in a command-started event is a fake cursorId +that MUST be ignored. (In the Command Monitoring Spec tests, fake cursorIds are +correlated with real ones, but that is not necessary for Transactions Spec +tests.) + +afterClusterTime +^^^^^^^^^^^^^^^^ + +A ``readConcern.afterClusterTime`` value of ``42`` in a command-started event +is a fake cluster time. Drivers MUST assert that the actual command includes an +afterClusterTime. diff --git a/t/data/transactions/abort.json b/t/data/transactions/abort.json new file mode 100644 index 00000000..084429b7 --- /dev/null +++ b/t/data/transactions/abort.json @@ -0,0 +1,607 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "abort", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": { + "afterClusterTime": 42 + }, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "implicit abort", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "two aborts", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + }, + { + "name": "abortTransaction", + "object": "session0", + "result": { + "errorContains": "cannot call abortTransaction twice" + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abort without start", + "operations": [ + { + "name": "abortTransaction", + "object": "session0", + "result": { + "errorContains": "no transaction started" + } + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abort directly after no-op commit", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "abortTransaction", + "object": "session0", + "result": { + "errorContains": "Cannot call abortTransaction after calling commitTransaction" + } + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abort directly after commit", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "abortTransaction", + "object": "session0", + "result": { + "errorContains": "Cannot call abortTransaction after calling commitTransaction" + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "abort ignores TransactionAborted", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "errorCodeName": "DuplicateKey", + "errorLabelsOmit": [ + "TransientTransactionError", + "UnknownTransactionCommitResult" + ] + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "errorCodeName": "NoSuchTransaction", + "errorLabelsContain": [ + "TransientTransactionError" + ], + "errorLabelsOmit": [ + "UnknownTransactionCommitResult" + ] + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abort does not apply writeConcern", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": 10 + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "outcome": { + "collection": { + "data": [] + } + } + } + ] +} diff --git a/t/data/transactions/abort.yml b/t/data/transactions/abort.yml new file mode 100644 index 00000000..f1bbca76 --- /dev/null +++ b/t/data/transactions/abort.yml @@ -0,0 +1,403 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] + +tests: + - description: abort + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + "$numberLong": "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + afterClusterTime: 42 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: implicit abort + + operations: + # Start a transaction but don't commit - the driver calls abortTransaction + # from ClientSession.endSession(). + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: two aborts + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + - name: abortTransaction + object: session0 + result: + errorContains: cannot call abortTransaction twice + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abort without start + + operations: + - name: abortTransaction + object: session0 + result: + errorContains: no transaction started + + expectations: [] + + outcome: + collection: + data: [] + + - description: abort directly after no-op commit + + operations: + - name: startTransaction + object: session0 + - name: commitTransaction + object: session0 + - name: abortTransaction # Error calling abort after no-op commit. + object: session0 + result: + errorContains: Cannot call abortTransaction after calling commitTransaction + + expectations: [] + + outcome: + collection: + data: [] + + - description: abort directly after commit + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + - name: abortTransaction # Error calling abort after commit. + object: session0 + result: + errorContains: Cannot call abortTransaction after calling commitTransaction + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: abort ignores TransactionAborted + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + # Abort the server transaction with a duplicate key error. + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + errorCodeName: DuplicateKey + errorLabelsOmit: ["TransientTransactionError", "UnknownTransactionCommitResult"] + # Make sure the server aborted the transaction. + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + errorCodeName: NoSuchTransaction + errorLabelsContain: ["TransientTransactionError"] + errorLabelsOmit: ["UnknownTransactionCommitResult"] + # abortTransaction must ignore the TransactionAborted and succeed. + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + + outcome: + collection: + data: [] + + - description: abort does not apply writeConcern + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: 10 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + # No write concern error. + + outcome: + collection: + data: [] diff --git a/t/data/transactions/bulk.json b/t/data/transactions/bulk.json new file mode 100644 index 00000000..c5a572df --- /dev/null +++ b/t/data/transactions/bulk.json @@ -0,0 +1,524 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "bulk", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "deleteOne", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "result": { + "deletedCount": 1 + } + }, + { + "name": "bulkWrite", + "object": "collection", + "arguments": { + "session": "session0", + "requests": [ + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + } + } + }, + { + "name": "updateOne", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$set": { + "x": 1 + } + } + } + }, + { + "name": "updateOne", + "arguments": { + "filter": { + "_id": 2 + }, + "update": { + "$set": { + "x": 2 + } + }, + "upsert": true + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 3 + } + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 4 + } + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 5 + } + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 6 + } + } + }, + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 7 + } + } + }, + { + "name": "replaceOne", + "arguments": { + "filter": { + "_id": 1 + }, + "replacement": { + "y": 1 + } + } + }, + { + "name": "replaceOne", + "arguments": { + "filter": { + "_id": 2 + }, + "replacement": { + "y": 2 + } + } + }, + { + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 3 + } + } + }, + { + "name": "deleteOne", + "arguments": { + "filter": { + "_id": 4 + } + } + }, + { + "name": "updateMany", + "arguments": { + "filter": { + "_id": { + "$gte": 2 + } + }, + "update": { + "$set": { + "z": 1 + } + } + } + }, + { + "name": "deleteMany", + "arguments": { + "filter": { + "_id": { + "$gte": 6 + } + } + } + } + ] + }, + "result": { + "deletedCount": 4, + "insertedIds": { + "0": 1, + "3": 3, + "4": 4, + "5": 5, + "6": 6, + "7": 7 + }, + "matchedCount": 7, + "modifiedCount": 7, + "upsertedCount": 1, + "upsertedIds": { + "2": 2 + } + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "_id": 1 + }, + "limit": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "delete", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 1 + }, + "u": { + "$set": { + "x": 1 + } + }, + "multi": false, + "upsert": false + }, + { + "q": { + "_id": 2 + }, + "u": { + "$set": { + "x": 2 + } + }, + "multi": false, + "upsert": true + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "update", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 3 + }, + { + "_id": 4 + }, + { + "_id": 5 + }, + { + "_id": 6 + }, + { + "_id": 7 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 1 + }, + "u": { + "y": 1 + }, + "multi": false, + "upsert": false + }, + { + "q": { + "_id": 2 + }, + "u": { + "y": 2 + }, + "multi": false, + "upsert": false + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "update", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "_id": 3 + }, + "limit": 1 + }, + { + "q": { + "_id": 4 + }, + "limit": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "delete", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": { + "$gte": 2 + } + }, + "u": { + "$set": { + "z": 1 + } + }, + "multi": true, + "upsert": false + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "update", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "_id": { + "$gte": 6 + } + }, + "limit": 0 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "delete", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "y": 1 + }, + { + "_id": 2, + "y": 2, + "z": 1 + }, + { + "_id": 5, + "z": 1 + } + ] + } + } + } + ] +} diff --git a/t/data/transactions/bulk.yml b/t/data/transactions/bulk.yml new file mode 100644 index 00000000..f940f71f --- /dev/null +++ b/t/data/transactions/bulk.yml @@ -0,0 +1,267 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] + +tests: + - description: bulk + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: deleteOne + object: collection + arguments: + session: session0 + filter: + _id: 1 + result: + deletedCount: 1 + - name: bulkWrite + object: collection + arguments: + session: session0 + requests: + - name: insertOne + arguments: + document: {_id: 1} + - name: updateOne + arguments: + filter: {_id: 1} + update: {$set: {x: 1}} + - name: updateOne + arguments: + filter: {_id: 2} + update: {$set: {x: 2}} + upsert: true # Produces upsertedIds: {2: 2} in the result. + - name: insertOne + arguments: + document: {_id: 3} + - name: insertOne + arguments: + document: {_id: 4} + - name: insertOne + arguments: + document: {_id: 5} + - name: insertOne + arguments: + document: {_id: 6} + - name: insertOne + arguments: + document: {_id: 7} + # Keep replaces segregated from updates, so that drivers that aren't able to coalesce + # adjacent updates and replaces into a single update command will still pass this test + - name: replaceOne + arguments: + filter: {_id: 1} + replacement: {y: 1} + - name: replaceOne + arguments: + filter: {_id: 2} + replacement: {y: 2} + - name: deleteOne + arguments: + filter: {_id: 3} + - name: deleteOne + arguments: + filter: {_id: 4} + - name: updateMany + arguments: + filter: {_id: {$gte: 2}} + update: {$set: {z: 1}} + # Keep deleteMany segregated from deleteOne, so that drivers that aren't able to coalesce + # adjacent mixed deletes into a single delete command will still pass this test + - name: deleteMany + arguments: + filter: {_id: {$gte: 6}} + result: + deletedCount: 4 + insertedIds: {0: 1, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7} + matchedCount: 7 + modifiedCount: 7 + upsertedCount: 1 + upsertedIds: {2: 2} + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + delete: *collection_name + deletes: + - q: {_id: 1} + limit: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: delete + database_name: *database_name + # Commands in the bulkWrite. + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + update: *collection_name + updates: + - q: {_id: 1} + u: {$set: {x: 1}} + multi: false + upsert: false + - q: {_id: 2} + u: {$set: {x: 2}} + multi: false + upsert: true + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: update + database_name: *database_name + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 3 + - _id: 4 + - _id: 5 + - _id: 6 + - _id: 7 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + update: *collection_name + updates: + - q: {_id: 1} + u: {y: 1} + multi: false + upsert: false + - q: {_id: 2} + u: {y: 2} + multi: false + upsert: false + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: update + database_name: *database_name + - command_started_event: + command: + delete: *collection_name + deletes: + - q: {_id: 3} + limit: 1 + - q: {_id: 4} + limit: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: delete + database_name: *database_name + - command_started_event: + command: + update: *collection_name + updates: + - q: {_id: {$gte: 2}} + u: {$set: {z: 1}} + multi: true + upsert: false + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: update + database_name: *database_name + - command_started_event: + command: + delete: *collection_name + deletes: + - q: {_id: {$gte: 6}} + limit: 0 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: delete + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - {_id: 1, y: 1} + - {_id: 2, y: 2, z: 1} + - {_id: 5, z: 1} diff --git a/t/data/transactions/causal-consistency.json b/t/data/transactions/causal-consistency.json new file mode 100644 index 00000000..567350f3 --- /dev/null +++ b/t/data/transactions/causal-consistency.json @@ -0,0 +1,294 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [ + { + "_id": 1, + "count": 0 + } + ], + "tests": [ + { + "description": "causal consistency", + "operations": [ + { + "name": "updateOne", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "count": 1 + } + }, + "upsert": false + }, + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "updateOne", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "count": 1 + } + }, + "upsert": false + }, + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 1 + }, + "u": { + "$inc": { + "count": 1 + } + }, + "multi": false, + "upsert": false + } + ], + "ordered": true, + "lsid": "session0", + "readConcern": null, + "txnNumber": null, + "startTransaction": null, + "autocommit": null, + "writeConcern": null + }, + "command_name": "update", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 1 + }, + "u": { + "$inc": { + "count": 1 + } + }, + "multi": false, + "upsert": false + } + ], + "ordered": true, + "readConcern": { + "afterClusterTime": 42 + }, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "update", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "count": 2 + } + ] + } + } + }, + { + "description": "causal consistency disabled", + "sessionOptions": { + "session0": { + "causalConsistency": false + } + }, + "operations": [ + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 2 + } + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "updateOne", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "count": 1 + } + }, + "upsert": false + }, + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": null, + "autocommit": null, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 1 + }, + "u": { + "$inc": { + "count": 1 + } + }, + "multi": false, + "upsert": false + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "update", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "count": 1 + }, + { + "_id": 2 + } + ] + } + } + } + ] +} diff --git a/t/data/transactions/causal-consistency.yml b/t/data/transactions/causal-consistency.yml new file mode 100644 index 00000000..bfb85725 --- /dev/null +++ b/t/data/transactions/causal-consistency.yml @@ -0,0 +1,173 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: + - _id: 1 + count: 0 + +tests: + - description: causal consistency + + operations: + # Update a document without a transaction. + - &updateOne + name: updateOne + object: collection + arguments: + session: session0 + filter: {_id: 1} + update: + $inc: {count: 1} + upsert: false + result: + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + # Updating the same document inside a transaction. + # Casual consistency ensures that the transaction snapshot is causally + # after the first updateOne. + - name: startTransaction + object: session0 + - *updateOne + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + update: *collection_name + updates: + - q: {_id: 1} + u: {$inc: {count: 1}} + multi: false + upsert: false + ordered: true + lsid: session0 + readConcern: + txnNumber: + startTransaction: + autocommit: + writeConcern: + command_name: update + database_name: *database_name + - command_started_event: + command: + update: *collection_name + updates: + - q: {_id: 1} + u: {$inc: {count: 1}} + multi: false + upsert: false + ordered: true + readConcern: + afterClusterTime: 42 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: update + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + count: 2 + + - description: causal consistency disabled + + sessionOptions: + session0: + causalConsistency: false + + operations: + # Insert a document without a transaction. + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 2 + result: + insertedId: 2 + - name: startTransaction + object: session0 + - name: updateOne + object: collection + arguments: + session: session0 + filter: {_id: 1} + update: + $inc: {count: 1} + upsert: false + result: + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + ordered: true + readConcern: + lsid: session0 + txnNumber: + autocommit: + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + update: *collection_name + updates: + - q: {_id: 1} + u: {$inc: {count: 1}} + multi: false + upsert: false + ordered: true + # No afterClusterTime + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: update + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + count: 1 + - _id: 2 diff --git a/t/data/transactions/commit.json b/t/data/transactions/commit.json new file mode 100644 index 00000000..173fae6c --- /dev/null +++ b/t/data/transactions/commit.json @@ -0,0 +1,899 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "commit", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 2 + } + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "readConcern": { + "afterClusterTime": 42 + }, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + } + } + }, + { + "description": "rerun commit after empty transaction", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "multiple commits in a row", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "write concern error on commit", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": 10 + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorLabelsOmit": [ + "TransientTransactionError", + "UnknownTransactionCommitResult" + ] + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commit without start", + "operations": [ + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorContains": "no transaction started" + } + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "commit after no-op abort", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "abortTransaction", + "object": "session0" + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorContains": "Cannot call commitTransaction after calling abortTransaction" + } + } + ], + "expectations": [], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "commit after abort", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorContains": "Cannot call commitTransaction after calling abortTransaction" + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ] + }, + { + "description": "multiple commits after empty transaction", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": { + "afterClusterTime": 42 + }, + "lsid": "session0", + "txnNumber": { + "$numberLong": "3" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "3" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "reset session state commit", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 2 + } + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorContains": "no transaction started" + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": null, + "startTransaction": null, + "autocommit": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + } + } + }, + { + "description": "reset session state abort", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 2 + } + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "abortTransaction", + "object": "session0", + "result": { + "errorContains": "no transaction started" + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": null, + "startTransaction": null, + "autocommit": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 2 + } + ] + } + } + } + ] +} diff --git a/t/data/transactions/commit.yml b/t/data/transactions/commit.yml new file mode 100644 index 00000000..baa5db86 --- /dev/null +++ b/t/data/transactions/commit.yml @@ -0,0 +1,593 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] +tests: + - description: commit + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + # Again, to verify that txnNumber is incremented. + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 2 + result: + insertedId: 2 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + ordered: true + readConcern: + afterClusterTime: 42 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + - _id: 2 + + - description: rerun commit after empty transaction + + operations: + - name: startTransaction + object: session0 + - name: commitTransaction + object: session0 + # Rerun the commit (which does not increment the txnNumber). + - name: commitTransaction + object: session0 + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: test + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: transaction-tests + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: multiple commits in a row + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + - name: commitTransaction + object: session0 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: write concern error on commit + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: 10 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + result: + # { + # 'ok': 1.0, + # 'writeConcernError': { + # 'code': 100, + # 'codeName': 'UnsatisfiableWriteConcern', + # 'errmsg': 'Not enough data-bearing nodes' + # } + # } + errorLabelsOmit: ["TransientTransactionError", "UnknownTransactionCommitResult"] + + outcome: + collection: + data: + - _id: 1 + + - description: commit without start + + operations: + - name: commitTransaction + object: session0 + result: + errorContains: no transaction started + + expectations: [] + + outcome: + collection: + data: [] + + - description: commit after no-op abort + + operations: + - name: startTransaction + object: session0 + - name: abortTransaction + object: session0 + - name: commitTransaction + object: session0 + result: + errorContains: Cannot call commitTransaction after calling abortTransaction + + expectations: [] + + outcome: + collection: + data: [] + + - description: commit after abort + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + - name: commitTransaction + object: session0 + result: + errorContains: Cannot call commitTransaction after calling abortTransaction + + expectations: + - command_started_event: + command: + insert: test + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: transaction-tests + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + - description: multiple commits after empty transaction + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + # Increments txnNumber. + - name: startTransaction + object: session0 + # These commits aren't sent to server, transaction is empty. + - name: commitTransaction + object: session0 + - name: commitTransaction + object: session0 + # Verify that previous, empty transaction incremented txnNumber. + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: test + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: transaction-tests + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + insert: test + documents: + - _id: 1 + ordered: true + readConcern: + afterClusterTime: 42 + lsid: session0 + # txnNumber 2 was skipped. + txnNumber: + $numberLong: "3" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: transaction-tests + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "3" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: reset session state commit + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + # Running any operation after an ended transaction resets the session + # state to "no transaction". + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 2 + result: + insertedId: 2 + # Calling commit again should error instead of re-running the commit. + - name: commitTransaction + object: session0 + result: + errorContains: no transaction started + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + ordered: true + readConcern: + lsid: session0 + txnNumber: + startTransaction: + autocommit: + command_name: insert + database_name: *database_name + + outcome: + collection: + data: + - _id: 1 + - _id: 2 + + - description: reset session state abort + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + # Running any operation after an ended transaction resets the session + # state to "no transaction". + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 2 + result: + insertedId: 2 + # Calling abort should error with "no transaction started" instead of + # "cannot call abortTransaction twice". + - name: abortTransaction + object: session0 + result: + errorContains: no transaction started + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + ordered: true + readConcern: + lsid: session0 + txnNumber: + startTransaction: + autocommit: + command_name: insert + database_name: *database_name + + outcome: + collection: + data: + - _id: 2 diff --git a/t/data/transactions/delete.json b/t/data/transactions/delete.json new file mode 100644 index 00000000..80ff1be9 --- /dev/null +++ b/t/data/transactions/delete.json @@ -0,0 +1,313 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + }, + { + "_id": 5 + } + ], + "tests": [ + { + "description": "delete", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "deleteOne", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "result": { + "deletedCount": 1 + } + }, + { + "name": "deleteMany", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": { + "$lte": 3 + } + } + }, + "result": { + "deletedCount": 2 + } + }, + { + "name": "deleteOne", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 4 + } + }, + "result": { + "deletedCount": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "_id": 1 + }, + "limit": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "delete", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "_id": { + "$lte": 3 + } + }, + "limit": 0 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "delete", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "_id": 4 + }, + "limit": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "delete", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 5 + } + ] + } + } + }, + { + "description": "collection writeConcern ignored for delete", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "deleteOne", + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": "majority" + } + }, + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "result": { + "deletedCount": 1 + } + }, + { + "name": "deleteMany", + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": "majority" + } + }, + "arguments": { + "session": "session0", + "filter": { + "_id": { + "$lte": 3 + } + } + }, + "result": { + "deletedCount": 2 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "_id": 1 + }, + "limit": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "delete", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "delete": "test", + "deletes": [ + { + "q": { + "_id": { + "$lte": 3 + } + }, + "limit": 0 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "delete", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ] + } + ] +} diff --git a/t/data/transactions/delete.yml b/t/data/transactions/delete.yml new file mode 100644 index 00000000..86b5a3a9 --- /dev/null +++ b/t/data/transactions/delete.yml @@ -0,0 +1,184 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: + - _id: 1 + - _id: 2 + - _id: 3 + - _id: 4 + - _id: 5 + +tests: + - description: delete + + operations: + - name: startTransaction + object: session0 + - name: deleteOne + object: collection + arguments: + session: session0 + filter: + _id: 1 + result: + deletedCount: 1 + - name: deleteMany + object: collection + arguments: + session: session0 + filter: + _id: {$lte: 3} + result: + deletedCount: 2 + - name: deleteOne + object: collection + arguments: + session: session0 + filter: + _id: 4 + result: + deletedCount: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + delete: *collection_name + deletes: + - q: {_id: 1} + limit: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: delete + database_name: *database_name + - command_started_event: + command: + delete: *collection_name + deletes: + - q: {_id: {$lte: 3}} + limit: 0 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: delete + database_name: *database_name + - command_started_event: + command: + delete: *collection_name + deletes: + - q: {_id: 4} + limit: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: delete + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 5 + + - description: collection writeConcern ignored for delete + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: deleteOne + object: collection + collectionOptions: + writeConcern: + w: majority + arguments: + session: session0 + filter: + _id: 1 + result: + deletedCount: 1 + - name: deleteMany + object: collection + collectionOptions: + writeConcern: + w: majority + arguments: + session: session0 + filter: + _id: {$lte: 3} + result: + deletedCount: 2 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + delete: *collection_name + deletes: + - q: {_id: 1} + limit: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: delete + database_name: *database_name + - command_started_event: + command: + delete: *collection_name + deletes: + - q: {_id: {$lte: 3}} + limit: 0 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: delete + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin diff --git a/t/data/transactions/error-labels.json b/t/data/transactions/error-labels.json new file mode 100644 index 00000000..2762767c --- /dev/null +++ b/t/data/transactions/error-labels.json @@ -0,0 +1,951 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "DuplicateKey errors do not contain transient label", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertMany", + "object": "collection", + "arguments": { + "session": "session0", + "documents": [ + { + "_id": 1 + }, + { + "_id": 1 + } + ] + }, + "result": { + "errorCodeName": "DuplicateKey", + "errorLabelsOmit": [ + "TransientTransactionError", + "UnknownTransactionCommitResult" + ] + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + }, + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "NotMaster errors contain transient label", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 10107 + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "errorLabelsContain": [ + "TransientTransactionError" + ], + "errorLabelsOmit": [ + "UnknownTransactionCommitResult" + ] + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "WriteConflict errors contain transient label", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 112 + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "errorLabelsContain": [ + "TransientTransactionError" + ], + "errorLabelsOmit": [ + "UnknownTransactionCommitResult" + ] + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "NoSuchTransaction errors contain transient label", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 251 + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "errorLabelsContain": [ + "TransientTransactionError" + ], + "errorLabelsOmit": [ + "UnknownTransactionCommitResult" + ] + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "NoSuchTransaction errors on commit contain transient label", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 251 + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorLabelsContain": [ + "TransientTransactionError" + ], + "errorLabelsOmit": [ + "UnknownTransactionCommitResult" + ] + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "add transient label to connection errors", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "closeConnection": true + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "errorLabelsContain": [ + "TransientTransactionError" + ], + "errorLabelsOmit": [ + "UnknownTransactionCommitResult" + ] + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "add unknown commit label to connection errors", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "closeConnection": true + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorLabelsContain": [ + "UnknownTransactionCommitResult" + ], + "errorLabelsOmit": [ + "TransientTransactionError" + ] + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "add unknown commit label to retryable commit errors", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 11602 + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorLabelsContain": [ + "UnknownTransactionCommitResult" + ], + "errorLabelsOmit": [ + "TransientTransactionError" + ] + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "add unknown commit label to writeConcernError ShutdownInProgress", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorLabelsContain": [ + "UnknownTransactionCommitResult" + ], + "errorLabelsOmit": [ + "TransientTransactionError" + ] + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + } + ] +} diff --git a/t/data/transactions/error-labels.yml b/t/data/transactions/error-labels.yml new file mode 100644 index 00000000..7d9500b5 --- /dev/null +++ b/t/data/transactions/error-labels.yml @@ -0,0 +1,600 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] + +tests: + - description: DuplicateKey errors do not contain transient label + + operations: + - name: startTransaction + object: session0 + - name: insertMany + object: collection + arguments: + session: session0 + documents: + - _id: 1 + - _id: 1 + result: + errorCodeName: DuplicateKey + errorLabelsOmit: ["TransientTransactionError", "UnknownTransactionCommitResult"] + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: NotMaster errors contain transient label + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 10107 # NotMaster + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + # Note, the server will return the errorLabel in this case. + errorLabelsContain: ["TransientTransactionError"] + errorLabelsOmit: ["UnknownTransactionCommitResult"] + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: WriteConflict errors contain transient label + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 112 # WriteConflict + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + # Note, the server will return the errorLabel in this case. + errorLabelsContain: ["TransientTransactionError"] + errorLabelsOmit: ["UnknownTransactionCommitResult"] + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: NoSuchTransaction errors contain transient label + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + errorCode: 251 # NoSuchTransaction + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + # Note, the server will return the errorLabel in this case. + errorLabelsContain: ["TransientTransactionError"] + errorLabelsOmit: ["UnknownTransactionCommitResult"] + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: NoSuchTransaction errors on commit contain transient label + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 251 # NoSuchTransaction + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + result: + # Note, the server will return the errorLabel in this case. + errorLabelsContain: ["TransientTransactionError"] + errorLabelsOmit: ["UnknownTransactionCommitResult"] + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: add transient label to connection errors + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + closeConnection: true + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + errorLabelsContain: ["TransientTransactionError"] + errorLabelsOmit: ["UnknownTransactionCommitResult"] + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: add unknown commit label to connection errors + + failPoint: + configureFailPoint: failCommand + mode: { times: 2 } + data: + failCommands: ["commitTransaction"] + closeConnection: true + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + result: + errorLabelsContain: ["UnknownTransactionCommitResult"] + errorLabelsOmit: ["TransientTransactionError"] + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: add unknown commit label to retryable commit errors + + failPoint: + configureFailPoint: failCommand + mode: { times: 2 } + data: + failCommands: ["commitTransaction"] + errorCode: 11602 # InterruptedDueToReplStateChange + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + result: + errorLabelsContain: ["UnknownTransactionCommitResult"] + errorLabelsOmit: ["TransientTransactionError"] + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: add unknown commit label to writeConcernError ShutdownInProgress + + failPoint: + configureFailPoint: failCommand + mode: { times: 2 } + data: + failCommands: ["commitTransaction"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + result: + errorLabelsContain: ["UnknownTransactionCommitResult"] + errorLabelsOmit: ["TransientTransactionError"] + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 diff --git a/t/data/transactions/errors.json b/t/data/transactions/errors.json new file mode 100644 index 00000000..159cc3ce --- /dev/null +++ b/t/data/transactions/errors.json @@ -0,0 +1,208 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "start insert start", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "startTransaction", + "object": "session0", + "result": { + "errorContains": "transaction already in progress" + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ] + }, + { + "description": "start twice", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0", + "result": { + "errorContains": "transaction already in progress" + } + } + ] + }, + { + "description": "commit and start twice", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0", + "result": { + "errorContains": "transaction already in progress" + } + } + ] + }, + { + "description": "write conflict commit", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "startTransaction", + "object": "session1" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session1", + "document": { + "_id": 1 + } + }, + "result": { + "errorCodeName": "WriteConflict", + "errorLabelsContain": [ + "TransientTransactionError" + ], + "errorLabelsOmit": [ + "UnknownTransactionCommitResult" + ] + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "commitTransaction", + "object": "session1", + "result": { + "errorCodeName": "NoSuchTransaction", + "errorLabelsContain": [ + "TransientTransactionError" + ], + "errorLabelsOmit": [ + "UnknownTransactionCommitResult" + ] + } + } + ] + }, + { + "description": "write conflict abort", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "startTransaction", + "object": "session1" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session1", + "document": { + "_id": 1 + } + }, + "result": { + "errorCodeName": "WriteConflict", + "errorLabelsContain": [ + "TransientTransactionError" + ], + "errorLabelsOmit": [ + "UnknownTransactionCommitResult" + ] + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "abortTransaction", + "object": "session1" + } + ] + } + ] +} diff --git a/t/data/transactions/errors.yml b/t/data/transactions/errors.yml new file mode 100644 index 00000000..57d0dc97 --- /dev/null +++ b/t/data/transactions/errors.yml @@ -0,0 +1,125 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] +tests: + - description: start insert start + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: startTransaction + object: session0 + result: + # Client-side error. + errorContains: transaction already in progress + # Just to clean up. + - name: commitTransaction + object: session0 + + - description: start twice + + operations: + - name: startTransaction + object: session0 + - name: startTransaction + object: session0 + result: + # Client-side error. + errorContains: transaction already in progress + + - description: commit and start twice + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + - name: startTransaction + object: session0 + - name: startTransaction + object: session0 + result: + # Client-side error. + errorContains: transaction already in progress + + - description: write conflict commit + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: startTransaction + object: session1 + - name: insertOne + object: collection + arguments: + session: session1 + document: + _id: 1 + result: + errorCodeName: WriteConflict + errorLabelsContain: ["TransientTransactionError"] + errorLabelsOmit: ["UnknownTransactionCommitResult"] + - name: commitTransaction + object: session0 + - name: commitTransaction + object: session1 + result: + errorCodeName: NoSuchTransaction + errorLabelsContain: ["TransientTransactionError"] + errorLabelsOmit: ["UnknownTransactionCommitResult"] + + - description: write conflict abort + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: startTransaction + object: session1 + - name: insertOne + object: collection + arguments: + session: session1 + document: + _id: 1 + result: + errorCodeName: WriteConflict + errorLabelsContain: ["TransientTransactionError"] + errorLabelsOmit: ["UnknownTransactionCommitResult"] + - name: commitTransaction + object: session0 + # Driver ignores "NoSuchTransaction" error. + - name: abortTransaction + object: session1 diff --git a/t/data/transactions/findOneAndDelete.json b/t/data/transactions/findOneAndDelete.json new file mode 100644 index 00000000..f02facf2 --- /dev/null +++ b/t/data/transactions/findOneAndDelete.json @@ -0,0 +1,207 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + } + ], + "tests": [ + { + "description": "findOneAndDelete", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "findOneAndDelete", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 3 + } + }, + "result": { + "_id": 3 + } + }, + { + "name": "findOneAndDelete", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 4 + } + }, + "result": null + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "test", + "query": { + "_id": 3 + }, + "remove": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "findAndModify", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "findAndModify": "test", + "query": { + "_id": 4 + }, + "remove": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "findAndModify", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + } + } + }, + { + "description": "collection writeConcern ignored for findOneAndDelete", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "findOneAndDelete", + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": "majority" + } + }, + "arguments": { + "session": "session0", + "filter": { + "_id": 3 + } + }, + "result": { + "_id": 3 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "test", + "query": { + "_id": 3 + }, + "remove": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "findAndModify", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ] + } + ] +} diff --git a/t/data/transactions/findOneAndDelete.yml b/t/data/transactions/findOneAndDelete.yml new file mode 100644 index 00000000..7ade9cbf --- /dev/null +++ b/t/data/transactions/findOneAndDelete.yml @@ -0,0 +1,126 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: + - _id: 1 + - _id: 2 + - _id: 3 + +tests: + - description: findOneAndDelete + + operations: + - name: startTransaction + object: session0 + - name: findOneAndDelete + object: collection + arguments: + session: session0 + filter: {_id: 3} + result: {_id: 3} + - name: findOneAndDelete + object: collection + arguments: + session: session0 + filter: {_id: 4} + result: + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + findAndModify: *collection_name + query: {_id: 3} + remove: True + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + writeConcern: + command_name: findAndModify + database_name: *database_name + - command_started_event: + command: + findAndModify: *collection_name + query: {_id: 4} + remove: True + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: findAndModify + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - {_id: 1} + - {_id: 2} + + - description: collection writeConcern ignored for findOneAndDelete + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: findOneAndDelete + object: collection + collectionOptions: + writeConcern: + w: majority + arguments: + session: session0 + filter: {_id: 3} + result: {_id: 3} + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + findAndModify: *collection_name + query: {_id: 3} + remove: True + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + writeConcern: + command_name: findAndModify + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin diff --git a/t/data/transactions/findOneAndReplace.json b/t/data/transactions/findOneAndReplace.json new file mode 100644 index 00000000..9c77ebf4 --- /dev/null +++ b/t/data/transactions/findOneAndReplace.json @@ -0,0 +1,241 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + } + ], + "tests": [ + { + "description": "findOneAndReplace", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "findOneAndReplace", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 3 + }, + "replacement": { + "x": 1 + }, + "returnDocument": "Before" + }, + "result": { + "_id": 3 + } + }, + { + "name": "findOneAndReplace", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 4 + }, + "replacement": { + "x": 1 + }, + "upsert": true, + "returnDocument": "After" + }, + "result": { + "_id": 4, + "x": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "test", + "query": { + "_id": 3 + }, + "update": { + "x": 1 + }, + "new": false, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "findAndModify", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "findAndModify": "test", + "query": { + "_id": 4 + }, + "update": { + "x": 1 + }, + "new": true, + "upsert": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "findAndModify", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3, + "x": 1 + }, + { + "_id": 4, + "x": 1 + } + ] + } + } + }, + { + "description": "collection writeConcern ignored for findOneAndReplace", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "findOneAndReplace", + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": "majority" + } + }, + "arguments": { + "session": "session0", + "filter": { + "_id": 3 + }, + "replacement": { + "x": 1 + }, + "returnDocument": "Before" + }, + "result": { + "_id": 3 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "test", + "query": { + "_id": 3 + }, + "update": { + "x": 1 + }, + "new": false, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "findAndModify", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ] + } + ] +} diff --git a/t/data/transactions/findOneAndReplace.yml b/t/data/transactions/findOneAndReplace.yml new file mode 100644 index 00000000..58db2a75 --- /dev/null +++ b/t/data/transactions/findOneAndReplace.yml @@ -0,0 +1,140 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: + - _id: 1 + - _id: 2 + - _id: 3 + +tests: + - description: findOneAndReplace + + operations: + - name: startTransaction + object: session0 + - name: findOneAndReplace + object: collection + arguments: + session: session0 + filter: {_id: 3} + replacement: {x: 1} + returnDocument: Before + result: {_id: 3} + - name: findOneAndReplace + object: collection + arguments: + session: session0 + filter: {_id: 4} + replacement: {x: 1} + upsert: true + returnDocument: After + result: {_id: 4, x: 1} + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + findAndModify: *collection_name + query: {_id: 3} + update: {x: 1} + new: false + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + writeConcern: + command_name: findAndModify + database_name: *database_name + - command_started_event: + command: + findAndModify: *collection_name + query: {_id: 4} + update: {x: 1} + new: true + upsert: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: findAndModify + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - {_id: 1} + - {_id: 2} + - {_id: 3, x: 1} + - {_id: 4, x: 1} + + - description: collection writeConcern ignored for findOneAndReplace + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: findOneAndReplace + object: collection + collectionOptions: + writeConcern: + w: majority + arguments: + session: session0 + filter: {_id: 3} + replacement: {x: 1} + returnDocument: Before + result: {_id: 3} + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + findAndModify: *collection_name + query: {_id: 3} + update: {x: 1} + new: false + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + writeConcern: + command_name: findAndModify + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + diff --git a/t/data/transactions/findOneAndUpdate.json b/t/data/transactions/findOneAndUpdate.json new file mode 100644 index 00000000..c56e631e --- /dev/null +++ b/t/data/transactions/findOneAndUpdate.json @@ -0,0 +1,399 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + } + ], + "tests": [ + { + "description": "findOneAndUpdate", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "findOneAndUpdate", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 3 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "Before" + }, + "result": { + "_id": 3 + } + }, + { + "name": "findOneAndUpdate", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 4 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "upsert": true, + "returnDocument": "After" + }, + "result": { + "_id": 4, + "x": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "findOneAndUpdate", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 3 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "Before" + }, + "result": { + "_id": 3, + "x": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "findOneAndUpdate", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 3 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "Before" + }, + "result": { + "_id": 3, + "x": 2 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "test", + "query": { + "_id": 3 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "new": false, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "findAndModify", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "findAndModify": "test", + "query": { + "_id": 4 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "new": true, + "upsert": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "findAndModify", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "findAndModify": "test", + "query": { + "_id": 3 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "new": false, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "afterClusterTime": 42 + }, + "writeConcern": null + }, + "command_name": "findAndModify", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "findAndModify": "test", + "query": { + "_id": 3 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "new": false, + "lsid": "session0", + "txnNumber": { + "$numberLong": "3" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "afterClusterTime": 42 + }, + "writeConcern": null + }, + "command_name": "findAndModify", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "3" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3, + "x": 2 + }, + { + "_id": 4, + "x": 1 + } + ] + } + } + }, + { + "description": "collection writeConcern ignored for findOneAndUpdate", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "findOneAndUpdate", + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": "majority" + } + }, + "arguments": { + "session": "session0", + "filter": { + "_id": 3 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "Before" + }, + "result": { + "_id": 3 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "test", + "query": { + "_id": 3 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "new": false, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "findAndModify", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ] + } + ] +} diff --git a/t/data/transactions/findOneAndUpdate.yml b/t/data/transactions/findOneAndUpdate.yml new file mode 100644 index 00000000..2616bb16 --- /dev/null +++ b/t/data/transactions/findOneAndUpdate.yml @@ -0,0 +1,228 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: + - _id: 1 + - _id: 2 + - _id: 3 + +tests: + - description: findOneAndUpdate + + operations: + - name: startTransaction + object: session0 + - name: findOneAndUpdate + object: collection + arguments: + session: session0 + filter: {_id: 3} + update: + $inc: {x: 1} + returnDocument: Before + result: {_id: 3} + - name: findOneAndUpdate + object: collection + arguments: + session: session0 + filter: {_id: 4} + update: + $inc: {x: 1} + upsert: true + returnDocument: After + result: {_id: 4, x: 1} + - name: commitTransaction + object: session0 + - name: startTransaction + object: session0 + # Test a second time to ensure txnNumber is incremented. + - name: findOneAndUpdate + object: collection + arguments: + session: session0 + filter: {_id: 3} + update: + $inc: {x: 1} + returnDocument: Before + result: {_id: 3, x: 1} + - name: commitTransaction + object: session0 + # Test a third time to ensure abort works. + - name: startTransaction + object: session0 + - name: findOneAndUpdate + object: collection + arguments: + session: session0 + filter: {_id: 3} + update: + $inc: {x: 1} + returnDocument: Before + result: {_id: 3, x: 2} + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + findAndModify: *collection_name + query: {_id: 3} + update: {$inc: {x: 1}} + new: false + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + writeConcern: + command_name: findAndModify + database_name: *database_name + - command_started_event: + command: + findAndModify: *collection_name + query: {_id: 4} + update: {$inc: {x: 1}} + new: true + upsert: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: findAndModify + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + findAndModify: *collection_name + query: {_id: 3} + update: {$inc: {x: 1}} + new: false + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: true + autocommit: false + readConcern: + afterClusterTime: 42 + writeConcern: + command_name: findAndModify + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + findAndModify: *collection_name + query: {_id: 3} + update: {$inc: {x: 1}} + new: false + lsid: session0 + txnNumber: + $numberLong: "3" + startTransaction: true + autocommit: false + readConcern: + afterClusterTime: 42 + writeConcern: + command_name: findAndModify + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "3" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: + - {_id: 1} + - {_id: 2} + - {_id: 3, x: 2} + - {_id: 4, x: 1} + + - description: collection writeConcern ignored for findOneAndUpdate + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: findOneAndUpdate + object: collection + collectionOptions: + writeConcern: + w: majority + arguments: + session: session0 + filter: {_id: 3} + update: + $inc: {x: 1} + returnDocument: Before + result: {_id: 3} + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + findAndModify: *collection_name + query: {_id: 3} + update: + $inc: {x: 1} + new: false + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + writeConcern: + command_name: findAndModify + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + diff --git a/t/data/transactions/insert.json b/t/data/transactions/insert.json new file mode 100644 index 00000000..4dc9b85f --- /dev/null +++ b/t/data/transactions/insert.json @@ -0,0 +1,441 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "insert", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "insertMany", + "object": "collection", + "arguments": { + "documents": [ + { + "_id": 2 + }, + { + "_id": 3 + } + ], + "session": "session0" + }, + "result": { + "insertedIds": { + "0": 2, + "1": 3 + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 4 + } + }, + "result": { + "insertedId": 4 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 5 + } + }, + "result": { + "insertedId": 5 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + }, + { + "_id": 3 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 4 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 5 + } + ], + "ordered": true, + "readConcern": { + "afterClusterTime": 42 + }, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + }, + { + "_id": 5 + } + ] + } + } + }, + { + "description": "collection writeConcern without transaction", + "operations": [ + { + "name": "insertOne", + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": "majority" + } + }, + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": null, + "startTransaction": null, + "autocommit": null, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "collection writeConcern ignored for insert", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": "majority" + } + }, + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "insertMany", + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": "majority" + } + }, + "arguments": { + "documents": [ + { + "_id": 2 + }, + { + "_id": 3 + } + ], + "session": "session0" + }, + "result": { + "insertedIds": { + "0": 2, + "1": 3 + } + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + }, + { + "_id": 3 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + } + ] + } + } + } + ] +} diff --git a/t/data/transactions/insert.yml b/t/data/transactions/insert.yml new file mode 100644 index 00000000..55f0a18a --- /dev/null +++ b/t/data/transactions/insert.yml @@ -0,0 +1,264 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] + +tests: + - description: insert + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: insertMany + object: collection + arguments: + documents: + - _id: 2 + - _id: 3 + session: session0 + result: + insertedIds: {0: 2, 1: 3} + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 4 + result: + insertedId: 4 + - name: commitTransaction + object: session0 + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 5 + result: + insertedId: 5 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + - _id: 3 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 4 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 5 + ordered: true + readConcern: + afterClusterTime: 42 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + - _id: 2 + - _id: 3 + - _id: 4 + - _id: 5 + + # This test proves that the driver parses the collectionOptions writeConcern. + - description: collection writeConcern without transaction + operations: + - name: insertOne + object: collection + collectionOptions: + writeConcern: + w: majority + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + startTransaction: + autocommit: + writeConcern: + w: majority + command_name: insert + database_name: *database_name + + outcome: + collection: + data: + - _id: 1 + + - description: collection writeConcern ignored for insert + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + collectionOptions: + writeConcern: + w: majority + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: insertMany + object: collection + collectionOptions: + writeConcern: + w: majority + arguments: + documents: + - _id: 2 + - _id: 3 + session: session0 + result: + insertedIds: {0: 2, 1: 3} + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + - _id: 3 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + - _id: 2 + - _id: 3 diff --git a/t/data/transactions/isolation.json b/t/data/transactions/isolation.json new file mode 100644 index 00000000..f5b5ea0c --- /dev/null +++ b/t/data/transactions/isolation.json @@ -0,0 +1,211 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "one transaction", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "find", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "result": [ + { + "_id": 1 + } + ] + }, + { + "name": "find", + "object": "collection", + "arguments": { + "session": "session1", + "filter": { + "_id": 1 + } + }, + "result": [] + }, + { + "name": "find", + "object": "collection", + "arguments": { + "filter": { + "_id": 1 + } + }, + "result": [] + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "find", + "object": "collection", + "arguments": { + "session": "session1", + "filter": { + "_id": 1 + } + }, + "result": [ + { + "_id": 1 + } + ] + }, + { + "name": "find", + "object": "collection", + "arguments": { + "filter": { + "_id": 1 + } + }, + "result": [ + { + "_id": 1 + } + ] + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "two transactions", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session1" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "find", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "result": [ + { + "_id": 1 + } + ] + }, + { + "name": "find", + "object": "collection", + "arguments": { + "session": "session1", + "filter": { + "_id": 1 + } + }, + "result": [] + }, + { + "name": "find", + "object": "collection", + "arguments": { + "filter": { + "_id": 1 + } + }, + "result": [] + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "find", + "object": "collection", + "arguments": { + "session": "session1", + "filter": { + "_id": 1 + } + }, + "result": [] + }, + { + "name": "find", + "object": "collection", + "arguments": { + "filter": { + "_id": 1 + } + }, + "result": [ + { + "_id": 1 + } + ] + }, + { + "name": "commitTransaction", + "object": "session1" + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + } + ] +} diff --git a/t/data/transactions/isolation.yml b/t/data/transactions/isolation.yml new file mode 100644 index 00000000..79e031f2 --- /dev/null +++ b/t/data/transactions/isolation.yml @@ -0,0 +1,125 @@ +# Test snapshot isolation. +# This test doesn't check contents of command-started events. +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] + +tests: + - description: one transaction + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: find + object: collection + arguments: + session: session0 + filter: + _id: 1 + result: + - {_id: 1} + - name: find + object: collection + arguments: + session: session1 + filter: + _id: 1 + result: [] + - name: find + object: collection + arguments: + filter: + _id: 1 + result: [] + - name: commitTransaction + object: session0 + - name: find + object: collection + arguments: + session: session1 + filter: + _id: 1 + result: + - {_id: 1} + - name: find + object: collection + arguments: + filter: + _id: 1 + result: + - {_id: 1} + + outcome: + collection: + data: + - _id: 1 + + - description: two transactions + + operations: + - name: startTransaction + object: session0 + - name: startTransaction + object: session1 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: find + object: collection + arguments: + session: session0 + filter: + _id: 1 + result: + - {_id: 1} + - name: find + object: collection + arguments: + session: session1 + filter: + _id: 1 + result: [] + - name: find + object: collection + arguments: + filter: + _id: 1 + result: [] + - name: commitTransaction + object: session0 + # Snapshot isolation in session1, not read-committed. + - name: find + object: collection + arguments: + session: session1 + filter: + _id: 1 + result: [] + - name: find + object: collection + arguments: + filter: + _id: 1 + result: + - {_id: 1} + - name: commitTransaction + object: session1 + + outcome: + collection: + data: + - {_id: 1} diff --git a/t/data/transactions/read-pref.json b/t/data/transactions/read-pref.json new file mode 100644 index 00000000..03a7eca1 --- /dev/null +++ b/t/data/transactions/read-pref.json @@ -0,0 +1,706 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "default readPreference", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertMany", + "object": "collection", + "arguments": { + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ], + "session": "session0" + }, + "result": { + "insertedIds": { + "0": 1, + "1": 2, + "2": 3, + "3": 4 + } + } + }, + { + "name": "aggregate", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Secondary" + } + }, + "arguments": { + "session": "session0", + "pipeline": [ + { + "$match": { + "_id": 1 + } + }, + { + "$count": "count" + } + ] + }, + "result": [ + { + "count": 1 + } + ] + }, + { + "name": "find", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Secondary" + } + }, + "arguments": { + "session": "session0", + "batchSize": 3 + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + { + "name": "aggregate", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Secondary" + } + }, + "arguments": { + "pipeline": [ + { + "$project": { + "_id": 1 + } + } + ], + "batchSize": 3, + "session": "session0" + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + } + } + }, + { + "description": "primary readPreference", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "readPreference": { + "mode": "Primary" + } + } + } + }, + { + "name": "insertMany", + "object": "collection", + "arguments": { + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ], + "session": "session0" + }, + "result": { + "insertedIds": { + "0": 1, + "1": 2, + "2": 3, + "3": 4 + } + } + }, + { + "name": "aggregate", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Secondary" + } + }, + "arguments": { + "session": "session0", + "pipeline": [ + { + "$match": { + "_id": 1 + } + }, + { + "$count": "count" + } + ] + }, + "result": [ + { + "count": 1 + } + ] + }, + { + "name": "find", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Secondary" + } + }, + "arguments": { + "session": "session0", + "batchSize": 3 + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + { + "name": "aggregate", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Secondary" + } + }, + "arguments": { + "pipeline": [ + { + "$project": { + "_id": 1 + } + } + ], + "batchSize": 3, + "session": "session0" + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + } + } + }, + { + "description": "secondary readPreference", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "readPreference": { + "mode": "Secondary" + } + } + } + }, + { + "name": "insertMany", + "object": "collection", + "arguments": { + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ], + "session": "session0" + }, + "result": { + "insertedIds": { + "0": 1, + "1": 2, + "2": 3, + "3": 4 + } + } + }, + { + "name": "aggregate", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Primary" + } + }, + "arguments": { + "session": "session0", + "pipeline": [ + { + "$match": { + "_id": 1 + } + }, + { + "$count": "count" + } + ] + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + }, + { + "name": "find", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Primary" + } + }, + "arguments": { + "session": "session0", + "batchSize": 3 + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + }, + { + "name": "aggregate", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Primary" + } + }, + "arguments": { + "pipeline": [ + { + "$project": { + "_id": 1 + } + } + ], + "batchSize": 3, + "session": "session0" + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "primaryPreferred readPreference", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "readPreference": { + "mode": "PrimaryPreferred" + } + } + } + }, + { + "name": "insertMany", + "object": "collection", + "arguments": { + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ], + "session": "session0" + }, + "result": { + "insertedIds": { + "0": 1, + "1": 2, + "2": 3, + "3": 4 + } + } + }, + { + "name": "aggregate", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Primary" + } + }, + "arguments": { + "session": "session0", + "pipeline": [ + { + "$match": { + "_id": 1 + } + }, + { + "$count": "count" + } + ] + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + }, + { + "name": "find", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Primary" + } + }, + "arguments": { + "session": "session0", + "batchSize": 3 + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + }, + { + "name": "aggregate", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Primary" + } + }, + "arguments": { + "pipeline": [ + { + "$project": { + "_id": 1 + } + } + ], + "batchSize": 3, + "session": "session0" + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "nearest readPreference", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "readPreference": { + "mode": "Nearest" + } + } + } + }, + { + "name": "insertMany", + "object": "collection", + "arguments": { + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ], + "session": "session0" + }, + "result": { + "insertedIds": { + "0": 1, + "1": 2, + "2": 3, + "3": 4 + } + } + }, + { + "name": "aggregate", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Primary" + } + }, + "arguments": { + "session": "session0", + "pipeline": [ + { + "$match": { + "_id": 1 + } + }, + { + "$count": "count" + } + ] + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + }, + { + "name": "find", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Primary" + } + }, + "arguments": { + "session": "session0", + "batchSize": 3 + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + }, + { + "name": "aggregate", + "object": "collection", + "collectionOptions": { + "readPreference": { + "mode": "Primary" + } + }, + "arguments": { + "pipeline": [ + { + "$project": { + "_id": 1 + } + } + ], + "batchSize": 3, + "session": "session0" + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "secondary write only", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "readPreference": { + "mode": "Secondary" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + } + ] +} diff --git a/t/data/transactions/read-pref.yml b/t/data/transactions/read-pref.yml new file mode 100644 index 00000000..de1b41f6 --- /dev/null +++ b/t/data/transactions/read-pref.yml @@ -0,0 +1,340 @@ +# This test doesn't check contents of command-started events. +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] + +tests: + - description: default readPreference + + operations: + - name: startTransaction + object: session0 + - name: insertMany + object: collection + arguments: + documents: &insertedDocs + - _id: 1 + - _id: 2 + - _id: 3 + - _id: 4 + session: session0 + result: + insertedIds: {0: 1, 1: 2, 2: 3, 3: 4} + - name: aggregate + object: collection + collectionOptions: + # The driver overrides the collection's read pref with the + # transaction's so count runs with Primary and succeeds. + readPreference: + mode: Secondary + arguments: + session: session0 + pipeline: + - $match: + _id: 1 + - $count: count + result: + - count: 1 + - name: find + object: collection + collectionOptions: + readPreference: + mode: Secondary + arguments: + session: session0 + batchSize: 3 + result: *insertedDocs + - name: aggregate + object: collection + collectionOptions: + readPreference: + mode: Secondary + arguments: + pipeline: + - $project: + _id: 1 + batchSize: 3 + session: session0 + result: *insertedDocs + - name: commitTransaction + object: session0 + + outcome: + collection: + data: *insertedDocs + + - description: primary readPreference + + operations: + - name: startTransaction + object: session0 + arguments: + options: + readPreference: + mode: Primary + - name: insertMany + object: collection + arguments: + documents: &insertedDocs + - _id: 1 + - _id: 2 + - _id: 3 + - _id: 4 + session: session0 + result: + insertedIds: {0: 1, 1: 2, 2: 3, 3: 4} + - name: aggregate + object: collection + collectionOptions: + readPreference: + mode: Secondary + arguments: + session: session0 + pipeline: + - $match: + _id: 1 + - $count: count + result: + - count: 1 + - name: find + object: collection + collectionOptions: + readPreference: + mode: Secondary + arguments: + session: session0 + batchSize: 3 + result: *insertedDocs + - name: aggregate + object: collection + collectionOptions: + readPreference: + mode: Secondary + arguments: + pipeline: + - $project: + _id: 1 + batchSize: 3 + session: session0 + result: *insertedDocs + - name: commitTransaction + object: session0 + + outcome: + collection: + data: *insertedDocs + + - description: secondary readPreference + + operations: + - name: startTransaction + object: session0 + arguments: + options: + readPreference: + mode: Secondary + - name: insertMany + object: collection + arguments: + documents: &insertedDocs + - _id: 1 + - _id: 2 + - _id: 3 + - _id: 4 + session: session0 + result: + insertedIds: {0: 1, 1: 2, 2: 3, 3: 4} + - name: aggregate + object: collection + collectionOptions: + readPreference: + mode: Primary + arguments: + session: session0 + pipeline: + - $match: + _id: 1 + - $count: count + result: + errorContains: read preference in a transaction must be primary + - name: find + object: collection + collectionOptions: + readPreference: + mode: Primary + arguments: + session: session0 + batchSize: 3 + result: + errorContains: read preference in a transaction must be primary + - name: aggregate + object: collection + collectionOptions: + readPreference: + mode: Primary + arguments: + pipeline: + - $project: + _id: 1 + batchSize: 3 + session: session0 + result: + errorContains: read preference in a transaction must be primary + - name: abortTransaction + object: session0 + + outcome: + collection: + data: [] + + - description: primaryPreferred readPreference + + operations: + - name: startTransaction + object: session0 + arguments: + options: + readPreference: + mode: PrimaryPreferred + - name: insertMany + object: collection + arguments: + documents: &insertedDocs + - _id: 1 + - _id: 2 + - _id: 3 + - _id: 4 + session: session0 + result: + insertedIds: {0: 1, 1: 2, 2: 3, 3: 4} + - name: aggregate + object: collection + collectionOptions: + readPreference: + mode: Primary + arguments: + session: session0 + pipeline: + - $match: + _id: 1 + - $count: count + result: + errorContains: read preference in a transaction must be primary + - name: find + object: collection + collectionOptions: + readPreference: + mode: Primary + arguments: + session: session0 + batchSize: 3 + result: + errorContains: read preference in a transaction must be primary + - name: aggregate + object: collection + collectionOptions: + readPreference: + mode: Primary + arguments: + pipeline: + - $project: + _id: 1 + batchSize: 3 + session: session0 + result: + errorContains: read preference in a transaction must be primary + - name: abortTransaction + object: session0 + + outcome: + collection: + data: [] + + - description: nearest readPreference + + operations: + - name: startTransaction + object: session0 + arguments: + options: + readPreference: + mode: Nearest + - name: insertMany + object: collection + arguments: + documents: &insertedDocs + - _id: 1 + - _id: 2 + - _id: 3 + - _id: 4 + session: session0 + result: + insertedIds: {0: 1, 1: 2, 2: 3, 3: 4} + - name: aggregate + object: collection + collectionOptions: + readPreference: + mode: Primary + arguments: + session: session0 + pipeline: + - $match: + _id: 1 + - $count: count + result: + errorContains: read preference in a transaction must be primary + - name: find + object: collection + collectionOptions: + readPreference: + mode: Primary + arguments: + session: session0 + batchSize: 3 + result: + errorContains: read preference in a transaction must be primary + - name: aggregate + object: collection + collectionOptions: + readPreference: + mode: Primary + arguments: + pipeline: + - $project: + _id: 1 + batchSize: 3 + session: session0 + result: + errorContains: read preference in a transaction must be primary + - name: abortTransaction + object: session0 + + outcome: + collection: + data: [] + + - description: secondary write only + + operations: + - name: startTransaction + object: session0 + arguments: + options: + readPreference: + mode: Secondary + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + outcome: + collection: + data: + - _id: 1 diff --git a/t/data/transactions/reads.json b/t/data/transactions/reads.json new file mode 100644 index 00000000..656150ec --- /dev/null +++ b/t/data/transactions/reads.json @@ -0,0 +1,611 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ], + "tests": [ + { + "description": "collection readConcern without transaction", + "operations": [ + { + "name": "find", + "object": "collection", + "collectionOptions": { + "readConcern": { + "level": "majority" + } + }, + "arguments": { + "session": "session0" + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "find": "test", + "readConcern": { + "level": "majority" + }, + "lsid": "session0", + "txnNumber": null, + "startTransaction": null, + "autocommit": null + }, + "command_name": "find", + "database_name": "transaction-tests" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + } + } + }, + { + "description": "count", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "count", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "result": { + "errorContains": "Cannot run 'count' in a multi-document transaction" + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "count": "test", + "query": { + "_id": 1 + }, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "count", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + } + } + }, + { + "description": "find", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "find", + "object": "collection", + "arguments": { + "session": "session0", + "batchSize": 3 + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + { + "name": "find", + "object": "collection", + "arguments": { + "session": "session0", + "batchSize": 3 + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "find": "test", + "batchSize": 3, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false + }, + "command_name": "find", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "getMore": { + "$numberLong": "42" + }, + "collection": "test", + "batchSize": 3, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false + }, + "command_name": "getMore", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "find": "test", + "batchSize": 3, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false + }, + "command_name": "find", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "getMore": { + "$numberLong": "42" + }, + "collection": "test", + "batchSize": 3, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false + }, + "command_name": "getMore", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + } + } + }, + { + "description": "aggregate", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "aggregate", + "object": "collection", + "arguments": { + "pipeline": [ + { + "$project": { + "_id": 1 + } + } + ], + "batchSize": 3, + "session": "session0" + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + { + "name": "aggregate", + "object": "collection", + "arguments": { + "pipeline": [ + { + "$project": { + "_id": 1 + } + } + ], + "batchSize": 3, + "session": "session0" + }, + "result": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "aggregate": "test", + "pipeline": [ + { + "$project": { + "_id": 1 + } + } + ], + "cursor": { + "batchSize": 3 + }, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false + }, + "command_name": "aggregate", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "getMore": { + "$numberLong": "42" + }, + "collection": "test", + "batchSize": 3, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false + }, + "command_name": "getMore", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "aggregate": "test", + "pipeline": [ + { + "$project": { + "_id": 1 + } + } + ], + "cursor": { + "batchSize": 3 + }, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false + }, + "command_name": "aggregate", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "getMore": { + "$numberLong": "42" + }, + "collection": "test", + "batchSize": 3, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false + }, + "command_name": "getMore", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + } + } + }, + { + "description": "distinct", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "distinct", + "object": "collection", + "arguments": { + "session": "session0", + "fieldName": "_id" + }, + "result": [ + 1, + 2, + 3, + 4 + ] + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "distinct": "test", + "key": "_id", + "lsid": "session0", + "readConcern": null, + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "distinct", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "readConcern": null, + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + }, + { + "_id": 4 + } + ] + } + } + } + ] +} diff --git a/t/data/transactions/reads.yml b/t/data/transactions/reads.yml new file mode 100644 index 00000000..c7822f06 --- /dev/null +++ b/t/data/transactions/reads.yml @@ -0,0 +1,297 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: &data + - {_id: 1} + - {_id: 2} + - {_id: 3} + - {_id: 4} + +tests: + - description: collection readConcern without transaction + + operations: + - name: find + object: collection + collectionOptions: + readConcern: + level: majority + arguments: + session: session0 + result: *data + + expectations: + - command_started_event: + command: + find: *collection_name + readConcern: + level: majority + lsid: session0 + txnNumber: + startTransaction: + autocommit: + command_name: find + database_name: *database_name + + outcome: &outcome + collection: + data: + *data + + - description: count + + operations: + - &startTransaction + name: startTransaction + object: session0 + - name: count + object: collection + arguments: + session: session0 + filter: + _id: 1 + result: + errorContains: "Cannot run 'count' in a multi-document transaction" + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + count: *collection_name + query: + _id: 1 + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: count + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: *outcome + + - description: find + + operations: + - *startTransaction + - &find + name: find + object: collection + arguments: + session: session0 + batchSize: 3 + result: *data + - *find + - &commitTransaction + name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + find: *collection_name + batchSize: 3 + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + command_name: find + database_name: *database_name + - command_started_event: + command: + getMore: + # 42 is a fake placeholder value for the cursorId. + $numberLong: '42' + collection: *collection_name + batchSize: 3 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + command_name: getMore + database_name: *database_name + - command_started_event: + command: + find: *collection_name + batchSize: 3 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + command_name: find + database_name: *database_name + - command_started_event: + command: + getMore: + $numberLong: '42' + collection: *collection_name + batchSize: 3 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + command_name: getMore + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: *outcome + + - description: aggregate + + operations: + - *startTransaction + - &aggregate + name: aggregate + object: collection + arguments: + pipeline: + - $project: + _id: 1 + batchSize: 3 + session: session0 + result: *data + - *aggregate + - *commitTransaction + + expectations: + - command_started_event: + command: + aggregate: *collection_name + pipeline: + - $project: + _id: 1 + cursor: + batchSize: 3 + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + command_name: aggregate + database_name: *database_name + - command_started_event: + command: + getMore: + # 42 is a fake placeholder value for the cursorId. + $numberLong: '42' + collection: *collection_name + batchSize: 3 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + command_name: getMore + database_name: *database_name + - command_started_event: + command: + aggregate: *collection_name + pipeline: + - $project: + _id: 1 + cursor: + batchSize: 3 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + command_name: aggregate + database_name: *database_name + - command_started_event: + command: + getMore: + $numberLong: '42' + collection: *collection_name + batchSize: 3 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + command_name: getMore + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: *outcome + + - description: distinct + + operations: + - *startTransaction + - name: distinct + object: collection + arguments: + session: session0 + fieldName: _id + result: [1, 2, 3, 4] + - *commitTransaction + + expectations: + - command_started_event: + command: + distinct: *collection_name + key: _id + lsid: session0 + readConcern: + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: distinct + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + readConcern: + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: *outcome diff --git a/t/data/transactions/retryable-abort.json b/t/data/transactions/retryable-abort.json new file mode 100644 index 00000000..d2ab05dc --- /dev/null +++ b/t/data/transactions/retryable-abort.json @@ -0,0 +1,1958 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "abortTransaction only performs a single retry", + "clientOptions": { + "retryWrites": false + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "closeConnection": true + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction does not retry after Interrupted", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "errorCode": 11601, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction does not retry after WriteConcernError Interrupted", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "writeConcernError": { + "code": 11601, + "errmsg": "operation was interrupted" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after connection error", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "closeConnection": true + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after NotMaster", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "errorCode": 10107, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after NotMasterOrSecondary", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "errorCode": 13436, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after NotMasterNoSlaveOk", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "errorCode": 13435, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after InterruptedDueToReplStateChange", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "errorCode": 11602, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after InterruptedAtShutdown", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "errorCode": 11600, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after PrimarySteppedDown", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "errorCode": 189, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after ShutdownInProgress", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "errorCode": 91, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after HostNotFound", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "errorCode": 7, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after HostUnreachable", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "errorCode": 6, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after SocketException", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "errorCode": 9001, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after NetworkTimeout", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "errorCode": 89, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after WriteConcernError InterruptedAtShutdown", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "writeConcernError": { + "code": 11600, + "errmsg": "Replication is being shut down" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after WriteConcernError InterruptedDueToReplStateChange", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "writeConcernError": { + "code": 11602, + "errmsg": "Replication is being shut down" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after WriteConcernError PrimarySteppedDown", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "writeConcernError": { + "code": 189, + "errmsg": "Replication is being shut down" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abortTransaction succeeds after WriteConcernError ShutdownInProgress", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "abortTransaction" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + } + ] +} diff --git a/t/data/transactions/retryable-abort.yml b/t/data/transactions/retryable-abort.yml new file mode 100644 index 00000000..3dd53209 --- /dev/null +++ b/t/data/transactions/retryable-abort.yml @@ -0,0 +1,1292 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] + +tests: + - description: abortTransaction only performs a single retry + + clientOptions: + retryWrites: false + + failPoint: + configureFailPoint: failCommand + mode: { times: 2 } + data: + failCommands: ["abortTransaction"] + closeConnection: true + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + # Call to abort returns no error even when the retry attempt fails. + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction does not retry after Interrupted + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + errorCode: 11601 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction does not retry after WriteConcernError Interrupted + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + writeConcernError: + code: 11601 + errmsg: operation was interrupted + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after connection error + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + closeConnection: true + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after NotMaster + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + errorCode: 10107 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after NotMasterOrSecondary + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + errorCode: 13436 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after NotMasterNoSlaveOk + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + errorCode: 13435 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after InterruptedDueToReplStateChange + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + errorCode: 11602 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after InterruptedAtShutdown + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + errorCode: 11600 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after PrimarySteppedDown + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + errorCode: 189 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after ShutdownInProgress + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + errorCode: 91 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after HostNotFound + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + errorCode: 7 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after HostUnreachable + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + errorCode: 6 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after SocketException + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + errorCode: 9001 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after NetworkTimeout + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + errorCode: 89 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after WriteConcernError InterruptedAtShutdown + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + writeConcernError: + code: 11600 + errmsg: Replication is being shut down + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after WriteConcernError InterruptedDueToReplStateChange + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + writeConcernError: + code: 11602 + errmsg: Replication is being shut down + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after WriteConcernError PrimarySteppedDown + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + writeConcernError: + code: 189 + errmsg: Replication is being shut down + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abortTransaction succeeds after WriteConcernError ShutdownInProgress + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["abortTransaction"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] diff --git a/t/data/transactions/retryable-commit.json b/t/data/transactions/retryable-commit.json new file mode 100644 index 00000000..a4a6850c --- /dev/null +++ b/t/data/transactions/retryable-commit.json @@ -0,0 +1,2069 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "commitTransaction fails after two errors", + "clientOptions": { + "retryWrites": false + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "closeConnection": true + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorLabelsContain": [ + "UnknownTransactionCommitResult" + ], + "errorLabelsOmit": [ + "TransientTransactionError" + ] + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction fails after Interrupted", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 11601, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorCodeName": "Interrupted", + "errorLabelsOmit": [ + "TransientTransactionError" + ] + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "commitTransaction fails after WriteConcernError Interrupted", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "writeConcernError": { + "code": 11601, + "errmsg": "operation was interrupted" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorLabelsOmit": [ + "TransientTransactionError" + ] + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after connection error", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "closeConnection": true + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after NotMaster", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 10107, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after NotMasterOrSecondary", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 13436, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after NotMasterNoSlaveOk", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 13435, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after InterruptedDueToReplStateChange", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 11602, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after InterruptedAtShutdown", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 11600, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after PrimarySteppedDown", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 189, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after ShutdownInProgress", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 91, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after HostNotFound", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 7, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after HostUnreachable", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 6, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after SocketException", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 9001, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after NetworkTimeout", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "errorCode": 89, + "closeConnection": false + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after WriteConcernError InterruptedAtShutdown", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "writeConcernError": { + "code": 11600, + "errmsg": "Replication is being shut down" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after WriteConcernError InterruptedDueToReplStateChange", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "writeConcernError": { + "code": 11602, + "errmsg": "Replication is being shut down" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after WriteConcernError PrimarySteppedDown", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "writeConcernError": { + "code": 189, + "errmsg": "Replication is being shut down" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commitTransaction succeeds after WriteConcernError ShutdownInProgress", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + } + ] +} diff --git a/t/data/transactions/retryable-commit.yml b/t/data/transactions/retryable-commit.yml new file mode 100644 index 00000000..d0354ddb --- /dev/null +++ b/t/data/transactions/retryable-commit.yml @@ -0,0 +1,1332 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] + +tests: + - description: commitTransaction fails after two errors + + clientOptions: + retryWrites: false + + failPoint: + configureFailPoint: failCommand + mode: { times: 2 } + data: + failCommands: ["commitTransaction"] + closeConnection: true + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + # First call to commit fails after a single retry attempt. + - name: commitTransaction + object: session0 + result: + errorLabelsContain: ["UnknownTransactionCommitResult"] + errorLabelsOmit: ["TransientTransactionError"] + # Second call to commit succeeds because the failpoint was disabled. + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction fails after Interrupted + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 11601 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + result: + errorCodeName: Interrupted + errorLabelsOmit: ["TransientTransactionError"] + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: commitTransaction fails after WriteConcernError Interrupted + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + writeConcernError: + code: 11601 + errmsg: operation was interrupted + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + result: + errorLabelsOmit: ["TransientTransactionError"] + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after connection error + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + closeConnection: true + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after NotMaster + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 10107 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after NotMasterOrSecondary + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 13436 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after NotMasterNoSlaveOk + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 13435 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after InterruptedDueToReplStateChange + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 11602 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after InterruptedAtShutdown + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 11600 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after PrimarySteppedDown + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 189 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after ShutdownInProgress + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 91 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after HostNotFound + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 7 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after HostUnreachable + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 6 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after SocketException + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 9001 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after NetworkTimeout + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + errorCode: 89 + closeConnection: false + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after WriteConcernError InterruptedAtShutdown + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + writeConcernError: + code: 11600 + errmsg: Replication is being shut down + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after WriteConcernError InterruptedDueToReplStateChange + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + writeConcernError: + code: 11602 + errmsg: Replication is being shut down + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after WriteConcernError PrimarySteppedDown + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + writeConcernError: + code: 189 + errmsg: Replication is being shut down + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commitTransaction succeeds after WriteConcernError ShutdownInProgress + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + writeConcernError: + code: 91 + errmsg: Replication is being shut down + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 diff --git a/t/data/transactions/retryable-writes.json b/t/data/transactions/retryable-writes.json new file mode 100644 index 00000000..e568de5c --- /dev/null +++ b/t/data/transactions/retryable-writes.json @@ -0,0 +1,329 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "increment txnNumber", + "clientOptions": { + "retryWrites": true + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 2 + } + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 3 + } + }, + "result": { + "insertedId": 3 + } + }, + { + "name": "abortTransaction", + "object": "session0" + }, + { + "name": "insertMany", + "object": "collection", + "arguments": { + "documents": [ + { + "_id": 4 + }, + { + "_id": 5 + } + ], + "session": "session0" + }, + "result": { + "insertedIds": { + "0": 4, + "1": 5 + } + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": null, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 3 + } + ], + "ordered": true, + "readConcern": { + "afterClusterTime": 42 + }, + "lsid": "session0", + "txnNumber": { + "$numberLong": "3" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "3" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 4 + }, + { + "_id": 5 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "4" + }, + "startTransaction": null, + "autocommit": null, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 4 + }, + { + "_id": 5 + } + ] + } + } + }, + { + "description": "writes are not retried", + "clientOptions": { + "retryWrites": true + }, + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "closeConnection": true + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "errorLabelsContain": [ + "TransientTransactionError" + ] + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + } + ] +} diff --git a/t/data/transactions/retryable-writes.yml b/t/data/transactions/retryable-writes.yml new file mode 100644 index 00000000..e60d04c3 --- /dev/null +++ b/t/data/transactions/retryable-writes.yml @@ -0,0 +1,208 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] + +tests: + - description: increment txnNumber + + clientOptions: + retryWrites: true + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + # Retryable write should include the next txnNumber + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 2 + result: + insertedId: 2 + # Next transaction should include the next txnNumber + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 3 + result: + insertedId: 3 + - name: abortTransaction + object: session0 + # Retryable write should include the next txnNumber + - name: insertMany + object: collection + arguments: + documents: + - _id: 4 + - _id: 5 + session: session0 + result: + insertedIds: {0: 4, 1: 5} + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 3 + ordered: true + readConcern: + afterClusterTime: 42 + lsid: session0 + txnNumber: + $numberLong: "3" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "3" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 4 + - _id: 5 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "4" + startTransaction: + autocommit: + writeConcern: + command_name: insert + database_name: *database_name + + outcome: + collection: + data: + - _id: 1 + - _id: 2 + - _id: 4 + - _id: 5 + + - description: writes are not retried + + clientOptions: + retryWrites: true + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["insert"] + closeConnection: true + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + errorLabelsContain: ["TransientTransactionError"] + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] diff --git a/t/data/transactions/run-command.json b/t/data/transactions/run-command.json new file mode 100644 index 00000000..a69a5445 --- /dev/null +++ b/t/data/transactions/run-command.json @@ -0,0 +1,292 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "run command with default read preference", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "runCommand", + "object": "database", + "command_name": "insert", + "arguments": { + "session": "session0", + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ] + } + }, + "result": { + "n": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ] + }, + { + "description": "run command with secondary read preference in client option and primary read preference in transaction options", + "clientOptions": { + "readPreference": "secondary" + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "readPreference": { + "mode": "Primary" + } + } + } + }, + { + "name": "runCommand", + "object": "database", + "command_name": "insert", + "arguments": { + "session": "session0", + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ] + } + }, + "result": { + "n": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ] + }, + { + "description": "run command with explicit primary read preference", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "runCommand", + "object": "database", + "command_name": "insert", + "arguments": { + "session": "session0", + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ] + }, + "readPreference": { + "mode": "Primary" + } + }, + "result": { + "n": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ] + }, + { + "description": "run command fails with explicit secondary read preference", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "runCommand", + "object": "database", + "command_name": "find", + "arguments": { + "session": "session0", + "command": { + "find": "test" + }, + "readPreference": { + "mode": "Secondary" + } + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + } + ] + }, + { + "description": "run command fails with secondary read preference from transaction options", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "readPreference": { + "mode": "secondary" + } + } + } + }, + { + "name": "runCommand", + "object": "database", + "command_name": "find", + "arguments": { + "session": "session0", + "command": { + "find": "test" + } + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + } + ] + } + ] +} diff --git a/t/data/transactions/run-command.yml b/t/data/transactions/run-command.yml new file mode 100644 index 00000000..e8444990 --- /dev/null +++ b/t/data/transactions/run-command.yml @@ -0,0 +1,189 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] + +tests: + - description: run command with default read preference + + operations: + - name: startTransaction + object: session0 + - name: runCommand + object: database + command_name: insert + arguments: + session: session0 + command: + insert: *collection_name + documents: + - _id : 1 + result: + n: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id : 1 + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + - description: run command with secondary read preference in client option and primary read preference in transaction options + + clientOptions: + readPreference: secondary + + operations: + - name: startTransaction + object: session0 + arguments: + options: + readPreference: + mode: Primary + - name: runCommand + object: database + command_name: insert + arguments: + session: session0 + command: + insert: *collection_name + documents: + - _id : 1 + result: + n: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id : 1 + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + - description: run command with explicit primary read preference + + operations: + - name: startTransaction + object: session0 + - name: runCommand + object: database + command_name: insert + arguments: + session: session0 + command: + insert: *collection_name + documents: + - _id : 1 + readPreference: + mode: Primary + result: + n: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id : 1 + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + - description: run command fails with explicit secondary read preference + + operations: + - name: startTransaction + object: session0 + - name: runCommand + object: database + command_name: find + arguments: + session: session0 + command: + find: *collection_name + readPreference: + mode: Secondary + result: + errorContains: read preference in a transaction must be primary + + - description: run command fails with secondary read preference from transaction options + + operations: + - name: startTransaction + object: session0 + arguments: + options: + readPreference: + mode: secondary + - name: runCommand + object: database + command_name: find + arguments: + session: session0 + command: + find: *collection_name + result: + errorContains: read preference in a transaction must be primary + diff --git a/t/data/transactions/transaction-options.json b/t/data/transactions/transaction-options.json new file mode 100644 index 00000000..c962f9f2 --- /dev/null +++ b/t/data/transactions/transaction-options.json @@ -0,0 +1,1534 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "no transaction options set", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 2 + } + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "afterClusterTime": 42 + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "transaction options inherited from client", + "clientOptions": { + "w": 1, + "readConcernLevel": "local" + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 2 + } + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "level": "local" + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": 1 + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "level": "local", + "afterClusterTime": 42 + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": 1 + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "transaction options inherited from defaultTransactionOptions", + "sessionOptions": { + "session0": { + "defaultTransactionOptions": { + "readConcern": { + "level": "snapshot" + }, + "writeConcern": { + "w": 1 + } + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 2 + } + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "level": "snapshot" + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": 1 + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "level": "snapshot", + "afterClusterTime": 42 + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": 1 + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "startTransaction options override defaults", + "clientOptions": { + "readConcernLevel": "local", + "w": 1 + }, + "sessionOptions": { + "session0": { + "defaultTransactionOptions": { + "readConcern": { + "level": "majority" + }, + "writeConcern": { + "w": 1 + } + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "readConcern": { + "level": "snapshot" + }, + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "readConcern": { + "level": "snapshot" + }, + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 2 + } + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "level": "snapshot" + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "level": "snapshot", + "afterClusterTime": 42 + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "defaultTransactionOptions override client options", + "clientOptions": { + "readConcernLevel": "local", + "w": 1 + }, + "sessionOptions": { + "session0": { + "defaultTransactionOptions": { + "readConcern": { + "level": "snapshot" + }, + "writeConcern": { + "w": "majority" + } + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 2 + } + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "level": "snapshot" + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "level": "snapshot", + "afterClusterTime": 42 + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "readConcern local in defaultTransactionOptions", + "clientOptions": { + "w": 1 + }, + "sessionOptions": { + "session0": { + "defaultTransactionOptions": { + "readConcern": { + "level": "local" + } + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 2 + } + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "level": "local" + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": 1 + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "level": "local", + "afterClusterTime": 42 + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": { + "w": 1 + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "readConcern local in startTransaction options", + "sessionOptions": { + "session0": { + "defaultTransactionOptions": { + "readConcern": { + "level": "majority" + } + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "readConcern": { + "level": "snapshot" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "readConcern": { + "level": "snapshot" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 2 + } + }, + "result": { + "insertedId": 2 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "level": "snapshot" + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 2 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": { + "level": "snapshot", + "afterClusterTime": 42 + }, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "2" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "client writeConcern ignored for bulk", + "clientOptions": { + "w": "majority" + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": 1 + } + } + } + }, + { + "name": "bulkWrite", + "object": "collection", + "arguments": { + "requests": [ + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1 + } + } + } + ], + "session": "session0" + }, + "result": { + "deletedCount": 0, + "insertedIds": { + "0": 1 + }, + "matchedCount": 0, + "modifiedCount": 0, + "upsertedCount": 0, + "upsertedIds": {} + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": 1 + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "readPreference inherited from client", + "clientOptions": { + "readPreference": "secondary" + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "find", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "readPreference inherited from defaultTransactionOptions", + "clientOptions": { + "readPreference": "primary" + }, + "sessionOptions": { + "session0": { + "defaultTransactionOptions": { + "readPreference": { + "mode": "Secondary" + } + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "find", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "startTransaction overrides readPreference", + "clientOptions": { + "readPreference": "primary" + }, + "sessionOptions": { + "session0": { + "defaultTransactionOptions": { + "readPreference": { + "mode": "Primary" + } + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "readPreference": { + "mode": "Secondary" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "find", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "result": { + "errorContains": "read preference in a transaction must be primary" + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "readConcern": null, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + } + ] +} diff --git a/t/data/transactions/transaction-options.yml b/t/data/transactions/transaction-options.yml new file mode 100644 index 00000000..5e183a35 --- /dev/null +++ b/t/data/transactions/transaction-options.yml @@ -0,0 +1,877 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] + +tests: + - description: no transaction options set + + operations: &commitAbortOperations + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + # Now test abort. + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 2 + result: + insertedId: 2 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: true + autocommit: false + readConcern: + afterClusterTime: 42 + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: &outcome + collection: + data: + - _id: 1 + + - description: transaction options inherited from client + + clientOptions: + w: 1 + readConcernLevel: local + + operations: *commitAbortOperations + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + level: local + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: 1 + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: true + autocommit: false + readConcern: + level: local + afterClusterTime: 42 + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: 1 + command_name: abortTransaction + database_name: admin + + outcome: *outcome + + - description: transaction options inherited from defaultTransactionOptions + + sessionOptions: + session0: + defaultTransactionOptions: + readConcern: + level: snapshot + writeConcern: + w: 1 + + operations: *commitAbortOperations + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + level: snapshot + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: 1 + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: true + autocommit: false + readConcern: + level: snapshot + afterClusterTime: 42 + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: 1 + command_name: abortTransaction + database_name: admin + + outcome: *outcome + + - description: startTransaction options override defaults + + clientOptions: + readConcernLevel: local + w: 1 + + sessionOptions: + session0: + defaultTransactionOptions: + readConcern: + level: majority + writeConcern: + w: 1 + + operations: + - name: startTransaction + object: session0 + arguments: + options: + readConcern: + level: snapshot + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + - name: startTransaction + object: session0 + arguments: + options: + readConcern: + level: snapshot + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 2 + result: + insertedId: 2 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + level: snapshot + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: true + autocommit: false + readConcern: + level: snapshot + afterClusterTime: 42 + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: majority + command_name: abortTransaction + database_name: admin + + outcome: *outcome + + - description: defaultTransactionOptions override client options + + clientOptions: + readConcernLevel: local + w: 1 + + sessionOptions: + session0: + defaultTransactionOptions: + readConcern: + level: snapshot + writeConcern: + w: majority + + operations: *commitAbortOperations + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + level: snapshot + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: true + autocommit: false + readConcern: + level: snapshot + afterClusterTime: 42 + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: majority + command_name: abortTransaction + database_name: admin + + outcome: *outcome + + - description: readConcern local in defaultTransactionOptions + + clientOptions: + w: 1 + + sessionOptions: + session0: + defaultTransactionOptions: + readConcern: + level: local + + operations: *commitAbortOperations + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + level: local + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: 1 + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: true + autocommit: false + readConcern: + level: local + afterClusterTime: 42 + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: false + readConcern: + writeConcern: + w: 1 + command_name: abortTransaction + database_name: admin + + outcome: *outcome + + - description: readConcern local in startTransaction options + + sessionOptions: + session0: + defaultTransactionOptions: + readConcern: + level: majority # Overridden. + + operations: + - name: startTransaction + object: session0 + arguments: + options: + readConcern: + level: snapshot + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + # Now test abort. + - name: startTransaction + object: session0 + arguments: + options: + readConcern: + level: snapshot + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 2 + result: + insertedId: 2 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + level: snapshot + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 2 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: true + autocommit: false + readConcern: + level: snapshot + afterClusterTime: 42 + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "2" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: *outcome + + - description: client writeConcern ignored for bulk + + clientOptions: + w: majority + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: 1 + - name: bulkWrite + object: collection + arguments: + requests: + - name: insertOne + arguments: + document: {_id: 1} + session: session0 + result: + deletedCount: 0 + insertedIds: {0: 1} + matchedCount: 0 + modifiedCount: 0 + upsertedCount: 0 + upsertedIds: {} + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + # No writeConcern. + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: 1 + command_name: commitTransaction + database_name: admin + + outcome: *outcome + + - description: readPreference inherited from client + + clientOptions: + readPreference: secondary + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: find + object: collection + arguments: + session: session0 + filter: + _id: 1 + result: + errorContains: read preference in a transaction must be primary + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: readPreference inherited from defaultTransactionOptions + + clientOptions: + readPreference: primary + + sessionOptions: + session0: + defaultTransactionOptions: + readPreference: + mode: Secondary + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: find + object: collection + arguments: + session: session0 + filter: + _id: 1 + result: + errorContains: read preference in a transaction must be primary + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: startTransaction overrides readPreference + + clientOptions: + readPreference: primary + + sessionOptions: + session0: + defaultTransactionOptions: + readPreference: + mode: Primary + + operations: + - name: startTransaction + object: session0 + arguments: + options: + readPreference: + mode: Secondary + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: find + object: collection + arguments: + session: session0 + filter: + _id: 1 + result: + errorContains: read preference in a transaction must be primary + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + readConcern: + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + readConcern: + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 diff --git a/t/data/transactions/update.json b/t/data/transactions/update.json new file mode 100644 index 00000000..26590c56 --- /dev/null +++ b/t/data/transactions/update.json @@ -0,0 +1,436 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + } + ], + "tests": [ + { + "description": "update", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "updateOne", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": 4 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "upsert": true + }, + "result": { + "matchedCount": 0, + "modifiedCount": 0, + "upsertedCount": 1, + "upsertedId": 4 + } + }, + { + "name": "replaceOne", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "x": 1 + }, + "replacement": { + "y": 1 + } + }, + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + }, + { + "name": "updateMany", + "object": "collection", + "arguments": { + "session": "session0", + "filter": { + "_id": { + "$gte": 3 + } + }, + "update": { + "$set": { + "z": 1 + } + } + }, + "result": { + "matchedCount": 2, + "modifiedCount": 2, + "upsertedCount": 0 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 4 + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": false, + "upsert": true + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "update", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "x": 1 + }, + "u": { + "y": 1 + }, + "multi": false, + "upsert": false + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "update", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": { + "$gte": 3 + } + }, + "u": { + "$set": { + "z": 1 + } + }, + "multi": true, + "upsert": false + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "update", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3, + "z": 1 + }, + { + "_id": 4, + "y": 1, + "z": 1 + } + ] + } + } + }, + { + "description": "collections writeConcern ignored for update", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "updateOne", + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": "majority" + } + }, + "arguments": { + "session": "session0", + "filter": { + "_id": 4 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "upsert": true + }, + "result": { + "matchedCount": 0, + "modifiedCount": 0, + "upsertedCount": 1, + "upsertedId": 4 + } + }, + { + "name": "replaceOne", + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": "majority" + } + }, + "arguments": { + "session": "session0", + "filter": { + "x": 1 + }, + "replacement": { + "y": 1 + } + }, + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + }, + { + "name": "updateMany", + "object": "collection", + "collectionOptions": { + "writeConcern": { + "w": "majority" + } + }, + "arguments": { + "session": "session0", + "filter": { + "_id": { + "$gte": 3 + } + }, + "update": { + "$set": { + "z": 1 + } + } + }, + "result": { + "matchedCount": 2, + "modifiedCount": 2, + "upsertedCount": 0 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": 4 + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": false, + "upsert": true + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "update", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "x": 1 + }, + "u": { + "y": 1 + }, + "multi": false, + "upsert": false + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "update", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": { + "$gte": 3 + } + }, + "u": { + "$set": { + "z": 1 + } + }, + "multi": true, + "upsert": false + } + ], + "ordered": true, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "update", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ] + } + ] +} diff --git a/t/data/transactions/update.yml b/t/data/transactions/update.yml new file mode 100644 index 00000000..b325b440 --- /dev/null +++ b/t/data/transactions/update.yml @@ -0,0 +1,246 @@ +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: + - _id: 1 + - _id: 2 + - _id: 3 + +tests: + - description: update + + operations: + - name: startTransaction + object: session0 + - name: updateOne + object: collection + arguments: + session: session0 + filter: {_id: 4} + update: + $inc: {x: 1} + upsert: true + result: + matchedCount: 0 + modifiedCount: 0 + upsertedCount: 1 + upsertedId: 4 + - name: replaceOne + object: collection + arguments: + session: session0 + filter: {x: 1} + replacement: {y: 1} + result: + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + - name: updateMany + object: collection + arguments: + session: session0 + filter: + _id: {$gte: 3} + update: + $set: {z: 1} + result: + matchedCount: 2 + modifiedCount: 2 + upsertedCount: 0 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + update: *collection_name + updates: + - q: {_id: 4} + u: {$inc: {x: 1}} + multi: false + upsert: true + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: update + database_name: *database_name + - command_started_event: + command: + update: *collection_name + updates: + - q: {x: 1} + u: {y: 1} + multi: false + upsert: false + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: update + database_name: *database_name + - command_started_event: + command: + update: *collection_name + updates: + - q: {_id: {$gte: 3}} + u: {$set: {z: 1}} + multi: true + upsert: false + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: update + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - {_id: 1} + - {_id: 2} + - {_id: 3, z: 1} + - {_id: 4, y: 1, z: 1} + + - description: collections writeConcern ignored for update + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: updateOne + object: collection + collectionOptions: + writeConcern: + w: majority + arguments: + session: session0 + filter: {_id: 4} + update: + $inc: {x: 1} + upsert: true + result: + matchedCount: 0 + modifiedCount: 0 + upsertedCount: 1 + upsertedId: 4 + - name: replaceOne + object: collection + collectionOptions: + writeConcern: + w: majority + arguments: + session: session0 + filter: {x: 1} + replacement: {y: 1} + result: + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + - name: updateMany + object: collection + collectionOptions: + writeConcern: + w: majority + arguments: + session: session0 + filter: + _id: {$gte: 3} + update: + $set: {z: 1} + result: + matchedCount: 2 + modifiedCount: 2 + upsertedCount: 0 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + update: *collection_name + updates: + - q: {_id: 4} + u: {$inc: {x: 1}} + multi: false + upsert: true + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: update + database_name: *database_name + - command_started_event: + command: + update: *collection_name + updates: + - q: {x: 1} + u: {y: 1} + multi: false + upsert: false + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: update + database_name: *database_name + - command_started_event: + command: + update: *collection_name + updates: + - q: {_id: {$gte: 3}} + u: {$set: {z: 1}} + multi: true + upsert: false + ordered: true + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: update + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin diff --git a/t/data/transactions/write-concern.json b/t/data/transactions/write-concern.json new file mode 100644 index 00000000..dfc75f76 --- /dev/null +++ b/t/data/transactions/write-concern.json @@ -0,0 +1,355 @@ +{ + "database_name": "transaction-tests", + "collection_name": "test", + "data": [], + "tests": [ + { + "description": "commit with majority", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "commit with default", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "abort with majority", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "abort with default", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "abortTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "abortTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": null + }, + "command_name": "abortTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [] + } + } + }, + { + "description": "start with unacknowledged write concern", + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": 0 + } + } + }, + "result": { + "errorContains": "transactions do not support unacknowledged write concern" + } + } + ] + }, + { + "description": "start with implicit unacknowledged write concern", + "clientOptions": { + "w": 0 + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "result": { + "errorContains": "transactions do not support unacknowledged write concern" + } + } + ] + } + ] +} diff --git a/t/data/transactions/write-concern.yml b/t/data/transactions/write-concern.yml new file mode 100644 index 00000000..adadbd8e --- /dev/null +++ b/t/data/transactions/write-concern.yml @@ -0,0 +1,236 @@ +# Assumes the default for transactions is the same as for all ops, tests +# setting the writeConcern to "majority". +database_name: &database_name "transaction-tests" +collection_name: &collection_name "test" + +data: [] + +tests: + - description: commit with majority + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: commit with default + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + + - _id: 1 + + - description: abort with majority + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: abort with default + + operations: + - name: startTransaction + object: session0 + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: abortTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + abortTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + command_name: abortTransaction + database_name: admin + + outcome: + collection: + data: [] + + - description: start with unacknowledged write concern + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: 0 + result: + # Client-side error. + errorContains: transactions do not support unacknowledged write concern + + - description: start with implicit unacknowledged write concern + + clientOptions: + w: 0 + + operations: + - name: startTransaction + object: session0 + result: + # Client-side error. + errorContains: transactions do not support unacknowledged write concern From 06697c2d274f32a561debce0067f429f0ae9bf0f Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Fri, 8 Jun 2018 16:35:26 +0100 Subject: [PATCH 14/38] PERL-875 added wip transactions-spec test --- .../config/replicaset-multi-4.0-w_arbiter.yml | 14 + t/transactions-spec.t | 455 +++++++++++++++++- 2 files changed, 448 insertions(+), 21 deletions(-) create mode 100644 devel/config/replicaset-multi-4.0-w_arbiter.yml diff --git a/devel/config/replicaset-multi-4.0-w_arbiter.yml b/devel/config/replicaset-multi-4.0-w_arbiter.yml new file mode 100644 index 00000000..e72df514 --- /dev/null +++ b/devel/config/replicaset-multi-4.0-w_arbiter.yml @@ -0,0 +1,14 @@ +--- +type: replica +setName: foo +default_args: -v --noprealloc --smallfiles --bind_ip 0.0.0.0 --nssize 6 --quiet +default_version: 4.0 +mongod: + - name: host1 + - name: host2 + - name: host3 + - name: host4 + rs_config: + arbiterOnly: true + +# vim: ts=4 sts=4 sw=4 et: diff --git a/t/transactions-spec.t b/t/transactions-spec.t index ad5f02e5..9b440776 100644 --- a/t/transactions-spec.t +++ b/t/transactions-spec.t @@ -16,9 +16,11 @@ use strict; use warnings; -use JSON::MaybeXS; +use JSON::MaybeXS qw( is_bool decode_json ); use Path::Tiny 0.054; # basename with suffix use Test::More 0.96; +use Test::Deep; +use Math::BigInt; use utf8; @@ -36,13 +38,20 @@ use MongoDBTest qw/ server_type clear_testdbs get_unique_collection + skip_unless_mongod + skip_unless_failpoints_available /; +skip_unless_mongod(); +# TODO skip_unless_failpoints_available(); + my @events; +#use Devel::Dwarn; + sub clear_events { @events = () } sub event_count { scalar @events } -sub event_cb { push @events, $_[0] } +sub event_cb { push @events, $_[0] }#; Dwarn $_[0] } my $conn = build_client(); my $server_version = server_version($conn); @@ -54,10 +63,24 @@ plan skip_all => "Requires MongoDB 4.0" plan skip_all => "deployment does not support transactions" unless $conn->_topology->_supports_transactions; +# defines which argument hash fields become positional arguments +my %method_args = ( + insert_one => [qw( document )], + insert_many => [qw( documents )], + delete_one => [qw( filter )], + delete_many => [qw( filter )], + replace_one => [qw( filter replacement )], + update_one => [qw( filter update )], + update_many => [qw( filter update )], + find => [qw( filter )], + count => [qw( filter )], + bulk_write => [qw( requests )], +); + my $dir = path("t/data/transactions"); -my $iterator = $dir->iterator; +my $iterator = $dir->iterator; my $index = 0; # TBSLIVER while ( my $path = $iterator->() ) { - next unless $path =~ /\.json$/; + next unless $path =~ /\.json$/; next unless ++$index == 3; # TBSLIVER my $plan = eval { decode_json( $path->slurp_utf8 ) }; if ($@) { die "Error decoding $path: $@"; @@ -67,7 +90,7 @@ while ( my $path = $iterator->() ) { subtest $path => sub { - for my $test ( @{ $plan->{tests} } ) { + for my $test ( @{ $plan->{tests} }[0] ) { # TBSLIVER my $description = $test->{description}; subtest $description => sub { my $client = build_client(); @@ -75,28 +98,68 @@ while ( my $path = $iterator->() ) { # Kills its own session as well eval { $client->send_admin_command([ killAllSessions => [] ]) }; my $test_db = $client->get_database( $test_db_name ); - $test_db - ->get_collection( $test_coll_name, { write_concern => { w => 'majority' } } ) - ->drop; - my $test_coll = $test_db->get_collection( $test_coll_name, { write_concern => { w => 'majority' } } ); -use Carp::Always; + # We crank wtimeout up to 10 seconds to help reduce + # replication timeouts in testing + $test_db->get_collection( + $test_coll_name, + { write_concern => { w => 'majority', wtimeout => 10000 } } + )->drop; + + # Drop first to make sure its clear for the next test. + # MongoDB::Collection doesnt have a ->create option so done as + # a seperate step. + $test_db->run_command([ create => $test_coll_name ]); + + my $test_coll = $test_db->get_collection( + $test_coll_name, + { write_concern => { w => 'majority', wtimeout => 10000 } } + ); + if ( scalar @{ $plan->{data} } > 0 ) { $test_coll->insert_many( $plan->{data} ); } + set_failpoint( $client, $test->{failPoint} ); run_test( $test_db_name, $test_coll_name, $test ); + clear_failpoint( $client, $test->{failPoint} ); }; } }; } +sub set_failpoint { + my ( $client, $failpoint ) = @_; + + return unless defined $failpoint; + $client->send_admin_command([ + configureFailPoint => $failpoint->{configureFailPoint}, + mode => $failpoint->{mode}, + defined $failpoint->{data} + ? ( data => $failpoint->{data} ) + : (), + ]); +} + +sub clear_failpoint { + my ( $client, $failpoint ) = @_; + + return unless defined $failpoint; + $client->send_admin_command([ + configureFailPoint => $failpoint->{configureFailPoint}, + mode => 'off', + ]); +} + sub to_snake_case { my $t = shift; $t =~ s{([A-Z])}{_\L$1}g; return $t; } +# Global so can get values when checking sessions +my %sessions; + sub run_test { my ( $test_db_name, $test_coll_name, $test ) = @_; @@ -109,33 +172,383 @@ sub run_test { } keys %$client_options }; - use Devel::Dwarn; Dwarn $client_options; + my $client = build_client( monitoring_callback => \&event_cb, %$client_options ); - ok 1; - return; + my $session_options = $test->{sessionOptions} // {}; - my $client = build_client( monitoring_callback => \&event_cb, %$client_options ); + %sessions = ( + session0 => $client->start_session( $session_options->{session0} ), + session1 => $client->start_session( $session_options->{session1} ), + ); + $sessions{session0_lsid} = $sessions{session0}->session_id; + $sessions{session1_lsid} = $sessions{session1}->session_id; - my $session0 = $client->start_session; - my $lsid0 = $session0->session_id; - my $session1 = $client->start_session; - my $lsid1 = $session1->session_id; + # Cant see any in the files? + my $collection_options = $test->{collectionOptions} // {}; + clear_events(); for my $operation ( @{ $test->{operations} } ) { eval { my $test_db = $client->get_database( $test_db_name ); - my $test_coll = $client->get_collection( $test_coll_name ); + my $test_coll = $test_db->get_collection( $test_coll_name, $collection_options ); my $cmd = to_snake_case( $operation->{name} ); + diag $cmd; + #Dwarn $operation; + if ( $cmd =~ /_transaction$/ ) { + $sessions{ $operation->{object} }->$cmd; + } else { + my @args = _adjust_arguments( $cmd, $operation->{arguments} ); + $args[-1]->{session} = $sessions{ $args[-1]->{session} } + if defined $args[-1]->{session}; + + $test_coll->$cmd( @args ); + } + }; + #Dwarn '----------------Session------------------'; + #Dwarn $sessions{session0}->_debug; + my $err = $@; + if ( $err ) { + #Dwarn '----------------Error------------------'; + #Dwarn $err; + my $err_contains = $operation->{result}->{errorContains}; + my $err_code_name = $operation->{result}->{errorCodeName}; + my $err_labels_contains = $operation->{result}->{errorLabelsContain}; + my $err_labels_omit = $operation->{result}->{errorLabelsOmit}; + if ( defined $err_contains ) { + like $err->message, qr/$err_contains/i, 'error contains' . $err_contains; + } + if ( defined $err_code_name ) { + is $err->result->output->{codeName}, + $err_code_name, + 'error has name ' . $err_code_name; + } + if ( defined $err_labels_omit ) { + for my $err_label ( @{ $err_labels_omit } ) { + ok ! $err->has_error_label( $err_label ), 'error doesnt have label ' . $err_label; + } + } + if ( defined $err_labels_omit ) { + for my $err_label ( @{ $err_labels_contains } ) { + ok $err->has_error_label( $err_label ), 'error has label ' . $err_label; + } + } + } elsif ( grep {/^error/} keys %{ $operation->{result} } ) { + ok 0, 'Should have found an error'; } } - $session0->end_session; - $session1->end_session; + $sessions{session0}->end_session; + $sessions{session1}->end_session; + #Dwarn \@events; + if ( defined $test->{expectations} ) { + check_event_expectations( _adjust_types( $test->{expectations} ) ); + } ok 1; } +# Following subs modified from monitoring_spec.t +# + + +# prepare collection method arguments +# adjusts data structures and extracts leading positional arguments +sub _adjust_arguments { + my ($method, $args) = @_; + + $args = _adjust_types($args); + my @fields = @{ $method_args{$method} }; + my @field_values = map { + my $val = delete $args->{$_}; + # bulk write is special cased to reuse argument extraction + ($method eq 'bulk_write' and $_ eq 'requests') + ? _adjust_bulk_write_requests($val) + : $val; + } @fields; + + return( + (grep { defined } @field_values), + scalar(keys %$args) ? $args : (), + ); +} + +# prepare bulk write requests for use as argument to ->bulk_write +sub _adjust_bulk_write_requests { + my ($requests) = @_; + + return [map { + # Different data structure in bulk writes compared to command_monitoring + my $name = to_snake_case( $_->{name} ); + +{ $name => [_adjust_arguments($name, $_->{arguments})] }; + } @$requests]; +} + +# some type transformations +# turns { '$numberLong' => $n } into 0+$n +sub _adjust_types { + my ($value) = @_; + if (ref $value eq 'HASH') { + if (scalar(keys %$value) == 1) { + my ($name, $value) = %$value; + if ($name eq '$numberLong') { + return 0+$value; + } + } + return +{map { + my $key = $_; + ($key, _adjust_types($value->{$key})); + } keys %$value}; + } + elsif (ref $value eq 'ARRAY') { + return [map { _adjust_types($_) } @$value]; + } + else { + return $value; + } +} + +# common overrides for event data expectations +sub prepare_data_spec { + my ($spec) = @_; + if ( ! defined $spec ) { + return $spec; + } + elsif (not ref $spec) { + if ($spec eq 'test') { + return any(qw( test test_collection )); + } + if ($spec eq 'test-unacknowledged-bulk-write') { + return code(\&_verify_is_nonempty_str); + } + if ($spec eq 'command-monitoring-tests.test') { + return code(\&_verify_is_nonempty_str); + } + return $spec; + } + elsif (is_bool $spec) { + my $specced = $spec ? 1 : 0; + return code(sub { + my $value = shift; + return(0, 'expected a true boolean value') + if $specced and not $value; + return(0, 'expected a false boolean value') + if $value and not $specced; + return 1; + }); + } + elsif (ref $spec eq 'ARRAY') { + return [map { + prepare_data_spec($_) + } @$spec]; + } + elsif (ref $spec eq 'HASH') { + return +{map { + ($_, prepare_data_spec($spec->{$_})) + } keys %$spec}; + } + else { + return $spec; + } +} + +sub check_event_expectations { + my ( $expected ) = @_; + my @got = grep { $_->{type} eq 'command_started' } @events; + + #Dwarn \@events; + for my $exp ( @$expected ) { + my ($exp_type, $exp_spec) = %$exp; + # We only have command_started_event checks + subtest $exp_type => sub { + ok(scalar(@got), 'event available') + or return; + my $event = shift @got; + is($event->{type}.'_event', $exp_type, "is a $exp_type") + or return; + my $event_tester = "check_$exp_type"; + main->can($event_tester)->($exp_spec, $event); + }; + } + + is scalar(@got), 0, 'no outstanding events'; +} + +sub check_event { + my ($exp, $event) = @_; + for my $key (sort keys %$exp) { + my $check = "check_${key}_field"; + main->can($check)->($exp->{$key}, $event); + } +} + +# +# per-event type test handlers +# + +sub check_command_started_event { + my ($exp, $event) = @_; + check_event($exp, $event); +} + +# +# verificationi subs for use with Test::Deep::code +# + +sub _verify_is_positive_num { + my $value = shift; + return(0, "error code is not defined") + unless defined $value; + return(0, "error code is not positive") + unless $value > 1; + return 1; +} + +sub _verify_is_nonempty_str { + my $value = shift; + return(0, "error message is not defined") + unless defined $value; + return(0, "error message is empty") + unless length $value; + return 1; +} + +# +# event field test handlers +# + +# $event.database_name +sub check_database_name_field { + my ($exp_name, $event) = @_; + ok defined($event->{databaseName}), "database_name defined"; + ok length($event->{databaseName}), "database_name non-empty"; +} + +# $event.command_name +sub check_command_name_field { + my ($exp_name, $event) = @_; + is $event->{commandName}, $exp_name, "command name"; +} + +# $event.reply +sub check_reply_field { + my ($exp_reply, $event) = @_; + my $event_reply = $event->{reply}; + + # special case for $event.reply.cursor.id + if (exists $exp_reply->{cursor}) { + if (exists $exp_reply->{cursor}{id}) { + $exp_reply->{cursor}{id} = code(\&_verify_is_positive_num) + if $exp_reply->{cursor}{id} eq '42'; + } + } + + # special case for $event.reply.writeErrors + if (exists $exp_reply->{writeErrors}) { + for my $i ( 0 .. $#{ $exp_reply->{writeErrors} } ) { + my $error = $exp_reply->{writeErrors}[$i]; + if (exists $error->{code} and $error->{code} eq 42) { + $error->{code} = code(\&_verify_is_positive_num); + } + if (exists $error->{errmsg} and $error->{errmsg} eq '') { + $error->{errmsg} = code(\&_verify_is_nonempty_str); + } + $exp_reply->{writeErrors}[$i] = superhashof( $error ); + } + } + + # special case for $event.command.cursorsUnknown on killCursors + if ($event->{commandName} eq 'killCursors' + and defined $exp_reply->{cursorsUnknown} + ) { + for my $index (0 .. $#{ $exp_reply->{cursorsUnknown} }) { + $exp_reply->{cursorsUnknown}[$index] + = code(\&_verify_is_positive_num) + if $exp_reply->{cursorsUnknown}[$index] eq 42; + } + } + + for my $exp_key (sort keys %$exp_reply) { + cmp_deeply + $event_reply->{$exp_key}, + prepare_data_spec($exp_reply->{$exp_key}), + "reply field $exp_key" or diag explain $event_reply->{$exp_key}; + } +} + +# $event.command +sub check_command_field { + my ($exp_command, $event) = @_; + my $event_command = $event->{command}; + + # ordered defaults to true + delete $exp_command->{ordered}; + + # special case for $event.command.getMore + if (exists $exp_command->{getMore}) { + $exp_command->{getMore} = code(\&_verify_is_positive_num) + if $exp_command->{getMore} eq '42'; + } + + # special case for $event.command.writeConcern.wtimeout + if (defined $exp_command->{writeConcern}) { + $exp_command->{writeConcern}{wtimeout} = ignore(); + } + + # special case for $event.command.cursors on killCursors + if ($event->{commandName} eq 'killCursors' + and defined $exp_command->{cursors} + ) { + for my $index (0 .. $#{ $exp_command->{cursors} }) { + $exp_command->{cursors}[$index] + = code(\&_verify_is_positive_num) + if $exp_command->{cursors}[$index] eq 42; + } + } + + if ( defined $exp_command->{lsid} ) { + # Stuff correct session id in + $exp_command->{lsid} = $sessions{ $exp_command->{lsid} . '_lsid' }; + } + + if ( defined $exp_command->{readConcern} ) { + $exp_command->{readConcern}{afterClusterTime} = Isa('BSON::Timestamp') + if $exp_command->{readConcern}{afterClusterTime} eq '42'; + } + + if ( defined $exp_command->{txnNumber} ) { + $exp_command->{txnNumber} = Math::BigInt->new($exp_command->{txnNumber}); + } + + #DwarnN $exp_command; + #DwarnN $event_command; + + for my $exp_key (sort keys %$exp_command) { + my $event_value = $event_command->{$exp_key}; + my $exp_value = prepare_data_spec($exp_command->{$exp_key}); + my $label = "command field '$exp_key'"; + + if ( + (grep { $exp_key eq $_ } qw( comment maxTimeMS )) + or + ($event->{commandName} eq 'getMore' and $exp_key eq 'batchSize') + ) { + TODO: { + local $TODO = + "Command field '$exp_key' requires other fixes"; + cmp_deeply $event_value, $exp_value, $label; + } + } + elsif ( !defined $exp_value ) + { + ok ! exists $event_command->{$exp_key}, $label . ' does not exist'; + } + else { + cmp_deeply $event_value, $exp_value, $label; + } + } +} + clear_testdbs; done_testing; From 11bf6b418904e710fbeafa2624d3f9b4c9f8d257 Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Fri, 8 Jun 2018 17:12:59 +0100 Subject: [PATCH 15/38] PERL-875 Fix issue caused in ErrorThrower --- lib/MongoDB/CommandResult.pm | 11 +++++++++++ lib/MongoDB/Role/_DatabaseErrorThrower.pm | 5 +++-- lib/MongoDB/Role/_WriteResult.pm | 11 +++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/MongoDB/CommandResult.pm b/lib/MongoDB/CommandResult.pm index 2ee2536d..dcc1864e 100644 --- a/lib/MongoDB/CommandResult.pm +++ b/lib/MongoDB/CommandResult.pm @@ -115,6 +115,17 @@ sub last_wtimeout { || exists $self->output->{writeConcernError} ); } +=method last_error_labels + +Returns any error labels from the command + +=cut + +sub last_error_labels { + my ( $self ) = @_; + return $self->output->{errorLabels}; +} + =method assert Throws an exception if the command failed. diff --git a/lib/MongoDB/Role/_DatabaseErrorThrower.pm b/lib/MongoDB/Role/_DatabaseErrorThrower.pm index f4e31255..40545166 100644 --- a/lib/MongoDB/Role/_DatabaseErrorThrower.pm +++ b/lib/MongoDB/Role/_DatabaseErrorThrower.pm @@ -27,7 +27,7 @@ use MongoDB::Error; use namespace::clean; -requires qw/last_errmsg last_code last_wtimeout/; +requires qw/last_errmsg last_code last_wtimeout last_error_labels/; my $ANY_DUP_KEY = [ DUPLICATE_KEY, DUPLICATE_KEY_UPDATE, DUPLICATE_KEY_CAPPED ]; my $ANY_NOT_MASTER = [ NOT_MASTER, NOT_MASTER_NO_SLAVE_OK, NOT_MASTER_OR_SECONDARY ]; @@ -40,6 +40,7 @@ sub _throw_database_error { my $err = $self->last_errmsg; my $code = $self->last_code; + my $error_labels = $self->last_error_labels; if ( grep { $code == $_ } @$ANY_NOT_MASTER || $err =~ /^(?:not master|node is recovering)/ ) { $error_class = "MongoDB::NotMasterError"; @@ -57,7 +58,7 @@ sub _throw_database_error { $error_class->throw( result => $self, code => $code || UNKNOWN_ERROR, - error_labels => $self->output->{errorLabels} || [], + error_labels => $error_labels || [], ( length($err) ? ( message => $err ) : () ), ); diff --git a/lib/MongoDB/Role/_WriteResult.pm b/lib/MongoDB/Role/_WriteResult.pm index 70739f65..7d6c8418 100644 --- a/lib/MongoDB/Role/_WriteResult.pm +++ b/lib/MongoDB/Role/_WriteResult.pm @@ -122,4 +122,15 @@ sub last_wtimeout { return !!( $self->count_write_concern_errors && !$self->count_write_errors ); } +sub last_error_labels { + my ( $self ) = @_; + if ( $self->count_write_errors ) { + return $self->write_errors->[-1]{errorLabels} || []; + } + elsif ( $self->count_write_concern_errors ) { + return $self->write_errors->[-1]{errorLabels} || []; + } + return []; +} + 1; From 48ea0f80feb55452fda7fb11ebaccacb3264aa09 Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Mon, 11 Jun 2018 18:49:00 +0100 Subject: [PATCH 16/38] PERL-875 working through spec tests --- lib/MongoDB/ClientSession.pm | 11 +++++ lib/MongoDB/Error.pm | 1 + lib/MongoDB/Op/_Command.pm | 4 +- lib/MongoDB/Role/_SessionSupport.pm | 25 +++++++++- lib/MongoDB/Role/_SingleBatchDocWrite.pm | 4 +- t/transactions-spec.t | 63 +++++++++++++++--------- 6 files changed, 81 insertions(+), 27 deletions(-) diff --git a/lib/MongoDB/ClientSession.pm b/lib/MongoDB/ClientSession.pm index 3c74d96b..d99ec980 100644 --- a/lib/MongoDB/ClientSession.pm +++ b/lib/MongoDB/ClientSession.pm @@ -379,6 +379,17 @@ sub _get_transaction_read_concern { return $self->client->read_concern; } +sub _get_transaction_write_concern { + my $self = shift; + # writeConcern is merged during start_transaction + if ( defined $self->_current_transaction_settings->{writeConcern} ) { + return MongoDB::WriteConcern->new( $self->_current_transaction_settings->{writeConcern} ); + } + + # Default to client write_concern, however unlikely to actually be used + return $self->client->write_concern; +} + # TODO TBSliver REMOVE ME ON RELEASE sub _debug { my $self = shift; diff --git a/lib/MongoDB/Error.pm b/lib/MongoDB/Error.pm index bd1cb1e2..1af93109 100644 --- a/lib/MongoDB/Error.pm +++ b/lib/MongoDB/Error.pm @@ -110,6 +110,7 @@ has 'previous_exception' => ( has error_labels => ( is => 'ro', isa => ArrayRef[Str], + default => sub { [] }, ); sub has_error_label { diff --git a/lib/MongoDB/Op/_Command.pm b/lib/MongoDB/Op/_Command.pm index fd3d9955..27729699 100644 --- a/lib/MongoDB/Op/_Command.pm +++ b/lib/MongoDB/Op/_Command.pm @@ -112,6 +112,7 @@ sub execute { }; if ( my $err = $@ ) { $self->publish_command_exception($err) if $self->monitoring_callback; + $self->_update_session_error( $err ); die $err; } @@ -123,8 +124,7 @@ sub execute { address => $link->address, ); - # Must happen even on an error (ie. the command fails) - $self->_update_operation_time( $res ); + $self->_update_session_pre_assert( $res ); $res->assert; diff --git a/lib/MongoDB/Role/_SessionSupport.pm b/lib/MongoDB/Role/_SessionSupport.pm index 93a7d657..c06e49f5 100644 --- a/lib/MongoDB/Role/_SessionSupport.pm +++ b/lib/MongoDB/Role/_SessionSupport.pm @@ -57,6 +57,20 @@ sub _apply_session_and_cluster_time { ($$query_ref)->Push( @{ $self->session->_get_transaction_read_concern->as_args( $self->session ) } ); } + # write concern not allowed in transactions except when ending. We can + # safely delete it here as you can only pass writeConcern through by + # arguments to client of collection. + + if ( $self->session->_in_transaction_state( qw/ starting in_progress / ) ) { + ($$query_ref)->Delete( 'writeConcern' ); + } + + if ( $self->session->_in_transaction_state( qw/ aborted committed / ) + && ! ($$query_ref)->EXISTS('writeConcern') + ) { + ($$query_ref)->Push( @{ $self->session->_get_transaction_write_concern->as_args() } ); + } + $self->session->_server_session->update_last_use; my $cluster_time = $self->session->get_latest_cluster_time; @@ -90,8 +104,7 @@ sub _update_session_and_cluster_time { return; } -# All items here happen before the response is checked for success -sub _update_operation_time { +sub _update_session_pre_assert { my ( $self, $response ) = @_; return unless defined $self->session; @@ -120,6 +133,14 @@ sub _assert_session_errors { return; } +sub _update_session_error { + my ( $self, $err ) = @_; + + if ( $self->session->_in_transaction_state( qw/ starting in_progress / ) ) { + push @{ $err->error_labels }, 'TransientTransactionError'; + } +} + sub __extract_from { my ( $self, $response, $key ) = @_; diff --git a/lib/MongoDB/Role/_SingleBatchDocWrite.pm b/lib/MongoDB/Role/_SingleBatchDocWrite.pm index abbc470a..a178ca8f 100644 --- a/lib/MongoDB/Role/_SingleBatchDocWrite.pm +++ b/lib/MongoDB/Role/_SingleBatchDocWrite.pm @@ -192,6 +192,7 @@ sub _send_write_command { }; if ( my $err = $@ ) { $self->publish_command_exception($err) if $self->monitoring_callback; + $self->_update_session_error( $err ); die $err; } @@ -200,11 +201,12 @@ sub _send_write_command { my $res = $self->bson_codec->decode_one( $result->{docs} ); - $self->_update_operation_time( $res ); + $self->_update_session_pre_assert( $res ); $self->_update_session_and_cluster_time($res); # Error checking depends on write concern + # TODO does this logic get affected by transactions???? Probably? if ( $self->write_concern->is_acknowledged ) { # errors in the command itself get handled as normal CommandResult diff --git a/t/transactions-spec.t b/t/transactions-spec.t index 9b440776..ef61028b 100644 --- a/t/transactions-spec.t +++ b/t/transactions-spec.t @@ -21,6 +21,7 @@ use Path::Tiny 0.054; # basename with suffix use Test::More 0.96; use Test::Deep; use Math::BigInt; +use Storable qw( dclone ); use utf8; @@ -42,7 +43,7 @@ use MongoDBTest qw/ skip_unless_failpoints_available /; -skip_unless_mongod(); +# TODO Keep not getting hosts???? skip_unless_mongod(); # TODO skip_unless_failpoints_available(); my @events; @@ -51,7 +52,8 @@ my @events; sub clear_events { @events = () } sub event_count { scalar @events } -sub event_cb { push @events, $_[0] }#; Dwarn $_[0] } +# Must use dclone, as was causing action at a distance for binc on txn number +sub event_cb { push @events, dclone $_[0] }#; Dwarn $_[0] } my $conn = build_client(); my $server_version = server_version($conn); @@ -80,7 +82,7 @@ my %method_args = ( my $dir = path("t/data/transactions"); my $iterator = $dir->iterator; my $index = 0; # TBSLIVER while ( my $path = $iterator->() ) { - next unless $path =~ /\.json$/; next unless ++$index == 3; # TBSLIVER + next unless $path =~ /\.json$/; next unless ++$index == 3; # TBSLIVER run specific file my $plan = eval { decode_json( $path->slurp_utf8 ) }; if ($@) { die "Error decoding $path: $@"; @@ -90,7 +92,7 @@ while ( my $path = $iterator->() ) { subtest $path => sub { - for my $test ( @{ $plan->{tests} }[0] ) { # TBSLIVER + for my $test ( @{ $plan->{tests} }[0..5] ) { # TBSLIVER run specific subtest my $description = $test->{description}; subtest $description => sub { my $client = build_client(); @@ -132,29 +134,47 @@ sub set_failpoint { my ( $client, $failpoint ) = @_; return unless defined $failpoint; - $client->send_admin_command([ + #Dwarn '-----------set failpoint-------------'; + #DwarnN $failpoint; + my $ret = $client->send_admin_command([ configureFailPoint => $failpoint->{configureFailPoint}, mode => $failpoint->{mode}, defined $failpoint->{data} ? ( data => $failpoint->{data} ) : (), ]); + #DwarnN $ret; + #Dwarn '----------/set failpoint-------------'; } sub clear_failpoint { my ( $client, $failpoint ) = @_; return unless defined $failpoint; - $client->send_admin_command([ + #Dwarn '-----------clear failpoint-------------'; + #DwarnN $failpoint; + my $ret = $client->send_admin_command([ configureFailPoint => $failpoint->{configureFailPoint}, mode => 'off', ]); + #DwarnN $ret; + #Dwarn '----------/clear failpoint-------------'; } sub to_snake_case { - my $t = shift; - $t =~ s{([A-Z])}{_\L$1}g; - return $t; + my $t = shift; + $t =~ s{([A-Z])}{_\L$1}g; + return $t; +} + +sub remap_hash_to_snake_case { + my $hash = shift; + return { + map { + my $k = to_snake_case( $_ ); + $k => $hash->{ $_ } + } keys %$hash + } } # Global so can get values when checking sessions @@ -164,13 +184,7 @@ sub run_test { my ( $test_db_name, $test_coll_name, $test ) = @_; my $client_options = $test->{clientOptions} // {}; - # Remap camel case to snake case - $client_options = { - map { - my $k = to_snake_case( $_ ); - $k => $client_options->{ $_ } - } keys %$client_options - }; + $client_options = remap_hash_to_snake_case( $client_options ); my $client = build_client( monitoring_callback => \&event_cb, %$client_options ); @@ -183,11 +197,12 @@ sub run_test { $sessions{session0_lsid} = $sessions{session0}->session_id; $sessions{session1_lsid} = $sessions{session1}->session_id; - # Cant see any in the files? - my $collection_options = $test->{collectionOptions} // {}; - clear_events(); for my $operation ( @{ $test->{operations} } ) { + + my $collection_options = $operation->{collectionOptions} // {}; + $collection_options = remap_hash_to_snake_case( $collection_options ); + eval { my $test_db = $client->get_database( $test_db_name ); my $test_coll = $test_db->get_collection( $test_coll_name, $collection_options ); @@ -196,7 +211,8 @@ sub run_test { diag $cmd; #Dwarn $operation; if ( $cmd =~ /_transaction$/ ) { - $sessions{ $operation->{object} }->$cmd; + my $op_args = $operation->{arguments} // {}; + $sessions{ $operation->{object} }->$cmd( $op_args->{options} ); } else { my @args = _adjust_arguments( $cmd, $operation->{arguments} ); $args[-1]->{session} = $sessions{ $args[-1]->{session} } @@ -234,7 +250,10 @@ sub run_test { } } } elsif ( grep {/^error/} keys %{ $operation->{result} } ) { - ok 0, 'Should have found an error'; + fail 'Should have found an error'; + #DwarnN $operation; + #DwarnN $events[-2]; + #DwarnN $events[-1]; } } @@ -245,7 +264,7 @@ sub run_test { if ( defined $test->{expectations} ) { check_event_expectations( _adjust_types( $test->{expectations} ) ); } - ok 1; + %sessions = (); } # Following subs modified from monitoring_spec.t From f5624d54d65be6b418248985d76e195bb67b179d Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Tue, 12 Jun 2018 13:08:43 +0100 Subject: [PATCH 17/38] PERL-875 adding unknown transaction commit result errorlabel --- .../config/replicaset-multi-4.0-w_arbiter.yml | 1 - lib/MongoDB/ClientSession.pm | 18 ++++++++++++++++- lib/MongoDB/Op/_Command.pm | 2 +- lib/MongoDB/Role/_SessionSupport.pm | 4 +++- lib/MongoDB/Role/_SingleBatchDocWrite.pm | 2 +- t/transactions-spec.t | 20 +++++++------------ 6 files changed, 29 insertions(+), 18 deletions(-) diff --git a/devel/config/replicaset-multi-4.0-w_arbiter.yml b/devel/config/replicaset-multi-4.0-w_arbiter.yml index e72df514..38d7094a 100644 --- a/devel/config/replicaset-multi-4.0-w_arbiter.yml +++ b/devel/config/replicaset-multi-4.0-w_arbiter.yml @@ -7,7 +7,6 @@ mongod: - name: host1 - name: host2 - name: host3 - - name: host4 rs_config: arbiterOnly: true diff --git a/lib/MongoDB/ClientSession.pm b/lib/MongoDB/ClientSession.pm index d99ec980..84087f89 100644 --- a/lib/MongoDB/ClientSession.pm +++ b/lib/MongoDB/ClientSession.pm @@ -314,7 +314,23 @@ sub commit_transaction { MongoDB::TransactionError->throw("Cannot call commit_transaction after calling abort_transaction") if $self->_transaction_state eq 'aborted'; - $self->_send_end_transaction_command( 'committed', [ commitTransaction => 1 ] ); + eval { + $self->_send_end_transaction_command( 'committed', [ commitTransaction => 1 ] ); + }; + if ( my $err = $@ ) { + # catch and re-throw after retryable errors + # TODO maybe need better checking logic that theres actually an error code in output? + my $err_code = $err->result->output->{codeName} || ''; + # If its a write concern error, retrying a commit would still error + unless ( grep { $_ eq $err_code } qw/ + CannotSatisfyWriteConcern + UnsatisfiableWriteConcern + UnknownReplWriteConcern + / ) { + push @{ $err->error_labels }, 'UnknownTransactionCommitResult'; + } + die $err; + } return; } diff --git a/lib/MongoDB/Op/_Command.pm b/lib/MongoDB/Op/_Command.pm index 27729699..5ec586f0 100644 --- a/lib/MongoDB/Op/_Command.pm +++ b/lib/MongoDB/Op/_Command.pm @@ -111,8 +111,8 @@ sub execute { ( $result = MongoDB::_Protocol::parse_reply( $link->read, $request_id ) ); }; if ( my $err = $@ ) { + $self->_update_session_connection_error( $err ); $self->publish_command_exception($err) if $self->monitoring_callback; - $self->_update_session_error( $err ); die $err; } diff --git a/lib/MongoDB/Role/_SessionSupport.pm b/lib/MongoDB/Role/_SessionSupport.pm index c06e49f5..b18f8227 100644 --- a/lib/MongoDB/Role/_SessionSupport.pm +++ b/lib/MongoDB/Role/_SessionSupport.pm @@ -133,11 +133,13 @@ sub _assert_session_errors { return; } -sub _update_session_error { +sub _update_session_connection_error { my ( $self, $err ) = @_; if ( $self->session->_in_transaction_state( qw/ starting in_progress / ) ) { push @{ $err->error_labels }, 'TransientTransactionError'; + # If already in_progress, no harm done + $self->session->_set__transaction_state( 'in_progress' ); } } diff --git a/lib/MongoDB/Role/_SingleBatchDocWrite.pm b/lib/MongoDB/Role/_SingleBatchDocWrite.pm index a178ca8f..76e4d58d 100644 --- a/lib/MongoDB/Role/_SingleBatchDocWrite.pm +++ b/lib/MongoDB/Role/_SingleBatchDocWrite.pm @@ -191,8 +191,8 @@ sub _send_write_command { ( $result = MongoDB::_Protocol::parse_reply( $link->read, $request_id ) ); }; if ( my $err = $@ ) { + $self->_update_session_connection_error( $err ); $self->publish_command_exception($err) if $self->monitoring_callback; - $self->_update_session_error( $err ); die $err; } diff --git a/t/transactions-spec.t b/t/transactions-spec.t index ef61028b..fde7cea6 100644 --- a/t/transactions-spec.t +++ b/t/transactions-spec.t @@ -48,7 +48,7 @@ use MongoDBTest qw/ my @events; -#use Devel::Dwarn; +use Devel::Dwarn; sub clear_events { @events = () } sub event_count { scalar @events } @@ -82,7 +82,7 @@ my %method_args = ( my $dir = path("t/data/transactions"); my $iterator = $dir->iterator; my $index = 0; # TBSLIVER while ( my $path = $iterator->() ) { - next unless $path =~ /\.json$/; next unless ++$index == 3; # TBSLIVER run specific file + next unless $path =~ /\.json$/; next unless ++$index == 3; # TBSLIVER run specific file - error-labels my $plan = eval { decode_json( $path->slurp_utf8 ) }; if ($@) { die "Error decoding $path: $@"; @@ -92,7 +92,7 @@ while ( my $path = $iterator->() ) { subtest $path => sub { - for my $test ( @{ $plan->{tests} }[0..5] ) { # TBSLIVER run specific subtest + for my $test ( @{ $plan->{tests} }[7] ) { # TBSLIVER run specific subtest my $description = $test->{description}; subtest $description => sub { my $client = build_client(); @@ -134,8 +134,6 @@ sub set_failpoint { my ( $client, $failpoint ) = @_; return unless defined $failpoint; - #Dwarn '-----------set failpoint-------------'; - #DwarnN $failpoint; my $ret = $client->send_admin_command([ configureFailPoint => $failpoint->{configureFailPoint}, mode => $failpoint->{mode}, @@ -143,22 +141,16 @@ sub set_failpoint { ? ( data => $failpoint->{data} ) : (), ]); - #DwarnN $ret; - #Dwarn '----------/set failpoint-------------'; } sub clear_failpoint { my ( $client, $failpoint ) = @_; return unless defined $failpoint; - #Dwarn '-----------clear failpoint-------------'; - #DwarnN $failpoint; my $ret = $client->send_admin_command([ configureFailPoint => $failpoint->{configureFailPoint}, mode => 'off', ]); - #DwarnN $ret; - #Dwarn '----------/clear failpoint-------------'; } sub to_snake_case { @@ -260,7 +252,7 @@ sub run_test { $sessions{session0}->end_session; $sessions{session1}->end_session; - #Dwarn \@events; + Dwarn \@events; if ( defined $test->{expectations} ) { check_event_expectations( _adjust_types( $test->{expectations} ) ); } @@ -373,7 +365,9 @@ sub prepare_data_spec { sub check_event_expectations { my ( $expected ) = @_; - my @got = grep { $_->{type} eq 'command_started' } @events; + # We only care about command_started events + # also ignoring ismaster commands caused by re-negotiation after network error + my @got = grep { $_->{type} eq 'command_started' && $_->{commandName} ne 'ismaster' } @events; #Dwarn \@events; for my $exp ( @$expected ) { From 02e5bdbe51bc4f757d25a7e0ed43412ce7ac2a8d Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Thu, 14 Jun 2018 16:05:57 +0100 Subject: [PATCH 18/38] PERL-875 finishing off spec tests. 2 TODO's left in the tests one requires PERL-918 to be finished, the other needs some thought --- lib/MongoDB/ClientSession.pm | 96 ++++++++++++-- lib/MongoDB/MongoClient.pm | 33 ++++- lib/MongoDB/Op/_Command.pm | 2 +- lib/MongoDB/Role/_SessionSupport.pm | 9 +- t/transactions-spec.t | 186 +++++++++++++++++++++------- 5 files changed, 267 insertions(+), 59 deletions(-) diff --git a/lib/MongoDB/ClientSession.pm b/lib/MongoDB/ClientSession.pm index 84087f89..a7e6cda4 100644 --- a/lib/MongoDB/ClientSession.pm +++ b/lib/MongoDB/ClientSession.pm @@ -29,6 +29,8 @@ use MongoDB::_Types qw( Document BSONTimestamp TransactionState + Boolish + WriteConcern ); use Types::Standard qw( Maybe @@ -125,6 +127,22 @@ has _transaction_state => ( default => 'none', ); +# Flag used to say we are still in a transaction +has _active_transaction => ( + is => 'rwp', + isa => Boolish, + default => 0, +); + +# Flag used to say whether any operations have been performed on the +# transaction - this is used to determine if the transaction has actually had +# any operations performed in it. +has _has_transaction_operations => ( + is => 'rwp', + isa => Boolish, + default => 0, +); + =attr operation_time The last operation time. This is updated when an operation is performed during @@ -284,16 +302,23 @@ sub start_transaction { $self->_set__current_transaction_settings( $opts ); + # Trigger build of write_concern - must error on start of transaction + $self->_clear_transaction_write_concern; + $self->_transaction_write_concern; + $self->_set__transaction_state('starting'); $self->_increment_transaction_id; + $self->_set__active_transaction( 1 ); + $self->_set__has_transaction_operations( 0 ); + return; } sub _increment_transaction_id { my $self = shift; - return if $self->_in_transaction_state( qw/ in_progress committed aborted / ); + return if $self->_active_transaction; $self->_server_session->transaction_id->binc(); } @@ -314,18 +339,32 @@ sub commit_transaction { MongoDB::TransactionError->throw("Cannot call commit_transaction after calling abort_transaction") if $self->_transaction_state eq 'aborted'; + # Commit can be called multiple times - even if the transaction completes + # correctly. Setting this here makes sure we dont increment transaction id + # until after another command has been called using this session + $self->_set__active_transaction( 1 ); + eval { $self->_send_end_transaction_command( 'committed', [ commitTransaction => 1 ] ); }; if ( my $err = $@ ) { # catch and re-throw after retryable errors # TODO maybe need better checking logic that theres actually an error code in output? - my $err_code = $err->result->output->{codeName} || ''; + my $err_code; + if ( $err->can('result') ) { + if ( $err->result->can('output') ) { + $err_code = $err->result->output->{codeName}; + $err_code ||= $err->result->output->{writeConcernError} + ? $err->result->output->{writeConcernError}->{codeName} + : ''; # Empty string just in case + } + } # If its a write concern error, retrying a commit would still error - unless ( grep { $_ eq $err_code } qw/ + unless ( defined( $err_code ) && grep { $_ eq $err_code } qw/ CannotSatisfyWriteConcern UnsatisfiableWriteConcern UnknownReplWriteConcern + NoSuchTransaction / ) { push @{ $err->error_labels }, 'UnknownTransactionCommitResult'; } @@ -364,7 +403,7 @@ sub _send_end_transaction_command { my ( $self, $end_state, $command ) = @_; # Only need to send commit command if the transaction actually sent anything - if ( ! $self->_in_transaction_state( qw/ starting / ) ) { + if ( $self->_has_transaction_operations ) { # Must set state before running the op as otherwise it wont be retried $self->_set__transaction_state( $end_state ); @@ -378,15 +417,28 @@ sub _send_end_transaction_command { monitoring_callback => $self->client->monitoring_callback, ); - $self->client->send_retryable_write_op( $op, 'force' ); + my $result = $self->client->send_retryable_write_op( $op, 'force' ); + MongoDB::WriteConcernError->throw( + message => $result->last_errmsg, + result => $result, + code => WRITE_CONCERN_ERROR, + ) if exists $result->output->{writeConcernError}; + } $self->_set__transaction_state( $end_state ); + + # If the commit/abort succeeded, we are no longer in an active transaction + $self->_set__active_transaction( 0 ); } +# read/write concern, and read preference are merged during start_transaction sub _get_transaction_read_concern { my $self = shift; - # readConcern is merged during start_transaction + + # Read concern is only read during the first operation in a transaction, so we can set this here + $self->_set__has_transaction_operations( 1 ); + if ( defined $self->_current_transaction_settings->{readConcern} ) { return MongoDB::ReadConcern->new( $self->_current_transaction_settings->{readConcern} ); } @@ -395,15 +447,37 @@ sub _get_transaction_read_concern { return $self->client->read_concern; } -sub _get_transaction_write_concern { +sub _get_transaction_read_preference { + my $self = shift; + + if ( defined $self->_current_transaction_settings->{readPreference} ) { + return MongoDB::ReadPreference->new( $self->_current_transaction_settings->{readPreference} ); + } + + return $self->client->read_preference; +} + +has _transaction_write_concern => ( + is => 'lazy', + isa => WriteConcern, + init_arg => undef, + builder => '_build_transaction_write_concern', + clearer => '_clear_transaction_write_concern', +); + +sub _build_transaction_write_concern { my $self = shift; - # writeConcern is merged during start_transaction + + my $write_concern = $self->client->write_concern; + # client is last default - provided settings override. if ( defined $self->_current_transaction_settings->{writeConcern} ) { - return MongoDB::WriteConcern->new( $self->_current_transaction_settings->{writeConcern} ); + $write_concern = MongoDB::WriteConcern->new( $self->_current_transaction_settings->{writeConcern} ); } - # Default to client write_concern, however unlikely to actually be used - return $self->client->write_concern; + unless ( $write_concern->is_acknowledged ) { + MongoDB::ConfigurationError->throw( 'transactions do not support unacknowledged write concerns' ); + } + return $write_concern; } # TODO TBSliver REMOVE ME ON RELEASE diff --git a/lib/MongoDB/MongoClient.pm b/lib/MongoDB/MongoClient.pm index 3d777016..f59da824 100644 --- a/lib/MongoDB/MongoClient.pm +++ b/lib/MongoDB/MongoClient.pm @@ -1077,7 +1077,8 @@ has _write_concern => ( sub _build__write_concern { my ($self) = @_; return MongoDB::WriteConcern->new( - ( $self->w ? ( w => $self->w ) : () ), + # Must check for defined as w can be 0, and defaults to undef + ( defined $self->w ? ( w => $self->w ) : () ), ( $self->wtimeout ? ( wtimeout => $self->wtimeout ) : () ), ( $self->j ? ( j => $self->j ) : () ), ); @@ -1532,6 +1533,12 @@ sub send_admin_command { sub send_direct_op { my ( $self, $op, $address ) = @_; my ( $link, $result ); + + # Reset session state if we're outside an active transaction + if ( defined $op->session && ! $op->session->_active_transaction ) { + $op->session->_set__transaction_state( 'none' ); + } + ( $link = $self->{_topology}->get_specific_link($address) ), ( eval { ($result) = $op->execute($link); 1 } or do { my $err = length($@) ? $@ : "caught error, but it was lost in eval unwind"; @@ -1553,6 +1560,12 @@ sub send_direct_op { sub send_write_op { my ( $self, $op ) = @_; my ( $link, $result ); + + # Reset session state if we're outside an active transaction + if ( defined $op->session && ! $op->session->_active_transaction ) { + $op->session->_set__transaction_state( 'none' ); + } + ( $link = $self->{_topology}->get_writable_link ), ( eval { ($result) = $op->execute($link, $self->{_topology}->type); 1 } or do { my $err = length($@) ? $@ : "caught error, but it was lost in eval unwind"; @@ -1585,6 +1598,11 @@ sub send_retryable_write_op { # TODO untangle the send_write_op and _try_write_op_for_link duplication return $self->send_write_op( $op ) unless $self->retry_writes || ( defined $force && $force eq 'force' ); + # Reset session state if we're outside an active transaction + if ( defined $op->session && ! $op->session->_active_transaction ) { + $op->session->_set__transaction_state( 'none' ); + } + my $result; my $link = $self->{_topology}->get_writable_link; @@ -1603,7 +1621,7 @@ sub send_retryable_write_op { # If we get this far and there is no session, then somethings gone really # wrong, so probably not worth worrying about. - # + # increment transaction id before write, but otherwise is the same for both attempts $op->session->_increment_transaction_id; $op->retryable_write( 1 ); @@ -1668,6 +1686,17 @@ sub _try_write_op_for_link { sub send_read_op { my ( $self, $op ) = @_; my ( $link, $type, $result ); + + # Get transaction read preference if in a transaction + if ( defined $op->session && $op->session->_active_transaction ) { + $op->read_preference( $op->session->_get_transaction_read_preference ); + MongoDB::ConfigurationError->throw("read preference in a transaction must be primary") + if $op->read_preference->mode ne 'primary'; + } elsif ( defined $op->session ) { + # Not in an active transaction, so reset state + $op->session->_set__transaction_state( 'none' ); + } + ( $link = $self->{_topology}->get_readable_link( $op->read_preference ) ), ( $type = $self->{_topology}->type ), ( eval { ($result) = $op->execute( $link, $type ); 1 } or do { diff --git a/lib/MongoDB/Op/_Command.pm b/lib/MongoDB/Op/_Command.pm index 5ec586f0..3579e224 100644 --- a/lib/MongoDB/Op/_Command.pm +++ b/lib/MongoDB/Op/_Command.pm @@ -52,7 +52,7 @@ has query_flags => ( ); has read_preference => ( - is => 'ro', + is => 'rw', isa => Maybe [ReadPreference], ); diff --git a/lib/MongoDB/Role/_SessionSupport.pm b/lib/MongoDB/Role/_SessionSupport.pm index b18f8227..c535475a 100644 --- a/lib/MongoDB/Role/_SessionSupport.pm +++ b/lib/MongoDB/Role/_SessionSupport.pm @@ -60,15 +60,19 @@ sub _apply_session_and_cluster_time { # write concern not allowed in transactions except when ending. We can # safely delete it here as you can only pass writeConcern through by # arguments to client of collection. - if ( $self->session->_in_transaction_state( qw/ starting in_progress / ) ) { ($$query_ref)->Delete( 'writeConcern' ); } + # read concern only valid outside a transaction or when starting + if ( ! $self->session->_in_transaction_state( qw/ none starting / ) ) { + ($$query_ref)->Delete( 'readConcern' ); + } + if ( $self->session->_in_transaction_state( qw/ aborted committed / ) && ! ($$query_ref)->EXISTS('writeConcern') ) { - ($$query_ref)->Push( @{ $self->session->_get_transaction_write_concern->as_args() } ); + ($$query_ref)->Push( @{ $self->session->_transaction_write_concern->as_args() } ); } $self->session->_server_session->update_last_use; @@ -111,6 +115,7 @@ sub _update_session_pre_assert { if ( $self->session->_in_transaction_state( 'starting' ) ) { $self->session->_set__transaction_state( 'in_progress' ); + $self->session->_set__has_transaction_operations( 1 ); } my $operation_time = $self->__extract_from( $response, 'operationTime' ); diff --git a/t/transactions-spec.t b/t/transactions-spec.t index fde7cea6..9b687600 100644 --- a/t/transactions-spec.t +++ b/t/transactions-spec.t @@ -26,6 +26,9 @@ use Storable qw( dclone ); use utf8; use MongoDB; +use MongoDB::_Types qw/ + to_IxHash +/; use MongoDB::Error; use lib "t/lib"; @@ -48,7 +51,8 @@ use MongoDBTest qw/ my @events; -use Devel::Dwarn; +# TODO strip all Dwarns +#use Devel::Dwarn; sub clear_events { @events = () } sub event_count { scalar @events } @@ -77,12 +81,18 @@ my %method_args = ( find => [qw( filter )], count => [qw( filter )], bulk_write => [qw( requests )], + find_one_and_update => [qw( filter update )], + find_one_and_replace => [qw( filter replacement )], + find_one_and_delete => [qw( filter )], + run_command => [qw( command readPreference )], + aggregate => [qw( pipeline )], + distinct => [qw( fieldName filter )], ); my $dir = path("t/data/transactions"); my $iterator = $dir->iterator; my $index = 0; # TBSLIVER while ( my $path = $iterator->() ) { - next unless $path =~ /\.json$/; next unless ++$index == 3; # TBSLIVER run specific file - error-labels + next unless $path =~ /\.json$/; #next unless ++$index == 19; # TBSLIVER run specific file my $plan = eval { decode_json( $path->slurp_utf8 ) }; if ($@) { die "Error decoding $path: $@"; @@ -92,8 +102,13 @@ while ( my $path = $iterator->() ) { subtest $path => sub { - for my $test ( @{ $plan->{tests} }[7] ) { # TBSLIVER run specific subtest + for my $test ( @{ $plan->{tests} } ) { # TBSLIVER run specific subtest my $description = $test->{description}; + local $TODO = 'does a run_command read_preference count as a user configurable read_preference?' if $path =~ /run-command/ && $description =~ /explicit secondary read preference/; + # TODO requires PERL-918 + local $TODO = 'requires PERL-918' if $path =~ /error-labels/ && $description =~ /add unknown commit label/; + local $TODO = 'requires PERL-918' if $path =~ /retryable-abort/ && $description =~ /abortTransaction succeeds after (NotMaster|Interrupted|Primary|Shutdown|Host|Socket|Network|WriteConcernError)/; + local $TODO = 'requires PERL-918' if $path =~ /retryable-commit/ && $description =~ /commitTransaction succeeds after (NotMaster|Interrupted|Primary|Shutdown|Host|Socket|Network|WriteConcernError)/; subtest $description => sub { my $client = build_client(); @@ -125,6 +140,8 @@ while ( my $path = $iterator->() ) { set_failpoint( $client, $test->{failPoint} ); run_test( $test_db_name, $test_coll_name, $test ); clear_failpoint( $client, $test->{failPoint} ); + + # TODO Check outcome data }; } }; @@ -178,6 +195,11 @@ sub run_test { my $client_options = $test->{clientOptions} // {}; $client_options = remap_hash_to_snake_case( $client_options ); + # TODO Why is read_preference a read only mutator????.... + if ( exists $client_options->{read_preference} ) { + $client_options->{read_pref_mode} = delete $client_options->{read_preference}; + } + my $client = build_client( monitoring_callback => \&event_cb, %$client_options ); my $session_options = $test->{sessionOptions} // {}; @@ -195,12 +217,14 @@ sub run_test { my $collection_options = $operation->{collectionOptions} // {}; $collection_options = remap_hash_to_snake_case( $collection_options ); + my $op_result = $operation->{result}; + eval { - my $test_db = $client->get_database( $test_db_name ); - my $test_coll = $test_db->get_collection( $test_coll_name, $collection_options ); + $sessions{ database } = $client->get_database( $test_db_name ); + $sessions{ collection } = $sessions{ database }->get_collection( $test_coll_name, $collection_options ); my $cmd = to_snake_case( $operation->{name} ); - diag $cmd; + # diag $cmd; #Dwarn $operation; if ( $cmd =~ /_transaction$/ ) { my $op_args = $operation->{arguments} // {}; @@ -208,57 +232,133 @@ sub run_test { } else { my @args = _adjust_arguments( $cmd, $operation->{arguments} ); $args[-1]->{session} = $sessions{ $args[-1]->{session} } - if defined $args[-1]->{session}; + if exists $args[-1]->{session}; + $args[-1]->{returnDocument} = lc $args[-1]->{returnDocument} + if exists $args[-1]->{returnDocument}; + + if ( $cmd eq 'find' ) { + # not every find command actually has a filter + @args = ( undef, $args[0] ) + if scalar( @args ) == 1; + } + if ( $cmd eq 'run_command' ) { + $args[0] = to_IxHash( $args[0] ); + # move command to the beginning of the hash + my $cmd_arg = $args[0]->DELETE( $operation->{command_name} ); + $args[0]->Unshift( $operation->{command_name}, $cmd_arg ); + # May not have had a readPreference set + @args = ( $args[0], undef, $args[1] ) + if scalar( @args ) == 2; + } + if ( $cmd eq 'distinct' ) { + @args = ( $args[0], undef, $args[1] ) + if scalar( @args ) == 2; + } + my $ret = $sessions{ $operation->{object} }->$cmd( @args ); + + # special case 'find' so commands are actually emitted + my $result = $ret; + $result = [ $ret->all ] + if ( grep { $cmd eq $_ } qw/ find aggregate distinct / ); - $test_coll->$cmd( @args ); + check_result_outcome( $result, $op_result ); } }; - #Dwarn '----------------Session------------------'; - #Dwarn $sessions{session0}->_debug; my $err = $@; - if ( $err ) { - #Dwarn '----------------Error------------------'; - #Dwarn $err; - my $err_contains = $operation->{result}->{errorContains}; - my $err_code_name = $operation->{result}->{errorCodeName}; - my $err_labels_contains = $operation->{result}->{errorLabelsContain}; - my $err_labels_omit = $operation->{result}->{errorLabelsOmit}; - if ( defined $err_contains ) { - like $err->message, qr/$err_contains/i, 'error contains' . $err_contains; - } - if ( defined $err_code_name ) { - is $err->result->output->{codeName}, - $err_code_name, - 'error has name ' . $err_code_name; - } - if ( defined $err_labels_omit ) { - for my $err_label ( @{ $err_labels_omit } ) { - ok ! $err->has_error_label( $err_label ), 'error doesnt have label ' . $err_label; - } - } - if ( defined $err_labels_omit ) { - for my $err_label ( @{ $err_labels_contains } ) { - ok $err->has_error_label( $err_label ), 'error has label ' . $err_label; - } - } - } elsif ( grep {/^error/} keys %{ $operation->{result} } ) { - fail 'Should have found an error'; - #DwarnN $operation; - #DwarnN $events[-2]; - #DwarnN $events[-1]; - } + check_error( $err, $op_result ); } $sessions{session0}->end_session; $sessions{session1}->end_session; - Dwarn \@events; + #Dwarn \@events; if ( defined $test->{expectations} ) { check_event_expectations( _adjust_types( $test->{expectations} ) ); } %sessions = (); } +sub check_error { + my ( $err, $exp ) = @_; + + my $expecting_error = 0; + if ( ref( $exp ) eq 'HASH' ) { + $expecting_error = grep {/^error/} keys %{ $exp }; + } + if ( $err ) { + unless ( $expecting_error ) { + my $diag_msg = 'Not expecting error, got "' . $err->message . '"'; + # abortTransactions are errors????? + if ( defined $events[-2] && $events[-2]->{commandName} eq 'abortTransaction' ) { + diag $diag_msg; + } else { + fail $diag_msg; + } + return; + } + #Dwarn $err; + my $err_contains = $exp->{errorContains}; + my $err_code_name = $exp->{errorCodeName}; + my $err_labels_contains = $exp->{errorLabelsContain}; + my $err_labels_omit = $exp->{errorLabelsOmit}; + if ( defined $err_contains ) { + $err_contains =~ s/abortTransaction/abort_transaction/; + $err_contains =~ s/commitTransaction/commit_transaction/; + like $err->message, qr/$err_contains/i, 'error contains ' . $err_contains; + } + if ( defined $err_code_name ) { + is $err->result->output->{codeName}, + $err_code_name, + 'error has name ' . $err_code_name; + } + if ( defined $err_labels_omit ) { + for my $err_label ( @{ $err_labels_omit } ) { + ok ! $err->has_error_label( $err_label ), 'error doesnt have label ' . $err_label; + } + } + if ( defined $err_labels_omit ) { + for my $err_label ( @{ $err_labels_contains } ) { + ok $err->has_error_label( $err_label ), 'error has label ' . $err_label; + } + } + } elsif ( $expecting_error ) { + fail 'Expecting error, but no error found'; + } +} + +sub check_result_outcome { + my ( $got, $exp ) = @_; + + #DwarnN $got; + #DwarnN $exp; + if ( ref( $exp ) eq 'ARRAY' ) { + check_array_result_outcome( $got, $exp ); + } else { + check_hash_result_outcome( $got, $exp ); + } +} + +sub check_array_result_outcome { + my ( $got, $exp ) = @_; + + cmp_deeply $got, $exp, 'result as expected'; +} + +sub check_hash_result_outcome { + my ( $got, $exp ) = @_; + + for my $key ( keys %$exp ) { + my $obj_key = to_snake_case( $key ); + next if ( $key eq 'upsertedCount' && !$got->can('upserted_count') ); + # Some results are just raw results + if ( ref $got eq 'HASH' ) { + cmp_deeply $got->{ $obj_key }, $exp->{ $key }, "$key result correct"; + } else { + cmp_deeply $got->$obj_key, $exp->{ $key }, "$key result correct"; + } + } +} + # Following subs modified from monitoring_spec.t # @@ -526,7 +626,7 @@ sub check_command_field { if ( defined $exp_command->{readConcern} ) { $exp_command->{readConcern}{afterClusterTime} = Isa('BSON::Timestamp') - if $exp_command->{readConcern}{afterClusterTime} eq '42'; + if ( defined $exp_command->{readConcern}{afterClusterTime} && $exp_command->{readConcern}{afterClusterTime} eq '42' ); } if ( defined $exp_command->{txnNumber} ) { From 9056a3f34e9970b64f35a8185e788482ff6d06fc Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Thu, 14 Jun 2018 16:10:36 +0100 Subject: [PATCH 19/38] PERL-875 remove unnecessary flag setting and redundant comments --- lib/MongoDB/ClientSession.pm | 3 +-- lib/MongoDB/Role/_SessionSupport.pm | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/MongoDB/ClientSession.pm b/lib/MongoDB/ClientSession.pm index a7e6cda4..7e12daeb 100644 --- a/lib/MongoDB/ClientSession.pm +++ b/lib/MongoDB/ClientSession.pm @@ -135,8 +135,7 @@ has _active_transaction => ( ); # Flag used to say whether any operations have been performed on the -# transaction - this is used to determine if the transaction has actually had -# any operations performed in it. +# transaction has _has_transaction_operations => ( is => 'rwp', isa => Boolish, diff --git a/lib/MongoDB/Role/_SessionSupport.pm b/lib/MongoDB/Role/_SessionSupport.pm index c535475a..072db1a6 100644 --- a/lib/MongoDB/Role/_SessionSupport.pm +++ b/lib/MongoDB/Role/_SessionSupport.pm @@ -115,7 +115,6 @@ sub _update_session_pre_assert { if ( $self->session->_in_transaction_state( 'starting' ) ) { $self->session->_set__transaction_state( 'in_progress' ); - $self->session->_set__has_transaction_operations( 1 ); } my $operation_time = $self->__extract_from( $response, 'operationTime' ); From 3af32c8b1b198770d361c67a48419d4c2e1fafaa Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Wed, 20 Jun 2018 16:02:41 +0100 Subject: [PATCH 20/38] PERL-875 Refactor transaction options into seperate class --- lib/MongoDB/ClientSession.pm | 110 +++++------------------ lib/MongoDB/MongoClient.pm | 2 - lib/MongoDB/Role/_SessionSupport.pm | 3 +- lib/MongoDB/_TransactionOptions.pm | 134 ++++++++++++++++++++++++++++ lib/MongoDB/_Types.pm | 3 + 5 files changed, 159 insertions(+), 93 deletions(-) create mode 100644 lib/MongoDB/_TransactionOptions.pm diff --git a/lib/MongoDB/ClientSession.pm b/lib/MongoDB/ClientSession.pm index 7e12daeb..3732b911 100644 --- a/lib/MongoDB/ClientSession.pm +++ b/lib/MongoDB/ClientSession.pm @@ -24,19 +24,18 @@ our $VERSION = 'v1.999.1'; use MongoDB::Error; use Moo; -use MongoDB::ReadConcern; use MongoDB::_Types qw( Document BSONTimestamp TransactionState Boolish - WriteConcern ); use Types::Standard qw( Maybe HashRef InstanceOf ); +use MongoDB::_TransactionOptions; use namespace::clean -except => 'meta'; =attr client @@ -70,7 +69,7 @@ has cluster_time => ( Options provided for this particular session. Available options include: -=for :list +=for :list * C - If true, will enable causalConsistency for this session. For more information, see L. @@ -114,11 +113,14 @@ has _server_session => ( clearer => '__clear_server_session', ); -has _current_transaction_settings => ( +has _current_transaction_options => ( is => 'rwp', - isa => HashRef, - init_arg => undef, - clearer => '_clear_current_transaction_settings', + isa => InstanceOf[ 'MongoDB::_TransactionOptions' ], + handles => { + _get_transaction_write_concern => 'write_concern', + _get_transaction_read_concern => 'read_concern', + _get_transaction_read_preference => 'read_preference', + }, ); has _transaction_state => ( @@ -297,13 +299,13 @@ sub start_transaction { unless $self->client->_topology->_supports_transactions; $opts ||= {}; - $opts = { %{ $self->options->{defaultTransactionOptions} }, %$opts }; - - $self->_set__current_transaction_settings( $opts ); + my $trans_opts = MongoDB::_TransactionOptions->new( + client => $self->client, + options => $opts, + default_options => $self->options->{defaultTransactionOptions}, + ); - # Trigger build of write_concern - must error on start of transaction - $self->_clear_transaction_write_concern; - $self->_transaction_write_concern; + $self->_set__current_transaction_options( $trans_opts ); $self->_set__transaction_state('starting'); @@ -389,7 +391,7 @@ sub abort_transaction { MongoDB::TransactionError->throw("Cannot call abort_transaction after calling commit_transaction") if $self->_in_transaction_state( 'committed' ); - # Error message tweaked to use our function names + # Error message tweaked to use our function names MongoDB::TransactionError->throw("Cannot call abort_transaction twice") if $self->_in_transaction_state( 'aborted' ); @@ -401,12 +403,10 @@ sub abort_transaction { sub _send_end_transaction_command { my ( $self, $end_state, $command ) = @_; + $self->_set__transaction_state( $end_state ); + # Only need to send commit command if the transaction actually sent anything if ( $self->_has_transaction_operations ) { - - # Must set state before running the op as otherwise it wont be retried - $self->_set__transaction_state( $end_state ); - my $op = MongoDB::Op::_Command->_new( db_name => 'admin', query => $command, @@ -417,84 +417,14 @@ sub _send_end_transaction_command { ); my $result = $self->client->send_retryable_write_op( $op, 'force' ); - MongoDB::WriteConcernError->throw( - message => $result->last_errmsg, - result => $result, - code => WRITE_CONCERN_ERROR, - ) if exists $result->output->{writeConcernError}; - + # TODO This may be redundant after 918 is merged? + $result->assert_no_write_concern_error; } - $self->_set__transaction_state( $end_state ); - # If the commit/abort succeeded, we are no longer in an active transaction $self->_set__active_transaction( 0 ); } -# read/write concern, and read preference are merged during start_transaction -sub _get_transaction_read_concern { - my $self = shift; - - # Read concern is only read during the first operation in a transaction, so we can set this here - $self->_set__has_transaction_operations( 1 ); - - if ( defined $self->_current_transaction_settings->{readConcern} ) { - return MongoDB::ReadConcern->new( $self->_current_transaction_settings->{readConcern} ); - } - - # Default to the clients read concern - return $self->client->read_concern; -} - -sub _get_transaction_read_preference { - my $self = shift; - - if ( defined $self->_current_transaction_settings->{readPreference} ) { - return MongoDB::ReadPreference->new( $self->_current_transaction_settings->{readPreference} ); - } - - return $self->client->read_preference; -} - -has _transaction_write_concern => ( - is => 'lazy', - isa => WriteConcern, - init_arg => undef, - builder => '_build_transaction_write_concern', - clearer => '_clear_transaction_write_concern', -); - -sub _build_transaction_write_concern { - my $self = shift; - - my $write_concern = $self->client->write_concern; - # client is last default - provided settings override. - if ( defined $self->_current_transaction_settings->{writeConcern} ) { - $write_concern = MongoDB::WriteConcern->new( $self->_current_transaction_settings->{writeConcern} ); - } - - unless ( $write_concern->is_acknowledged ) { - MongoDB::ConfigurationError->throw( 'transactions do not support unacknowledged write concerns' ); - } - return $write_concern; -} - -# TODO TBSliver REMOVE ME ON RELEASE -sub _debug { - my $self = shift; - return { - state => $self->_transaction_state, - client => defined $self->client ? 'defined' : '', - session => defined $self->_server_session ? 'defined' : '', - session_id => $self->session_id, - transaction_id => defined $self->_server_session ? $self->_server_session->transaction_id : '', - cluster_time => $self->cluster_time, - options => $self->options, - transaction_settings => $self->_current_transaction_settings, - operation_time => $self->operation_time, - }; -} - =method end_session $session->end_session; diff --git a/lib/MongoDB/MongoClient.pm b/lib/MongoDB/MongoClient.pm index f59da824..5278ba0e 100644 --- a/lib/MongoDB/MongoClient.pm +++ b/lib/MongoDB/MongoClient.pm @@ -1690,8 +1690,6 @@ sub send_read_op { # Get transaction read preference if in a transaction if ( defined $op->session && $op->session->_active_transaction ) { $op->read_preference( $op->session->_get_transaction_read_preference ); - MongoDB::ConfigurationError->throw("read preference in a transaction must be primary") - if $op->read_preference->mode ne 'primary'; } elsif ( defined $op->session ) { # Not in an active transaction, so reset state $op->session->_set__transaction_state( 'none' ); diff --git a/lib/MongoDB/Role/_SessionSupport.pm b/lib/MongoDB/Role/_SessionSupport.pm index 072db1a6..a686face 100644 --- a/lib/MongoDB/Role/_SessionSupport.pm +++ b/lib/MongoDB/Role/_SessionSupport.pm @@ -54,6 +54,7 @@ sub _apply_session_and_cluster_time { if ( $self->session->_in_transaction_state( 'starting' ) ) { ($$query_ref)->Push( 'startTransaction' => true ); + $self->session->_set__has_transaction_operations( 1 ); ($$query_ref)->Push( @{ $self->session->_get_transaction_read_concern->as_args( $self->session ) } ); } @@ -72,7 +73,7 @@ sub _apply_session_and_cluster_time { if ( $self->session->_in_transaction_state( qw/ aborted committed / ) && ! ($$query_ref)->EXISTS('writeConcern') ) { - ($$query_ref)->Push( @{ $self->session->_transaction_write_concern->as_args() } ); + ($$query_ref)->Push( @{ $self->session->_get_transaction_write_concern->as_args() } ); } $self->session->_server_session->update_last_use; diff --git a/lib/MongoDB/_TransactionOptions.pm b/lib/MongoDB/_TransactionOptions.pm new file mode 100644 index 00000000..857b18f9 --- /dev/null +++ b/lib/MongoDB/_TransactionOptions.pm @@ -0,0 +1,134 @@ +# Copyright 2018 - present MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +use strict; +use warnings; +package MongoDB::_TransactionOptions; + +# MongoDB options for transactions + +use version; +our $VERSION = 'v1.999.0'; + +use MongoDB::Error; + +use Moo; +use MongoDB::ReadConcern; +use MongoDB::WriteConcern; +use MongoDB::ReadPreference; +use MongoDB::_Types qw( + MongoDBClient + WriteConcern + ReadConcern + ReadPreference +); +use Types::Standard qw( + HashRef + Any +); +use namespace::clean -except => 'meta'; + +# Options provided during strart transaction +has options => ( + is => 'ro', + required => 1, + isa => HashRef, +); + +# Options provided during start session +has default_options => ( + is => 'ro', + required => 1, + isa => HashRef, +); + +# needed for defaults +has client => ( + is => 'ro', + required => 1, + isa => MongoDBClient, +); + +has write_concern => ( + # must error on start_transaction, so is built immediately + is => 'ro', + isa => WriteConcern, + init_arg => undef, + builder => '_build_write_concern', +); + +sub _build_write_concern { + my $self = shift; + + my $options = $self->options->{writeConcern}; + $options ||= $self->default_options->{writeConcern}; + + my $write_concern; + $write_concern = MongoDB::WriteConcern->new( $options ) if defined $options; + $write_concern ||= $self->client->write_concern; + + unless ( $write_concern->is_acknowledged ) { + MongoDB::ConfigurationError->throw( + 'transactions do not support unacknowledged write concerns' ); + } + + return $write_concern; +} + +has read_concern => ( + is => 'lazy', + isa => ReadConcern, + init_arg => undef, + builder => '_build_read_concern', +); + +# Read concern errors are returned by the database, so no need to check for +# errors +sub _build_read_concern { + my $self = shift; + + my $options = $self->options->{readConcern}; + $options ||= $self->default_options->{readConcern}; + + return MongoDB::ReadConcern->new( $options ) if defined $options; + return $self->client->read_concern; +} + +has read_preference => ( + is => 'lazy', + isa => ReadPreference, + init_arg => undef, + builder => '_build_read_preference', +); + +# Read preferences must be primary at present, so check after building it +sub _build_read_preference { + my $self = shift; + + my $options = $self->options->{readPreference}; + $options ||= $self->default_options->{readPreference}; + + my $read_pref; + $read_pref = MongoDB::ReadPreference->new( $options ) if defined $options; + $read_pref ||= $self->client->read_preference; + + if ( $read_pref->mode ne 'primary' ) { + MongoDB::ConfigurationError->throw( + "read preference in a transaction must be primary" ); + } + + return $read_pref; +} + +1; diff --git a/lib/MongoDB/_Types.pm b/lib/MongoDB/_Types.pm index 17c482cc..87cb1d0b 100644 --- a/lib/MongoDB/_Types.pm +++ b/lib/MongoDB/_Types.pm @@ -49,6 +49,7 @@ use Type::Library IxHash MaxStalenessNum MaybeHashRef + MongoDBClient MongoDBCollection MongoDBDatabase BSONTimestamp @@ -152,6 +153,8 @@ class_type IxHash, { class => 'Tie::IxHash' }; declare MaybeHashRef, as Maybe[ HashRef ]; +class_type MongoDBClient, { class => 'MongoDB::MongoClient' }; + class_type MongoDBCollection, { class => 'MongoDB::Collection' }; class_type MongoDBDatabase, { class => 'MongoDB::Database' }; From cabe08122f4247886be106da6d365ddd05c39cc4 Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Wed, 20 Jun 2018 17:13:05 +0100 Subject: [PATCH 21/38] PERL-875 rework session option merging --- lib/MongoDB/ClientSession.pm | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/MongoDB/ClientSession.pm b/lib/MongoDB/ClientSession.pm index 3732b911..0d99e095 100644 --- a/lib/MongoDB/ClientSession.pm +++ b/lib/MongoDB/ClientSession.pm @@ -90,18 +90,16 @@ has options => ( # Shallow copy to prevent action at a distance. # Upgrade to use Storable::dclone if a more complex option is required coerce => sub { - $_[0] = { - causalConsistency => 1, - %{ $_[0] }, - # applied after to not override the clone with the original - defaultTransactionOptions => { - defined( $_[0] ) - && ref( $_[0] ) eq 'HASH' - && defined( $_[0]->{defaultTransactionOptions} ) - ? ( %{ $_[0]->{defaultTransactionOptions} } ) - : (), - }, - }; + # Will cause the isa requirement to fire + return unless defined( $_[0] ) && ref( $_[0] ) eq 'HASH'; + my $dto = $_[0]->{defaultTransactionOptions}; + $dto ||= {}; + $_[0] = { + causalConsistency => 1, + %{ $_[0] }, + # applied after to not override the clone with the original + defaultTransactionOptions => $dto, + }; }, ); From 1fe1a519e6f653ed1b29c1f18eb9cf130129a910 Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Wed, 20 Jun 2018 17:22:35 +0100 Subject: [PATCH 22/38] PERL-875 remove TransactionError class and use UsageError class --- lib/MongoDB/ClientSession.pm | 12 ++++++------ lib/MongoDB/Error.pm | 10 ++-------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/lib/MongoDB/ClientSession.pm b/lib/MongoDB/ClientSession.pm index 0d99e095..695e0630 100644 --- a/lib/MongoDB/ClientSession.pm +++ b/lib/MongoDB/ClientSession.pm @@ -290,7 +290,7 @@ Start a transaction in this session. Takes a hashref of options which can contai sub start_transaction { my ( $self, $opts ) = @_; - MongoDB::TransactionError->throw("Transaction already in progress") + MongoDB::UsageError->throw("Transaction already in progress") if $self->_in_transaction_state( 'starting', 'in_progress' ); MongoDB::ConfigurationError->throw("Transactions are unsupported on this deployment") @@ -331,11 +331,11 @@ Commit the current transaction. This will use the writeConcern set on this trans sub commit_transaction { my $self = shift; - MongoDB::TransactionError->throw("No transaction started") + MongoDB::UsageError->throw("No transaction started") if $self->_transaction_state eq 'none'; # Error message tweaked to use our function names - MongoDB::TransactionError->throw("Cannot call commit_transaction after calling abort_transaction") + MongoDB::UsageError->throw("Cannot call commit_transaction after calling abort_transaction") if $self->_transaction_state eq 'aborted'; # Commit can be called multiple times - even if the transaction completes @@ -382,15 +382,15 @@ Abort the current transaction. This will use the writeConcern set on this transa sub abort_transaction { my $self = shift; - MongoDB::TransactionError->throw("No transaction started") + MongoDB::UsageError->throw("No transaction started") if $self->_in_transaction_state( 'none' ); # Error message tweaked to use our function names - MongoDB::TransactionError->throw("Cannot call abort_transaction after calling commit_transaction") + MongoDB::UsageError->throw("Cannot call abort_transaction after calling commit_transaction") if $self->_in_transaction_state( 'committed' ); # Error message tweaked to use our function names - MongoDB::TransactionError->throw("Cannot call abort_transaction twice") + MongoDB::UsageError->throw("Cannot call abort_transaction twice") if $self->_in_transaction_state( 'aborted' ); $self->_send_end_transaction_command( 'aborted', [ abortTransaction => 1 ] ); diff --git a/lib/MongoDB/Error.pm b/lib/MongoDB/Error.pm index 1af93109..38cd2995 100644 --- a/lib/MongoDB/Error.pm +++ b/lib/MongoDB/Error.pm @@ -312,12 +312,6 @@ use Moo; use namespace::clean; extends 'MongoDB::TimeoutError'; -#Transaction errors -package MongoDB::TransactionError; -use Moo; -use namespace::clean; -extends 'MongoDB::Error'; - # Database errors package MongoDB::DuplicateKeyError; use Moo; @@ -497,8 +491,6 @@ To retry failures automatically, consider using L. | | | |->MongoDB::NetworkTimeout | - |->MongoDB::TransactionError - | |->MongoDB::UsageError All classes inherit from C. @@ -629,6 +621,8 @@ will throw this — only ones originating directly from the MongoDB::* library files. Some type and usage errors will originate from the L library if the objects are used incorrectly. +Also used to indicate usage errors for transaction commands. + =head1 ERROR CODES The following error code constants are automatically exported by this module. From 70d09a6cb10193cf316a4fb5c6dbdc91292ddd74 Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Thu, 21 Jun 2018 15:21:58 +0100 Subject: [PATCH 23/38] PERL-875 refactor write_op and retryable_write_op to be easier to read --- lib/MongoDB/MongoClient.pm | 46 +++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/lib/MongoDB/MongoClient.pm b/lib/MongoDB/MongoClient.pm index 5278ba0e..393eb2ed 100644 --- a/lib/MongoDB/MongoClient.pm +++ b/lib/MongoDB/MongoClient.pm @@ -1009,9 +1009,7 @@ delete_many operations. =cut has retry_writes => ( - # need rwp to allow for retryable writes inside transactions - is => 'rwp', - lazy => '1', + is => 'lazy', isa => Boolish, builder => '_build_retry_writes', ); @@ -1567,16 +1565,8 @@ sub send_write_op { } ( $link = $self->{_topology}->get_writable_link ), ( - eval { ($result) = $op->execute($link, $self->{_topology}->type); 1 } or do { + eval { ($result) = $self->_try_write_op_for_link( $link, $op ); 1 } or do { my $err = length($@) ? $@ : "caught error, but it was lost in eval unwind"; - if ( $err->$_isa("MongoDB::ConnectionError") ) { - $self->{_topology}->mark_server_unknown( $link->server, $err ); - } - elsif ( $err->$_isa("MongoDB::NotMasterError") ) { - $self->{_topology}->mark_server_unknown( $link->server, $err ); - $self->{_topology}->mark_stale; - } - # regardless of cleanup, rethrow the error WITH_ASSERTS ? ( confess $err ) : ( die $err ); } ), @@ -1594,24 +1584,26 @@ BEGIN { sub send_retryable_write_op { my ( $self, $op, $force ) = @_; - # force is used specifically for retrying writes in transactions - # TODO untangle the send_write_op and _try_write_op_for_link duplication - return $self->send_write_op( $op ) unless $self->retry_writes || ( defined $force && $force eq 'force' ); + my $result; + my $link = $self->{_topology}->get_writable_link; # Reset session state if we're outside an active transaction if ( defined $op->session && ! $op->session->_active_transaction ) { $op->session->_set__transaction_state( 'none' ); } - my $result; - my $link = $self->{_topology}->get_writable_link; - - # Not sent to send_write_op to use the link we just got - # If server doesnt support retryable writes, pretend its not enabled - # active transactions also dont support retryable writes - # except in abort and commit state + # Need to force to do a retryable write on a Transaction Commit or Abort. $force is an override for retry_writes, but theres no point trying that if the link doesnt support it anyway. + # This triggers on the following: + # * $force is not set to 'force' + # (specifically for retrying writes in ending transaction operations) + # * retry writes is not enabled or the link doesnt support retryWrites + # * if an active transaction is starting or in progress unless ( $link->supports_retryWrites - && !$op->session->_in_transaction_state( qw/ starting in_progress / ) ) { + && ( $self->retry_writes || ( defined $force && $force eq 'force' ) ) + && ( defined $op->session + && ! $op->session->_in_transaction_state( qw/ starting in_progress / ) + ) + ) { eval { ($result) = $self->_try_write_op_for_link( $link, $op ); 1 } or do { my $err = length($@) ? $@ : "caught error, but it was lost in eval unwind"; WITH_ASSERTS ? ( confess $err ) : ( die $err ); @@ -1622,7 +1614,8 @@ sub send_retryable_write_op { # If we get this far and there is no session, then somethings gone really # wrong, so probably not worth worrying about. - # increment transaction id before write, but otherwise is the same for both attempts + # increment transaction id before write, but otherwise is the same for both + # attempts. If not in a transaction, is a no-op $op->session->_increment_transaction_id; $op->retryable_write( 1 ); @@ -1687,8 +1680,11 @@ sub send_read_op { my ( $self, $op ) = @_; my ( $link, $type, $result ); - # Get transaction read preference if in a transaction + # Get transaction read preference if in a transaction. if ( defined $op->session && $op->session->_active_transaction ) { + # Transactions may only read from primary in MongoDB 4.0, so get and + # check the read preference from the transaction settings as per + # transaction spec - see MongoDB::_TransactionOptions $op->read_preference( $op->session->_get_transaction_read_preference ); } elsif ( defined $op->session ) { # Not in an active transaction, so reset state From 24f81edbbb3203f2138c8ef281bcc80f9bd325d7 Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Thu, 21 Jun 2018 15:29:07 +0100 Subject: [PATCH 24/38] PERL-875 make last_error_labels return empty array if no errorLabels found --- lib/MongoDB/CommandResult.pm | 5 +++-- lib/MongoDB/Role/_DatabaseErrorThrower.pm | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/MongoDB/CommandResult.pm b/lib/MongoDB/CommandResult.pm index dcc1864e..f86a6545 100644 --- a/lib/MongoDB/CommandResult.pm +++ b/lib/MongoDB/CommandResult.pm @@ -117,13 +117,14 @@ sub last_wtimeout { =method last_error_labels -Returns any error labels from the command +Returns an array of error labels from the command, or an empty array if there +are none =cut sub last_error_labels { my ( $self ) = @_; - return $self->output->{errorLabels}; + return $self->output->{errorLabels} || []; } =method assert diff --git a/lib/MongoDB/Role/_DatabaseErrorThrower.pm b/lib/MongoDB/Role/_DatabaseErrorThrower.pm index 40545166..5f423d83 100644 --- a/lib/MongoDB/Role/_DatabaseErrorThrower.pm +++ b/lib/MongoDB/Role/_DatabaseErrorThrower.pm @@ -58,7 +58,7 @@ sub _throw_database_error { $error_class->throw( result => $self, code => $code || UNKNOWN_ERROR, - error_labels => $error_labels || [], + error_labels => $error_labels, ( length($err) ? ( message => $err ) : () ), ); From 4f971489f62c1c1737385af67ae2c83c5bfa981c Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Thu, 21 Jun 2018 15:38:39 +0100 Subject: [PATCH 25/38] PERL-875 Factor out session state reset --- lib/MongoDB/MongoClient.pm | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/MongoDB/MongoClient.pm b/lib/MongoDB/MongoClient.pm index 393eb2ed..9b273bbd 100644 --- a/lib/MongoDB/MongoClient.pm +++ b/lib/MongoDB/MongoClient.pm @@ -1527,15 +1527,20 @@ sub send_admin_command { return $self->send_read_op( $op ); } +# Reset session state if we're outside an active transaction +sub _maybe_reset_session_state { + my ( $self, $op ) = @_; + if ( defined $op->session && ! $op->session->_active_transaction ) { + $op->session->_set__transaction_state( 'none' ); + } +} + # op dispatcher written in highly optimized style sub send_direct_op { my ( $self, $op, $address ) = @_; my ( $link, $result ); - # Reset session state if we're outside an active transaction - if ( defined $op->session && ! $op->session->_active_transaction ) { - $op->session->_set__transaction_state( 'none' ); - } + $self->_maybe_reset_session_state( $op ); ( $link = $self->{_topology}->get_specific_link($address) ), ( eval { ($result) = $op->execute($link); 1 } or do { @@ -1559,10 +1564,7 @@ sub send_write_op { my ( $self, $op ) = @_; my ( $link, $result ); - # Reset session state if we're outside an active transaction - if ( defined $op->session && ! $op->session->_active_transaction ) { - $op->session->_set__transaction_state( 'none' ); - } + $self->_maybe_reset_session_state( $op ); ( $link = $self->{_topology}->get_writable_link ), ( eval { ($result) = $self->_try_write_op_for_link( $link, $op ); 1 } or do { @@ -1587,10 +1589,7 @@ sub send_retryable_write_op { my $result; my $link = $self->{_topology}->get_writable_link; - # Reset session state if we're outside an active transaction - if ( defined $op->session && ! $op->session->_active_transaction ) { - $op->session->_set__transaction_state( 'none' ); - } + $self->_maybe_reset_session_state( $op ); # Need to force to do a retryable write on a Transaction Commit or Abort. $force is an override for retry_writes, but theres no point trying that if the link doesnt support it anyway. # This triggers on the following: @@ -1686,9 +1685,8 @@ sub send_read_op { # check the read preference from the transaction settings as per # transaction spec - see MongoDB::_TransactionOptions $op->read_preference( $op->session->_get_transaction_read_preference ); - } elsif ( defined $op->session ) { - # Not in an active transaction, so reset state - $op->session->_set__transaction_state( 'none' ); + } else { + $self->_maybe_reset_session_state( $op ); } ( $link = $self->{_topology}->get_readable_link( $op->read_preference ) ), From 6f74c71cecac1767f481c18a17b0956952971d62 Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Thu, 21 Jun 2018 15:40:18 +0100 Subject: [PATCH 26/38] PERL-875 Comment on why needing rw on read_preference --- lib/MongoDB/Op/_Command.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/MongoDB/Op/_Command.pm b/lib/MongoDB/Op/_Command.pm index 3579e224..05780c7a 100644 --- a/lib/MongoDB/Op/_Command.pm +++ b/lib/MongoDB/Op/_Command.pm @@ -52,6 +52,7 @@ has query_flags => ( ); has read_preference => ( + # Needs to be rw for transactions is => 'rw', isa => Maybe [ReadPreference], ); From 87f1e966df8e89882b82227ecf2afdb44e9aef5e Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Thu, 21 Jun 2018 16:04:10 +0100 Subject: [PATCH 27/38] PERL-875 resync spec tests - error-labels test still requires PERL-918 --- t/data/transactions/README.rst | 19 +- t/data/transactions/commit.yml | 16 +- t/data/transactions/error-labels.json | 592 +++++++++++++++++++++++++- t/data/transactions/error-labels.yml | 354 ++++++++++++++- t/data/transactions/reads.json | 6 +- t/data/transactions/reads.yml | 1 + t/data/transactions/run-command.yml | 2 +- t/transactions-spec.t | 1 + 8 files changed, 965 insertions(+), 26 deletions(-) diff --git a/t/data/transactions/README.rst b/t/data/transactions/README.rst index 9b1273b9..838fb588 100644 --- a/t/data/transactions/README.rst +++ b/t/data/transactions/README.rst @@ -108,9 +108,11 @@ Each YAML file has the following keys: - ``arguments``: Optional, the names and values of arguments. - - ``result``: The return value from the operation, if any. If the - operation is expected to return an error, the ``result`` has one or more - of the following fields: + - ``result``: The return value from the operation, if any. This field may + be a single document or an array of documents in the case of a + multi-document read. If the operation is expected to return an error, the + ``result`` is a single document that has one or more of the following + fields: - ``errorContains``: A substring of the expected error message. @@ -138,9 +140,9 @@ Use as integration tests ======================== Run a MongoDB replica set with a primary, a secondary, and an arbiter, -server version 4.0 or later. (Including a secondary ensures that server -selection in a transaction works properly. Including an arbiter helps ensure -that no new bugs have been introduced related to arbiters.) +**server version 4.0.0-rc4 or later**. (Including a secondary ensures that +server selection in a transaction works properly. Including an arbiter helps +ensure that no new bugs have been introduced related to arbiters.) Load each YAML (or JSON) file using a Canonical Extended JSON parser. @@ -230,11 +232,6 @@ Then for each element in ``tests``: Primary read preference even when the MongoClient is configured with another read preference. -TODO: - -- drivers MUST retry commit/abort, needs to use failpoint. -- test writeConcernErrors - Command-Started Events `````````````````````` diff --git a/t/data/transactions/commit.yml b/t/data/transactions/commit.yml index baa5db86..32f73099 100644 --- a/t/data/transactions/commit.yml +++ b/t/data/transactions/commit.yml @@ -119,7 +119,7 @@ tests: expectations: - command_started_event: command: - insert: test + insert: *collection_name documents: - _id: 1 ordered: true @@ -131,7 +131,7 @@ tests: autocommit: false writeConcern: command_name: insert - database_name: transaction-tests + database_name: *database_name - command_started_event: command: commitTransaction: 1 @@ -314,7 +314,7 @@ tests: expectations: - command_started_event: command: - insert: test + insert: *collection_name documents: - _id: 1 ordered: true @@ -326,7 +326,7 @@ tests: autocommit: false writeConcern: command_name: insert - database_name: transaction-tests + database_name: *database_name - command_started_event: command: abortTransaction: 1 @@ -379,7 +379,7 @@ tests: expectations: - command_started_event: command: - insert: test + insert: *collection_name documents: - _id: 1 ordered: true @@ -391,7 +391,7 @@ tests: autocommit: false writeConcern: command_name: insert - database_name: transaction-tests + database_name: *database_name - command_started_event: command: abortTransaction: 1 @@ -405,7 +405,7 @@ tests: database_name: admin - command_started_event: command: - insert: test + insert: *collection_name documents: - _id: 1 ordered: true @@ -419,7 +419,7 @@ tests: autocommit: false writeConcern: command_name: insert - database_name: transaction-tests + database_name: *database_name - command_started_event: command: abortTransaction: 1 diff --git a/t/data/transactions/error-labels.json b/t/data/transactions/error-labels.json index 2762767c..edb65e74 100644 --- a/t/data/transactions/error-labels.json +++ b/t/data/transactions/error-labels.json @@ -451,11 +451,14 @@ "failPoint": { "configureFailPoint": "failCommand", "mode": { - "times": 1 + "times": 4 }, "data": { "failCommands": [ - "insert" + "insert", + "find", + "aggregate", + "distinct" ], "closeConnection": true } @@ -483,6 +486,59 @@ ] } }, + { + "name": "find", + "object": "collection", + "arguments": { + "session": "session0" + }, + "result": { + "errorLabelsContain": [ + "TransientTransactionError" + ], + "errorLabelsOmit": [ + "UnknownTransactionCommitResult" + ] + } + }, + { + "name": "aggregate", + "object": "collection", + "arguments": { + "pipeline": [ + { + "$project": { + "_id": 1 + } + } + ], + "session": "session0" + }, + "result": { + "errorLabelsContain": [ + "TransientTransactionError" + ], + "errorLabelsOmit": [ + "UnknownTransactionCommitResult" + ] + } + }, + { + "name": "distinct", + "object": "collection", + "arguments": { + "fieldName": "_id", + "session": "session0" + }, + "result": { + "errorLabelsContain": [ + "TransientTransactionError" + ], + "errorLabelsOmit": [ + "UnknownTransactionCommitResult" + ] + } + }, { "name": "abortTransaction", "object": "session0" @@ -512,6 +568,62 @@ "database_name": "transaction-tests" } }, + { + "command_started_event": { + "command": { + "find": "test", + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false + }, + "command_name": "find", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "aggregate": "test", + "pipeline": [ + { + "$project": { + "_id": 1 + } + } + ], + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false + }, + "command_name": "aggregate", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "distinct": "test", + "key": "_id", + "lsid": "session0", + "readConcern": null, + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false + }, + "command_name": "distinct", + "database_name": "transaction-tests" + } + }, { "command_started_event": { "command": { @@ -946,6 +1058,482 @@ ] } } + }, + { + "description": "add unknown commit label to writeConcernError WriteConcernFailed", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "writeConcernError": { + "code": 64, + "errmsg": "multiple errors reported" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorLabelsContain": [ + "UnknownTransactionCommitResult" + ], + "errorLabelsOmit": [ + "TransientTransactionError" + ] + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "add unknown commit label to writeConcernError WriteConcernFailed with wtimeout", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "writeConcernError": { + "code": 64, + "codeName": "WriteConcernFailed", + "errmsg": "waiting for replication timed out", + "errInfo": { + "wtimeout": true + } + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorLabelsContain": [ + "UnknownTransactionCommitResult" + ], + "errorLabelsOmit": [ + "TransientTransactionError" + ] + } + }, + { + "name": "commitTransaction", + "object": "session0" + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "omit unknown commit label to writeConcernError UnsatisfiableWriteConcern", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "writeConcernError": { + "code": 100, + "errmsg": "Not enough data-bearing nodes" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorLabelsOmit": [ + "TransientTransactionError", + "UnknownTransactionCommitResult" + ] + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } + }, + { + "description": "omit unknown commit label to writeConcernError UnknownReplWriteConcern", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "commitTransaction" + ], + "writeConcernError": { + "code": 79, + "errmsg": "No write concern mode named 'blah' found in replica set configuration" + } + } + }, + "operations": [ + { + "name": "startTransaction", + "object": "session0", + "arguments": { + "options": { + "writeConcern": { + "w": "majority" + } + } + } + }, + { + "name": "insertOne", + "object": "collection", + "arguments": { + "session": "session0", + "document": { + "_id": 1 + } + }, + "result": { + "insertedId": 1 + } + }, + { + "name": "commitTransaction", + "object": "session0", + "result": { + "errorLabelsOmit": [ + "TransientTransactionError", + "UnknownTransactionCommitResult" + ] + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ], + "ordered": true, + "readConcern": null, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": null + }, + "command_name": "insert", + "database_name": "transaction-tests" + } + }, + { + "command_started_event": { + "command": { + "commitTransaction": 1, + "lsid": "session0", + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": null, + "autocommit": false, + "writeConcern": { + "w": "majority" + } + }, + "command_name": "commitTransaction", + "database_name": "admin" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1 + } + ] + } + } } ] } diff --git a/t/data/transactions/error-labels.yml b/t/data/transactions/error-labels.yml index 7d9500b5..d40e93ca 100644 --- a/t/data/transactions/error-labels.yml +++ b/t/data/transactions/error-labels.yml @@ -289,9 +289,9 @@ tests: failPoint: configureFailPoint: failCommand - mode: { times: 1 } + mode: { times: 4 } data: - failCommands: ["insert"] + failCommands: ["insert", "find", "aggregate", "distinct"] closeConnection: true operations: @@ -303,9 +303,28 @@ tests: session: session0 document: _id: 1 - result: + result: &transient_label_only errorLabelsContain: ["TransientTransactionError"] errorLabelsOmit: ["UnknownTransactionCommitResult"] + - name: find + object: collection + arguments: + session: session0 + result: *transient_label_only + - name: aggregate + object: collection + arguments: + pipeline: + - $project: + _id: 1 + session: session0 + result: *transient_label_only + - name: distinct + object: collection + arguments: + fieldName: _id + session: session0 + result: *transient_label_only - name: abortTransaction object: session0 @@ -325,6 +344,43 @@ tests: writeConcern: command_name: insert database_name: *database_name + - command_started_event: + command: + find: *collection_name + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + command_name: find + database_name: *database_name + - command_started_event: + command: + aggregate: *collection_name + pipeline: + - $project: + _id: 1 + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + command_name: aggregate + database_name: *database_name + - command_started_event: + command: + distinct: *collection_name + key: _id + lsid: session0 + readConcern: + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + command_name: distinct + database_name: *database_name - command_started_event: command: abortTransaction: 1 @@ -598,3 +654,295 @@ tests: collection: data: - _id: 1 + + - description: add unknown commit label to writeConcernError WriteConcernFailed + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + writeConcernError: + code: 64 # WriteConcernFailed without wtimeout + errmsg: multiple errors reported + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + result: + errorLabelsContain: ["UnknownTransactionCommitResult"] + errorLabelsOmit: ["TransientTransactionError"] + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: add unknown commit label to writeConcernError WriteConcernFailed with wtimeout + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + writeConcernError: + code: 64 + codeName: WriteConcernFailed + errmsg: waiting for replication timed out + errInfo: {wtimeout: True} + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + result: + errorLabelsContain: ["UnknownTransactionCommitResult"] + errorLabelsOmit: ["TransientTransactionError"] + - name: commitTransaction + object: session0 + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: omit unknown commit label to writeConcernError UnsatisfiableWriteConcern + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + writeConcernError: + code: 100 # UnsatisfiableWriteConcern/CannotSatisfyWriteConcern + errmsg: Not enough data-bearing nodes + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + result: + errorLabelsOmit: ["TransientTransactionError", "UnknownTransactionCommitResult"] + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 + + - description: omit unknown commit label to writeConcernError UnknownReplWriteConcern + + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["commitTransaction"] + writeConcernError: + code: 79 # UnknownReplWriteConcern + errmsg: No write concern mode named 'blah' found in replica set configuration + + operations: + - name: startTransaction + object: session0 + arguments: + options: + writeConcern: + w: majority + - name: insertOne + object: collection + arguments: + session: session0 + document: + _id: 1 + result: + insertedId: 1 + - name: commitTransaction + object: session0 + result: + errorLabelsOmit: ["TransientTransactionError", "UnknownTransactionCommitResult"] + + expectations: + - command_started_event: + command: + insert: *collection_name + documents: + - _id: 1 + ordered: true + readConcern: + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: true + autocommit: false + writeConcern: + command_name: insert + database_name: *database_name + - command_started_event: + command: + commitTransaction: 1 + lsid: session0 + txnNumber: + $numberLong: "1" + startTransaction: + autocommit: false + writeConcern: + w: majority + command_name: commitTransaction + database_name: admin + + outcome: + collection: + data: + - _id: 1 diff --git a/t/data/transactions/reads.json b/t/data/transactions/reads.json index 656150ec..0b7cf836 100644 --- a/t/data/transactions/reads.json +++ b/t/data/transactions/reads.json @@ -100,7 +100,11 @@ } }, "result": { - "errorContains": "Cannot run 'count' in a multi-document transaction" + "errorContains": "Cannot run 'count' in a multi-document transaction", + "errorLabelsOmit": [ + "TransientTransactionError", + "UnknownTransactionCommitResult" + ] } }, { diff --git a/t/data/transactions/reads.yml b/t/data/transactions/reads.yml index c7822f06..4ebc4dfa 100644 --- a/t/data/transactions/reads.yml +++ b/t/data/transactions/reads.yml @@ -52,6 +52,7 @@ tests: _id: 1 result: errorContains: "Cannot run 'count' in a multi-document transaction" + errorLabelsOmit: ["TransientTransactionError", "UnknownTransactionCommitResult"] - name: abortTransaction object: session0 diff --git a/t/data/transactions/run-command.yml b/t/data/transactions/run-command.yml index e8444990..16a633a7 100644 --- a/t/data/transactions/run-command.yml +++ b/t/data/transactions/run-command.yml @@ -176,7 +176,7 @@ tests: arguments: options: readPreference: - mode: secondary + mode: Secondary - name: runCommand object: database command_name: find diff --git a/t/transactions-spec.t b/t/transactions-spec.t index 9b687600..b0ad548a 100644 --- a/t/transactions-spec.t +++ b/t/transactions-spec.t @@ -107,6 +107,7 @@ while ( my $path = $iterator->() ) { local $TODO = 'does a run_command read_preference count as a user configurable read_preference?' if $path =~ /run-command/ && $description =~ /explicit secondary read preference/; # TODO requires PERL-918 local $TODO = 'requires PERL-918' if $path =~ /error-labels/ && $description =~ /add unknown commit label/; + local $TODO = 'requires PERL-918' if $path =~ /error-labels/ && $description =~ /omit unknown commit label/; local $TODO = 'requires PERL-918' if $path =~ /retryable-abort/ && $description =~ /abortTransaction succeeds after (NotMaster|Interrupted|Primary|Shutdown|Host|Socket|Network|WriteConcernError)/; local $TODO = 'requires PERL-918' if $path =~ /retryable-commit/ && $description =~ /commitTransaction succeeds after (NotMaster|Interrupted|Primary|Shutdown|Host|Socket|Network|WriteConcernError)/; subtest $description => sub { From bd12150587d46b193ae2cf075b6cf118d916fe4e Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Thu, 21 Jun 2018 18:11:58 +0100 Subject: [PATCH 28/38] PERL-875 Update transaction-spec test, removing 918 todos --- lib/MongoDB/ClientSession.pm | 31 ++++++++++++++++------- lib/MongoDB/Error.pm | 1 + t/lib/MongoDBTest.pm | 8 ++++++ t/transactions-spec.t | 48 ++++++++++++------------------------ 4 files changed, 47 insertions(+), 41 deletions(-) diff --git a/lib/MongoDB/ClientSession.pm b/lib/MongoDB/ClientSession.pm index 695e0630..8c44228a 100644 --- a/lib/MongoDB/ClientSession.pm +++ b/lib/MongoDB/ClientSession.pm @@ -349,22 +349,35 @@ sub commit_transaction { if ( my $err = $@ ) { # catch and re-throw after retryable errors # TODO maybe need better checking logic that theres actually an error code in output? + my $err_code_name; my $err_code; if ( $err->can('result') ) { if ( $err->result->can('output') ) { - $err_code = $err->result->output->{codeName}; + $err_code_name = $err->result->output->{codeName}; + $err_code = $err->result->output->{code}; + $err_code_name ||= $err->result->output->{writeConcernError} + ? $err->result->output->{writeConcernError}->{codeName} + : ''; # Empty string just in case $err_code ||= $err->result->output->{writeConcernError} - ? $err->result->output->{writeConcernError}->{codeName} - : ''; # Empty string just in case + ? $err->result->output->{writeConcernError}->{code} + : 0; # just in case } } # If its a write concern error, retrying a commit would still error - unless ( defined( $err_code ) && grep { $_ eq $err_code } qw/ - CannotSatisfyWriteConcern - UnsatisfiableWriteConcern - UnknownReplWriteConcern - NoSuchTransaction - / ) { + unless ( + ( defined( $err_code_name ) && grep { $_ eq $err_code_name } qw/ + CannotSatisfyWriteConcern + UnsatisfiableWriteConcern + UnknownReplWriteConcern + NoSuchTransaction + / ) + # Spec tests include code numbers only with no codeName + || ( defined ( $err_code ) && grep { $_ == $err_code } + 100, # UnsatisfiableWriteConcern/CannotSatisfyWriteConcern + 79, # UnknownReplWriteConcern + 251, # NoSuchTransaction + ) + ) { push @{ $err->error_labels }, 'UnknownTransactionCommitResult'; } die $err; diff --git a/lib/MongoDB/Error.pm b/lib/MongoDB/Error.pm index 38cd2995..026bd2a8 100644 --- a/lib/MongoDB/Error.pm +++ b/lib/MongoDB/Error.pm @@ -168,6 +168,7 @@ sub _check_is_retryable_code { sub _check_is_retryable_message { my $message = $_[-1]; + return 0 unless defined $message; return 1 if $message =~ /(not master|node is recovering)/i; return 0; } diff --git a/t/lib/MongoDBTest.pm b/t/lib/MongoDBTest.pm index 83ca55a4..7e9d03c2 100644 --- a/t/lib/MongoDBTest.pm +++ b/t/lib/MongoDBTest.pm @@ -36,6 +36,7 @@ our @EXPORT_OK = qw( skip_unless_mongod skip_unless_failpoints_available skip_unless_sessions + skip_unless_transactions to_snake_case remap_hashref_to_snake_case uri_escape @@ -211,6 +212,13 @@ sub skip_unless_sessions { unless $conn->_topology->_supports_sessions; } +sub skip_unless_transactions { + my $conn = build_client; + + plan skip_all => "Transaction support not available" + unless $conn->_topology->_supports_transactions; +} + sub server_version { my $conn = shift; diff --git a/t/transactions-spec.t b/t/transactions-spec.t index b0ad548a..fdffaba7 100644 --- a/t/transactions-spec.t +++ b/t/transactions-spec.t @@ -44,20 +44,19 @@ use MongoDBTest qw/ get_unique_collection skip_unless_mongod skip_unless_failpoints_available + skip_unless_transactions /; -# TODO Keep not getting hosts???? skip_unless_mongod(); -# TODO skip_unless_failpoints_available(); +skip_unless_mongod(); +skip_unless_failpoints_available(); +skip_unless_transactions(); my @events; -# TODO strip all Dwarns -#use Devel::Dwarn; - sub clear_events { @events = () } sub event_count { scalar @events } # Must use dclone, as was causing action at a distance for binc on txn number -sub event_cb { push @events, dclone $_[0] }#; Dwarn $_[0] } +sub event_cb { push @events, dclone $_[0] } my $conn = build_client(); my $server_version = server_version($conn); @@ -90,9 +89,9 @@ my %method_args = ( ); my $dir = path("t/data/transactions"); -my $iterator = $dir->iterator; my $index = 0; # TBSLIVER +my $iterator = $dir->iterator; while ( my $path = $iterator->() ) { - next unless $path =~ /\.json$/; #next unless ++$index == 19; # TBSLIVER run specific file + next unless $path =~ /\.json$/; my $plan = eval { decode_json( $path->slurp_utf8 ) }; if ($@) { die "Error decoding $path: $@"; @@ -102,14 +101,9 @@ while ( my $path = $iterator->() ) { subtest $path => sub { - for my $test ( @{ $plan->{tests} } ) { # TBSLIVER run specific subtest + for my $test ( @{ $plan->{tests} } ) { my $description = $test->{description}; local $TODO = 'does a run_command read_preference count as a user configurable read_preference?' if $path =~ /run-command/ && $description =~ /explicit secondary read preference/; - # TODO requires PERL-918 - local $TODO = 'requires PERL-918' if $path =~ /error-labels/ && $description =~ /add unknown commit label/; - local $TODO = 'requires PERL-918' if $path =~ /error-labels/ && $description =~ /omit unknown commit label/; - local $TODO = 'requires PERL-918' if $path =~ /retryable-abort/ && $description =~ /abortTransaction succeeds after (NotMaster|Interrupted|Primary|Shutdown|Host|Socket|Network|WriteConcernError)/; - local $TODO = 'requires PERL-918' if $path =~ /retryable-commit/ && $description =~ /commitTransaction succeeds after (NotMaster|Interrupted|Primary|Shutdown|Host|Socket|Network|WriteConcernError)/; subtest $description => sub { my $client = build_client(); @@ -119,21 +113,17 @@ while ( my $path = $iterator->() ) { # We crank wtimeout up to 10 seconds to help reduce # replication timeouts in testing - $test_db->get_collection( + my $test_coll = $test_db->get_collection( $test_coll_name, { write_concern => { w => 'majority', wtimeout => 10000 } } - )->drop; + ); + $test_coll->drop; # Drop first to make sure its clear for the next test. # MongoDB::Collection doesnt have a ->create option so done as # a seperate step. $test_db->run_command([ create => $test_coll_name ]); - my $test_coll = $test_db->get_collection( - $test_coll_name, - { write_concern => { w => 'majority', wtimeout => 10000 } } - ); - if ( scalar @{ $plan->{data} } > 0 ) { $test_coll->insert_many( $plan->{data} ); } @@ -142,7 +132,10 @@ while ( my $path = $iterator->() ) { run_test( $test_db_name, $test_coll_name, $test ); clear_failpoint( $client, $test->{failPoint} ); - # TODO Check outcome data + if ( defined $test->{outcome}{collection}{data} ) { + my @outcome = $test_coll->find()->all; + cmp_deeply( \@outcome, $test->{outcome}{collection}{data}, 'outcome as expected' ) + } }; } }; @@ -225,8 +218,6 @@ sub run_test { $sessions{ collection } = $sessions{ database }->get_collection( $test_coll_name, $collection_options ); my $cmd = to_snake_case( $operation->{name} ); - # diag $cmd; - #Dwarn $operation; if ( $cmd =~ /_transaction$/ ) { my $op_args = $operation->{arguments} // {}; $sessions{ $operation->{object} }->$cmd( $op_args->{options} ); @@ -272,7 +263,6 @@ sub run_test { $sessions{session0}->end_session; $sessions{session1}->end_session; - #Dwarn \@events; if ( defined $test->{expectations} ) { check_event_expectations( _adjust_types( $test->{expectations} ) ); } @@ -297,7 +287,7 @@ sub check_error { } return; } - #Dwarn $err; + my $err_contains = $exp->{errorContains}; my $err_code_name = $exp->{errorCodeName}; my $err_labels_contains = $exp->{errorLabelsContain}; @@ -330,8 +320,6 @@ sub check_error { sub check_result_outcome { my ( $got, $exp ) = @_; - #DwarnN $got; - #DwarnN $exp; if ( ref( $exp ) eq 'ARRAY' ) { check_array_result_outcome( $got, $exp ); } else { @@ -470,7 +458,6 @@ sub check_event_expectations { # also ignoring ismaster commands caused by re-negotiation after network error my @got = grep { $_->{type} eq 'command_started' && $_->{commandName} ne 'ismaster' } @events; - #Dwarn \@events; for my $exp ( @$expected ) { my ($exp_type, $exp_spec) = %$exp; # We only have command_started_event checks @@ -634,9 +621,6 @@ sub check_command_field { $exp_command->{txnNumber} = Math::BigInt->new($exp_command->{txnNumber}); } - #DwarnN $exp_command; - #DwarnN $event_command; - for my $exp_key (sort keys %$exp_command) { my $event_value = $event_command->{$exp_key}; my $exp_value = prepare_data_spec($exp_command->{$exp_key}); From cdb30666fd1bbfe970cae1eaf088eb9b6722f59c Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Thu, 21 Jun 2018 18:38:12 +0100 Subject: [PATCH 29/38] PERL-875 Move session state change to MongoClient --- lib/MongoDB/MongoClient.pm | 17 ++++++++++------- lib/MongoDB/Role/_SessionSupport.pm | 1 - t/testrules.yml | 1 + 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/MongoDB/MongoClient.pm b/lib/MongoDB/MongoClient.pm index 9b273bbd..5e1fb8c5 100644 --- a/lib/MongoDB/MongoClient.pm +++ b/lib/MongoDB/MongoClient.pm @@ -1527,11 +1527,14 @@ sub send_admin_command { return $self->send_read_op( $op ); } -# Reset session state if we're outside an active transaction -sub _maybe_reset_session_state { +# Reset session state if we're outside an active transaction, otherwise set +# that this transaction actually has operations +sub _maybe_update_session_state { my ( $self, $op ) = @_; if ( defined $op->session && ! $op->session->_active_transaction ) { $op->session->_set__transaction_state( 'none' ); + } elsif ( defined $op->session ) { + $op->session->_set__has_transaction_operations( 1 ); } } @@ -1540,7 +1543,7 @@ sub send_direct_op { my ( $self, $op, $address ) = @_; my ( $link, $result ); - $self->_maybe_reset_session_state( $op ); + $self->_maybe_update_session_state( $op ); ( $link = $self->{_topology}->get_specific_link($address) ), ( eval { ($result) = $op->execute($link); 1 } or do { @@ -1564,7 +1567,7 @@ sub send_write_op { my ( $self, $op ) = @_; my ( $link, $result ); - $self->_maybe_reset_session_state( $op ); + $self->_maybe_update_session_state( $op ); ( $link = $self->{_topology}->get_writable_link ), ( eval { ($result) = $self->_try_write_op_for_link( $link, $op ); 1 } or do { @@ -1589,7 +1592,7 @@ sub send_retryable_write_op { my $result; my $link = $self->{_topology}->get_writable_link; - $self->_maybe_reset_session_state( $op ); + $self->_maybe_update_session_state( $op ); # Need to force to do a retryable write on a Transaction Commit or Abort. $force is an override for retry_writes, but theres no point trying that if the link doesnt support it anyway. # This triggers on the following: @@ -1685,10 +1688,10 @@ sub send_read_op { # check the read preference from the transaction settings as per # transaction spec - see MongoDB::_TransactionOptions $op->read_preference( $op->session->_get_transaction_read_preference ); - } else { - $self->_maybe_reset_session_state( $op ); } + $self->_maybe_update_session_state( $op ); + ( $link = $self->{_topology}->get_readable_link( $op->read_preference ) ), ( $type = $self->{_topology}->type ), ( eval { ($result) = $op->execute( $link, $type ); 1 } or do { diff --git a/lib/MongoDB/Role/_SessionSupport.pm b/lib/MongoDB/Role/_SessionSupport.pm index a686face..2834f2af 100644 --- a/lib/MongoDB/Role/_SessionSupport.pm +++ b/lib/MongoDB/Role/_SessionSupport.pm @@ -54,7 +54,6 @@ sub _apply_session_and_cluster_time { if ( $self->session->_in_transaction_state( 'starting' ) ) { ($$query_ref)->Push( 'startTransaction' => true ); - $self->session->_set__has_transaction_operations( 1 ); ($$query_ref)->Push( @{ $self->session->_get_transaction_read_concern->as_args( $self->session ) } ); } diff --git a/t/testrules.yml b/t/testrules.yml index 1864a79c..0df6bb62 100644 --- a/t/testrules.yml +++ b/t/testrules.yml @@ -7,4 +7,5 @@ seq: - seq: t/examples/changestream.t - seq: t/retryable-writes-spec.t - seq: t/retryable-writes-split-batch.t + - seq: t/transactions-spec.t - par: ** From 6024c111240baab760093ddcff4af98a67d40ceb Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Fri, 22 Jun 2018 11:56:12 +0100 Subject: [PATCH 30/38] PERL-918 Build client from clientOptions in test spec --- t/retryable-writes-spec.t | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/t/retryable-writes-spec.t b/t/retryable-writes-spec.t index 549c0e32..43235de4 100644 --- a/t/retryable-writes-spec.t +++ b/t/retryable-writes-spec.t @@ -208,7 +208,11 @@ while ( my $path = $iterator->() ) { } for my $test ( @{ $plan->{tests} } ) { - my $coll = get_unique_collection( $testdb, 'retry_write' ); + my $client_options = $test->{clientOptions}; + $client_options = remap_hashref_to_snake_case( $client_options ); + my $test_conn = build_client( %$client_options ); + my $test_db = get_test_db( $test_conn ); + my $coll = get_unique_collection( $test_db, 'retry_write' ); my $ret = $coll->insert_many( $plan->{data} ); my $description = $test->{description}; From 64283b7546c74e948b68335eb30acbaa1ebcd2d4 Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Fri, 22 Jun 2018 12:26:01 +0100 Subject: [PATCH 31/38] PERL-875 rearrange readConcern modification and state setting during a transaction --- lib/MongoDB/Role/_SessionSupport.pm | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/MongoDB/Role/_SessionSupport.pm b/lib/MongoDB/Role/_SessionSupport.pm index 2834f2af..289bce91 100644 --- a/lib/MongoDB/Role/_SessionSupport.pm +++ b/lib/MongoDB/Role/_SessionSupport.pm @@ -55,6 +55,9 @@ sub _apply_session_and_cluster_time { if ( $self->session->_in_transaction_state( 'starting' ) ) { ($$query_ref)->Push( 'startTransaction' => true ); ($$query_ref)->Push( @{ $self->session->_get_transaction_read_concern->as_args( $self->session ) } ); + } elsif ( ! $self->session->_in_transaction_state( 'none' ) ) { + # read concern only valid outside a transaction or when starting + ($$query_ref)->Delete( 'readConcern' ); } # write concern not allowed in transactions except when ending. We can @@ -64,17 +67,20 @@ sub _apply_session_and_cluster_time { ($$query_ref)->Delete( 'writeConcern' ); } - # read concern only valid outside a transaction or when starting - if ( ! $self->session->_in_transaction_state( qw/ none starting / ) ) { - ($$query_ref)->Delete( 'readConcern' ); - } - if ( $self->session->_in_transaction_state( qw/ aborted committed / ) && ! ($$query_ref)->EXISTS('writeConcern') ) { ($$query_ref)->Push( @{ $self->session->_get_transaction_write_concern->as_args() } ); } + # MUST be the last thing to touch the transaction state before sending, + # so the various starting specific query modifications can be applied + # The spec states that this should happen after the command even on error, + # so happening before the command is sent is still valid + if ( $self->session->_in_transaction_state( 'starting') ) { + $self->session->_set__transaction_state( 'in_progress' ); + } + $self->session->_server_session->update_last_use; my $cluster_time = $self->session->get_latest_cluster_time; @@ -113,10 +119,6 @@ sub _update_session_pre_assert { return unless defined $self->session; - if ( $self->session->_in_transaction_state( 'starting' ) ) { - $self->session->_set__transaction_state( 'in_progress' ); - } - my $operation_time = $self->__extract_from( $response, 'operationTime' ); $self->session->advance_operation_time( $operation_time ) if defined $operation_time; @@ -142,8 +144,6 @@ sub _update_session_connection_error { if ( $self->session->_in_transaction_state( qw/ starting in_progress / ) ) { push @{ $err->error_labels }, 'TransientTransactionError'; - # If already in_progress, no harm done - $self->session->_set__transaction_state( 'in_progress' ); } } From 8bf6d732585b044b5ff6b6e1bddc783fd1861fd2 Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Fri, 22 Jun 2018 12:31:59 +0100 Subject: [PATCH 32/38] PERL-875 supress count deprecation warning specifically for one test --- t/transactions-spec.t | 3 +++ 1 file changed, 3 insertions(+) diff --git a/t/transactions-spec.t b/t/transactions-spec.t index fdffaba7..fe6cdb7b 100644 --- a/t/transactions-spec.t +++ b/t/transactions-spec.t @@ -218,6 +218,9 @@ sub run_test { $sessions{ collection } = $sessions{ database }->get_collection( $test_coll_name, $collection_options ); my $cmd = to_snake_case( $operation->{name} ); + # TODO count is checked specifically for errors during a transaction so warning here is not useful - we cannot change to count_documents, which is actually allowed in transactions. + local $ENV{PERL_MONGO_NO_DEP_WARNINGS} = 1 if $cmd eq 'count'; + if ( $cmd =~ /_transaction$/ ) { my $op_args = $operation->{arguments} // {}; $sessions{ $operation->{object} }->$cmd( $op_args->{options} ); From 906c128d538080a63f528ac209061a2d57c19551 Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Fri, 22 Jun 2018 12:43:09 +0100 Subject: [PATCH 33/38] PERL-875 Move error label application into session code --- lib/MongoDB/ClientSession.pm | 10 ++++++++++ lib/MongoDB/Role/_SessionSupport.pm | 5 ++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/MongoDB/ClientSession.pm b/lib/MongoDB/ClientSession.pm index 8c44228a..e21f26e1 100644 --- a/lib/MongoDB/ClientSession.pm +++ b/lib/MongoDB/ClientSession.pm @@ -436,6 +436,16 @@ sub _send_end_transaction_command { $self->_set__active_transaction( 0 ); } +# For applying connection errors etc +sub _maybe_apply_error_labels { + my ( $self, $err ) = @_; + + if ( $self->_in_transaction_state( qw/ starting in_progress / ) ) { + push @{ $err->error_labels }, 'TransientTransactionError'; + } + return; +} + =method end_session $session->end_session; diff --git a/lib/MongoDB/Role/_SessionSupport.pm b/lib/MongoDB/Role/_SessionSupport.pm index 289bce91..ded8c8e7 100644 --- a/lib/MongoDB/Role/_SessionSupport.pm +++ b/lib/MongoDB/Role/_SessionSupport.pm @@ -142,9 +142,8 @@ sub _assert_session_errors { sub _update_session_connection_error { my ( $self, $err ) = @_; - if ( $self->session->_in_transaction_state( qw/ starting in_progress / ) ) { - push @{ $err->error_labels }, 'TransientTransactionError'; - } + return unless defined $self->session; + return $self->session->_maybe_apply_error_labels( $err ); } sub __extract_from { From 7c0fb285d5dd7867ef0a56af63312a8e2ba87ff7 Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Fri, 22 Jun 2018 12:52:37 +0100 Subject: [PATCH 34/38] PERL-875 Change option setting to direct assignment --- lib/MongoDB/ClientSession.pm | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/MongoDB/ClientSession.pm b/lib/MongoDB/ClientSession.pm index e21f26e1..4d1831ba 100644 --- a/lib/MongoDB/ClientSession.pm +++ b/lib/MongoDB/ClientSession.pm @@ -92,13 +92,13 @@ has options => ( coerce => sub { # Will cause the isa requirement to fire return unless defined( $_[0] ) && ref( $_[0] ) eq 'HASH'; - my $dto = $_[0]->{defaultTransactionOptions}; - $dto ||= {}; $_[0] = { - causalConsistency => 1, - %{ $_[0] }, - # applied after to not override the clone with the original - defaultTransactionOptions => $dto, + causalConsistency => defined $_[0]->{causalConsistency} + ? $_[0]->{causalConsistency} + : 1, + defaultTransactionOptions => { + %{ $_[0]->{defaultTransactionOptions} || {} } + }, }; }, ); From 23d9eeea75324547c7b5455251ac9207c9269c6f Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Fri, 22 Jun 2018 13:06:02 +0100 Subject: [PATCH 35/38] PERL-875 Change to using constants for transaction state tracking --- lib/MongoDB/ClientSession.pm | 23 ++++++++++++----------- lib/MongoDB/MongoClient.pm | 4 ++-- lib/MongoDB/Role/_SessionSupport.pm | 17 +++++++++-------- lib/MongoDB/_Constants.pm | 6 ++++++ lib/MongoDB/_Types.pm | 3 ++- 5 files changed, 31 insertions(+), 22 deletions(-) diff --git a/lib/MongoDB/ClientSession.pm b/lib/MongoDB/ClientSession.pm index 4d1831ba..82a81a6a 100644 --- a/lib/MongoDB/ClientSession.pm +++ b/lib/MongoDB/ClientSession.pm @@ -24,6 +24,7 @@ our $VERSION = 'v1.999.1'; use MongoDB::Error; use Moo; +use MongoDB::_Constants; use MongoDB::_Types qw( Document BSONTimestamp @@ -291,7 +292,7 @@ sub start_transaction { my ( $self, $opts ) = @_; MongoDB::UsageError->throw("Transaction already in progress") - if $self->_in_transaction_state( 'starting', 'in_progress' ); + if $self->_in_transaction_state( TXN_STARTING, TXN_IN_PROGRESS ); MongoDB::ConfigurationError->throw("Transactions are unsupported on this deployment") unless $self->client->_topology->_supports_transactions; @@ -305,7 +306,7 @@ sub start_transaction { $self->_set__current_transaction_options( $trans_opts ); - $self->_set__transaction_state('starting'); + $self->_set__transaction_state( TXN_STARTING ); $self->_increment_transaction_id; @@ -332,11 +333,11 @@ sub commit_transaction { my $self = shift; MongoDB::UsageError->throw("No transaction started") - if $self->_transaction_state eq 'none'; + if $self->_in_transaction_state( TXN_NONE ); # Error message tweaked to use our function names MongoDB::UsageError->throw("Cannot call commit_transaction after calling abort_transaction") - if $self->_transaction_state eq 'aborted'; + if $self->_in_transaction_state( TXN_ABORTED ); # Commit can be called multiple times - even if the transaction completes # correctly. Setting this here makes sure we dont increment transaction id @@ -344,7 +345,7 @@ sub commit_transaction { $self->_set__active_transaction( 1 ); eval { - $self->_send_end_transaction_command( 'committed', [ commitTransaction => 1 ] ); + $self->_send_end_transaction_command( TXN_COMMITTED, [ commitTransaction => 1 ] ); }; if ( my $err = $@ ) { # catch and re-throw after retryable errors @@ -396,17 +397,17 @@ sub abort_transaction { my $self = shift; MongoDB::UsageError->throw("No transaction started") - if $self->_in_transaction_state( 'none' ); + if $self->_in_transaction_state( TXN_NONE ); # Error message tweaked to use our function names MongoDB::UsageError->throw("Cannot call abort_transaction after calling commit_transaction") - if $self->_in_transaction_state( 'committed' ); + if $self->_in_transaction_state( TXN_COMMITTED ); # Error message tweaked to use our function names MongoDB::UsageError->throw("Cannot call abort_transaction twice") - if $self->_in_transaction_state( 'aborted' ); + if $self->_in_transaction_state( TXN_ABORTED ); - $self->_send_end_transaction_command( 'aborted', [ abortTransaction => 1 ] ); + $self->_send_end_transaction_command( TXN_ABORTED, [ abortTransaction => 1 ] ); return; } @@ -440,7 +441,7 @@ sub _send_end_transaction_command { sub _maybe_apply_error_labels { my ( $self, $err ) = @_; - if ( $self->_in_transaction_state( qw/ starting in_progress / ) ) { + if ( $self->_in_transaction_state( TXN_STARTING, TXN_IN_PROGRESS ) ) { push @{ $err->error_labels }, 'TransientTransactionError'; } return; @@ -458,7 +459,7 @@ recycling. Has no effect after calling for the first time. sub end_session { my ( $self ) = @_; - if ( $self->_transaction_state eq 'in_progress' ) { + if ( $self->_in_transaction_state ( TXN_IN_PROGRESS ) ) { # Ignore all errors eval { $self->abort_transaction }; } diff --git a/lib/MongoDB/MongoClient.pm b/lib/MongoDB/MongoClient.pm index 5e1fb8c5..2d815609 100644 --- a/lib/MongoDB/MongoClient.pm +++ b/lib/MongoDB/MongoClient.pm @@ -1532,7 +1532,7 @@ sub send_admin_command { sub _maybe_update_session_state { my ( $self, $op ) = @_; if ( defined $op->session && ! $op->session->_active_transaction ) { - $op->session->_set__transaction_state( 'none' ); + $op->session->_set__transaction_state( TXN_NONE ); } elsif ( defined $op->session ) { $op->session->_set__has_transaction_operations( 1 ); } @@ -1603,7 +1603,7 @@ sub send_retryable_write_op { unless ( $link->supports_retryWrites && ( $self->retry_writes || ( defined $force && $force eq 'force' ) ) && ( defined $op->session - && ! $op->session->_in_transaction_state( qw/ starting in_progress / ) + && ! $op->session->_in_transaction_state( TXN_STARTING, TXN_IN_PROGRESS ) ) ) { eval { ($result) = $self->_try_write_op_for_link( $link, $op ); 1 } or do { diff --git a/lib/MongoDB/Role/_SessionSupport.pm b/lib/MongoDB/Role/_SessionSupport.pm index ded8c8e7..284420f9 100644 --- a/lib/MongoDB/Role/_SessionSupport.pm +++ b/lib/MongoDB/Role/_SessionSupport.pm @@ -23,6 +23,7 @@ our $VERSION = 'v1.999.1'; use Moo::Role; use MongoDB::_Types -types, 'to_IxHash'; +use MongoDB::_Constants; use Safe::Isa; use boolean; use namespace::clean; @@ -44,18 +45,18 @@ sub _apply_session_and_cluster_time { $$query_ref = to_IxHash( $$query_ref ); ($$query_ref)->Push( 'lsid' => $self->session->session_id ); - if ( $self->retryable_write || ! $self->session->_in_transaction_state( 'none' ) ) { + if ( $self->retryable_write || ! $self->session->_in_transaction_state( TXN_NONE ) ) { ($$query_ref)->Push( 'txnNumber' => $self->session->_server_session->transaction_id ); } - if ( ! $self->session->_in_transaction_state( qw/ none / ) ) { + if ( ! $self->session->_in_transaction_state( TXN_NONE ) ) { ($$query_ref)->Push( 'autocommit' => false ); } - if ( $self->session->_in_transaction_state( 'starting' ) ) { + if ( $self->session->_in_transaction_state( TXN_STARTING ) ) { ($$query_ref)->Push( 'startTransaction' => true ); ($$query_ref)->Push( @{ $self->session->_get_transaction_read_concern->as_args( $self->session ) } ); - } elsif ( ! $self->session->_in_transaction_state( 'none' ) ) { + } elsif ( ! $self->session->_in_transaction_state( TXN_NONE ) ) { # read concern only valid outside a transaction or when starting ($$query_ref)->Delete( 'readConcern' ); } @@ -63,11 +64,11 @@ sub _apply_session_and_cluster_time { # write concern not allowed in transactions except when ending. We can # safely delete it here as you can only pass writeConcern through by # arguments to client of collection. - if ( $self->session->_in_transaction_state( qw/ starting in_progress / ) ) { + if ( $self->session->_in_transaction_state( TXN_STARTING, TXN_IN_PROGRESS ) ) { ($$query_ref)->Delete( 'writeConcern' ); } - if ( $self->session->_in_transaction_state( qw/ aborted committed / ) + if ( $self->session->_in_transaction_state( TXN_ABORTED, TXN_COMMITTED ) && ! ($$query_ref)->EXISTS('writeConcern') ) { ($$query_ref)->Push( @{ $self->session->_get_transaction_write_concern->as_args() } ); @@ -77,8 +78,8 @@ sub _apply_session_and_cluster_time { # so the various starting specific query modifications can be applied # The spec states that this should happen after the command even on error, # so happening before the command is sent is still valid - if ( $self->session->_in_transaction_state( 'starting') ) { - $self->session->_set__transaction_state( 'in_progress' ); + if ( $self->session->_in_transaction_state( TXN_STARTING ) ) { + $self->session->_set__transaction_state( TXN_IN_PROGRESS ); } $self->session->_server_session->update_last_use; diff --git a/lib/MongoDB/_Constants.pm b/lib/MongoDB/_Constants.pm index e875f600..15a39f61 100644 --- a/lib/MongoDB/_Constants.pm +++ b/lib/MongoDB/_Constants.pm @@ -49,6 +49,12 @@ BEGIN { P_INT32 => $] lt '5.010' ? 'l' : 'l<', SMALLEST_MAX_STALENESS_SEC => 90, WITH_ASSERTS => $ENV{PERL_MONGO_WITH_ASSERTS}, + # Transaction state tracking + TXN_NONE => 'none', + TXN_STARTING => 'starting', + TXN_IN_PROGRESS => 'in_progress', + TXN_COMMITTED => 'committed', + TXN_ABORTED => 'aborted', }; } diff --git a/lib/MongoDB/_Types.pm b/lib/MongoDB/_Types.pm index 87cb1d0b..d4b50478 100644 --- a/lib/MongoDB/_Types.pm +++ b/lib/MongoDB/_Types.pm @@ -91,6 +91,7 @@ use Types::Standard qw( use Scalar::Util qw/reftype/; use boolean 0.25; +use MongoDB::_Constants; require Tie::IxHash; #--------------------------------------------------------------------------# @@ -200,7 +201,7 @@ enum TopologyType, [qw/Single ReplicaSetNoPrimary ReplicaSetWithPrimary Sharded Direct Unknown/]; enum TransactionState, - [ qw/ none starting committed in_progress aborted / ]; + [ TXN_NONE, TXN_STARTING, TXN_IN_PROGRESS, TXN_COMMITTED, TXN_ABORTED ]; class_type WriteConcern, { class => 'MongoDB::WriteConcern' }; From edb44faea3f3cd9a3dc0ee7c3ffdb27f740d3d48 Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Fri, 22 Jun 2018 13:06:39 +0100 Subject: [PATCH 36/38] PERL-875 Fix typo in comment --- lib/MongoDB/_TransactionOptions.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/MongoDB/_TransactionOptions.pm b/lib/MongoDB/_TransactionOptions.pm index 857b18f9..b423036a 100644 --- a/lib/MongoDB/_TransactionOptions.pm +++ b/lib/MongoDB/_TransactionOptions.pm @@ -39,7 +39,7 @@ use Types::Standard qw( ); use namespace::clean -except => 'meta'; -# Options provided during strart transaction +# Options provided during start transaction has options => ( is => 'ro', required => 1, From d8bd31fd00692624c6bc2f3e0dd7389bacd8f129 Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Fri, 22 Jun 2018 13:31:29 +0100 Subject: [PATCH 37/38] PERL-875 Ignore all errors from abortTransaction network calls --- lib/MongoDB/ClientSession.pm | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/MongoDB/ClientSession.pm b/lib/MongoDB/ClientSession.pm index 82a81a6a..fcca3051 100644 --- a/lib/MongoDB/ClientSession.pm +++ b/lib/MongoDB/ClientSession.pm @@ -407,7 +407,10 @@ sub abort_transaction { MongoDB::UsageError->throw("Cannot call abort_transaction twice") if $self->_in_transaction_state( TXN_ABORTED ); - $self->_send_end_transaction_command( TXN_ABORTED, [ abortTransaction => 1 ] ); + eval { + $self->_send_end_transaction_command( TXN_ABORTED, [ abortTransaction => 1 ] ); + }; + # Ignore all errors thrown by abortTransaction return; } From 086d2e081a06a716168c67920537ceb9a9c4b7de Mon Sep 17 00:00:00 2001 From: Thomas Bloor Date: Fri, 22 Jun 2018 13:40:54 +0100 Subject: [PATCH 38/38] PERL-875 $_call_if_can requires Safe::Isa >= 1.000007 --- Makefile.PL | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.PL b/Makefile.PL index 9acaa058..159a2822 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -43,7 +43,7 @@ my %WriteMakefileArgs = ( "Moo" => 2, "Moo::Role" => 0, "Net::DNS" => 0, - "Safe::Isa" => 0, + "Safe::Isa" => '1.000007', "Scalar::Util" => 0, "Socket" => 0, "Sub::Quote" => 0,