summaryrefslogtreecommitdiff
path: root/src/mongo/shell/session.js
diff options
context:
space:
mode:
authorMax Hirschhorn <max.hirschhorn@mongodb.com>2017-11-08 19:29:28 -0500
committerMax Hirschhorn <max.hirschhorn@mongodb.com>2017-11-08 19:29:28 -0500
commit6d8e6b9cce052cdd442e207a27df10e698b2bb00 (patch)
tree5a7bb7b5cf93902bf00a681dccb0f54adb2f1ab0 /src/mongo/shell/session.js
parentf7756c41c5ed54e8e546461395bc8898d885af0c (diff)
downloadmongo-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.js332
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.