diff options
author | Max Hirschhorn <max.hirschhorn@mongodb.com> | 2018-01-30 19:45:42 -0500 |
---|---|---|
committer | Max Hirschhorn <max.hirschhorn@mongodb.com> | 2018-01-30 19:45:42 -0500 |
commit | 35b5b72146ca570b5c6fed8aaa7e891edf7d6a78 (patch) | |
tree | 982b7782084039d440c75911b51ac6568569faec /jstests/libs | |
parent | 784e55320f72ab9b9ec8b4f766d4be0c1b5e4a5b (diff) | |
download | mongo-35b5b72146ca570b5c6fed8aaa7e891edf7d6a78.tar.gz |
SERVER-32522 Clean up {read,write}Concern and readPreference overrides.
Introduces OverrideHelpers object with convenience methods for
inspecting certain aggregation and map-reduce commands, as well as
overriding startParallelShell(), Mongo.prototype.runCommand(), and
Mongo.prototype.runCommandWithMetadata().
Also removes a number of tests that were incorrectly blacklisted from
the read_concern_majority_passthrough.yml and
read_concern_linearizable_passthrough.yml test suites.
Diffstat (limited to 'jstests/libs')
7 files changed, 357 insertions, 261 deletions
diff --git a/jstests/libs/override_methods/auto_retry_on_network_error.js b/jstests/libs/override_methods/auto_retry_on_network_error.js index f7f2138da28..14aa83a0a7f 100644 --- a/jstests/libs/override_methods/auto_retry_on_network_error.js +++ b/jstests/libs/override_methods/auto_retry_on_network_error.js @@ -11,6 +11,7 @@ (function() { "use strict"; + load("jstests/libs/override_methods/override_helpers.js"); load("jstests/libs/retryable_writes_util.js"); const kMaxNumRetries = 3; @@ -350,20 +351,8 @@ } while (numRetries >= 0); } - const startParallelShellOriginal = startParallelShell; - - startParallelShell = function(jsCode, port, noConnect) { - let newCode; - const overridesFile = "jstests/libs/override_methods/auto_retry_on_network_error.js"; - if (typeof(jsCode) === "function") { - // Load the override file and immediately invoke the supplied function. - newCode = `load("${overridesFile}"); (${jsCode})();`; - } else { - newCode = `load("${overridesFile}"); ${jsCode};`; - } - - return startParallelShellOriginal(newCode, port, noConnect); - }; + OverrideHelpers.prependOverrideInParallelShell( + "jstests/libs/override_methods/auto_retry_on_network_error.js"); const connectOriginal = connect; diff --git a/jstests/libs/override_methods/enable_causal_consistency.js b/jstests/libs/override_methods/enable_causal_consistency.js index f932304df7a..9e8a4bdde59 100644 --- a/jstests/libs/override_methods/enable_causal_consistency.js +++ b/jstests/libs/override_methods/enable_causal_consistency.js @@ -4,20 +4,11 @@ (function() { "use strict"; + load("jstests/libs/override_methods/override_helpers.js"); + db.getMongo().setCausalConsistency(); db.getMongo().setReadPref("secondary"); - var originalStartParallelShell = startParallelShell; - startParallelShell = function(jsCode, port, noConnect) { - var newCode; - var overridesFile = "jstests/libs/override_methods/enable_causal_consistency.js"; - if (typeof(jsCode) === "function") { - // Load the override file and immediately invoke the supplied function. - newCode = `load("${overridesFile}"); (${jsCode})();`; - } else { - newCode = `load("${overridesFile}"); ${jsCode};`; - } - - return originalStartParallelShell(newCode, port, noConnect); - }; + OverrideHelpers.prependOverrideInParallelShell( + "jstests/libs/override_methods/enable_causal_consistency.js"); })(); diff --git a/jstests/libs/override_methods/enable_sessions.js b/jstests/libs/override_methods/enable_sessions.js index ebb78f703c3..2d304927c35 100644 --- a/jstests/libs/override_methods/enable_sessions.js +++ b/jstests/libs/override_methods/enable_sessions.js @@ -4,6 +4,8 @@ (function() { "use strict"; + load("jstests/libs/override_methods/override_helpers.js"); + var runCommandOriginal = Mongo.prototype.runCommand; var runCommandWithMetadataOriginal = Mongo.prototype.runCommandWithMetadata; var getDBOriginal = Mongo.prototype.getDB; @@ -18,19 +20,8 @@ db = driverSession.getDatabase(db.getName()); sessionMap.set(db.getMongo(), driverSession); - var originalStartParallelShell = startParallelShell; - startParallelShell = function(jsCode, port, noConnect) { - var newCode; - var overridesFile = "jstests/libs/override_methods/enable_sessions.js"; - if (typeof(jsCode) === "function") { - // Load the override file and immediately invoke the supplied function. - newCode = `load("${overridesFile}"); (${jsCode})();`; - } else { - newCode = `load("${overridesFile}"); ${jsCode};`; - } - - return originalStartParallelShell(newCode, port, noConnect); - }; + OverrideHelpers.prependOverrideInParallelShell( + "jstests/libs/override_methods/enable_sessions.js"); function startSession(conn) { const driverSession = conn.startSession(sessionOptions); diff --git a/jstests/libs/override_methods/override_helpers.js b/jstests/libs/override_methods/override_helpers.js new file mode 100644 index 00000000000..437f1a07669 --- /dev/null +++ b/jstests/libs/override_methods/override_helpers.js @@ -0,0 +1,109 @@ +/** + * The OverrideHelpers object defines convenience methods for overriding commands and functions in + * the mongo shell. + */ +var OverrideHelpers = (function() { + "use strict"; + + function makeIsAggregationWithFirstStage(stageName) { + return function(commandName, commandObj) { + if (commandName !== "aggregate" || typeof commandObj !== "object" || + commandObj === null) { + return false; + } + + if (!Array.isArray(commandObj.pipeline) || commandObj.pipeline.length === 0) { + return false; + } + + const firstStage = commandObj.pipeline[0]; + if (typeof firstStage !== "object" || firstStage === null) { + return false; + } + + return Object.keys(firstStage)[0] === stageName; + }; + } + + function isAggregationWithOutStage(commandName, commandObj) { + if (commandName !== "aggregate" || typeof commandObj !== "object" || commandObj === null) { + return false; + } + + if (!Array.isArray(commandObj.pipeline) || commandObj.pipeline.length === 0) { + return false; + } + + const lastStage = commandObj.pipeline[commandObj.pipeline.length - 1]; + if (typeof lastStage !== "object" || lastStage === null) { + return false; + } + + return Object.keys(lastStage)[0] === "$out"; + } + + function isMapReduceWithInlineOutput(commandName, commandObj) { + if ((commandName !== "mapReduce" && commandName !== "mapreduce") || + typeof commandObj !== "object" || commandObj === null) { + return false; + } + + if (typeof commandObj.out !== "object") { + return false; + } + + return commandObj.out.hasOwnProperty("inline"); + } + + function prependOverrideInParallelShell(overrideFile) { + const startParallelShellOriginal = startParallelShell; + + startParallelShell = function(jsCode, port, noConnect) { + let newCode; + if (typeof jsCode === "function") { + // Load the override file and immediately invoke the supplied function. + newCode = `load("${overrideFile}"); (${jsCode})();`; + } else { + newCode = `load("${overrideFile}"); ${jsCode};`; + } + + return startParallelShellOriginal(newCode, port, noConnect); + }; + } + + function overrideRunCommand(overrideFunc) { + const mongoRunCommandOriginal = Mongo.prototype.runCommand; + const mongoRunCommandWithMetadataOriginal = Mongo.prototype.runCommandWithMetadata; + + Mongo.prototype.runCommand = function(dbName, commandObj, options) { + const commandName = Object.keys(commandObj)[0]; + return overrideFunc(this, + dbName, + commandName, + commandObj, + mongoRunCommandOriginal, + (commandObj) => [dbName, commandObj, options]); + }; + + Mongo.prototype.runCommandWithMetadata = function(dbName, metadata, commandArgs) { + const commandName = Object.keys(commandArgs)[0]; + return overrideFunc(this, + dbName, + commandName, + commandArgs, + mongoRunCommandWithMetadataOriginal, + (commandArgs) => [dbName, metadata, commandArgs]); + }; + } + + return { + isAggregationWithListLocalCursorsStage: + makeIsAggregationWithFirstStage("$listLocalCursors"), + isAggregationWithListLocalSessionsStage: + makeIsAggregationWithFirstStage("$listLocalSessions"), + isAggregationWithOutStage: isAggregationWithOutStage, + isMapReduceWithInlineOutput: isMapReduceWithInlineOutput, + prependOverrideInParallelShell: prependOverrideInParallelShell, + overrideRunCommand: overrideRunCommand, + }; +})(); diff --git a/jstests/libs/override_methods/retry_writes_at_least_once.js b/jstests/libs/override_methods/retry_writes_at_least_once.js index f0562c958d1..5e211d88b49 100644 --- a/jstests/libs/override_methods/retry_writes_at_least_once.js +++ b/jstests/libs/override_methods/retry_writes_at_least_once.js @@ -6,6 +6,7 @@ (function() { "use strict"; + load("jstests/libs/override_methods/override_helpers.js"); load("jstests/libs/retryable_writes_util.js"); Random.setRandomSeed(); @@ -64,18 +65,6 @@ return res; } - const startParallelShellOriginal = startParallelShell; - - startParallelShell = function(jsCode, port, noConnect) { - let newCode; - const overridesFile = "jstests/libs/override_methods/retry_writes_at_least_once.js"; - if (typeof(jsCode) === "function") { - // Load the override file and immediately invoke the supplied function. - newCode = `load("${overridesFile}"); (${jsCode})();`; - } else { - newCode = `load("${overridesFile}"); ${jsCode};`; - } - - return startParallelShellOriginal(newCode, port, noConnect); - }; + OverrideHelpers.prependOverrideInParallelShell( + "jstests/libs/override_methods/retry_writes_at_least_once.js"); })(); diff --git a/jstests/libs/override_methods/set_read_and_write_concerns.js b/jstests/libs/override_methods/set_read_and_write_concerns.js index a14efa41a7f..00af2133916 100644 --- a/jstests/libs/override_methods/set_read_and_write_concerns.js +++ b/jstests/libs/override_methods/set_read_and_write_concerns.js @@ -1,201 +1,221 @@ /** - * Use prototype overrides to set read concern and write concern while running core tests. + * Use prototype overrides to set read concern and write concern while running tests. */ (function() { "use strict"; + load("jstests/libs/override_methods/override_helpers.js"); + if (typeof TestData === "undefined" || !TestData.hasOwnProperty("defaultReadConcernLevel")) { throw new Error( - "The default read-concern level must be set as the 'defaultReadConcernLevel' " + - "property on TestData"); - } - var defaultReadConcern = {level: TestData.defaultReadConcernLevel}; - - var defaultWriteConcern = { - w: "majority", - // Use a "signature" value that won't typically match a value assigned in normal use. - // This way the wtimeout set by this override is distinguishable in the server logs. - wtimeout: 5 * 60 * 1000 + 321, // 300321ms - }; - if (TestData.hasOwnProperty("defaultWriteConcern")) { - defaultWriteConcern = TestData.defaultWriteConcern; + "The readConcern level to use must be set as the 'defaultReadConcernLevel'" + + " property on the global TestData object"); } - var originalDBQuery = DBQuery; + const kDefaultReadConcern = {level: TestData.defaultReadConcernLevel}; + const kDefaultWriteConcern = + (TestData.hasOwnProperty("defaultWriteConcern")) ? TestData.defaultWriteConcern : { + w: "majority", + // Use a "signature" value that won't typically match a value assigned in normal use. + // This way the wtimeout set by this override is distinguishable in the server logs. + wtimeout: 5 * 60 * 1000 + 321, // 300321ms + }; + + const kCommandsSupportingReadConcern = new Set([ + "aggregate", + "count", + "distinct", + "find", + "geoNear", + "geoSearch", + "group", + "parallelCollectionScan", + ]); + + const kCommandsSupportingWriteConcern = new Set([ + "_configsvrAddShard", + "_configsvrAddShardToZone", + "_configsvrCommitChunkMerge", + "_configsvrCommitChunkMigration", + "_configsvrCommitChunkSplit", + "_configsvrCreateDatabase", + "_configsvrEnableSharding", + "_configsvrMoveChunk", + "_configsvrMovePrimary", + "_configsvrRemoveShard", + "_configsvrRemoveShardFromZone", + "_configsvrShardCollection", + "_configsvrUpdateZoneKeyRange", + "_mergeAuthzCollections", + "_recvChunkStart", + "appendOplogNote", + "applyOps", + "aggregate", + "captrunc", + "cleanupOrphaned", + "clone", + "cloneCollection", + "cloneCollectionAsCapped", + "collMod", + "convertToCapped", + "copydb", + "create", + "createIndexes", + "createRole", + "createUser", + "delete", + "doTxn", + "drop", + "dropAllRolesFromDatabase", + "dropAllUsersFromDatabase", + "dropDatabase", + "dropIndexes", + "dropRole", + "dropUser", + "emptycapped", + "findAndModify", + "findandmodify", + "godinsert", + "grantPrivilegesToRole", + "grantRolesToRole", + "grantRolesToUser", + "insert", + "mapReduce", + "mapreduce", + "mapreduce.shardedfinish", + "moveChunk", + "renameCollection", + "revokePrivilegesFromRole", + "revokeRolesFromRole", + "revokeRolesFromUser", + "setFeatureCompatibilityVersion", + "update", + "updateRole", + "updateUser", + ]); + + function runCommandWithReadAndWriteConcerns( + conn, dbName, commandName, commandObj, func, makeFuncArgs) { + if (typeof commandObj !== "object" || commandObj === null) { + return func.apply(conn, makeFuncArgs(commandObj)); + } - DBQuery = function(mongo, db, collection, ns, query, fields, limit, skip, batchSize, options) { - if (ns.endsWith("$cmd")) { - if (query.hasOwnProperty("writeConcern") && - bsonWoCompare(query.writeConcern, defaultWriteConcern) !== 0) { - jsTestLog("Warning: DBQuery overriding existing writeConcern of: " + - tojson(query.writeConcern)); - query.writeConcern = defaultWriteConcern; - } + // If the command is in a wrapped form, then we look for the actual command object inside + // the query/$query object. + let commandObjUnwrapped = commandObj; + if (commandName === "query" || commandName === "$query") { + commandObjUnwrapped = commandObj[commandName]; + commandName = Object.keys(commandObjUnwrapped)[0]; } - return originalDBQuery.apply(this, arguments); - }; + if (commandName === "eval" || commandName === "$eval") { + throw new Error("Cowardly refusing to run test with overridden write concern when it" + + " uses a command that can only perform w=1 writes: " + + tojson(commandObj)); + } - DBQuery.Option = originalDBQuery.Option; + let shouldForceReadConcern = kCommandsSupportingReadConcern.has(commandName); + let shouldForceWriteConcern = kCommandsSupportingWriteConcern.has(commandName); - var originalStartParallelShell = startParallelShell; - startParallelShell = function(jsCode, port, noConnect) { - var newCode; - var overridesFile = "jstests/libs/override_methods/set_read_and_write_concerns.js"; - if (typeof(jsCode) === "function") { - // Load the override file and immediately invoke the supplied function. - newCode = `load("${overridesFile}"); (${jsCode})();`; - } else { - newCode = `load("${overridesFile}"); ${jsCode};`; - } + if (commandName === "aggregate") { + if (OverrideHelpers.isAggregationWithListLocalCursorsStage(commandName, + commandObjUnwrapped)) { + // The $listLocalCursors stage can only be used with readConcern={level: "local"}. + shouldForceReadConcern = false; + } - return originalStartParallelShell(newCode, port, noConnect); - }; + if (OverrideHelpers.isAggregationWithListLocalSessionsStage(commandName, + commandObjUnwrapped)) { + // The $listLocalSessions stage can only be used with readConcern={level: "local"}. + shouldForceReadConcern = false; + } - const originalRunCommand = DB.prototype._runCommandImpl; - DB.prototype._runCommandImpl = function(dbName, obj, options) { - var cmdName = ""; - for (var fieldName in obj) { - cmdName = fieldName; - break; - } + if (OverrideHelpers.isAggregationWithOutStage(commandName, commandObjUnwrapped)) { + // The $out stage can only be used with readConcern={level: "local"}. + shouldForceReadConcern = false; + } else { + // A writeConcern can only be used with a $out stage. + shouldForceWriteConcern = false; + } - // These commands directly support a writeConcern argument. - var commandsToForceWriteConcern = [ - "_configsvrAddShard", - "_configsvrAddShardToZone", - "_configsvrCommitChunkMerge", - "_configsvrCommitChunkMigration", - "_configsvrCommitChunkSplit", - "_configsvrCreateDatabase", - "_configsvrEnableSharding", - "_configsvrMoveChunk", - "_configsvrMovePrimary", - "_configsvrRemoveShard", - "_configsvrRemoveShardFromZone", - "_configsvrShardCollection", - "_configsvrUpdateZoneKeyRange", - "_mergeAuthzCollections", - "_recvChunkStart", - "appendOplogNote", - "applyOps", - "captrunc", - "cleanupOrphaned", - "clone", - "cloneCollection", - "cloneCollectionAsCapped", - "collMod", - "convertToCapped", - "copydb", - "create", - "createIndexes", - "createRole", - "createUser", - "delete", - "doTxn", - "drop", - "dropAllRolesFromDatabase", - "dropAllUsersFromDatabase", - "dropDatabase", - "dropIndexes", - "dropRole", - "dropUser", - "emptycapped", - "findAndModify", - "findandmodify", - "godinsert", - "grantPrivilegesToRole", - "grantRolesToRole", - "grantRolesToUser", - "insert", - "mapreduce.shardedfinish", - "moveChunk", - "renameCollection", - "revokePrivilegesFromRole", - "revokeRolesFromRole", - "revokeRolesFromUser", - "setFeatureCompatibilityVersion", - "update", - "updateRole", - "updateUser", - ]; - - // These are reading commands that support majority readConcern. - var commandsToForceReadConcern = [ - "count", - "distinct", - "find", - "geoNear", - "geoSearch", - "group", - "parallelCollectionScan", - ]; - - var forceWriteConcern = Array.contains(commandsToForceWriteConcern, cmdName); - var forceReadConcern = Array.contains(commandsToForceReadConcern, cmdName); - - if (cmdName === "aggregate") { - // Aggregate can be either a read or a write depending on whether it has a $out stage. - // $out is required to be the last stage of the pipeline. - var stages = obj.pipeline; - const lastStage = stages && Array.isArray(stages) && (stages.length !== 0) - ? stages[stages.length - 1] - : undefined; - const hasOut = - lastStage && (typeof lastStage === 'object') && lastStage.hasOwnProperty('$out'); - const hasExplain = obj.hasOwnProperty("explain"); - if (!hasExplain) { - if (hasOut) { - forceWriteConcern = true; - } else { - forceReadConcern = true; - } + if (commandObjUnwrapped.explain) { + // Attempting to specify a readConcern while explaining an aggregation would always + // return an error prior to SERVER-30582 and it otherwise only compatible with + // readConcern={level: "local"}. + shouldForceReadConcern = false; } + } else if (OverrideHelpers.isMapReduceWithInlineOutput(commandName, commandObjUnwrapped)) { + // A writeConcern can only be used with non-inline output. + shouldForceWriteConcern = false; } - else if (cmdName === "mapReduce") { - var stages = obj.pipeline; - const lastStage = stages && Array.isArray(stages) && (stages.length !== 0) - ? stages[stages.length - 1] - : undefined; - const hasOut = - lastStage && (typeof lastStage === 'object') && lastStage.hasOwnProperty('$out'); - if (hasOut) { - forceWriteConcern = true; + const inWrappedForm = commandObj !== commandObjUnwrapped; + + if (shouldForceReadConcern) { + // We create a copy of 'commandObj' to avoid mutating the parameter the caller + // specified. + commandObj = Object.assign({}, commandObj); + if (inWrappedForm) { + commandObjUnwrapped = Object.assign({}, commandObjUnwrapped); + commandObj[Object.keys(commandObj)[0]] = commandObjUnwrapped; + } else { + commandObjUnwrapped = commandObj; } - } - if (forceWriteConcern) { - if (obj.hasOwnProperty("writeConcern")) { - if (bsonWoCompare(obj.writeConcern, defaultWriteConcern) !== 0) { - jsTestLog("Warning: _runCommandImpl overriding existing writeConcern of: " + - tojson(obj.writeConcern)); - obj.writeConcern = defaultWriteConcern; + if (commandObjUnwrapped.hasOwnProperty("readConcern")) { + let readConcern = commandObjUnwrapped.readConcern; + + if (typeof readConcern !== "object" || readConcern === null || + (readConcern.hasOwnProperty("level") && + bsonWoCompare({_: readConcern.level}, {_: kDefaultReadConcern.level}) !== 0)) { + throw new Error("Cowardly refusing to override read concern of command: " + + tojson(commandObj)); } + + // We create a copy of the readConcern object to avoid mutating the parameter the + // caller specified. + readConcern = Object.assign({}, readConcern, kDefaultReadConcern); + commandObjUnwrapped.readConcern = readConcern; + } else { + commandObjUnwrapped.readConcern = kDefaultReadConcern; + } + } + + if (shouldForceWriteConcern) { + // We create a copy of 'commandObj' to avoid mutating the parameter the caller + // specified. + commandObj = Object.assign({}, commandObj); + if (inWrappedForm) { + commandObjUnwrapped = Object.assign({}, commandObjUnwrapped); + commandObj[Object.keys(commandObj)[0]] = commandObjUnwrapped; } else { - obj.writeConcern = defaultWriteConcern; + commandObjUnwrapped = commandObj; } - } else if (forceReadConcern) { - if (obj.hasOwnProperty("readConcern")) { - if (bsonWoCompare(obj.readConcern, defaultReadConcern) !== 0) { - jsTestLog("Warning: _runCommandImpl overriding existing readConcern of: " + - tojson(obj.readConcern)); - obj.readConcern = defaultReadConcern; + if (commandObjUnwrapped.hasOwnProperty("writeConcern")) { + let writeConcern = commandObjUnwrapped.writeConcern; + + if (typeof writeConcern !== "object" || writeConcern === null || + (writeConcern.hasOwnProperty("w") && + bsonWoCompare({_: writeConcern.w}, {_: kDefaultWriteConcern.w}) !== 0)) { + throw new Error("Cowardly refusing to override write concern of command: " + + tojson(commandObj)); } + + // We create a copy of the writeConcern object to avoid mutating the parameter the + // caller specified. + writeConcern = Object.assign({}, writeConcern, kDefaultWriteConcern); + commandObjUnwrapped.writeConcern = writeConcern; } else { - obj.readConcern = defaultReadConcern; + commandObjUnwrapped.writeConcern = kDefaultWriteConcern; } } - var res = originalRunCommand.call(this, dbName, obj, options); - - return res; - }; + return func.apply(conn, makeFuncArgs(commandObj)); + } - // Use a majority write concern if the operation does not specify one. - DBCollection.prototype.getWriteConcern = function() { - return new WriteConcern(defaultWriteConcern); - }; + OverrideHelpers.prependOverrideInParallelShell( + "jstests/libs/override_methods/set_read_and_write_concerns.js"); + OverrideHelpers.overrideRunCommand(runCommandWithReadAndWriteConcerns); })(); diff --git a/jstests/libs/override_methods/set_read_preference_secondary.js b/jstests/libs/override_methods/set_read_preference_secondary.js index 9955c7f016d..d81532bf8d5 100644 --- a/jstests/libs/override_methods/set_read_preference_secondary.js +++ b/jstests/libs/override_methods/set_read_preference_secondary.js @@ -4,31 +4,16 @@ (function() { "use strict"; - const readPreferenceSecondary = {mode: "secondary"}; + load("jstests/libs/override_methods/override_helpers.js"); - db.getMongo().setReadPref("secondary"); - - const originalStartParallelShell = startParallelShell; - startParallelShell = function(jsCode, port, noConnect) { - let newCode; - const overridesFile = "jstests/libs/override_methods/set_read_preference_secondary.js"; - if (typeof(jsCode) === "function") { - // Load the override file and immediately invoke the supplied function. - newCode = `load("${overridesFile}"); (${jsCode})();`; - } else { - newCode = `load("${overridesFile}"); ${jsCode};`; - } - - return originalStartParallelShell(newCode, port, noConnect); - }; - - // These are reading commands that support a read preference. - const commandsToForceReadPreference = new Set([ + const kReadPreferenceSecondary = {mode: "secondary"}; + const kCommandsSupportingReadPreference = new Set([ "aggregate", "collStats", "count", "dbStats", "distinct", + "find", "geoNear", "geoSearch", "group", @@ -37,33 +22,55 @@ "parallelCollectionScan", ]); - const originalRunCommand = DB.prototype._runCommandImpl; - DB.prototype._runCommandImpl = function(dbName, obj, options) { - const cmdName = Object.keys(obj)[0]; + function runCommandWithReadPreferenceSecondary( + conn, dbName, commandName, commandObj, func, makeFuncArgs) { + if (typeof commandObj !== "object" || commandObj === null) { + return func.apply(conn, makeFuncArgs(commandObj)); + } - let forceReadPreference = commandsToForceReadPreference.has(cmdName); - if (cmdName === "aggregate" && obj.pipeline && Array.isArray(obj.pipeline) && - obj.pipeline.length > 0 && - obj.pipeline[obj.pipeline.length - 1].hasOwnProperty("$out")) { - forceReadPreference = false; - } else if ((cmdName === "mapReduce" || cmdName === "mapreduce") && - obj.hasOwnProperty("out") && typeof obj.out === "object" && !obj.out.inline) { - forceReadPreference = false; + // If the command is in a wrapped form, then we look for the actual command object inside + // the query/$query object. + let commandObjUnwrapped = commandObj; + if (commandName === "query" || commandName === "$query") { + commandObjUnwrapped = commandObj[commandName]; + commandName = Object.keys(commandObjUnwrapped)[0]; } - if (forceReadPreference) { - if (obj.hasOwnProperty("$readPreference")) { - if (bsonWoCompare(obj.$readPreference, readPreferenceSecondary) !== 0) { - jsTestLog("Warning: _runCommandImpl overriding existing $readPreference of: " + - tojson(obj.$readPreference)); - obj.$readPreference = readPreferenceSecondary; - } + let shouldForceReadPreference = kCommandsSupportingReadPreference.has(commandName); + if (OverrideHelpers.isAggregationWithOutStage(commandName, commandObjUnwrapped)) { + // An aggregation with a $out stage must be sent to the primary. + shouldForceReadPreference = false; + } else if ((commandName === "mapReduce" || commandName === "mapreduce") && + !OverrideHelpers.isMapReduceWithInlineOutput(commandName, commandObjUnwrapped)) { + // A map-reduce operation with non-inline output must be sent to the primary. + shouldForceReadPreference = false; + } + + if (shouldForceReadPreference) { + if (commandObj === commandObjUnwrapped) { + // We wrap the command object using a "query" field rather than a "$query" field to + // match the implementation of DB.prototype._attachReadPreferenceToCommand(). + commandObj = {query: commandObj}; } else { - obj.$readPreference = readPreferenceSecondary; + // We create a copy of 'commandObj' to avoid mutating the parameter the caller + // specified. + commandObj = Object.assign({}, commandObj); + } + + if (commandObj.hasOwnProperty("$readPreference") && + !bsonBinaryEqual({_: commandObj.$readPreference}, {_: kReadPreferenceSecondary})) { + throw new Error("Cowardly refusing to override read preference of command: " + + tojson(commandObj)); } + + commandObj.$readPreference = kReadPreferenceSecondary; } - return originalRunCommand.call(this, dbName, obj, options); - }; + return func.apply(conn, makeFuncArgs(commandObj)); + } + + OverrideHelpers.prependOverrideInParallelShell( + "jstests/libs/override_methods/set_read_preference_secondary.js"); + OverrideHelpers.overrideRunCommand(runCommandWithReadPreferenceSecondary); })(); |