mongodb 3.3.0-beta1 → 3.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/HISTORY.md +117 -0
  2. package/index.js +1 -0
  3. package/lib/admin.js +6 -6
  4. package/lib/aggregation_cursor.js +180 -226
  5. package/lib/change_stream.js +233 -121
  6. package/lib/collection.js +74 -39
  7. package/lib/command_cursor.js +87 -165
  8. package/lib/core/connection/connect.js +1 -1
  9. package/lib/core/connection/msg.js +2 -1
  10. package/lib/core/connection/pool.js +66 -41
  11. package/lib/core/cursor.js +605 -461
  12. package/lib/core/error.js +70 -1
  13. package/lib/core/index.js +1 -1
  14. package/lib/core/sdam/server.js +109 -10
  15. package/lib/core/sdam/server_description.js +14 -0
  16. package/lib/core/sdam/topology.js +34 -14
  17. package/lib/core/sdam/topology_description.js +6 -14
  18. package/lib/core/sessions.js +68 -12
  19. package/lib/core/topologies/mongos.js +10 -3
  20. package/lib/core/topologies/replset.js +49 -11
  21. package/lib/core/topologies/server.js +38 -3
  22. package/lib/core/topologies/shared.js +22 -0
  23. package/lib/core/transactions.js +1 -0
  24. package/lib/core/utils.js +45 -1
  25. package/lib/core/wireprotocol/command.js +2 -2
  26. package/lib/core/wireprotocol/get_more.js +4 -0
  27. package/lib/core/wireprotocol/query.js +8 -1
  28. package/lib/cursor.js +780 -840
  29. package/lib/db.js +31 -73
  30. package/lib/error.js +10 -8
  31. package/lib/gridfs-stream/download.js +12 -5
  32. package/lib/mongo_client.js +33 -21
  33. package/lib/operations/aggregate.js +77 -95
  34. package/lib/operations/close.js +46 -0
  35. package/lib/operations/collection_ops.js +7 -916
  36. package/lib/operations/command.js +2 -1
  37. package/lib/operations/command_v2.js +109 -0
  38. package/lib/operations/common_functions.js +48 -14
  39. package/lib/operations/connect.js +73 -9
  40. package/lib/operations/count.js +8 -8
  41. package/lib/operations/count_documents.js +24 -29
  42. package/lib/operations/create_collection.js +1 -0
  43. package/lib/operations/cursor_ops.js +22 -32
  44. package/lib/operations/db_ops.js +0 -178
  45. package/lib/operations/distinct.js +20 -21
  46. package/lib/operations/drop.js +1 -0
  47. package/lib/operations/estimated_document_count.js +43 -18
  48. package/lib/operations/execute_operation.js +115 -3
  49. package/lib/operations/explain.js +2 -6
  50. package/lib/operations/find.js +35 -0
  51. package/lib/operations/insert_one.js +1 -37
  52. package/lib/operations/list_collections.js +106 -0
  53. package/lib/operations/list_databases.js +38 -0
  54. package/lib/operations/list_indexes.js +28 -52
  55. package/lib/operations/operation.js +7 -1
  56. package/lib/operations/rename.js +1 -1
  57. package/lib/operations/to_array.js +3 -5
  58. package/lib/read_concern.js +7 -1
  59. package/lib/topologies/mongos.js +1 -1
  60. package/lib/topologies/replset.js +1 -1
  61. package/lib/topologies/server.js +2 -2
  62. package/lib/topologies/topology_base.js +4 -5
  63. package/lib/utils.js +4 -4
  64. package/package.json +1 -1
  65. package/lib/operations/aggregate_operation.js +0 -127
  66. package/lib/operations/mongo_client_ops.js +0 -731
package/lib/core/error.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const mongoErrorContextSymbol = Symbol('mongoErrorContextSymbol');
4
+ const maxWireVersion = require('./utils').maxWireVersion;
4
5
 
