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/shell/session.js | |
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/shell/session.js')
-rw-r--r-- | src/mongo/shell/session.js | 332 |
1 files changed, 203 insertions, 129 deletions
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. |