summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--buildscripts/resmokeconfig/suites/multi_shard_multi_stmt_txn_jscore_passthrough.yml2
-rw-r--r--buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_jscore_passthrough.yml2
-rw-r--r--buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_kill_primary_jscore_passthrough.yml4
-rw-r--r--buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_stepdown_jscore_passthrough.yml4
-rw-r--r--buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_terminate_primary_jscore_passthrough.yml4
-rw-r--r--buildscripts/resmokeconfig/suites/sharded_multi_stmt_txn_jscore_passthrough.yml2
-rw-r--r--jstests/libs/txns/txn_override.js37
-rw-r--r--jstests/noPassthrough/txn_override_causal_consistency.js204
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();
+})();