5
6
  /**
6
7
  * Creates a new MongoError
@@ -152,6 +153,73 @@ function isRetryableError(error) {
152
153
  );
153
154
  }
154
155
 
156
+ const SDAM_RECOVERING_CODES = new Set([
157
+ 91, // ShutdownInProgress
158
+ 189, // PrimarySteppedDown
159
+ 11600, // InterruptedAtShutdown
160
+ 11602, // InterruptedDueToReplStateChange
161
+ 13436 // NotMasterOrSecondary
162
+ ]);
163
+
164
+ const SDAM_NOTMASTER_CODES = new Set([
165
+ 10107, // NotMaster
166
+ 13435 // NotMasterNoSlaveOk
167
+ ]);
168
+
169
+ const SDAM_NODE_SHUTTING_DOWN_ERROR_CODES = new Set([
170
+ 11600, // InterruptedAtShutdown
171
+ 91 // ShutdownInProgress
172
+ ]);
173
+
174
+ function isRecoveringError(err) {
175
+ if (err.code && SDAM_RECOVERING_CODES.has(err.code)) {
176
+ return true;
177
+ }
178
+
179
+ return err.message.match(/not master or secondary/) || err.message.match(/node is recovering/);
180
+ }
181
+
182
+ function isNotMasterError(err) {
183
+ if (err.code && SDAM_NOTMASTER_CODES.has(err.code)) {
184
+ return true;
185
+ }
186
+
187
+ if (isRecoveringError(err)) {
188
+ return false;
189
+ }
190
+
191
+ return err.message.match(/not master/);
192
+ }
193
+
194
+ function isNodeShuttingDownError(err) {
195
+ return err.code && SDAM_NODE_SHUTTING_DOWN_ERROR_CODES.has(err.code);
196
+ }
197
+
198
+ /**
199
+ * Determines whether SDAM can recover from a given error. If it cannot
200
+ * then the pool will be cleared, and server state will completely reset
201
+ * locally.
202
+ *
203
+ * @see https://github.com/mongodb/specifications/blob/master/source/server-discovery-and-monitoring/server-discovery-and-monitoring.rst#not-master-and-node-is-recovering
204
+ * @param {MongoError|Error} error
205
+ * @param {Server} server
206
+ */
207
+ function isSDAMUnrecoverableError(error, server) {
208
+ if (error instanceof MongoParseError) {
209
+ return true;
210
+ }
211
+
212
+ if (isRecoveringError(error) || isNotMasterError(error)) {
213
+ if (maxWireVersion(server) >= 8 && !isNodeShuttingDownError(error)) {
214
+ return false;
215
+ }
216
+
217
+ return true;
218
+ }
219
+
220
+ return false;
221
+ }
222
+
155
223
  module.exports = {
156
224
  MongoError,
157
225
  MongoNetworkError,
@@ -159,5 +227,6 @@ module.exports = {
159
227
  MongoTimeoutError,
160
228
  MongoWriteConcernError,
161
229
  mongoErrorContextSymbol,
162
- isRetryableError
230
+ isRetryableError,
231
+ isSDAMUnrecoverableError
163
232
  };
package/lib/core/index.js CHANGED
@@ -28,7 +28,7 @@ module.exports = {
28
28
  ReplSet: require('./topologies/replset'),
29
29
  Mongos: require('./topologies/mongos'),
30
30
  Logger: require('./connection/logger'),
31
- Cursor: require('./cursor'),
31
+ Cursor: require('./cursor').CoreCursor,
32
32
  ReadPreference: require('./topologies/read_preference'),
33
33
  Sessions: require('./sessions'),
34
34
  BSON: BSON,
@@ -14,6 +14,7 @@ const MongoParseError = require('../error').MongoParseError;
14
14
  const MongoNetworkError = require('../error').MongoNetworkError;
15
15
  const collationNotSupported = require('../utils').collationNotSupported;
16
16
  const debugOptions = require('../connection/utils').debugOptions;
17
+ const isSDAMUnrecoverableError = require('../error').isSDAMUnrecoverableError;
17
18
 
18
19
  // Used for filtering out fields for logging
19
20
  const DEBUG_FIELDS = [
@@ -95,6 +96,13 @@ class Server extends EventEmitter {
95
96
  return this.s.description.address;
96
97
  }
97
98
 
99
+ get autoEncrypter() {
100
+ if (this.s.options && this.s.options.autoEncrypter) {
101
+ return this.s.options.autoEncrypter;
102
+ }
103
+ return null;
104
+ }
105
+
98
106
  /**
99
107
  * Initiate server connect
100
108
  */
@@ -154,13 +162,16 @@ class Server extends EventEmitter {
154
162
  if (typeof options === 'function') (callback = options), (options = {});
155
163
  options = Object.assign({}, { force: false }, options);
156
164
 
157
- if (!this.s.pool) {
165
+ const done = err => {
166
+ this.emit('closed');
158
167
  this.s.state = STATE_DISCONNECTED;
159
168
  if (typeof callback === 'function') {
160
- callback(null, null);
169
+ callback(err, null);
161
170
  }
171
+ };
162
172
 
163
- return;
173
+ if (!this.s.pool) {
174
+ return done();
164
175
  }
165
176
 
166
177
  ['close', 'error', 'timeout', 'parseError', 'connect'].forEach(event => {
@@ -171,10 +182,7 @@ class Server extends EventEmitter {
171
182
  clearTimeout(this.s.monitorId);
172
183
  }
173
184
 
174
- this.s.pool.destroy(options.force, err => {
175
- this.s.state = STATE_DISCONNECTED;
176
- callback(err);
177
- });
185
+ this.s.pool.destroy(options.force, done);
178
186
  }
179
187
 
180
188
  /**
@@ -231,7 +239,86 @@ class Server extends EventEmitter {
231
239
  return;
232
240
  }
233
241
 
234
- wireProtocol.command(this, ns, cmd, options, callback);
242
+ wireProtocol.command(this, ns, cmd, options, (err, result) => {
243
+ if (err) {
244
+ if (options.session && err instanceof MongoNetworkError) {
245
+ options.session.serverSession.isDirty = true;
246
+ }
247
+
248
+ if (isSDAMUnrecoverableError(err, this)) {
249
+ this.emit('error', err);
250
+ }
251
+ }
252
+
253
+ callback(err, result);
254
+ });
255
+ }
256
+
257
+ /**
258
+ * Execute a query against the server
259
+ *
260
+ * @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1)
261
+ * @param {object} cmd The command document for the query
262
+ * @param {object} options Optional settings
263
+ * @param {function} callback
264
+ */
265
+ query(ns, cmd, cursorState, options, callback) {
266
+ wireProtocol.query(this, ns, cmd, cursorState, options, (err, result) => {
267
+ if (err) {
268
+ if (options.session && err instanceof MongoNetworkError) {
269
+ options.session.serverSession.isDirty = true;
270
+ }
271
+
272
+ if (isSDAMUnrecoverableError(err, this)) {
273
+ this.emit('error', err);
274
+ }
275
+ }
276
+
277
+ callback(err, result);
278
+ });
279
+ }
280
+
281
+ /**
282
+ * Execute a `getMore` against the server
283
+ *
284
+ * @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1)
285
+ * @param {object} cursorState State data associated with the cursor calling this method
286
+ * @param {object} options Optional settings
287
+ * @param {function} callback
288
+ */
289
+ getMore(ns, cursorState, batchSize, options, callback) {
290
+ wireProtocol.getMore(this, ns, cursorState, batchSize, options, (err, result) => {
291
+ if (err) {
292
+ if (options.session && err instanceof MongoNetworkError) {
293
+ options.session.serverSession.isDirty = true;
294
+ }
295
+
296
+ if (isSDAMUnrecoverableError(err, this)) {
297
+ this.emit('error', err);
298
+ }
299
+ }
300
+
301
+ callback(err, result);
302
+ });
303
+ }
304
+
305
+ /**
306
+ * Execute a `killCursors` command against the server
307
+ *
308
+ * @param {string} ns The MongoDB fully qualified namespace (ex: db1.collection1)
309
+ * @param {object} cursorState State data associated with the cursor calling this method
310
+ * @param {function} callback
311
+ */
312
+ killCursors(ns, cursorState, callback) {
313
+ wireProtocol.killCursors(this, ns, cursorState, (err, result) => {
314
+ if (err && isSDAMUnrecoverableError(err, this)) {
315
+ this.emit('error', err);
316
+ }
317
+
318
+ if (typeof callback === 'function') {
319
+ callback(err, result);
320
+ }
321
+ });
235
322
  }
236
323
 
237
324
  /**
@@ -332,11 +419,23 @@ function executeWriteOperation(args, options, callback) {
332
419
  }
333
420
 
334
421
  if (collationNotSupported(server, options)) {
335
- callback(new MongoError(`server ${this.name} does not support collation`));
422
+ callback(new MongoError(`server ${server.name} does not support collation`));
336
423
  return;
337
424
  }
338
425
 
339
- return wireProtocol[op](server, ns, ops, options, callback);
426
+ return wireProtocol[op](server, ns, ops, options, (err, result) => {
427
+ if (err) {
428
+ if (options.session && err instanceof MongoNetworkError) {
429
+ options.session.serverSession.isDirty = true;
430
+ }
431
+
432
+ if (isSDAMUnrecoverableError(err, server)) {
433
+ server.emit('error', err);
434
+ }
435
+ }
436
+
437
+ callback(err, result);
438
+ });
340
439
  }
341
440
 
342
441
  function connectEventHandler(server) {
@@ -19,6 +19,13 @@ const WRITABLE_SERVER_TYPES = new Set([
19
19
  ServerType.Mongos
20
20
  ]);
21
21
 
22
+ const DATA_BEARING_SERVER_TYPES = new Set([
23
+ ServerType.RSPrimary,
24
+ ServerType.RSSecondary,
25
+ ServerType.Mongos,
26
+ ServerType.Standalone
27
+ ]);
28
+
22
29
  const ISMASTER_FIELDS = [
23
30
  'minWireVersion',
24
31
  'maxWireVersion',
@@ -99,6 +106,13 @@ class ServerDescription {
99
106
  return this.type === ServerType.RSSecondary || this.isWritable;
100
107
  }
101
108
 
109
+ /**
110
+ * @return {Boolean} Is this server data bearing
111
+ */
112
+ get isDataBearing() {
113
+ return DATA_BEARING_SERVER_TYPES.has(this.type);
114
+ }
115
+
102
116
  /**
103
117
  * @return {Boolean} Is this server available for writes
104
118
  */
@@ -13,17 +13,18 @@ const ReadPreference = require('../topologies/read_preference');
13
13
  const readPreferenceServerSelector = require('./server_selectors').readPreferenceServerSelector;
14
14
  const writableServerSelector = require('./server_selectors').writableServerSelector;
15
15
  const isRetryableWritesSupported = require('../topologies/shared').isRetryableWritesSupported;
16
- const Cursor = require('../cursor');
16
+ const CoreCursor = require('../cursor').CoreCursor;
17
17
  const deprecate = require('util').deprecate;
18
18
  const BSON = require('../connection/utils').retrieveBSON();
19
19
  const createCompressionInfo = require('../topologies/shared').createCompressionInfo;
20
20
  const isRetryableError = require('../error').isRetryableError;
21
- const MongoParseError = require('../error').MongoParseError;
21
+ const isSDAMUnrecoverableError = require('../error').isSDAMUnrecoverableError;
22
22
  const ClientSession = require('../sessions').ClientSession;
23
23
  const createClientInfo = require('../topologies/shared').createClientInfo;
24
24
  const MongoError = require('../error').MongoError;
25
25
  const resolveClusterTime = require('../topologies/shared').resolveClusterTime;
26
26
  const SrvPoller = require('./srv_polling').SrvPoller;
27
+ const getMMAPError = require('../topologies/shared').getMMAPError;
27
28
 
28
29
  // Global state
29
30
  let globalTopologyCounter = 0;
@@ -31,8 +32,8 @@ let globalTopologyCounter = 0;
31
32
  // Constants
32
33
  const TOPOLOGY_DEFAULTS = {
33
34
  localThresholdMS: 15,
34
- serverSelectionTimeoutMS: 10000,
35
- heartbeatFrequencyMS: 30000,
35
+ serverSelectionTimeoutMS: 30000,
36
+ heartbeatFrequencyMS: 10000,
36
37
  minHeartbeatFrequencyMS: 500
37
38
  };
38
39
 
@@ -130,7 +131,7 @@ class Topology extends EventEmitter {
130
131
  heartbeatFrequencyMS: options.heartbeatFrequencyMS,
131
132
  minHeartbeatIntervalMS: options.minHeartbeatIntervalMS,
132
133
  // allow users to override the cursor factory
133
- Cursor: options.cursorFactory || Cursor,
134
+ Cursor: options.cursorFactory || CoreCursor,
134
135
  // the bson parser
135
136
  bson: options.bson || new BSON(),
136
137
  // a map of server instances to normalized addresses
@@ -138,7 +139,7 @@ class Topology extends EventEmitter {
138
139
  // Server Session Pool
139
140
  sessionPool: null,
140
141
  // Active client sessions
141
- sessions: [],
142
+ sessions: new Set(),
142
143
  // Promise library
143
144
  promiseLibrary: options.promiseLibrary || Promise,
144
145
  credentials: options.credentials,
@@ -344,8 +345,14 @@ class Topology extends EventEmitter {
344
345
  if (typeof selector !== 'function') {
345
346
  options = selector;
346
347
 
347
- translateReadPreference(options);
348
- const readPreference = options.readPreference || ReadPreference.primary;
348
+ let readPreference;
349
+ if (selector instanceof ReadPreference) {
350
+ readPreference = selector;
351
+ } else {
352
+ translateReadPreference(options);
353
+ readPreference = options.readPreference || ReadPreference.primary;
354
+ }
355
+
349
356
  selector = readPreferenceServerSelector(readPreference);
350
357
  } else {
351
358
  options = {};
@@ -390,6 +397,17 @@ class Topology extends EventEmitter {
390
397
  }
391
398
 
392
399
  // Sessions related methods
400
+
401
+ /**
402
+ * @return Whether the topology should initiate selection to determine session support
403
+ */
404
+ shouldCheckForSessionSupport() {
405
+ return (
406
+ (this.description.type === TopologyType.Single && !this.description.hasKnownServers) ||
407
+ !this.description.hasDataBearingServers
408
+ );
409
+ }
410
+
393
411
  /**
394
412
  * @return Whether sessions are supported on the current topology
395
413
  */
@@ -403,10 +421,10 @@ class Topology extends EventEmitter {
403
421
  startSession(options, clientOptions) {
404
422
  const session = new ClientSession(this, this.s.sessionPool, options, clientOptions);
405
423
  session.once('ended', () => {
406
- this.s.sessions = this.s.sessions.filter(s => !s.equals(session));
424
+ this.s.sessions.delete(session);
407
425
  });
408
426
 
409
- this.s.sessions.push(session);
427
+ this.s.sessions.add(session);
410
428
  return session;
411
429
  }
412
430
 
@@ -632,7 +650,7 @@ class Topology extends EventEmitter {
632
650
  const CursorClass = options.cursorFactory || this.s.Cursor;
633
651
  translateReadPreference(options);
634
652
 
635
- return new CursorClass(this.s.bson, ns, cmd, options, topology, this.s.options);
653
+ return new CursorClass(topology, ns, cmd, options);
636
654
  }
637
655
 
638
656
  get clientInfo() {
@@ -919,7 +937,7 @@ function serverErrorEventHandler(server, topology) {
919
937
  new monitoring.ServerClosedEvent(topology.s.id, server.description.address)
920
938
  );
921
939
 
922
- if (err instanceof MongoParseError) {
940
+ if (isSDAMUnrecoverableError(err, server)) {
923
941
  resetServerState(server, err, { clearPool: true });
924
942
  return;
925
943
  }
@@ -954,6 +972,7 @@ function executeWriteOperation(args, options, callback) {
954
972
  const handler = (err, result) => {
955
973
  if (!err) return callback(null, result);
956
974
  if (!isRetryableError(err)) {
975
+ err = getMMAPError(err);
957
976
  return callback(err);
958
977
  }
959
978
 
@@ -997,10 +1016,11 @@ function resetServerState(server, error, options) {
997
1016
  'descriptionReceived',
998
1017
  new ServerDescription(server.description.address, null, { error })
999
1018
  );
1019
+ server.monitor();
1000
1020
  }
1001
1021
 
1002
- if (options.clearPool && server.pool) {
1003
- server.pool.reset(() => resetState());
1022
+ if (options.clearPool && server.s.pool) {
1023
+ server.s.pool.reset(() => resetState());
1004
1024
  return;
1005
1025
  }
1006
1026
 
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
  const ServerType = require('./server_description').ServerType;
3
3
  const ServerDescription = require('./server_description').ServerDescription;
4
- const ReadPreference = require('../topologies/read_preference');
5
4
  const WIRE_CONSTANTS = require('../wireprotocol/constants');
6
5
 
7
6
  // contstants related to compatability checks
@@ -258,24 +257,17 @@ class TopologyDescription {
258
257
  }
259
258
 
260
259
  /**
261
- * Determines if the topology has a readable server available. See the table in the
262
- * following section for behaviour rules.
263
- *
264
- * @param {ReadPreference} [readPreference] An optional read preference for determining if a readable server is present
265
- * @return {Boolean} Whether there is a readable server in this topology
260
+ * Determines if the topology description has any known servers
266
261
  */
267
- hasReadableServer(/* readPreference */) {
268
- // To be implemented when server selection is implemented
262
+ get hasKnownServers() {
263
+ return Array.from(this.servers.values()).some(sd => sd.type !== ServerDescription.Unknown);
269
264
  }
270
265
 
271
266
  /**
272
- * Determines if the topology has a writable server available. See the table in the
273
- * following section for behaviour rules.
274
- *
275
- * @return {Boolean} Whether there is a writable server in this topology
267
+ * Determines if this topology description has a data-bearing server available.
276
268
  */
277
- hasWritableServer() {
278
- return this.hasReadableServer(ReadPreference.primary);
269
+ get hasDataBearingServers() {
270
+ return Array.from(this.servers.values()).some(sd => sd.isDataBearing);
279
271
  }
280
272
 
281
273
  /**
@@ -18,7 +18,7 @@ const resolveClusterTime = require('./topologies/shared').resolveClusterTime;
18
18
  const isSharded = require('./wireprotocol/shared').isSharded;
19
19
  const maxWireVersion = require('./utils').maxWireVersion;
20
20
 
21
- const MAX_FOR_TRANSACTIONS = 7;
21
+ const minWireVersionForShardedTransactions = 8;
22
22
 
23
23
  function assertAlive(session, callback) {
24
24
  if (session.serverSession == null) {
@@ -191,8 +191,9 @@ class ClientSession extends EventEmitter {
191
191
 
192
192
  const topologyMaxWireVersion = maxWireVersion(this.topology);
193
193
  if (
194
- isSharded(this.topology) ||
195
- (topologyMaxWireVersion != null && topologyMaxWireVersion < MAX_FOR_TRANSACTIONS)
194
+ isSharded(this.topology) &&
195
+ topologyMaxWireVersion != null &&
196
+ topologyMaxWireVersion < minWireVersionForShardedTransactions
196
197
  ) {
197
198
  throw new MongoError('Transactions are not supported on sharded clusters in MongoDB < 4.2.');
198
199
  }
@@ -286,6 +287,7 @@ class ClientSession extends EventEmitter {
286
287
  const MAX_WITH_TRANSACTION_TIMEOUT = 120000;
287
288
  const UNSATISFIABLE_WRITE_CONCERN_CODE = 100;
288
289
  const UNKNOWN_REPL_WRITE_CONCERN_CODE = 79;
290
+ const MAX_TIME_MS_EXPIRED_CODE = 50;
289
291
  const NON_DETERMINISTIC_WRITE_CONCERN_ERRORS = new Set([
290
292
  'CannotSatisfyWriteConcern',
291
293
  'UnknownReplWriteConcern',
@@ -298,15 +300,27 @@ function hasNotTimedOut(startTime, max) {
298
300
 
299
301
  function isUnknownTransactionCommitResult(err) {
300
302
  return (
301
- !NON_DETERMINISTIC_WRITE_CONCERN_ERRORS.has(err.codeName) &&
302
- err.code !== UNSATISFIABLE_WRITE_CONCERN_CODE &&
303
- err.code !== UNKNOWN_REPL_WRITE_CONCERN_CODE
303
+ isMaxTimeMSExpiredError(err) ||
304
+ (!NON_DETERMINISTIC_WRITE_CONCERN_ERRORS.has(err.codeName) &&
305
+ err.code !== UNSATISFIABLE_WRITE_CONCERN_CODE &&
306
+ err.code !== UNKNOWN_REPL_WRITE_CONCERN_CODE)
307
+ );
308
+ }
309
+
310
+ function isMaxTimeMSExpiredError(err) {
311
+ return (
312
+ err.code === MAX_TIME_MS_EXPIRED_CODE ||
313
+ (err.writeConcernError && err.writeConcernError.code === MAX_TIME_MS_EXPIRED_CODE)
304
314
  );
305
315
  }
306
316
 
307
317
  function attemptTransactionCommit(session, startTime, fn, options) {
308
318
  return session.commitTransaction().catch(err => {
309
- if (err instanceof MongoError && hasNotTimedOut(startTime, MAX_WITH_TRANSACTION_TIMEOUT)) {
319
+ if (
320
+ err instanceof MongoError &&
321
+ hasNotTimedOut(startTime, MAX_WITH_TRANSACTION_TIMEOUT) &&
322
+ !isMaxTimeMSExpiredError(err)
323
+ ) {
310
324
  if (err.hasErrorLabel('UnknownTransactionCommitResult')) {
311
325
  return attemptTransactionCommit(session, startTime, fn, options);
312
326
  }
@@ -363,6 +377,13 @@ function attemptTransaction(session, startTime, fn, options) {
363
377
  return attemptTransaction(session, startTime, fn, options);
364
378
  }
365
379
 
380
+ if (isMaxTimeMSExpiredError(err)) {
381
+ if (err.errorLabels == null) {
382
+ err.errorLabels = [];
383
+ }
384
+ err.errorLabels.push('UnknownTransactionCommitResult');
385
+ }
386
+
366
387
  throw err;
367
388
  }
368
389
 
@@ -444,6 +465,10 @@ function endTransaction(session, commandName, callback) {
444
465
  Object.assign(command, { writeConcern });
445
466
  }
446
467
 
468
+ if (commandName === 'commitTransaction' && session.transaction.options.maxTimeMS) {
469
+ Object.assign(command, { maxTimeMS: session.transaction.options.maxTimeMS });
470
+ }
471
+
447
472
  function commandHandler(e, r) {
448
473
  if (commandName === 'commitTransaction') {
449
474
  session.transaction.transition(TxnState.TRANSACTION_COMMITTED);
@@ -452,7 +477,8 @@ function endTransaction(session, commandName, callback) {
452
477
  e &&
453
478
  (e instanceof MongoNetworkError ||
454
479
  e instanceof MongoWriteConcernError ||
455
- isRetryableError(e))
480
+ isRetryableError(e) ||
481
+ isMaxTimeMSExpiredError(e))
456
482
  ) {
457
483
  if (e.errorLabels) {
458
484
  const idx = e.errorLabels.indexOf('TransientTransactionError');
@@ -527,6 +553,7 @@ class ServerSession {
527
553
  this.id = { id: new Binary(uuidV4(), Binary.SUBTYPE_UUID) };
528
554
  this.lastUse = Date.now();
529
555
  this.txnNumber = 0;
556
+ this.isDirty = false;
530
557
  }
531
558
 
532
559
  /**
@@ -603,8 +630,8 @@ class ServerSessionPool {
603
630
  release(session) {
604
631
  const sessionTimeoutMinutes = this.topology.logicalSessionTimeoutMinutes;
605
632
  while (this.sessions.length) {
606
- const session = this.sessions[this.sessions.length - 1];
607
- if (session.hasTimedOut(sessionTimeoutMinutes)) {
633
+ const pooledSession = this.sessions[this.sessions.length - 1];
634
+ if (pooledSession.hasTimedOut(sessionTimeoutMinutes)) {
608
635
  this.sessions.pop();
609
636
  } else {
610
637
  break;
@@ -612,11 +639,38 @@ class ServerSessionPool {
612
639
  }
613
640
 
614
641
  if (!session.hasTimedOut(sessionTimeoutMinutes)) {
642
+ if (session.isDirty) {
643
+ return;
644
+ }
645
+
646
+ // otherwise, readd this session to the session pool
615
647
  this.sessions.unshift(session);
616
648
  }
617
649
  }
618
650
  }
619
651
 
652
+ // TODO: this should be codified in command construction
653
+ // @see https://github.com/mongodb/specifications/blob/master/source/read-write-concern/read-write-concern.rst#read-concern
654
+ function commandSupportsReadConcern(command, options) {
655
+ if (
656
+ command.aggregate ||
657
+ command.count ||
658
+ command.distinct ||
659
+ command.find ||
660
+ command.parallelCollectionScan ||
661
+ command.geoNear ||
662
+ command.geoSearch
663
+ ) {
664
+ return true;
665
+ }
666
+
667
+ if (command.mapReduce && options.out && (options.out.inline === 1 || options.out === 'inline')) {
668
+ return true;
669
+ }
670
+
671
+ return false;
672
+ }
673
+
620
674
  /**
621
675
  * Optionally decorate a command with sessions specific keys
622
676
  *
@@ -640,6 +694,7 @@ function applySession(session, command, options) {
640
694
  // first apply non-transaction-specific sessions data
641
695
  const inTransaction = session.inTransaction() || isTransactionCommand(command);
642
696
  const isRetryableWrite = options.willRetryWrite;
697
+ const shouldApplyReadConcern = commandSupportsReadConcern(command);
643
698
 
644
699
  if (serverSession.txnNumber && (isRetryableWrite || inTransaction)) {
645
700
  command.txnNumber = BSON.Long.fromNumber(serverSession.txnNumber);
@@ -653,7 +708,7 @@ function applySession(session, command, options) {
653
708
 
654
709
  // TODO: the following should only be applied to read operation per spec.
655
710
  // for causal consistency
656
- if (session.supports.causalConsistency && session.operationTime) {
711
+ if (session.supports.causalConsistency && session.operationTime && shouldApplyReadConcern) {
657
712
  command.readConcern = command.readConcern || {};
658
713
  Object.assign(command.readConcern, { afterClusterTime: session.operationTime });
659
714
  }
@@ -707,5 +762,6 @@ module.exports = {
707
762
  ServerSessionPool,
708
763
  TxnState,
709
764
  applySession,
710
- updateSessionFromResponse
765
+ updateSessionFromResponse,
766
+ commandSupportsReadConcern
711
767
  };