diff options
author | Max Hirschhorn <max.hirschhorn@mongodb.com> | 2017-11-08 19:29:28 -0500 |
---|---|---|
committer | Max Hirschhorn <max.hirschhorn@mongodb.com> | 2017-11-08 19:29:28 -0500 |
commit | 6d8e6b9cce052cdd442e207a27df10e698b2bb00 (patch) | |
tree | 5a7bb7b5cf93902bf00a681dccb0f54adb2f1ab0 /src/mongo | |
parent | f7756c41c5ed54e8e546461395bc8898d885af0c (diff) | |
download | mongo-6d8e6b9cce052cdd442e207a27df10e698b2bb00.tar.gz |
SERVER-31296 Update sessions, causal, and retryable in the mongo shell.
* Removes the initialClusterTime and initialOperationTime session
options.
* Enables causal consistency by default when using an explicit
session.
* Adds a --retryWrites command line option to the mongo shell for
enabling retryable writes in the mongo shell. The retryWrites
options to SessionOptions is left for convenience with testing.
* Renames setClusterTime() to advanceClusterTime(), and adds a
corresponding advanceOperationTime() method to DriverSession.
* Enables assigning transaction numbers for write commands where
ordered=false.
* Prevents the mongo shell from sending afterClusterTime or assigning
transaction numbers when talking to a stand-alone mongod.
* Prevents the mongo shell from assigning transaction numbers when
using an unacknowledged (w=0) writeConcern.
* Changes DBClientRS to re-discover the current primary of the replica
set when it receives an error code representing "not master" in
addition to an error message representing "not master".
* Adds a shellPrint() pretty-printer for SessionOptions and
DriverSession instances so they no longer print out their entire
object definition.
Diffstat (limited to 'src/mongo')
-rw-r--r-- | src/mongo/client/dbclient.cpp | 15 | ||||
-rw-r--r-- | src/mongo/client/dbclientinterface.h | 8 | ||||
-rw-r--r-- | src/mongo/db/service_entry_point_mongod.cpp | 7 | ||||
-rw-r--r-- | src/mongo/scripting/mozjs/mongo.cpp | 31 | ||||
-rw-r--r-- | src/mongo/scripting/mozjs/mongo.h | 8 | ||||
-rw-r--r-- | src/mongo/shell/dbshell.cpp | 8 | ||||
-rw-r--r-- | src/mongo/shell/session.js | 332 | ||||
-rw-r--r-- | src/mongo/shell/shell_options.cpp | 9 | ||||
-rw-r--r-- | src/mongo/shell/shell_options.h | 1 | ||||
-rw-r--r-- | src/mongo/shell/shell_utils.cpp | 5 | ||||
-rw-r--r-- | src/mongo/shell/utils.js | 29 |
11 files changed, 294 insertions, 159 deletions
diff --git a/src/mongo/client/dbclient.cpp b/src/mongo/client/dbclient.cpp index e3869b773df..8028cc1b3bc 100644 --- a/src/mongo/client/dbclient.cpp +++ b/src/mongo/client/dbclient.cpp @@ -998,7 +998,7 @@ std::pair<rpc::UniqueReply, DBClientBase*> DBClientConnection::runCommandWithTar if (!_parentReplSetName.empty()) { const auto replyBody = out.first->getCommandReply(); if (!isOk(replyBody)) { - handleNotMasterResponse(replyBody["errmsg"]); + handleNotMasterResponse(replyBody, "errmsg"); } } @@ -1011,7 +1011,7 @@ std::pair<rpc::UniqueReply, std::shared_ptr<DBClientBase>> DBClientConnection::r if (!_parentReplSetName.empty()) { const auto replyBody = out.first->getCommandReply(); if (!isOk(replyBody)) { - handleNotMasterResponse(replyBody["errmsg"]); + handleNotMasterResponse(replyBody, "errmsg"); } } @@ -1474,7 +1474,7 @@ void DBClientConnection::checkResponse(const std::vector<BSONObj>& batch, *host = _serverAddress.toString(); if (!_parentReplSetName.empty() && !batch.empty()) { - handleNotMasterResponse(getErrField(batch[0])); + handleNotMasterResponse(batch[0], "$err"); } } @@ -1482,8 +1482,13 @@ void DBClientConnection::setParentReplSetName(const string& replSetName) { _parentReplSetName = replSetName; } -void DBClientConnection::handleNotMasterResponse(const BSONElement& elemToCheck) { - if (!isNotMasterErrorString(elemToCheck)) { +void DBClientConnection::handleNotMasterResponse(const BSONObj& replyBody, + StringData errorMsgFieldName) { + const BSONElement errorMsgElem = replyBody[errorMsgFieldName]; + const BSONElement codeElem = replyBody["code"]; + + if (!isNotMasterErrorString(errorMsgElem) && + !ErrorCodes::isNotMasterError(ErrorCodes::Error(codeElem.numberInt()))) { return; } diff --git a/src/mongo/client/dbclientinterface.h b/src/mongo/client/dbclientinterface.h index fdfc816b126..de81dabdd92 100644 --- a/src/mongo/client/dbclientinterface.h +++ b/src/mongo/client/dbclientinterface.h @@ -1084,11 +1084,11 @@ protected: private: /** - * Checks the BSONElement for the 'not master' keyword and if it does exist, - * try to inform the replica set monitor that the host this connects to is - * no longer primary. + * Inspects the contents of 'replyBody' and informs the replica set monitor that the host 'this' + * is connected with is no longer the primary if a "not master" error message or error code was + * returned. */ - void handleNotMasterResponse(const BSONElement& elemToCheck); + void handleNotMasterResponse(const BSONObj& replyBody, StringData errorMsgFieldName); // Contains the string for the replica set name of the host this is connected to. // Should be empty if this connection is not pointing to a replica set member. diff --git a/src/mongo/db/service_entry_point_mongod.cpp b/src/mongo/db/service_entry_point_mongod.cpp index 3814ce52919..bae7d227be6 100644 --- a/src/mongo/db/service_entry_point_mongod.cpp +++ b/src/mongo/db/service_entry_point_mongod.cpp @@ -87,6 +87,7 @@ namespace mongo { MONGO_FP_DECLARE(rsStopGetMore); +MONGO_FP_DECLARE(respondWithNotPrimaryInCommandDispatch); namespace { using logger::LogComponent; @@ -658,7 +659,11 @@ void execCommandDatabase(OperationContext* opCtx, uasserted(ErrorCodes::NotMasterNoSlaveOk, "not master and slaveOk=false"); } - uassert(ErrorCodes::NotMaster, "not master", canRunHere); + if (MONGO_FAIL_POINT(respondWithNotPrimaryInCommandDispatch)) { + uassert(ErrorCodes::NotMaster, "not primary", canRunHere); + } else { + uassert(ErrorCodes::NotMaster, "not master", canRunHere); + } if (!command->maintenanceOk() && replCoord->getReplicationMode() == repl::ReplicationCoordinator::modeReplSet && diff --git a/src/mongo/scripting/mozjs/mongo.cpp b/src/mongo/scripting/mozjs/mongo.cpp index debede5bee7..c740e3acadc 100644 --- a/src/mongo/scripting/mozjs/mongo.cpp +++ b/src/mongo/scripting/mozjs/mongo.cpp @@ -88,7 +88,10 @@ const JSFunctionSpec MongoBase::methods[] = { isReplicaSetMember, MongoLocalInfo, MongoExternalInfo), MONGO_ATTACH_JS_CONSTRAINED_METHOD_NO_PROTO(isMongos, MongoLocalInfo, MongoExternalInfo), MONGO_ATTACH_JS_CONSTRAINED_METHOD_NO_PROTO(getClusterTime, MongoLocalInfo, MongoExternalInfo), - MONGO_ATTACH_JS_CONSTRAINED_METHOD_NO_PROTO(setClusterTime, MongoLocalInfo, MongoExternalInfo), + MONGO_ATTACH_JS_CONSTRAINED_METHOD_NO_PROTO( + advanceClusterTime, MongoLocalInfo, MongoExternalInfo), + MONGO_ATTACH_JS_CONSTRAINED_METHOD_NO_PROTO( + resetClusterTime_forTesting, MongoLocalInfo, MongoExternalInfo), MONGO_ATTACH_JS_CONSTRAINED_METHOD_NO_PROTO(_startSession, MongoLocalInfo, MongoExternalInfo), JS_FS_END, }; @@ -194,9 +197,14 @@ BSONObj MongoBase::getClusterTime() { return latestlogicalTime; } -void MongoBase::setClusterTime(const BSONObj& newTime) { +void MongoBase::resetClusterTime_forTesting() { + stdx::lock_guard<stdx::mutex> lk(logicalTimeMutex); + latestlogicalTime = BSONObj(); +} + +void MongoBase::advanceClusterTime(const BSONObj& newTime) { if (newTime["clusterTime"].eoo()) - uasserted(ErrorCodes::BadValue, "missing clusterTime field in setClusterTime argument"); + uasserted(ErrorCodes::BadValue, "missing clusterTime field in advanceClusterTime argument"); stdx::lock_guard<stdx::mutex> lk(logicalTimeMutex); if (latestlogicalTime.isEmpty() || @@ -791,17 +799,26 @@ void MongoBase::Functions::getClusterTime::call(JSContext* cx, JS::CallArgs args args.rval().setUndefined(); } -void MongoBase::Functions::setClusterTime::call(JSContext* cx, JS::CallArgs args) { +void MongoBase::Functions::advanceClusterTime::call(JSContext* cx, JS::CallArgs args) { if (args.length() != 1) - uasserted(ErrorCodes::BadValue, "setClusterTime takes 1 argument"); + uasserted(ErrorCodes::BadValue, "advanceClusterTime takes 1 argument"); if (!args.get(0).isObject()) - uasserted(ErrorCodes::BadValue, "the parameter to setClusterTime must be an object"); + uasserted(ErrorCodes::BadValue, "the parameter to advanceClusterTime must be an object"); auto newTime = ObjectWrapper(cx, args.get(0)).toBSON(); - MongoBase::setClusterTime(newTime); + MongoBase::advanceClusterTime(newTime); + + args.rval().setUndefined(); +} + +void MongoBase::Functions::resetClusterTime_forTesting::call(JSContext* cx, JS::CallArgs args) { + if (args.length() != 0) { + uasserted(ErrorCodes::BadValue, "resetClusterTime_forTesting takes no arguments"); + } + MongoBase::resetClusterTime_forTesting(); args.rval().setUndefined(); } diff --git a/src/mongo/scripting/mozjs/mongo.h b/src/mongo/scripting/mozjs/mongo.h index f6c6a535c1c..4d7a0200bd7 100644 --- a/src/mongo/scripting/mozjs/mongo.h +++ b/src/mongo/scripting/mozjs/mongo.h @@ -65,17 +65,19 @@ struct MongoBase : public BaseInfo { MONGO_DECLARE_JS_FUNCTION(isReplicaSetMember); MONGO_DECLARE_JS_FUNCTION(isMongos); MONGO_DECLARE_JS_FUNCTION(getClusterTime); - MONGO_DECLARE_JS_FUNCTION(setClusterTime); + MONGO_DECLARE_JS_FUNCTION(advanceClusterTime); + MONGO_DECLARE_JS_FUNCTION(resetClusterTime_forTesting); MONGO_DECLARE_JS_FUNCTION(_startSession); }; - static const JSFunctionSpec methods[24]; + static const JSFunctionSpec methods[25]; static const char* const className; static const unsigned classFlags = JSCLASS_HAS_PRIVATE; static BSONObj getClusterTime(); - static void setClusterTime(const BSONObj& newTime); + static void advanceClusterTime(const BSONObj& newTime); + static void resetClusterTime_forTesting(); }; /** diff --git a/src/mongo/shell/dbshell.cpp b/src/mongo/shell/dbshell.cpp index 03ca476e744..f2b635d7883 100644 --- a/src/mongo/shell/dbshell.cpp +++ b/src/mongo/shell/dbshell.cpp @@ -750,7 +750,13 @@ int _main(int argc, char* argv[], char** envp) { ss << "db = connect( \"" << getURIFromArgs( shellGlobalParams.url, shellGlobalParams.dbhost, shellGlobalParams.port) - << "\")"; + << "\");"; + + if (shellGlobalParams.shouldRetryWrites) { + // If the user specified --retryWrites to the mongo shell, then we replace the global + // `db` object with a DB object that has retryable writes enabled. + ss << "db = db.getMongo().startSession({retryWrites: true}).getDatabase(db.getName());"; + } mongo::shell_utils::_dbConnect = ss.str(); diff --git a/src/mongo/shell/session.js b/src/mongo/shell/session.js index ae49db63f72..48e3eab554a 100644 --- a/src/mongo/shell/session.js +++ b/src/mongo/shell/session.js @@ -6,14 +6,31 @@ var { } = (function() { "use strict"; + const kShellDefaultShouldRetryWrites = + typeof _shouldRetryWrites === "function" ? _shouldRetryWrites() : false; + + function isNonNullObject(obj) { + return typeof obj === "object" && obj !== null; + } + function SessionOptions(rawOptions = {}) { + if (!(this instanceof SessionOptions)) { + return new SessionOptions(rawOptions); + } + let _readPreference = rawOptions.readPreference; let _readConcern = rawOptions.readConcern; let _writeConcern = rawOptions.writeConcern; - const _initialClusterTime = rawOptions.initialClusterTime; - const _initialOperationTime = rawOptions.initialOperationTime; - let _causalConsistency = rawOptions.causalConsistency; - let _retryWrites = rawOptions.retryWrites; + + // Causal consistency is implicitly enabled when a session is explicitly started. + const _causalConsistency = + rawOptions.hasOwnProperty("causalConsistency") ? rawOptions.causalConsistency : true; + + // If the user specified --retryWrites to the mongo shell, then we enable retryable + // writes automatically. + const _retryWrites = rawOptions.hasOwnProperty("retryWrites") + ? rawOptions.retryWrites + : kShellDefaultShouldRetryWrites; this.getReadPreference = function getReadPreference() { return _readPreference; @@ -42,34 +59,30 @@ var { _writeConcern = writeConcern; }; - this.getInitialClusterTime = function getInitialClusterTime() { - return _initialClusterTime; - }; - - this.getInitialOperationTime = function getInitialOperationTime() { - return _initialOperationTime; - }; - this.isCausalConsistency = function isCausalConsistency() { return _causalConsistency; }; - this.setCausalConsistency = function setCausalConsistency(causalConsistency = true) { - _causalConsistency = causalConsistency; - }; - this.shouldRetryWrites = function shouldRetryWrites() { return _retryWrites; }; - this.setRetryWrites = function setRetryWrites(retryWrites = true) { - _retryWrites = retryWrites; + this.shellPrint = function _shellPrint() { + return this.toString(); + }; + + this.tojson = function _tojson(...args) { + return tojson(rawOptions, ...args); + }; + + this.toString = function toString() { + return "SessionOptions(" + this.tojson() + ")"; }; } function SessionAwareClient(client) { - const kWireVersionSupportingLogicalSession = 6; const kWireVersionSupportingCausalConsistency = 6; + const kWireVersionSupportingLogicalSession = 6; const kWireVersionSupportingRetryableWrites = 6; this.getReadPreference = function getReadPreference(driverSession) { @@ -104,6 +117,8 @@ var { wireVersion <= client.getMaxWireVersion(); } + // TODO: Update this whitelist, or convert it to a blacklist depending on the outcome of + // SERVER-31743. const kCommandsThatSupportReadConcern = new Set([ "aggregate", "count", @@ -163,32 +178,26 @@ var { function injectAfterClusterTime(cmdObj, operationTime) { cmdObj = Object.assign({}, cmdObj); - if (operationTime !== undefined) { - const cmdName = Object.keys(cmdObj)[0]; + const cmdName = Object.keys(cmdObj)[0]; - // If the command is in a wrapped form, then we look for the actual command object - // inside the query/$query object. - let cmdObjUnwrapped = cmdObj; - if (cmdName === "query" || cmdName === "$query") { - cmdObj[cmdName] = Object.assign({}, cmdObj[cmdName]); - cmdObjUnwrapped = cmdObj[cmdName]; - } + // If the command is in a wrapped form, then we look for the actual command object + // inside the query/$query object. + let cmdObjUnwrapped = cmdObj; + if (cmdName === "query" || cmdName === "$query") { + cmdObj[cmdName] = Object.assign({}, cmdObj[cmdName]); + cmdObjUnwrapped = cmdObj[cmdName]; + } - cmdObjUnwrapped.readConcern = Object.assign({}, cmdObjUnwrapped.readConcern); - const readConcern = cmdObjUnwrapped.readConcern; + cmdObjUnwrapped.readConcern = Object.assign({}, cmdObjUnwrapped.readConcern); + const readConcern = cmdObjUnwrapped.readConcern; - if (!readConcern.hasOwnProperty("afterClusterTime")) { - readConcern.afterClusterTime = operationTime; - } + if (!readConcern.hasOwnProperty("afterClusterTime")) { + readConcern.afterClusterTime = operationTime; } return cmdObj; } - function isNonNullObject(obj) { - return typeof obj === "object" && obj !== null; - } - function prepareCommandRequest(driverSession, cmdObj) { if (serverSupports(kWireVersionSupportingLogicalSession)) { cmdObj = driverSession._serverSession.injectSessionId(cmdObj); @@ -230,15 +239,24 @@ var { } } + // TODO SERVER-31868: A user should get back an error if they attempt to advance the + // DriverSession's operationTime manually when talking to a stand-alone mongod. Removing + // the `(client.isReplicaSetMember() || client.isMongos())` condition will also involve + // calling resetOperationTime_forTesting() in JavaScript tests that start different + // cluster types. if (serverSupports(kWireVersionSupportingCausalConsistency) && + (client.isReplicaSetMember() || client.isMongos()) && (driverSession.getOptions().isCausalConsistency() || client.isCausalConsistency()) && canUseReadConcern(cmdObj)) { - // `driverSession._operationTime` is the smallest time needed for performing a + // `driverSession.getOperationTime()` is the smallest time needed for performing a // causally consistent read using the current session. Note that // `client.getClusterTime()` is no smaller than the operation time and would // therefore only be less efficient to wait until. - cmdObj = injectAfterClusterTime(cmdObj, driverSession._operationTime); + const operationTime = driverSession.getOperationTime(); + if (operationTime !== undefined) { + cmdObj = injectAfterClusterTime(cmdObj, driverSession.getOperationTime()); + } } if (jsTest.options().alwaysInjectTransactionNumber && @@ -252,13 +270,13 @@ var { } function processCommandResponse(driverSession, res) { - if (res.hasOwnProperty("operationTime") && - bsonWoCompare({_: res.operationTime}, {_: driverSession._operationTime}) > 0) { - driverSession._operationTime = res.operationTime; + if (res.hasOwnProperty("operationTime")) { + driverSession.advanceOperationTime(res.operationTime); } if (res.hasOwnProperty("$clusterTime")) { - client.setClusterTime(res.$clusterTime); + driverSession.advanceClusterTime(res.$clusterTime); + client.advanceClusterTime(res.$clusterTime); } } @@ -280,13 +298,34 @@ var { do { try { - return clientFunction.apply(client, clientFunctionArguments); + const res = clientFunction.apply(client, clientFunctionArguments); + if (res.ok === 1 || numRetries === 0 || + !ErrorCodes.isNotMasterError(res.code)) { + return res; + } } catch (e) { - // TODO: Should we run an explicit "isMaster" command in order to compare the - // wire version of the server after we reconnect to it? if (!isNetworkError(e) || numRetries === 0) { throw e; } + + // We run an "isMaster" command explicitly to force the underlying DBClient to + // reconnect to the server. + const res = client.adminCommand({isMaster: 1}); + if (res.ok !== 1) { + throw e; + } + + // It's possible that the server we're connected with after re-establishing our + // connection doesn't support retryable writes. If that happens, then we just + // return the original network error back to the user. + const serverSupportsRetryableWrites = res.hasOwnProperty("minWireVersion") && + res.hasOwnProperty("maxWireVersion") && + res.minWireVersion <= kWireVersionSupportingRetryableWrites && + kWireVersionSupportingRetryableWrites <= res.maxWireVersion; + + if (!serverSupportsRetryableWrites) { + throw e; + } } --numRetries; @@ -388,6 +427,18 @@ var { cmdName = Object.keys(cmdObj)[0]; } + if (isNonNullObject(cmdObj.writeConcern)) { + const writeConcern = cmdObj.writeConcern; + + // We use bsonWoCompare() in order to handle cases where the "w" field is specified + // as a NumberInt() or NumberLong() instance. + if (writeConcern.hasOwnProperty("w") && + bsonWoCompare({_: writeConcern.w}, {_: 0}) === 0) { + // Unacknowledged writes cannot be retried. + return false; + } + } + if (cmdName === "insert") { if (!Array.isArray(cmdObj.documents)) { // The command object is malformed, so we'll just leave it as-is and let the @@ -395,14 +446,10 @@ var { return false; } - if (cmdObj.documents.length === 1) { - // Single-statement operations (e.g. insertOne()) can be retried. - return true; - } - - // Multi-statement operations (e.g. insertMany()) can be retried if they are + // Both single-statement operations (e.g. insertOne()) and multi-statement + // operations (e.g. insertMany()) can be retried regardless of whether they are // executed in order by the server. - return cmdObj.ordered ? true : false; + return true; } else if (cmdName === "update") { if (!Array.isArray(cmdObj.updates)) { // The command object is malformed, so we'll just leave it as-is and let the @@ -417,15 +464,10 @@ var { return false; } - if (cmdObj.updates.length === 1) { - // Single-statement operations that modify a single document (e.g. updateOne()) - // can be retried. - return true; - } - - // Multi-statement operations that each modify a single document (e.g. bulkWrite()) - // can be retried if they are executed in order by the server. - return cmdObj.ordered ? true : false; + // Both single-statement operations (e.g. updateOne()) and multi-statement + // operations (e.g. bulkWrite()) can be retried regardless of whether they are + // executed in order by the server. + return true; } else if (cmdName === "delete") { if (!Array.isArray(cmdObj.deletes)) { // The command object is malformed, so we'll just leave it as-is and let the @@ -443,15 +485,10 @@ var { return false; } - if (cmdObj.deletes.length === 1) { - // Single-statement operations that modify a single document (e.g. deleteOne()) - // can be retried. - return true; - } - - // Multi-statement operations that each modify a single document (e.g. bulkWrite()) - // can be retried if they are executed in order by the server. - return cmdObj.ordered ? true : false; + // Both single-statement operations (e.g. deleteOne()) and multi-statement + // operations (e.g. bulkWrite()) can be retried regardless of whether they are + // executed in order by the server. + return true; } else if (cmdName === "findAndModify" || cmdName === "findandmodify") { // Operations that modify a single document (e.g. findOneAndUpdate()) can be // retried. @@ -462,21 +499,19 @@ var { }; } - function makeDriverSessionConstructor(implMethods) { - return function(client, options = {}) { + function makeDriverSessionConstructor(implMethods, defaultOptions = {}) { + return function(client, options = defaultOptions) { let _options = options; let _hasEnded = false; + let _operationTime; + let _clusterTime; + if (!(_options instanceof SessionOptions)) { _options = new SessionOptions(_options); } this._serverSession = implMethods.createServerSession(client); - this._operationTime = _options.getInitialOperationTime(); - - if (_options.getInitialClusterTime() !== undefined) { - client.setClusterTime(_options.getInitialClusterTime()); - } this.getClient = function getClient() { return client; @@ -490,12 +525,42 @@ var { return _options; }; + this.getSessionId = function getSessionId() { + if (!this._serverSession.hasOwnProperty("handle")) { + return null; + } + return this._serverSession.handle.getId(); + }; + this.getOperationTime = function getOperationTime() { - return this._operationTime; + return _operationTime; + }; + + this.advanceOperationTime = function advanceOperationTime(operationTime) { + if (!isNonNullObject(_operationTime) || + bsonWoCompare({_: operationTime}, {_: _operationTime}) > 0) { + _operationTime = operationTime; + } + }; + + this.resetOperationTime_forTesting = function resetOperationTime_forTesting() { + _operationTime = undefined; }; this.getClusterTime = function getClusterTime() { - return client.getClusterTime(); + return _clusterTime; + }; + + this.advanceClusterTime = function advanceClusterTime(clusterTime) { + if (!isNonNullObject(_clusterTime) || + bsonWoCompare({_: clusterTime.clusterTime}, {_: _clusterTime.clusterTime}) > + 0) { + _clusterTime = clusterTime; + } + }; + + this.resetClusterTime_forTesting = function resetClusterTime_forTesting() { + _clusterTime = undefined; }; this.getDatabase = function getDatabase(dbName) { @@ -516,6 +581,22 @@ var { this._hasEnded = true; implMethods.endSession(this._serverSession); }; + + this.shellPrint = function() { + return this.toString(); + }; + + this.tojson = function _tojson(...args) { + return tojson(this.getSessionId(), ...args); + }; + + this.toString = function toString() { + const sessionId = this.getSessionId(); + if (sessionId === null) { + return "dummy session"; + } + return "session " + tojson(sessionId); + }; }; } @@ -530,15 +611,6 @@ var { }); function DelegatingDriverSession(client, originalSession) { - this._serverSession = originalSession._serverSession; - Object.defineProperty(this, "_operationTime", { - get: function() { - return originalSession._operationTime; - }, - set: function(operationTime) { - originalSession._operationTime = operationTime; - }, - }); const sessionAwareClient = new SessionAwareClient(client); this.getClient = function() { @@ -549,54 +621,56 @@ var { return sessionAwareClient; }; - this.getOptions = function() { - return originalSession.getOptions(); - }; - - this.getOperationTime = function getOperationTime() { - return this._operationTime; - }; - - this.getClusterTime = function getClusterTime() { - return originalSession.getClusterTime(); - }; - this.getDatabase = function(dbName) { const db = client.getDB(dbName); db._session = this; return db; }; - this.hasEnded = function() { - return originalSession.hasEnded(); - }; - - this.endSession = function() { - return originalSession.endSession(); - }; + return new Proxy(this, { + get: function get(target, property, receiver) { + // If the property is defined on the DelegatingDriverSession instance itself, then + // return it. Otherwise, get the value of the property from the `originalSession` + // instance. + if (target.hasOwnProperty(property)) { + return target[property]; + } + return originalSession[property]; + }, + }); } - const DummyDriverSession = makeDriverSessionConstructor({ - createServerSession: function createServerSession(client) { - return { - client: new SessionAwareClient(client), - - injectSessionId: function injectSessionId(cmdObj) { - return cmdObj; - }, - - assignTransactionNumber: function assignTransactionNumber(cmdObj) { - return cmdObj; - }, - - canRetryWrites: function canRetryWrites(cmdObj) { - return false; - }, - }; - }, - - endSession: function endSession(serverSession) {}, - }); + // The default session on the Mongo connection object should report that causal consistency + // isn't enabled when interrogating the SessionOptions since it must be enabled on the Mongo + // connection object. + // + // The default session on the Mongo connection object should also report that retryable + // writes isn't enabled when interrogating the SessionOptions since `DummyDriverSession` won't + // ever assign a transaction number. + const DummyDriverSession = + makeDriverSessionConstructor( // Force clang-format to break this line. + { + createServerSession: function createServerSession(client) { + return { + client: new SessionAwareClient(client), + + injectSessionId: function injectSessionId(cmdObj) { + return cmdObj; + }, + + assignTransactionNumber: function assignTransactionNumber(cmdObj) { + return cmdObj; + }, + + canRetryWrites: function canRetryWrites(cmdObj) { + return false; + }, + }; + }, + + endSession: function endSession(serverSession) {}, + }, + {causalConsistency: false, retryWrites: false}); // We don't actually put anything on DriverSession.prototype, but this way // `session instanceof DriverSession` will work for DummyDriverSession instances. diff --git a/src/mongo/shell/shell_options.cpp b/src/mongo/shell/shell_options.cpp index 7898fc6fa1b..f14798a2273 100644 --- a/src/mongo/shell/shell_options.cpp +++ b/src/mongo/shell/shell_options.cpp @@ -208,6 +208,12 @@ Status addMongoShellOptions(moe::OptionSection* options) { " commands, compatibility, legacy") .hidden(); + options->addOptionChaining( + "retryWrites", + "retryWrites", + moe::Switch, + "automatically retry write operations upon transient network errors"); + options ->addOptionChaining( "rpcProtocols", "rpcProtocols", moe::String, " none, opQueryOnly, opCommandOnly, all") @@ -363,6 +369,9 @@ Status storeMongoShellOptions(const moe::Environment& params, } shellGlobalParams.readMode = mode; } + if (params.count("retryWrites")) { + shellGlobalParams.shouldRetryWrites = true; + } if (params.count("rpcProtocols")) { std::string protos = params["rpcProtocols"].as<string>(); auto parsedRPCProtos = rpc::parseProtocolSet(protos); diff --git a/src/mongo/shell/shell_options.h b/src/mongo/shell/shell_options.h index 52b02ce1d34..04a66ed7fec 100644 --- a/src/mongo/shell/shell_options.h +++ b/src/mongo/shell/shell_options.h @@ -71,6 +71,7 @@ struct ShellGlobalParams { std::string writeMode = "commands"; std::string readMode = "compatibility"; + bool shouldRetryWrites = false; boost::optional<rpc::ProtocolSet> rpcProtocols = boost::none; diff --git a/src/mongo/shell/shell_utils.cpp b/src/mongo/shell/shell_utils.cpp index 3699a8baa47..ad3b441d7c7 100644 --- a/src/mongo/shell/shell_utils.cpp +++ b/src/mongo/shell/shell_utils.cpp @@ -213,6 +213,10 @@ BSONObj readMode(const BSONObj&, void*) { return BSON("" << shellGlobalParams.readMode); } +BSONObj shouldRetryWrites(const BSONObj&, void* data) { + return BSON("" << shellGlobalParams.shouldRetryWrites); +} + BSONObj interpreterVersion(const BSONObj& a, void* data) { uassert(16453, "interpreterVersion accepts no arguments", a.nFields() == 0); return BSON("" << getGlobalScriptEngine()->getInterpreterVersionString()); @@ -240,6 +244,7 @@ void initScope(Scope& scope) { scope.injectNative("_useWriteCommandsDefault", useWriteCommandsDefault); scope.injectNative("_writeMode", writeMode); scope.injectNative("_readMode", readMode); + scope.injectNative("_shouldRetryWrites", shouldRetryWrites); scope.externalSetup(); mongo::shell_utils::installShellUtils(scope); scope.execSetup(JSFiles::servers); diff --git a/src/mongo/shell/utils.js b/src/mongo/shell/utils.js index d626f4e600d..359a543075c 100644 --- a/src/mongo/shell/utils.js +++ b/src/mongo/shell/utils.js @@ -471,27 +471,38 @@ isMasterStatePrompt = function(isMasterResponse) { return state + '> '; }; -if (typeof(_useWriteCommandsDefault) == 'undefined') { - // This is for cases when the v8 engine is used other than the mongo shell, like map reduce. - _useWriteCommandsDefault = function() { +if (typeof _useWriteCommandsDefault === "undefined") { + // We ensure the _useWriteCommandsDefault() function is always defined, in case the JavaScript + // engine is being used from someplace other than the mongo shell (e.g. map-reduce). + _useWriteCommandsDefault = function _useWriteCommandsDefault() { return false; }; } -if (typeof(_writeMode) == 'undefined') { - // This is for cases when the v8 engine is used other than the mongo shell, like map reduce. - _writeMode = function() { +if (typeof _writeMode === "undefined") { + // We ensure the _writeMode() function is always defined, in case the JavaScript engine is being + // used from someplace other than the mongo shell (e.g. map-reduce). + _writeMode = function _writeMode() { return "commands"; }; } -if (typeof(_readMode) == 'undefined') { - // This is for cases when the v8 engine is used other than the mongo shell, like map reduce. - _readMode = function() { +if (typeof _readMode === "undefined") { + // We ensure the _readMode() function is always defined, in case the JavaScript engine is being + // used from someplace other than the mongo shell (e.g. map-reduce). + _readMode = function _readMode() { return "legacy"; }; } +if (typeof _shouldRetryWrites === 'undefined') { + // We ensure the _shouldRetryWrites() function is always defined, in case the JavaScript engine + // is being used from someplace other than the mongo shell (e.g. map-reduce). + _shouldRetryWrites = function _shouldRetryWrites() { + return false; + }; +} + shellPrintHelper = function(x) { if (typeof(x) == "undefined") { // Make sure that we have a db var before we use it |