diff options
8 files changed, 248 insertions, 11 deletions
diff --git a/buildscripts/resmokeconfig/suites/multi_shard_multi_stmt_txn_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/multi_shard_multi_stmt_txn_jscore_passthrough.yml index de171bd48ea..53c968a780e 100644 --- a/buildscripts/resmokeconfig/suites/multi_shard_multi_stmt_txn_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/multi_shard_multi_stmt_txn_jscore_passthrough.yml @@ -387,6 +387,8 @@ executor: global_vars: TestData: sessionOptions: + # Tests in this suite only read from primaries and only one node is electable, so causal + # consistency is not required to read your own writes. causalConsistency: false readMode: commands hooks: diff --git a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_jscore_passthrough.yml index 097852d5efa..91bb82a4fdd 100644 --- a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_jscore_passthrough.yml @@ -277,6 +277,8 @@ executor: global_vars: TestData: sessionOptions: + # Tests in this suite only read from primaries and only one node is electable, so causal + # consistency is not required to read your own writes. causalConsistency: false readMode: commands hooks: diff --git a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_kill_primary_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_kill_primary_jscore_passthrough.yml index e2f10b18645..8f41372074e 100644 --- a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_kill_primary_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_kill_primary_jscore_passthrough.yml @@ -372,7 +372,9 @@ executor: logRetryAttempts: true overrideRetryAttempts: 3 sessionOptions: - causalConsistency: false + # Read your own writes is not guaranteed without causal consistency if all nodes are + # electable. + causalConsistency: true retryWrites: true # We specify nodb so the shell used by each test will attempt to connect after loading the # retry logic in auto_retry_on_network_error.js. diff --git a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_stepdown_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_stepdown_jscore_passthrough.yml index 724d508b32a..5957c1e71ee 100644 --- a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_stepdown_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_stepdown_jscore_passthrough.yml @@ -349,7 +349,9 @@ executor: logRetryAttempts: true overrideRetryAttempts: 3 sessionOptions: - causalConsistency: false + # Read your own writes is not guaranteed without causal consistency if all nodes are + # electable. + causalConsistency: true retryWrites: true # We specify nodb so the shell used by each test will attempt to connect after loading the # retry logic in auto_retry_on_network_error.js. diff --git a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_terminate_primary_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_terminate_primary_jscore_passthrough.yml index 39c9e8f6d6d..4a1fe07196d 100644 --- a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_terminate_primary_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_terminate_primary_jscore_passthrough.yml @@ -363,7 +363,9 @@ executor: logRetryAttempts: true overrideRetryAttempts: 3 sessionOptions: - causalConsistency: false + # Read your own writes is not guaranteed without causal consistency if all nodes are + # electable. + causalConsistency: true retryWrites: true # We specify nodb so the shell used by each test will attempt to connect after loading the # retry logic in auto_retry_on_network_error.js. diff --git a/buildscripts/resmokeconfig/suites/sharded_multi_stmt_txn_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/sharded_multi_stmt_txn_jscore_passthrough.yml index bb1bf8cf8ff..17a57ddd4d1 100644 --- a/buildscripts/resmokeconfig/suites/sharded_multi_stmt_txn_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/sharded_multi_stmt_txn_jscore_passthrough.yml @@ -326,6 +326,8 @@ executor: global_vars: TestData: sessionOptions: + # Tests in this suite only read from primaries and only one node is electable, so causal + # consistency is not required to read your own writes. causalConsistency: false readMode: commands hooks: diff --git a/jstests/libs/txns/txn_override.js b/jstests/libs/txns/txn_override.js index 51c3931cc6f..920c359147a 100644 --- a/jstests/libs/txns/txn_override.js +++ b/jstests/libs/txns/txn_override.js @@ -158,12 +158,6 @@ shouldForceWriteConcern = false; } } else if (commandName === "aggregate") { - if (OverrideHelpers.isAggregationWithListLocalCursorsStage(commandName, commandObj)) { - // The $listLocalCursors stage can only be used with readConcern={level: - // "local"}. - shouldForceReadConcern = false; - } - if (OverrideHelpers.isAggregationWithListLocalSessionsStage(commandName, commandObj)) { // The $listLocalSessions stage can only be used with readConcern={level: // "local"}. @@ -197,17 +191,29 @@ readConcernLevel = "majority"; } - if (commandObj.readConcern && commandObj.readConcern.level !== readConcernLevel) { + if (commandObj.hasOwnProperty("readConcern") && + commandObj.readConcern.hasOwnProperty("level") && + commandObj.readConcern.level !== readConcernLevel) { throw new Error("refusing to override existing readConcern " + commandObj.readConcern.level + " with readConcern " + readConcernLevel); } else if (readConcernLevel) { commandObj.readConcern = {level: readConcernLevel}; + } + // Only attach afterClusterTime if causal consistency is explicitly enabled. Note, it is + // OK to send a readConcern with only afterClusterTime, which is interpreted as local + // read concern by the server. + if (TestData.hasOwnProperty("sessionOptions") && + TestData.sessionOptions.causalConsistency === true) { const driverSession = conn.getDB(dbName).getSession(); const operationTime = driverSession.getOperationTime(); if (operationTime !== undefined) { - commandObj.readConcern.afterClusterTime = operationTime; + if (commandObj.hasOwnProperty("readConcern")) { + commandObj.readConcern.afterClusterTime = operationTime; + } else { + commandObj.readConcern = {afterClusterTime: operationTime}; + } } } } @@ -346,6 +352,21 @@ // is false, this op is a write command that we are retrying thus this op has already // been added to the ops array. if (!TestData.retryingOnNetworkError && !retryOp) { + // If the command object was created in a causally consistent session but did not + // specify a readConcern level, it may have a readConcern object with only + // afterClusterTime. The correct read concern options are added in + // appendReadAndWriteConcern, so remove the readConcern before saving the operation in + // this case. + if (cmdObj.hasOwnProperty("readConcern")) { + // Only remove the readConcern if it only contains afterClusterTime. + const readConcernKeys = Object.keys(cmdObj.readConcern); + if (readConcernKeys.length !== 1 || readConcernKeys[0] !== "afterClusterTime") { + throw new Error("Refusing to remove existing readConcern from command: " + + tojson(cmdObj)); + } + delete cmdObj.readConcern; + } + ops.push({dbName, cmdName, cmdObj, makeFuncArgs}); } diff --git a/jstests/noPassthrough/txn_override_causal_consistency.js b/jstests/noPassthrough/txn_override_causal_consistency.js new file mode 100644 index 00000000000..78d3f6feed1 --- /dev/null +++ b/jstests/noPassthrough/txn_override_causal_consistency.js @@ -0,0 +1,204 @@ +/** + * Verifies the txn_override passthrough respects the causal consistency setting on TestData when + * starting a transaction. + * + * @tags: [requires_replication, uses_transactions] + */ +(function() { + "use strict"; + + const dbName = "test"; + const collName = "foo"; + + const rst = new ReplSetTest({nodes: 1}); + rst.startSet(); + rst.initiate(); + const conn = new Mongo(rst.getPrimary().host); + + // Create the collection so the override doesn't try to when it is not expected. + assert.commandWorked(conn.getDB(dbName).createCollection(collName)); + + // Override runCommand to add each command it sees to a global array that can be inspected by + // this test and to allow mocking certain responses. + let cmdObjsSeen = []; + let mockNetworkError, mockFirstResponse, mockFirstCommitResponse; + const mongoRunCommandOriginal = Mongo.prototype.runCommand; + Mongo.prototype.runCommand = function runCommandSpy(dbName, cmdObj, options) { + cmdObjsSeen.push(cmdObj); + + if (mockNetworkError) { + mockNetworkError = undefined; + throw new Error("network error"); + } + + if (mockFirstResponse) { + const mockedRes = mockFirstResponse; + mockFirstResponse = undefined; + return mockedRes; + } + + const cmdName = Object.keys(cmdObj)[0]; + if (cmdName === "commitTransaction" && mockFirstCommitResponse) { + const mockedRes = mockFirstCommitResponse; + mockFirstCommitResponse = undefined; + return mockedRes; + } + + return mongoRunCommandOriginal.apply(this, arguments); + }; + + // Runs the given function with a collection from a session made with the sessionOptions on + // TestData and asserts the seen commands that would start a transaction have or do not have + // afterClusterTime. + function inspectFirstCommandForAfterClusterTime(conn, cmdName, isCausal, expectRetry, func) { + const session = conn.startSession(TestData.sessionOptions); + const sessionDB = session.getDatabase(dbName); + const sessionColl = sessionDB[collName]; + + cmdObjsSeen = []; + func(sessionColl); + + // Find all requests sent with the expected command name, in case the scenario allows + // retrying more than once or expects to end with a commit. + let cmds = []; + if (!expectRetry) { + assert.eq(1, cmdObjsSeen.length); + cmds.push(cmdObjsSeen[0]); + } else { + assert.lt(1, cmdObjsSeen.length); + cmds = cmdObjsSeen.filter(obj => Object.keys(obj)[0] === cmdName); + } + + for (let cmd of cmds) { + if (isCausal) { + assert(cmd.hasOwnProperty("readConcern"), + "Expected " + tojson(cmd) + " to have a read concern."); + assert(cmd.readConcern.hasOwnProperty("afterClusterTime"), + "Expected " + tojson(cmd) + " to have an afterClusterTime."); + } else { + if (TestData.hasOwnProperty("enableMajorityReadConcern") && + TestData.enableMajorityReadConcern === false) { + // Commands not allowed in a transaction without causal consistency will not + // have a read concern on variants that don't enable majority read concern. + continue; + } + + assert(cmd.hasOwnProperty("readConcern"), + "Expected " + tojson(cmd) + " to have a read concern."); + assert(!cmd.readConcern.hasOwnProperty("afterClusterTime"), + "Expected " + tojson(cmd) + " to not have an afterClusterTime."); + } + } + + // Run a command not runnable in a transaction to reset the override's transaction state. + assert.commandWorked(sessionDB.runCommand({ping: 1})); + + session.endSession(); + } + + // Helper methods for testing specific commands. + + function testInsert(conn, isCausal, expectRetry) { + inspectFirstCommandForAfterClusterTime(conn, "insert", isCausal, expectRetry, (coll) => { + assert.writeOK(coll.insert({x: 1})); + }); + } + + function testFind(conn, isCausal, expectRetry) { + inspectFirstCommandForAfterClusterTime(conn, "find", isCausal, expectRetry, (coll) => { + assert.eq(0, coll.find({y: 1}).itcount()); + }); + } + + function testCount(conn, isCausal, expectRetry) { + inspectFirstCommandForAfterClusterTime(conn, "count", isCausal, expectRetry, (coll) => { + assert.eq(0, coll.count({y: 1})); + }); + } + + function testCommit(conn, isCausal, expectRetry) { + inspectFirstCommandForAfterClusterTime(conn, "count", isCausal, expectRetry, (coll) => { + assert.eq(0, coll.count({y: 1})); + assert.commandWorked(coll.getDB().runCommand({ping: 1})); // commits the transaction. + }); + } + + // Load the txn_override after creating the spy, so the spy will see commands after being + // transformed by the override. Also load auto_retry_on_network_error because several suites use + // both. + load("jstests/libs/txns/txn_override.js"); + load("jstests/libs/override_methods/auto_retry_on_network_error.js"); + + TestData.logRetryAttempts = true; + + // Run a command to guarantee operation time is initialized on the database's session. + assert.commandWorked(conn.getDB(dbName).runCommand({ping: 1})); + + function runTest() { + for (let isCausal of[false, true]) { + TestData.sessionOptions = {causalConsistency: isCausal}; + + // Commands that accept read and write concern allowed in a transaction. + testInsert(conn, isCausal, false /*expectRetry*/); + testFind(conn, isCausal, false /*expectRetry*/); + + // Command that can accept read concern not allowed in a transaction. + testCount(conn, isCausal, false /*expectRetry*/); + + // Command that attempts to implicitly create a collection. + conn.getDB(dbName)[collName].drop(); + testInsert(conn, isCausal, true /*expectRetry*/); + + // Command that can accept read concern with retryable error. + mockFirstResponse = {ok: 0, code: ErrorCodes.CursorKilled}; + testFind(conn, isCausal, true /*expectRetry*/); + + // Commands that can accept read and write concern with network error. + mockNetworkError = true; + testInsert(conn, isCausal, true /*expectRetry*/); + + mockNetworkError = true; + testFind(conn, isCausal, true /*expectRetry*/); + + // Command that can accept read concern not allowed in a transaction with network error. + mockNetworkError = true; + testCount(conn, isCausal, true /*expectRetry*/); + + // Commands that can accept read and write concern with transient transaction error. + mockFirstResponse = { + ok: 0, + code: ErrorCodes.NoSuchTransaction, + errorLabels: ["TransientTransactionError"] + }; + testFind(conn, isCausal, true /*expectRetry*/); + + mockFirstResponse = { + ok: 0, + code: ErrorCodes.NoSuchTransaction, + errorLabels: ["TransientTransactionError"] + }; + testInsert(conn, isCausal, true /*expectRetry*/); + + // Transient transaction error on commit attempt. + mockFirstCommitResponse = { + ok: 0, + code: ErrorCodes.NoSuchTransaction, + errorLabels: ["TransientTransactionError"] + }; + testCommit(conn, isCausal, true /*expectRetry*/); + + // Network error on commit attempt. + mockFirstCommitResponse = {ok: 0, code: ErrorCodes.NotMaster}; + testCommit(conn, isCausal, true /*expectRetry*/); + } + } + + runTest(); + + // With read concern majority disabled. + TestData.enableMajorityReadConcern = false; + runTest(); + delete TestData.enableMajorityReadConcern; + + rst.stopSet(); +})(); |