mongodb 3.5.7 → 3.5.8

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.
package/HISTORY.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ <a name="3.5.8"></a>
6
+ ## [3.5.8](https://github.com/mongodb/node-mongodb-native/compare/v3.5.7...v3.5.8) (2020-05-28)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * always clear cancelled wait queue members during processing ([0394f9d](https://github.com/mongodb/node-mongodb-native/commit/0394f9d))
12
+ * always include `writeErrors` on a `BulkWriteError` instance ([58b4f94](https://github.com/mongodb/node-mongodb-native/commit/58b4f94))
13
+ * ensure implicit sessions are ended consistently ([5c6fda1](https://github.com/mongodb/node-mongodb-native/commit/5c6fda1))
14
+ * filter servers before applying reducers ([4faf9f5](https://github.com/mongodb/node-mongodb-native/commit/4faf9f5))
15
+ * unordered bulk write should attempt to execute all batches ([6cee96b](https://github.com/mongodb/node-mongodb-native/commit/6cee96b))
16
+ * **ChangeStream:** should resume from errors when iterating ([5ecf18e](https://github.com/mongodb/node-mongodb-native/commit/5ecf18e))
17
+ * honor journal=true in connection string ([#2359](https://github.com/mongodb/node-mongodb-native/issues/2359)) ([246669f](https://github.com/mongodb/node-mongodb-native/commit/246669f))
18
+ * **ChangeStream:** whitelist resumable errors ([#2337](https://github.com/mongodb/node-mongodb-native/issues/2337)) ([a9d3965](https://github.com/mongodb/node-mongodb-native/commit/a9d3965)), closes [#17](https://github.com/mongodb/node-mongodb-native/issues/17) [#18](https://github.com/mongodb/node-mongodb-native/issues/18)
19
+
20
+
21
+
5
22
  <a name="3.5.7"></a>
6
23
  ## [3.5.7](https://github.com/mongodb/node-mongodb-native/compare/v3.5.6...v3.5.7) (2020-04-29)
7
24
 
@@ -1200,19 +1200,10 @@ class BulkOperationBase {
1200
1200
  * @ignore
1201
1201
  * @param {function} callback
1202
1202
  * @param {BulkWriteResult} writeResult
1203
- * @param {class} self either OrderedBulkOperation or UnorderdBulkOperation
1203
+ * @param {class} self either OrderedBulkOperation or UnorderedBulkOperation
1204
1204
  */
1205
1205
  handleWriteError(callback, writeResult) {
1206
1206
  if (this.s.bulkResult.writeErrors.length > 0) {
1207
- if (this.s.bulkResult.writeErrors.length === 1) {
1208
- handleCallback(
1209
- callback,
1210
- new BulkWriteError(toError(this.s.bulkResult.writeErrors[0]), writeResult),
1211
- null
1212
- );
1213
- return true;
1214
- }
1215
-
1216
1207
  const msg = this.s.bulkResult.writeErrors[0].errmsg
1217
1208
  ? this.s.bulkResult.writeErrors[0].errmsg
1218
1209
  : 'write operation failed';
@@ -1230,7 +1221,9 @@ class BulkOperationBase {
1230
1221
  null
1231
1222
  );
1232
1223
  return true;
1233
- } else if (writeResult.getWriteConcernError()) {
1224
+ }
1225
+
1226
+ if (writeResult.getWriteConcernError()) {
1234
1227
  handleCallback(
1235
1228
  callback,
1236
1229
  new BulkWriteError(toError(writeResult.getWriteConcernError()), writeResult),
@@ -108,6 +108,14 @@ class UnorderedBulkOperation extends BulkOperationBase {
108
108
 
109
109
  super(topology, collection, options, false);
110
110
  }
111
+
112
+ handleWriteError(callback, writeResult) {
113
+ if (this.s.batches.length) {
114
+ return false;
115
+ }
116
+
117
+ return super.handleWriteError(callback, writeResult);
118
+ }
111
119
  }
112
120
 
113
121
  /**
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ const Denque = require('denque');
3
4
  const EventEmitter = require('events');
4
5
  const isResumableError = require('./error').isResumableError;
5
6
  const MongoError = require('./core').MongoError;
@@ -9,6 +10,8 @@ const maxWireVersion = require('./core/utils').maxWireVersion;
9
10
  const maybePromise = require('./utils').maybePromise;
10
11
  const AggregateOperation = require('./operations/aggregate');
11
12
 
13
+ const kResumeQueue = Symbol('resumeQueue');
14
+
12
15
  const CHANGE_STREAM_OPTIONS = ['resumeAfter', 'startAfter', 'startAtOperationTime', 'fullDocument'];
13
16
  const CURSOR_OPTIONS = ['batchSize', 'maxAwaitTimeMS', 'collation', 'readPreference'].concat(
14
17
  CHANGE_STREAM_OPTIONS
@@ -91,15 +94,17 @@ class ChangeStream extends EventEmitter {
91
94
  this.options.readPreference = parent.s.readPreference;
92
95
  }
93
96
 
97
+ this[kResumeQueue] = new Denque();
98
+
94
99
  // Create contained Change Stream cursor
95
100
  this.cursor = createChangeStreamCursor(this, options);
96
101
 
102
+ this.closed = false;
103
+
97
104
  // Listen for any `change` listeners being added to ChangeStream
98
105
  this.on('newListener', eventName => {
99
106
  if (eventName === 'change' && this.cursor && this.listenerCount('change') === 0) {
100
- this.cursor.on('data', change =>
101
- processNewChange({ changeStream: this, change, eventEmitter: true })
102
- );
107
+ this.cursor.on('data', change => processNewChange(this, change));
103
108
  }
104
109
  });
105
110
 
@@ -128,7 +133,12 @@ class ChangeStream extends EventEmitter {
128
133
  * @returns {Promise|void} returns Promise if no callback passed
129
134
  */
130
135
  hasNext(callback) {
131
- return maybePromise(this.parent, callback, cb => this.cursor.hasNext(cb));
136
+ return maybePromise(this.parent, callback, cb => {
137
+ getCursor(this, (err, cursor) => {
138
+ if (err) return cb(err); // failed to resume, raise an error
139
+ cursor.hasNext(cb);
140
+ });
141
+ });
132
142
  }
133
143
 
134
144
  /**
@@ -140,25 +150,28 @@ class ChangeStream extends EventEmitter {
140
150
  */
141
151
  next(callback) {
142
152
  return maybePromise(this.parent, callback, cb => {
143
- if (this.isClosed()) {
144
- return cb(new Error('Change Stream is not open.'));
145
- }
146
- this.cursor.next((error, change) => {
147
- processNewChange({ changeStream: this, error, change, callback: cb });
153
+ getCursor(this, (err, cursor) => {
154
+ if (err) return cb(err); // failed to resume, raise an error
155
+ cursor.next((error, change) => {
156
+ if (error) {
157
+ this[kResumeQueue].push(() => this.next(cb));
158
+ processError(this, error, cb);
159
+ return;
160
+ }
161
+ processNewChange(this, change, cb);
162
+ });
148
163
  });
149
164
  });
150
165
  }
151
166
 
152
167
  /**
153
- * Is the cursor closed
168
+ * Is the change stream closed
154
169
  * @method ChangeStream.prototype.isClosed
170
+ * @param {boolean} [checkCursor=true] also check if the underlying cursor is closed
155
171
  * @return {boolean}
156
172
  */
157
173
  isClosed() {
158
- if (this.cursor) {
159
- return this.cursor.isClosed();
160
- }
161
- return true;
174
+ return this.closed || (this.cursor && this.cursor.isClosed());
162
175
  }
163
176
 
164
177
  /**
@@ -168,31 +181,22 @@ class ChangeStream extends EventEmitter {
168
181
  * @return {Promise} returns Promise if no callback passed
169
182
  */
170
183
  close(callback) {
171
- if (!this.cursor) {
172
- if (callback) return callback();
173
- return this.promiseLibrary.resolve();
174
- }
184
+ return maybePromise(this.parent, callback, cb => {
185
+ if (this.closed) return cb();
175
186
 
176
- // Tidy up the existing cursor
177
- const cursor = this.cursor;
187
+ // flag the change stream as explicitly closed
188
+ this.closed = true;
178
189
 
179
- if (callback) {
180
- return cursor.close(err => {
181
- ['data', 'close', 'end', 'error'].forEach(event => cursor.removeAllListeners(event));
182
- delete this.cursor;
190
+ if (!this.cursor) return cb();
183
191
 
184
- return callback(err);
185
- });
186
- }
192
+ // Tidy up the existing cursor
193
+ const cursor = this.cursor;
187
194
 
188
- const PromiseCtor = this.promiseLibrary || Promise;
189
- return new PromiseCtor((resolve, reject) => {
190
- cursor.close(err => {
195
+ return cursor.close(err => {
191
196
  ['data', 'close', 'end', 'error'].forEach(event => cursor.removeAllListeners(event));
192
- delete this.cursor;
197
+ this.cursor = undefined;
193
198
 
194
- if (err) return reject(err);
195
- resolve();
199
+ return cb(err);
196
200
  });
197
201
  });
198
202
  }
@@ -287,7 +291,9 @@ class ChangeStreamCursor extends Cursor {
287
291
  ['resumeAfter', 'startAfter', 'startAtOperationTime'].forEach(key => delete result[key]);
288
292
 
289
293
  if (this.resumeToken) {
290
- result.resumeAfter = this.resumeToken;
294
+ const resumeKey =
295
+ this.options.startAfter && !this.hasReceived ? 'startAfter' : 'resumeAfter';
296
+ result[resumeKey] = this.resumeToken;
291
297
  } else if (this.startAtOperationTime && maxWireVersion(this.server) >= 7) {
292
298
  result.startAtOperationTime = this.startAtOperationTime;
293
299
  }
@@ -296,10 +302,30 @@ class ChangeStreamCursor extends Cursor {
296
302
  return result;
297
303
  }
298
304
 
305
+ cacheResumeToken(resumeToken) {
306
+ if (this.bufferedCount() === 0 && this.cursorState.postBatchResumeToken) {
307
+ this.resumeToken = this.cursorState.postBatchResumeToken;
308
+ } else {
309
+ this.resumeToken = resumeToken;
310
+ }
311
+ this.hasReceived = true;
312
+ }
313
+
314
+ _processBatch(batchName, response) {
315
+ const cursor = response.cursor;
316
+ if (cursor.postBatchResumeToken) {
317
+ this.cursorState.postBatchResumeToken = cursor.postBatchResumeToken;
318
+
319
+ if (cursor[batchName].length === 0) {
320
+ this.resumeToken = cursor.postBatchResumeToken;
321
+ }
322
+ }
323
+ }
324
+
299
325
  _initializeCursor(callback) {
300
326
  super._initializeCursor((err, result) => {
301
327
  if (err) {
302
- callback(err, null);
328
+ callback(err);
303
329
  return;
304
330
  }
305
331
 
@@ -314,15 +340,9 @@ class ChangeStreamCursor extends Cursor {
314
340
  this.startAtOperationTime = response.operationTime;
315
341
  }
316
342
 
317
- const cursor = response.cursor;
318
- if (cursor.postBatchResumeToken) {
319
- this.cursorState.postBatchResumeToken = cursor.postBatchResumeToken;
320
-
321
- if (cursor.firstBatch.length === 0) {
322
- this.resumeToken = cursor.postBatchResumeToken;
323
- }
324
- }
343
+ this._processBatch('firstBatch', response);
325
344
 
345
+ this.emit('init', result);
326
346
  this.emit('response');
327
347
  callback(err, result);
328
348
  });
@@ -331,19 +351,13 @@ class ChangeStreamCursor extends Cursor {
331
351
  _getMore(callback) {
332
352
  super._getMore((err, response) => {
333
353
  if (err) {
334
- callback(err, null);
354
+ callback(err);
335
355
  return;
336
356
  }
337
357
 
338
- const cursor = response.cursor;
339
- if (cursor.postBatchResumeToken) {
340
- this.cursorState.postBatchResumeToken = cursor.postBatchResumeToken;
341
-
342
- if (cursor.nextBatch.length === 0) {
343
- this.resumeToken = cursor.postBatchResumeToken;
344
- }
345
- }
358
+ this._processBatch('nextBatch', response);
346
359
 
360
+ this.emit('more', response);
347
361
  this.emit('response');
348
362
  callback(err, response);
349
363
  });
@@ -366,6 +380,7 @@ function createChangeStreamCursor(self, options) {
366
380
 
367
381
  const pipeline = [{ $changeStream: changeStreamStageOptions }].concat(self.pipeline);
368
382
  const cursorOptions = applyKnownOptions({}, options, CURSOR_OPTIONS);
383
+
369
384
  const changeStreamCursor = new ChangeStreamCursor(
370
385
  self.topology,
371
386
  new AggregateOperation(self.parent, pipeline, options),
@@ -384,7 +399,7 @@ function createChangeStreamCursor(self, options) {
384
399
  */
385
400
  if (self.listenerCount('change') > 0) {
386
401
  changeStreamCursor.on('data', function(change) {
387
- processNewChange({ changeStream: self, change, eventEmitter: true });
402
+ processNewChange(self, change);
388
403
  });
389
404
  }
390
405
 
@@ -416,7 +431,7 @@ function createChangeStreamCursor(self, options) {
416
431
  * @type {Error}
417
432
  */
418
433
  changeStreamCursor.on('error', function(error) {
419
- processNewChange({ changeStream: self, error, eventEmitter: true });
434
+ processError(self, error);
420
435
  });
421
436
 
422
437
  if (self.pipeDestinations) {
@@ -449,120 +464,141 @@ function waitForTopologyConnected(topology, options, callback) {
449
464
  const timeout = options.timeout || SELECTION_TIMEOUT;
450
465
  const readPreference = options.readPreference;
451
466
 
452
- if (topology.isConnected({ readPreference })) return callback(null, null);
467
+ if (topology.isConnected({ readPreference })) return callback();
453
468
  const hrElapsed = process.hrtime(start);
454
469
  const elapsed = (hrElapsed[0] * 1e9 + hrElapsed[1]) / 1e6;
455
470
  if (elapsed > timeout) return callback(new MongoError('Timed out waiting for connection'));
456
471
  waitForTopologyConnected(topology, options, callback);
457
- }, 3000); // this is an arbitrary wait time to allow SDAM to transition
472
+ }, 500); // this is an arbitrary wait time to allow SDAM to transition
458
473
  }
459
474
 
460
- // Handle new change events. This method brings together the routes from the callback, event emitter, and promise ways of using ChangeStream.
461
- function processNewChange(args) {
462
- const changeStream = args.changeStream;
463
- const error = args.error;
464
- const change = args.change;
465
- const callback = args.callback;
466
- const eventEmitter = args.eventEmitter || false;
475
+ function processNewChange(changeStream, change, callback) {
476
+ const cursor = changeStream.cursor;
467
477
 
468
- // If the changeStream is closed, then it should not process a change.
469
- if (changeStream.isClosed()) {
470
- // We do not error in the eventEmitter case.
471
- if (eventEmitter) {
472
- return;
473
- }
478
+ if (changeStream.closed) {
479
+ if (callback) callback(new MongoError('ChangeStream is closed'));
480
+ return;
481
+ }
474
482
 
475
- const error = new MongoError('ChangeStream is closed');
476
- return typeof callback === 'function'
477
- ? callback(error, null)
478
- : changeStream.promiseLibrary.reject(error);
483
+ if (change && !change._id) {
484
+ const noResumeTokenError = new Error(
485
+ 'A change stream document has been received that lacks a resume token (_id).'
486
+ );
487
+
488
+ if (!callback) return changeStream.emit('error', noResumeTokenError);
489
+ return callback(noResumeTokenError);
479
490
  }
480
491
 
481
- const cursor = changeStream.cursor;
492
+ // cache the resume token
493
+ cursor.cacheResumeToken(change._id);
494
+
495
+ // wipe the startAtOperationTime if there was one so that there won't be a conflict
496
+ // between resumeToken and startAtOperationTime if we need to reconnect the cursor
497
+ changeStream.options.startAtOperationTime = undefined;
498
+
499
+ // Return the change
500
+ if (!callback) return changeStream.emit('change', change);
501
+ return callback(undefined, change);
502
+ }
503
+
504
+ function processError(changeStream, error, callback) {
482
505
  const topology = changeStream.topology;
483
- const options = changeStream.cursor.options;
506
+ const cursor = changeStream.cursor;
484
507
 
485
- if (error) {
486
- if (isResumableError(error) && !changeStream.attemptingResume) {
487
- changeStream.attemptingResume = true;
508
+ // If the change stream has been closed explictly, do not process error.
509
+ if (changeStream.closed) {
510
+ if (callback) callback(new MongoError('ChangeStream is closed'));
511
+ return;
512
+ }
488
513
 
489
- // stop listening to all events from old cursor
490
- ['data', 'close', 'end', 'error'].forEach(event =>
491
- changeStream.cursor.removeAllListeners(event)
492
- );
514
+ // if the resume succeeds, continue with the new cursor
515
+ function resumeWithCursor(newCursor) {
516
+ changeStream.cursor = newCursor;
517
+ processResumeQueue(changeStream);
518
+ }
493
519
 
494
- // close internal cursor, ignore errors
495
- changeStream.cursor.close();
520
+ // otherwise, raise an error and close the change stream
521
+ function unresumableError(err) {
522
+ if (!callback) {
523
+ changeStream.emit('error', err);
524
+ changeStream.emit('close');
525
+ }
526
+ processResumeQueue(changeStream, err);
527
+ changeStream.closed = true;
528
+ }
496
529
 
497
- // attempt recreating the cursor
498
- if (eventEmitter) {
499
- waitForTopologyConnected(topology, { readPreference: options.readPreference }, err => {
500
- if (err) {
501
- changeStream.emit('error', err);
502
- changeStream.emit('close');
503
- return;
504
- }
505
- changeStream.cursor = createChangeStreamCursor(changeStream, cursor.resumeOptions);
506
- });
530
+ if (cursor && isResumableError(error, maxWireVersion(cursor.server))) {
531
+ changeStream.cursor = undefined;
507
532
 
508
- return;
509
- }
533
+ // stop listening to all events from old cursor
534
+ ['data', 'close', 'end', 'error'].forEach(event => cursor.removeAllListeners(event));
510
535
 
511
- if (callback) {
512
- waitForTopologyConnected(topology, { readPreference: options.readPreference }, err => {
513
- if (err) return callback(err, null);
536
+ // close internal cursor, ignore errors
537
+ cursor.close();
514
538
 
515
- changeStream.cursor = createChangeStreamCursor(changeStream, cursor.resumeOptions);
516
- changeStream.next(callback);
517
- });
539
+ waitForTopologyConnected(topology, { readPreference: cursor.options.readPreference }, err => {
540
+ // if the topology can't reconnect, close the stream
541
+ if (err) return unresumableError(err);
518
542
 
519
- return;
520
- }
543
+ // create a new cursor, preserving the old cursor's options
544
+ const newCursor = createChangeStreamCursor(changeStream, cursor.resumeOptions);
521
545
 
522
- return new Promise((resolve, reject) => {
523
- waitForTopologyConnected(topology, { readPreference: options.readPreference }, err => {
524
- if (err) return reject(err);
525
- resolve();
526
- });
527
- })
528
- .then(
529
- () => (changeStream.cursor = createChangeStreamCursor(changeStream, cursor.resumeOptions))
530
- )
531
- .then(() => changeStream.next());
532
- }
546
+ // attempt to continue in emitter mode
547
+ if (!callback) return resumeWithCursor(newCursor);
533
548
 
534
- if (eventEmitter) return changeStream.emit('error', error);
535
- if (typeof callback === 'function') return callback(error, null);
536
- return changeStream.promiseLibrary.reject(error);
549
+ // attempt to continue in iterator mode
550
+ newCursor.hasNext(err => {
551
+ // if there's an error immediately after resuming, close the stream
552
+ if (err) return unresumableError(err);
553
+ resumeWithCursor(newCursor);
554
+ });
555
+ });
556
+ return;
537
557
  }
538
558
 
539
- changeStream.attemptingResume = false;
540
-
541
- if (change && !change._id) {
542
- const noResumeTokenError = new Error(
543
- 'A change stream document has been received that lacks a resume token (_id).'
544
- );
559
+ if (!callback) return changeStream.emit('error', error);
560
+ return callback(error);
561
+ }
545
562
 
546
- if (eventEmitter) return changeStream.emit('error', noResumeTokenError);
547
- if (typeof callback === 'function') return callback(noResumeTokenError, null);
548
- return changeStream.promiseLibrary.reject(noResumeTokenError);
563
+ /**
564
+ * Safely provides a cursor across resume attempts
565
+ *
566
+ * @param {ChangeStream} changeStream the parent ChangeStream
567
+ * @param {function} callback gets the cursor or error
568
+ * @param {ChangeStreamCursor} [oldCursor] when resuming from an error, carry over options from previous cursor
569
+ */
570
+ function getCursor(changeStream, callback) {
571
+ if (changeStream.isClosed()) {
572
+ callback(new MongoError('ChangeStream is closed.'));
573
+ return;
549
574
  }
550
575
 
551
- // cache the resume token
552
- if (cursor.bufferedCount() === 0 && cursor.cursorState.postBatchResumeToken) {
553
- cursor.resumeToken = cursor.cursorState.postBatchResumeToken;
554
- } else {
555
- cursor.resumeToken = change._id;
576
+ // if a cursor exists and it is open, return it
577
+ if (changeStream.cursor) {
578
+ callback(undefined, changeStream.cursor);
579
+ return;
556
580
  }
557
581
 
558
- // wipe the startAtOperationTime if there was one so that there won't be a conflict
559
- // between resumeToken and startAtOperationTime if we need to reconnect the cursor
560
- changeStream.options.startAtOperationTime = undefined;
582
+ // no cursor, queue callback until topology reconnects
583
+ changeStream[kResumeQueue].push(callback);
584
+ }
561
585
 
562
- // Return the change
563
- if (eventEmitter) return changeStream.emit('change', change);
564
- if (typeof callback === 'function') return callback(error, change);
565
- return changeStream.promiseLibrary.resolve(change);
586
+ /**
587
+ * Drain the resume queue when a new has become available
588
+ *
589
+ * @param {ChangeStream} changeStream the parent ChangeStream
590
+ * @param {ChangeStreamCursor?} changeStream.cursor the new cursor
591
+ * @param {Error} [err] error getting a new cursor
592
+ */
593
+ function processResumeQueue(changeStream, err) {
594
+ while (changeStream[kResumeQueue].length) {
595
+ const request = changeStream[kResumeQueue].pop();
596
+ if (changeStream.isClosed() && !err) {
597
+ request(new MongoError('Change Stream is not open.'));
598
+ return;
599
+ }
600
+ request(err, changeStream.cursor);
601
+ }
566
602
  }
567
603
 
568
604
  /**
@@ -198,6 +198,10 @@ class ConnectionPool extends EventEmitter {
198
198
  return this[kConnections].length;
199
199
  }
200
200
 
201
+ get waitQueueSize() {
202
+ return this[kWaitQueue].length;
203
+ }
204
+
201
205
  /**
202
206
  * Check a connection out of this pool. The connection will continue to be tracked, but no reference to it
203
207
  * will be held by the pool. This means that if a connection is checked out it MUST be checked back in or
@@ -295,7 +299,7 @@ class ConnectionPool extends EventEmitter {
295
299
  this[kCancellationToken].emit('cancel');
296
300
 
297
301
  // drain the wait queue
298
- while (this[kWaitQueue].length) {
302
+ while (this.waitQueueSize) {
299
303
  const waitQueueMember = this[kWaitQueue].pop();
300
304
  clearTimeout(waitQueueMember.timer);
301
305
  if (!waitQueueMember[kCancelled]) {
@@ -449,13 +453,17 @@ function processWaitQueue(pool) {
449
453
  return;
450
454
  }
451
455
 
452
- while (pool[kWaitQueue].length && pool.availableConnectionCount) {
456
+ while (pool.waitQueueSize) {
453
457
  const waitQueueMember = pool[kWaitQueue].peekFront();
454
458
  if (waitQueueMember[kCancelled]) {
455
459
  pool[kWaitQueue].shift();
456
460
  continue;
457
461
  }
458
462
 
463
+ if (!pool.availableConnectionCount) {
464
+ break;
465
+ }
466
+
459
467
  const connection = pool[kConnections].shift();
460
468
  const isStale = connectionIsStale(pool, connection);
461
469
  const isIdle = connectionIsIdle(pool, connection);
@@ -472,7 +480,7 @@ function processWaitQueue(pool) {
472
480
  }
473
481
 
474
482
  const maxPoolSize = pool.options.maxPoolSize;
475
- if (pool[kWaitQueue].length && (maxPoolSize <= 0 || pool.totalConnectionCount < maxPoolSize)) {
483
+ if (pool.waitQueueSize && (maxPoolSize <= 0 || pool.totalConnectionCount < maxPoolSize)) {
476
484
  createConnection(pool, (err, connection) => {
477
485
  const waitQueueMember = pool[kWaitQueue].shift();
478
486
  if (waitQueueMember == null) {
@@ -4,7 +4,6 @@ const Logger = require('./connection/logger');
4
4
  const retrieveBSON = require('./connection/utils').retrieveBSON;
5
5
  const MongoError = require('./error').MongoError;
6
6
  const MongoNetworkError = require('./error').MongoNetworkError;
7
- const mongoErrorContextSymbol = require('./error').mongoErrorContextSymbol;
8
7
  const collationNotSupported = require('./utils').collationNotSupported;
9
8
  const ReadPreference = require('./topologies/read_preference');
10
9
  const isUnifiedTopology = require('./utils').isUnifiedTopology;
@@ -412,7 +411,15 @@ class CoreCursor extends Readable {
412
411
  batchSize = this.cursorState.limit - this.cursorState.currentLimit;
413
412
  }
414
413
 
415
- this.server.getMore(this.ns, this.cursorState, batchSize, this.options, callback);
414
+ const cursorState = this.cursorState;
415
+ this.server.getMore(this.ns, cursorState, batchSize, this.options, (err, result, conn) => {
416
+ // NOTE: `getMore` modifies `cursorState`, would be very ideal not to do so in the future
417
+ if (err || (cursorState.cursorId && cursorState.cursorId.isZero())) {
418
+ this._endSession();
419
+ }
420
+
421
+ callback(err, result, conn);
422
+ });
416
423
  }
417
424
 
418
425
  _initializeCursor(callback) {
@@ -433,18 +440,15 @@ class CoreCursor extends Readable {
433
440
  }
434
441
 
435
442
  function done(err, result) {
436
- if (
437
- cursor.cursorState.cursorId &&
438
- cursor.cursorState.cursorId.isZero() &&
439
- cursor._endSession
440
- ) {
443
+ const cursorState = cursor.cursorState;
444
+ if (err || (cursorState.cursorId && cursorState.cursorId.isZero())) {
441
445
  cursor._endSession();
442
446
  }
443
447
 
444
448
  if (
445
- cursor.cursorState.documents.length === 0 &&
446
- cursor.cursorState.cursorId &&
447
- cursor.cursorState.cursorId.isZero() &&
449
+ cursorState.documents.length === 0 &&
450
+ cursorState.cursorId &&
451
+ cursorState.cursorId.isZero() &&
448
452
  !cursor.cmd.tailable &&
449
453
  !cursor.cmd.awaitData
450
454
  ) {
@@ -690,8 +694,8 @@ function _setCursorNotifiedImpl(self, callback) {
690
694
  self.cursorState.documents = [];
691
695
  self.cursorState.cursorIndex = 0;
692
696
 
693
- if (self._endSession) {
694
- self._endSession(undefined, () => callback());
697
+ if (self.cursorState.session) {
698
+ self._endSession(callback);
695
699
  return;
696
700
  }
697
701
 
@@ -774,17 +778,9 @@ function nextFunction(self, callback) {
774
778
  // Execute the next get more
775
779
  self._getMore(function(err, doc, connection) {
776
780
  if (err) {
777
- if (err instanceof MongoError) {
778
- err[mongoErrorContextSymbol].isGetMore = true;
779
- }
780
-
781
781
  return handleCallback(callback, err);
782
782
  }
783
783
 
784
- if (self.cursorState.cursorId && self.cursorState.cursorId.isZero() && self._endSession) {
785
- self._endSession();
786
- }
787
-
788
784
  // Save the returned connection to ensure all getMore's fire over the same connection
789
785
  self.connection = connection;
790
786
 
package/lib/core/error.js CHANGED
@@ -1,7 +1,5 @@
1
1
  'use strict';
2
2
 
3
- const mongoErrorContextSymbol = Symbol('mongoErrorContextSymbol');
4
-
5
3
  /**
6
4
  * Creates a new MongoError
7
5
  *
@@ -21,6 +19,10 @@ class MongoError extends Error {
21
19
  } else {
22
20
  super(message.message || message.errmsg || message.$err || 'n/a');
23
21
  for (var name in message) {
22
+ if (name === 'errmsg') {
23
+ continue;
24
+ }
25
+
24
26
  this[name] = message[name];
25
27
  }
26
28
  }
@@ -29,7 +31,13 @@ class MongoError extends Error {
29
31
  }
30
32
 
31
33
  this.name = 'MongoError';
32
- this[mongoErrorContextSymbol] = this[mongoErrorContextSymbol] || {};
34
+ }
35
+
36
+ /**
37
+ * Legacy name for server error responses
38
+ */
39
+ get errmsg() {
40
+ return this.message;
33
41
  }
34
42
 
35
43
  /**
@@ -262,7 +270,6 @@ module.exports = {
262
270
  MongoTimeoutError,
263
271
  MongoServerSelectionError,
264
272
  MongoWriteConcernError,
265
- mongoErrorContextSymbol,
266
273
  isRetryableError,
267
274
  isSDAMUnrecoverableError,
268
275
  isNodeShuttingDownError,
package/lib/core/index.js CHANGED
@@ -22,7 +22,6 @@ module.exports = {
22
22
  MongoTimeoutError: require('./error').MongoTimeoutError,
23
23
  MongoServerSelectionError: require('./error').MongoServerSelectionError,
24
24
  MongoWriteConcernError: require('./error').MongoWriteConcernError,
25
- mongoErrorContextSymbol: require('./error').mongoErrorContextSymbol,
26
25
  // Core
27
26
  Connection: require('./connection/connection'),
28
27
  Server: require('./topologies/server'),
@@ -48,7 +48,7 @@ function maxStalenessReducer(readPreference, topologyDescription, servers) {
48
48
  }
49
49
 
50
50
  if (topologyDescription.type === TopologyType.ReplicaSetWithPrimary) {
51
- const primary = servers.filter(primaryFilter)[0];
51
+ const primary = Array.from(topologyDescription.servers.values()).filter(primaryFilter)[0];
52
52
  return servers.reduce((result, server) => {
53
53
  const stalenessMS =
54
54
  server.lastUpdateTime -
@@ -197,50 +197,32 @@ function readPreferenceServerSelector(readPreference) {
197
197
  return latencyWindowReducer(topologyDescription, servers.filter(knownFilter));
198
198
  }
199
199
 
200
- if (readPreference.mode === ReadPreference.PRIMARY) {
200
+ const mode = readPreference.mode;
201
+ if (mode === ReadPreference.PRIMARY) {
201
202
  return servers.filter(primaryFilter);
202
203
  }
203
204
 
204
- if (readPreference.mode === ReadPreference.SECONDARY) {
205
- return latencyWindowReducer(
206
- topologyDescription,
207
- tagSetReducer(
208
- readPreference,
209
- maxStalenessReducer(readPreference, topologyDescription, servers)
210
- )
211
- ).filter(secondaryFilter);
212
- } else if (readPreference.mode === ReadPreference.NEAREST) {
213
- return latencyWindowReducer(
214
- topologyDescription,
215
- tagSetReducer(
216
- readPreference,
217
- maxStalenessReducer(readPreference, topologyDescription, servers)
218
- )
219
- ).filter(nearestFilter);
220
- } else if (readPreference.mode === ReadPreference.SECONDARY_PREFERRED) {
221
- const result = latencyWindowReducer(
222
- topologyDescription,
223
- tagSetReducer(
224
- readPreference,
225
- maxStalenessReducer(readPreference, topologyDescription, servers)
226
- )
227
- ).filter(secondaryFilter);
228
-
229
- return result.length === 0 ? servers.filter(primaryFilter) : result;
230
- } else if (readPreference.mode === ReadPreference.PRIMARY_PREFERRED) {
205
+ if (mode === ReadPreference.PRIMARY_PREFERRED) {
231
206
  const result = servers.filter(primaryFilter);
232
207
  if (result.length) {
233
208
  return result;
234
209
  }
210
+ }
211
+
212
+ const filter = mode === ReadPreference.NEAREST ? nearestFilter : secondaryFilter;
213
+ const selectedServers = latencyWindowReducer(
214
+ topologyDescription,
215
+ tagSetReducer(
216
+ readPreference,
217
+ maxStalenessReducer(readPreference, topologyDescription, servers.filter(filter))
218
+ )
219
+ );
235
220
 
236
- return latencyWindowReducer(
237
- topologyDescription,
238
- tagSetReducer(
239
- readPreference,
240
- maxStalenessReducer(readPreference, topologyDescription, servers)
241
- )
242
- ).filter(secondaryFilter);
221
+ if (mode === ReadPreference.SECONDARY_PREFERRED && selectedServers.length === 0) {
222
+ return servers.filter(primaryFilter);
243
223
  }
224
+
225
+ return selectedServers;
244
226
  };
245
227
  }
246
228
 
package/lib/core/utils.js CHANGED
@@ -83,22 +83,24 @@ function retrieveEJSON() {
83
83
  * @param {(Topology|Server)} topologyOrServer
84
84
  */
85
85
  function maxWireVersion(topologyOrServer) {
86
- if (topologyOrServer.ismaster) {
87
- return topologyOrServer.ismaster.maxWireVersion;
88
- }
86
+ if (topologyOrServer) {
87
+ if (topologyOrServer.ismaster) {
88
+ return topologyOrServer.ismaster.maxWireVersion;
89
+ }
89
90
 
90
- if (typeof topologyOrServer.lastIsMaster === 'function') {
91
- const lastIsMaster = topologyOrServer.lastIsMaster();
92
- if (lastIsMaster) {
93
- return lastIsMaster.maxWireVersion;
91
+ if (typeof topologyOrServer.lastIsMaster === 'function') {
92
+ const lastIsMaster = topologyOrServer.lastIsMaster();
93
+ if (lastIsMaster) {
94
+ return lastIsMaster.maxWireVersion;
95
+ }
94
96
  }
95
- }
96
97
 
97
- if (topologyOrServer.description) {
98
- return topologyOrServer.description.maxWireVersion;
98
+ if (topologyOrServer.description) {
99
+ return topologyOrServer.description.maxWireVersion;
100
+ }
99
101
  }
100
102
 
101
- return null;
103
+ return 0;
102
104
  }
103
105
 
104
106
  /*
package/lib/cursor.js CHANGED
@@ -832,9 +832,7 @@ class Cursor extends CoreCursor {
832
832
  const fetchDocs = () => {
833
833
  cursor._next((err, doc) => {
834
834
  if (err) {
835
- return cursor._endSession
836
- ? cursor._endSession(() => handleCallback(cb, err))
837
- : handleCallback(cb, err);
835
+ return handleCallback(cb, err);
838
836
  }
839
837
 
840
838
  if (doc == null) {
@@ -914,38 +912,18 @@ class Cursor extends CoreCursor {
914
912
  if (typeof options === 'function') (callback = options), (options = {});
915
913
  options = Object.assign({}, { skipKillCursors: false }, options);
916
914
 
917
- this.s.state = CursorState.CLOSED;
918
- if (!options.skipKillCursors) {
919
- // Kill the cursor
920
- this.kill();
921
- }
922
-
923
- const completeClose = () => {
924
- // Emit the close event for the cursor
925
- this.emit('close');
926
-
927
- // Callback if provided
928
- if (typeof callback === 'function') {
929
- return handleCallback(callback, null, this);
930
- }
931
-
932
- // Return a Promise
933
- return new this.s.promiseLibrary(resolve => {
934
- resolve();
935
- });
936
- };
937
-
938
- if (this.cursorState.session) {
939
- if (typeof callback === 'function') {
940
- return this._endSession(() => completeClose());
915
+ return maybePromise(this, callback, cb => {
916
+ this.s.state = CursorState.CLOSED;
917
+ if (!options.skipKillCursors) {
918
+ // Kill the cursor
919
+ this.kill();
941
920
  }
942
921
 
943
- return new this.s.promiseLibrary(resolve => {
944
- this._endSession(() => completeClose().then(resolve));
922
+ this._endSession(() => {
923
+ this.emit('close');
924
+ cb(null, this);
945
925
  });
946
- }
947
-
948
- return completeClose();
926
+ });
949
927
  }
950
928
 
951
929
  /**
package/lib/error.js CHANGED
@@ -1,45 +1,38 @@
1
1
  'use strict';
2
2
 
3
3
  const MongoNetworkError = require('./core').MongoNetworkError;
4
- const mongoErrorContextSymbol = require('./core').mongoErrorContextSymbol;
5
4
 
6
- const GET_MORE_NON_RESUMABLE_CODES = new Set([
7
- 136, // CappedPositionLost
8
- 237, // CursorKilled
9
- 11601 // Interrupted
5
+ // From spec@https://github.com/mongodb/specifications/blob/f93d78191f3db2898a59013a7ed5650352ef6da8/source/change-streams/change-streams.rst#resumable-error
6
+ const GET_MORE_RESUMABLE_CODES = new Set([
7
+ 6, // HostUnreachable
8
+ 7, // HostNotFound
9
+ 89, // NetworkTimeout
10
+ 91, // ShutdownInProgress
11
+ 189, // PrimarySteppedDown
12
+ 262, // ExceededTimeLimit
13
+ 9001, // SocketException
14
+ 10107, // NotMaster
15
+ 11600, // InterruptedAtShutdown
16
+ 11602, // InterruptedDueToReplStateChange
17
+ 13435, // NotMasterNoSlaveOk
18
+ 13436, // NotMasterOrSecondary
19
+ 63, // StaleShardVersion
20
+ 150, // StaleEpoch
21
+ 13388, // StaleConfig
22
+ 234, // RetryChangeStream
23
+ 133 // FailedToSatisfyReadPreference
10
24
  ]);
11
25
 
12
- // From spec@https://github.com/mongodb/specifications/blob/7a2e93d85935ee4b1046a8d2ad3514c657dc74fa/source/change-streams/change-streams.rst#resumable-error:
13
- //
14
- // An error is considered resumable if it meets any of the following criteria:
15
- // - any error encountered which is not a server error (e.g. a timeout error or network error)
16
- // - any server error response from a getMore command excluding those containing the error label
17
- // NonRetryableChangeStreamError and those containing the following error codes:
18
- // - Interrupted: 11601
19
- // - CappedPositionLost: 136
20
- // - CursorKilled: 237
21
- //
22
- // An error on an aggregate command is not a resumable error. Only errors on a getMore command may be considered resumable errors.
23
-
24
- function isGetMoreError(error) {
25
- if (error[mongoErrorContextSymbol]) {
26
- return error[mongoErrorContextSymbol].isGetMore;
27
- }
28
- }
29
-
30
- function isResumableError(error) {
31
- if (!isGetMoreError(error)) {
32
- return false;
33
- }
34
-
26
+ function isResumableError(error, wireVersion) {
35
27
  if (error instanceof MongoNetworkError) {
36
28
  return true;
37
29
  }
38
30
 
39
- return !(
40
- GET_MORE_NON_RESUMABLE_CODES.has(error.code) ||
41
- error.hasErrorLabel('NonRetryableChangeStreamError')
42
- );
31
+ if (wireVersion >= 9) {
32
+ return error.hasErrorLabel('ResumableChangeStreamError');
33
+ }
34
+
35
+ return GET_MORE_RESUMABLE_CODES.has(error.code);
43
36
  }
44
37
 
45
- module.exports = { GET_MORE_NON_RESUMABLE_CODES, isResumableError };
38
+ module.exports = { GET_MORE_RESUMABLE_CODES, isResumableError };
@@ -290,6 +290,12 @@ function connect(mongoClient, url, options, callback) {
290
290
  delete _finalOptions.db_options.auth;
291
291
  }
292
292
 
293
+ // `journal` should be translated to `j` for the driver
294
+ if (_finalOptions.journal != null) {
295
+ _finalOptions.j = _finalOptions.journal;
296
+ _finalOptions.journal = undefined;
297
+ }
298
+
293
299
  // resolve tls options if needed
294
300
  resolveTLSOptions(_finalOptions);
295
301
 
@@ -134,10 +134,9 @@ function toArray(cursor, callback) {
134
134
  const fetchDocs = () => {
135
135
  cursor._next((err, doc) => {
136
136
  if (err) {
137
- return cursor._endSession
138
- ? cursor._endSession(() => handleCallback(callback, err))
139
- : handleCallback(callback, err);
137
+ return handleCallback(callback, err);
140
138
  }
139
+
141
140
  if (doc == null) {
142
141
  return cursor.close({ skipKillCursors: true }, () => handleCallback(callback, null, items));
143
142
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mongodb",
3
- "version": "3.5.7",
3
+ "version": "3.5.8",
4
4
  "description": "The official MongoDB driver for Node.js",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -68,7 +68,7 @@
68
68
  "test": "npm run lint && mocha --recursive test/functional test/unit test/core",
69
69
  "test-nolint": "mocha --recursive test/functional test/unit test/core",
70
70
  "coverage": "istanbul cover mongodb-test-runner -- -t 60000 test/core test/unit test/functional",
71
- "lint": "eslint lib test",
71
+ "lint": "eslint -v && eslint lib test",
72
72
  "format": "prettier --print-width 100 --tab-width 2 --single-quote --write 'test/**/*.js' 'lib/**/*.js'",
73
73
  "bench": "node test/benchmarks/driverBench/",
74
74
  "generate-evergreen": "node .evergreen/generate_evergreen_tasks.js",