mongodb 3.5.7 → 3.5.11
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 +44 -0
- package/lib/bulk/common.js +4 -11
- package/lib/bulk/unordered.js +8 -0
- package/lib/change_stream.js +194 -145
- package/lib/cmap/connection.js +8 -4
- package/lib/cmap/connection_pool.js +11 -3
- package/lib/collection.js +2 -3
- package/lib/core/connection/apm.js +1 -1
- package/lib/core/connection/pool.js +2 -1
- package/lib/core/cursor.js +46 -59
- package/lib/core/error.js +11 -4
- package/lib/core/index.js +0 -1
- package/lib/core/sdam/monitor.js +53 -61
- package/lib/core/sdam/server.js +5 -2
- package/lib/core/sdam/server_description.js +12 -1
- package/lib/core/sdam/server_selection.js +25 -37
- package/lib/core/sdam/topology.js +4 -26
- package/lib/core/sessions.js +12 -6
- package/lib/core/topologies/read_preference.js +58 -6
- package/lib/core/topologies/replset.js +4 -4
- package/lib/core/utils.js +13 -23
- package/lib/core/wireprotocol/command.js +1 -6
- package/lib/core/wireprotocol/get_more.js +6 -1
- package/lib/cursor.js +10 -32
- package/lib/db.js +3 -3
- package/lib/error.js +26 -33
- package/lib/mongo_client.js +8 -0
- package/lib/operations/collection_ops.js +1 -2
- package/lib/operations/command.js +2 -3
- package/lib/operations/command_v2.js +7 -6
- package/lib/operations/connect.js +11 -0
- package/lib/operations/cursor_ops.js +2 -3
- package/lib/operations/db_ops.js +1 -2
- package/lib/operations/find.js +2 -2
- package/lib/operations/geo_haystack_search.js +2 -2
- package/lib/operations/map_reduce.js +2 -2
- package/lib/operations/operation.js +2 -1
- package/lib/operations/run_command.js +19 -0
- package/lib/topologies/native_topology.js +12 -2
- package/lib/topologies/topology_base.js +4 -4
- package/lib/utils.js +96 -60
- package/package.json +7 -4
package/HISTORY.md
CHANGED
|
@@ -2,6 +2,50 @@
|
|
|
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.11"></a>
|
|
6
|
+
## [3.5.11](https://github.com/mongodb/node-mongodb-native/compare/v3.5.10...v3.5.11) (2020-09-10)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* add host/port to cmap connection ([07b2b0d](https://github.com/mongodb/node-mongodb-native/commit/07b2b0d))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
<a name="3.5.9"></a>
|
|
16
|
+
## [3.5.9](https://github.com/mongodb/node-mongodb-native/compare/v3.5.8...v3.5.9) (2020-06-12)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* don't try to calculate sMax if there are no viable servers ([be51347](https://github.com/mongodb/node-mongodb-native/commit/be51347))
|
|
22
|
+
* use async interruptable interval for server monitoring ([1f855a4](https://github.com/mongodb/node-mongodb-native/commit/1f855a4))
|
|
23
|
+
* use duration of handshake if no previous roundTripTime exists ([ddfa41b](https://github.com/mongodb/node-mongodb-native/commit/ddfa41b))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Features
|
|
27
|
+
|
|
28
|
+
* introduce an interruptable async interval timer ([9e12cd5](https://github.com/mongodb/node-mongodb-native/commit/9e12cd5))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
<a name="3.5.8"></a>
|
|
33
|
+
## [3.5.8](https://github.com/mongodb/node-mongodb-native/compare/v3.5.7...v3.5.8) (2020-05-28)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
### Bug Fixes
|
|
37
|
+
|
|
38
|
+
* always clear cancelled wait queue members during processing ([0394f9d](https://github.com/mongodb/node-mongodb-native/commit/0394f9d))
|
|
39
|
+
* always include `writeErrors` on a `BulkWriteError` instance ([58b4f94](https://github.com/mongodb/node-mongodb-native/commit/58b4f94))
|
|
40
|
+
* ensure implicit sessions are ended consistently ([5c6fda1](https://github.com/mongodb/node-mongodb-native/commit/5c6fda1))
|
|
41
|
+
* filter servers before applying reducers ([4faf9f5](https://github.com/mongodb/node-mongodb-native/commit/4faf9f5))
|
|
42
|
+
* unordered bulk write should attempt to execute all batches ([6cee96b](https://github.com/mongodb/node-mongodb-native/commit/6cee96b))
|
|
43
|
+
* **ChangeStream:** should resume from errors when iterating ([5ecf18e](https://github.com/mongodb/node-mongodb-native/commit/5ecf18e))
|
|
44
|
+
* 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))
|
|
45
|
+
* **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)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
5
49
|
<a name="3.5.7"></a>
|
|
6
50
|
## [3.5.7](https://github.com/mongodb/node-mongodb-native/compare/v3.5.6...v3.5.7) (2020-04-29)
|
|
7
51
|
|
package/lib/bulk/common.js
CHANGED
|
@@ -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
|
|
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
|
-
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
if (writeResult.getWriteConcernError()) {
|
|
1234
1227
|
handleCallback(
|
|
1235
1228
|
callback,
|
|
1236
1229
|
new BulkWriteError(toError(writeResult.getWriteConcernError()), writeResult),
|
package/lib/bulk/unordered.js
CHANGED
|
@@ -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
|
/**
|
package/lib/change_stream.js
CHANGED
|
@@ -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;
|
|
@@ -7,8 +8,12 @@ const Cursor = require('./cursor');
|
|
|
7
8
|
const relayEvents = require('./core/utils').relayEvents;
|
|
8
9
|
const maxWireVersion = require('./core/utils').maxWireVersion;
|
|
9
10
|
const maybePromise = require('./utils').maybePromise;
|
|
11
|
+
const now = require('./utils').now;
|
|
12
|
+
const calculateDurationInMs = require('./utils').calculateDurationInMs;
|
|
10
13
|
const AggregateOperation = require('./operations/aggregate');
|
|
11
14
|
|
|
15
|
+
const kResumeQueue = Symbol('resumeQueue');
|
|
16
|
+
|
|
12
17
|
const CHANGE_STREAM_OPTIONS = ['resumeAfter', 'startAfter', 'startAtOperationTime', 'fullDocument'];
|
|
13
18
|
const CURSOR_OPTIONS = ['batchSize', 'maxAwaitTimeMS', 'collation', 'readPreference'].concat(
|
|
14
19
|
CHANGE_STREAM_OPTIONS
|
|
@@ -91,15 +96,17 @@ class ChangeStream extends EventEmitter {
|
|
|
91
96
|
this.options.readPreference = parent.s.readPreference;
|
|
92
97
|
}
|
|
93
98
|
|
|
99
|
+
this[kResumeQueue] = new Denque();
|
|
100
|
+
|
|
94
101
|
// Create contained Change Stream cursor
|
|
95
102
|
this.cursor = createChangeStreamCursor(this, options);
|
|
96
103
|
|
|
104
|
+
this.closed = false;
|
|
105
|
+
|
|
97
106
|
// Listen for any `change` listeners being added to ChangeStream
|
|
98
107
|
this.on('newListener', eventName => {
|
|
99
108
|
if (eventName === 'change' && this.cursor && this.listenerCount('change') === 0) {
|
|
100
|
-
this.cursor.on('data', change =>
|
|
101
|
-
processNewChange({ changeStream: this, change, eventEmitter: true })
|
|
102
|
-
);
|
|
109
|
+
this.cursor.on('data', change => processNewChange(this, change));
|
|
103
110
|
}
|
|
104
111
|
});
|
|
105
112
|
|
|
@@ -128,7 +135,12 @@ class ChangeStream extends EventEmitter {
|
|
|
128
135
|
* @returns {Promise|void} returns Promise if no callback passed
|
|
129
136
|
*/
|
|
130
137
|
hasNext(callback) {
|
|
131
|
-
return maybePromise(this.parent, callback, cb =>
|
|
138
|
+
return maybePromise(this.parent, callback, cb => {
|
|
139
|
+
getCursor(this, (err, cursor) => {
|
|
140
|
+
if (err) return cb(err); // failed to resume, raise an error
|
|
141
|
+
cursor.hasNext(cb);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
132
144
|
}
|
|
133
145
|
|
|
134
146
|
/**
|
|
@@ -140,25 +152,28 @@ class ChangeStream extends EventEmitter {
|
|
|
140
152
|
*/
|
|
141
153
|
next(callback) {
|
|
142
154
|
return maybePromise(this.parent, callback, cb => {
|
|
143
|
-
|
|
144
|
-
return cb(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
155
|
+
getCursor(this, (err, cursor) => {
|
|
156
|
+
if (err) return cb(err); // failed to resume, raise an error
|
|
157
|
+
cursor.next((error, change) => {
|
|
158
|
+
if (error) {
|
|
159
|
+
this[kResumeQueue].push(() => this.next(cb));
|
|
160
|
+
processError(this, error, cb);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
processNewChange(this, change, cb);
|
|
164
|
+
});
|
|
148
165
|
});
|
|
149
166
|
});
|
|
150
167
|
}
|
|
151
168
|
|
|
152
169
|
/**
|
|
153
|
-
* Is the
|
|
170
|
+
* Is the change stream closed
|
|
154
171
|
* @method ChangeStream.prototype.isClosed
|
|
172
|
+
* @param {boolean} [checkCursor=true] also check if the underlying cursor is closed
|
|
155
173
|
* @return {boolean}
|
|
156
174
|
*/
|
|
157
175
|
isClosed() {
|
|
158
|
-
|
|
159
|
-
return this.cursor.isClosed();
|
|
160
|
-
}
|
|
161
|
-
return true;
|
|
176
|
+
return this.closed || (this.cursor && this.cursor.isClosed());
|
|
162
177
|
}
|
|
163
178
|
|
|
164
179
|
/**
|
|
@@ -168,31 +183,22 @@ class ChangeStream extends EventEmitter {
|
|
|
168
183
|
* @return {Promise} returns Promise if no callback passed
|
|
169
184
|
*/
|
|
170
185
|
close(callback) {
|
|
171
|
-
|
|
172
|
-
if (
|
|
173
|
-
return this.promiseLibrary.resolve();
|
|
174
|
-
}
|
|
186
|
+
return maybePromise(this.parent, callback, cb => {
|
|
187
|
+
if (this.closed) return cb();
|
|
175
188
|
|
|
176
|
-
|
|
177
|
-
|
|
189
|
+
// flag the change stream as explicitly closed
|
|
190
|
+
this.closed = true;
|
|
178
191
|
|
|
179
|
-
|
|
180
|
-
return cursor.close(err => {
|
|
181
|
-
['data', 'close', 'end', 'error'].forEach(event => cursor.removeAllListeners(event));
|
|
182
|
-
delete this.cursor;
|
|
192
|
+
if (!this.cursor) return cb();
|
|
183
193
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
194
|
+
// Tidy up the existing cursor
|
|
195
|
+
const cursor = this.cursor;
|
|
187
196
|
|
|
188
|
-
|
|
189
|
-
return new PromiseCtor((resolve, reject) => {
|
|
190
|
-
cursor.close(err => {
|
|
197
|
+
return cursor.close(err => {
|
|
191
198
|
['data', 'close', 'end', 'error'].forEach(event => cursor.removeAllListeners(event));
|
|
192
|
-
|
|
199
|
+
this.cursor = undefined;
|
|
193
200
|
|
|
194
|
-
|
|
195
|
-
resolve();
|
|
201
|
+
return cb(err);
|
|
196
202
|
});
|
|
197
203
|
});
|
|
198
204
|
}
|
|
@@ -287,7 +293,9 @@ class ChangeStreamCursor extends Cursor {
|
|
|
287
293
|
['resumeAfter', 'startAfter', 'startAtOperationTime'].forEach(key => delete result[key]);
|
|
288
294
|
|
|
289
295
|
if (this.resumeToken) {
|
|
290
|
-
|
|
296
|
+
const resumeKey =
|
|
297
|
+
this.options.startAfter && !this.hasReceived ? 'startAfter' : 'resumeAfter';
|
|
298
|
+
result[resumeKey] = this.resumeToken;
|
|
291
299
|
} else if (this.startAtOperationTime && maxWireVersion(this.server) >= 7) {
|
|
292
300
|
result.startAtOperationTime = this.startAtOperationTime;
|
|
293
301
|
}
|
|
@@ -296,10 +304,30 @@ class ChangeStreamCursor extends Cursor {
|
|
|
296
304
|
return result;
|
|
297
305
|
}
|
|
298
306
|
|
|
307
|
+
cacheResumeToken(resumeToken) {
|
|
308
|
+
if (this.bufferedCount() === 0 && this.cursorState.postBatchResumeToken) {
|
|
309
|
+
this.resumeToken = this.cursorState.postBatchResumeToken;
|
|
310
|
+
} else {
|
|
311
|
+
this.resumeToken = resumeToken;
|
|
312
|
+
}
|
|
313
|
+
this.hasReceived = true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
_processBatch(batchName, response) {
|
|
317
|
+
const cursor = response.cursor;
|
|
318
|
+
if (cursor.postBatchResumeToken) {
|
|
319
|
+
this.cursorState.postBatchResumeToken = cursor.postBatchResumeToken;
|
|
320
|
+
|
|
321
|
+
if (cursor[batchName].length === 0) {
|
|
322
|
+
this.resumeToken = cursor.postBatchResumeToken;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
299
327
|
_initializeCursor(callback) {
|
|
300
328
|
super._initializeCursor((err, result) => {
|
|
301
|
-
if (err) {
|
|
302
|
-
callback(err,
|
|
329
|
+
if (err || result == null) {
|
|
330
|
+
callback(err, result);
|
|
303
331
|
return;
|
|
304
332
|
}
|
|
305
333
|
|
|
@@ -314,15 +342,9 @@ class ChangeStreamCursor extends Cursor {
|
|
|
314
342
|
this.startAtOperationTime = response.operationTime;
|
|
315
343
|
}
|
|
316
344
|
|
|
317
|
-
|
|
318
|
-
if (cursor.postBatchResumeToken) {
|
|
319
|
-
this.cursorState.postBatchResumeToken = cursor.postBatchResumeToken;
|
|
320
|
-
|
|
321
|
-
if (cursor.firstBatch.length === 0) {
|
|
322
|
-
this.resumeToken = cursor.postBatchResumeToken;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
345
|
+
this._processBatch('firstBatch', response);
|
|
325
346
|
|
|
347
|
+
this.emit('init', result);
|
|
326
348
|
this.emit('response');
|
|
327
349
|
callback(err, result);
|
|
328
350
|
});
|
|
@@ -331,19 +353,13 @@ class ChangeStreamCursor extends Cursor {
|
|
|
331
353
|
_getMore(callback) {
|
|
332
354
|
super._getMore((err, response) => {
|
|
333
355
|
if (err) {
|
|
334
|
-
callback(err
|
|
356
|
+
callback(err);
|
|
335
357
|
return;
|
|
336
358
|
}
|
|
337
359
|
|
|
338
|
-
|
|
339
|
-
if (cursor.postBatchResumeToken) {
|
|
340
|
-
this.cursorState.postBatchResumeToken = cursor.postBatchResumeToken;
|
|
341
|
-
|
|
342
|
-
if (cursor.nextBatch.length === 0) {
|
|
343
|
-
this.resumeToken = cursor.postBatchResumeToken;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
360
|
+
this._processBatch('nextBatch', response);
|
|
346
361
|
|
|
362
|
+
this.emit('more', response);
|
|
347
363
|
this.emit('response');
|
|
348
364
|
callback(err, response);
|
|
349
365
|
});
|
|
@@ -366,6 +382,7 @@ function createChangeStreamCursor(self, options) {
|
|
|
366
382
|
|
|
367
383
|
const pipeline = [{ $changeStream: changeStreamStageOptions }].concat(self.pipeline);
|
|
368
384
|
const cursorOptions = applyKnownOptions({}, options, CURSOR_OPTIONS);
|
|
385
|
+
|
|
369
386
|
const changeStreamCursor = new ChangeStreamCursor(
|
|
370
387
|
self.topology,
|
|
371
388
|
new AggregateOperation(self.parent, pipeline, options),
|
|
@@ -384,7 +401,7 @@ function createChangeStreamCursor(self, options) {
|
|
|
384
401
|
*/
|
|
385
402
|
if (self.listenerCount('change') > 0) {
|
|
386
403
|
changeStreamCursor.on('data', function(change) {
|
|
387
|
-
processNewChange(
|
|
404
|
+
processNewChange(self, change);
|
|
388
405
|
});
|
|
389
406
|
}
|
|
390
407
|
|
|
@@ -416,7 +433,7 @@ function createChangeStreamCursor(self, options) {
|
|
|
416
433
|
* @type {Error}
|
|
417
434
|
*/
|
|
418
435
|
changeStreamCursor.on('error', function(error) {
|
|
419
|
-
|
|
436
|
+
processError(self, error);
|
|
420
437
|
});
|
|
421
438
|
|
|
422
439
|
if (self.pipeDestinations) {
|
|
@@ -444,125 +461,157 @@ function applyKnownOptions(target, source, optionNames) {
|
|
|
444
461
|
const SELECTION_TIMEOUT = 30000;
|
|
445
462
|
function waitForTopologyConnected(topology, options, callback) {
|
|
446
463
|
setTimeout(() => {
|
|
447
|
-
if (options && options.start == null)
|
|
448
|
-
|
|
464
|
+
if (options && options.start == null) {
|
|
465
|
+
options.start = now();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const start = options.start || now();
|
|
449
469
|
const timeout = options.timeout || SELECTION_TIMEOUT;
|
|
450
470
|
const readPreference = options.readPreference;
|
|
471
|
+
if (topology.isConnected({ readPreference })) {
|
|
472
|
+
return callback();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (calculateDurationInMs(start) > timeout) {
|
|
476
|
+
return callback(new MongoError('Timed out waiting for connection'));
|
|
477
|
+
}
|
|
451
478
|
|
|
452
|
-
if (topology.isConnected({ readPreference })) return callback(null, null);
|
|
453
|
-
const hrElapsed = process.hrtime(start);
|
|
454
|
-
const elapsed = (hrElapsed[0] * 1e9 + hrElapsed[1]) / 1e6;
|
|
455
|
-
if (elapsed > timeout) return callback(new MongoError('Timed out waiting for connection'));
|
|
456
479
|
waitForTopologyConnected(topology, options, callback);
|
|
457
|
-
},
|
|
480
|
+
}, 500); // this is an arbitrary wait time to allow SDAM to transition
|
|
458
481
|
}
|
|
459
482
|
|
|
460
|
-
|
|
461
|
-
|
|
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;
|
|
483
|
+
function processNewChange(changeStream, change, callback) {
|
|
484
|
+
const cursor = changeStream.cursor;
|
|
467
485
|
|
|
468
|
-
//
|
|
469
|
-
if (
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
486
|
+
// a null change means the cursor has been notified, implicitly closing the change stream
|
|
487
|
+
if (change == null) {
|
|
488
|
+
changeStream.closed = true;
|
|
489
|
+
}
|
|
474
490
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
: changeStream.promiseLibrary.reject(error);
|
|
491
|
+
if (changeStream.closed) {
|
|
492
|
+
if (callback) callback(new MongoError('ChangeStream is closed'));
|
|
493
|
+
return;
|
|
479
494
|
}
|
|
480
495
|
|
|
481
|
-
|
|
496
|
+
if (change && !change._id) {
|
|
497
|
+
const noResumeTokenError = new Error(
|
|
498
|
+
'A change stream document has been received that lacks a resume token (_id).'
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
if (!callback) return changeStream.emit('error', noResumeTokenError);
|
|
502
|
+
return callback(noResumeTokenError);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// cache the resume token
|
|
506
|
+
cursor.cacheResumeToken(change._id);
|
|
507
|
+
|
|
508
|
+
// wipe the startAtOperationTime if there was one so that there won't be a conflict
|
|
509
|
+
// between resumeToken and startAtOperationTime if we need to reconnect the cursor
|
|
510
|
+
changeStream.options.startAtOperationTime = undefined;
|
|
511
|
+
|
|
512
|
+
// Return the change
|
|
513
|
+
if (!callback) return changeStream.emit('change', change);
|
|
514
|
+
return callback(undefined, change);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function processError(changeStream, error, callback) {
|
|
482
518
|
const topology = changeStream.topology;
|
|
483
|
-
const
|
|
519
|
+
const cursor = changeStream.cursor;
|
|
484
520
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
521
|
+
// If the change stream has been closed explictly, do not process error.
|
|
522
|
+
if (changeStream.closed) {
|
|
523
|
+
if (callback) callback(new MongoError('ChangeStream is closed'));
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
488
526
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
527
|
+
// if the resume succeeds, continue with the new cursor
|
|
528
|
+
function resumeWithCursor(newCursor) {
|
|
529
|
+
changeStream.cursor = newCursor;
|
|
530
|
+
processResumeQueue(changeStream);
|
|
531
|
+
}
|
|
493
532
|
|
|
494
|
-
|
|
495
|
-
|
|
533
|
+
// otherwise, raise an error and close the change stream
|
|
534
|
+
function unresumableError(err) {
|
|
535
|
+
if (!callback) {
|
|
536
|
+
changeStream.emit('error', err);
|
|
537
|
+
changeStream.emit('close');
|
|
538
|
+
}
|
|
539
|
+
processResumeQueue(changeStream, err);
|
|
540
|
+
changeStream.closed = true;
|
|
541
|
+
}
|
|
496
542
|
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
});
|
|
543
|
+
if (cursor && isResumableError(error, maxWireVersion(cursor.server))) {
|
|
544
|
+
changeStream.cursor = undefined;
|
|
507
545
|
|
|
508
|
-
|
|
509
|
-
|
|
546
|
+
// stop listening to all events from old cursor
|
|
547
|
+
['data', 'close', 'end', 'error'].forEach(event => cursor.removeAllListeners(event));
|
|
510
548
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
if (err) return callback(err, null);
|
|
549
|
+
// close internal cursor, ignore errors
|
|
550
|
+
cursor.close();
|
|
514
551
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
552
|
+
waitForTopologyConnected(topology, { readPreference: cursor.options.readPreference }, err => {
|
|
553
|
+
// if the topology can't reconnect, close the stream
|
|
554
|
+
if (err) return unresumableError(err);
|
|
518
555
|
|
|
519
|
-
|
|
520
|
-
|
|
556
|
+
// create a new cursor, preserving the old cursor's options
|
|
557
|
+
const newCursor = createChangeStreamCursor(changeStream, cursor.resumeOptions);
|
|
521
558
|
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
}
|
|
559
|
+
// attempt to continue in emitter mode
|
|
560
|
+
if (!callback) return resumeWithCursor(newCursor);
|
|
533
561
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
562
|
+
// attempt to continue in iterator mode
|
|
563
|
+
newCursor.hasNext(err => {
|
|
564
|
+
// if there's an error immediately after resuming, close the stream
|
|
565
|
+
if (err) return unresumableError(err);
|
|
566
|
+
resumeWithCursor(newCursor);
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
return;
|
|
537
570
|
}
|
|
538
571
|
|
|
539
|
-
changeStream.
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
const noResumeTokenError = new Error(
|
|
543
|
-
'A change stream document has been received that lacks a resume token (_id).'
|
|
544
|
-
);
|
|
572
|
+
if (!callback) return changeStream.emit('error', error);
|
|
573
|
+
return callback(error);
|
|
574
|
+
}
|
|
545
575
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
576
|
+
/**
|
|
577
|
+
* Safely provides a cursor across resume attempts
|
|
578
|
+
*
|
|
579
|
+
* @param {ChangeStream} changeStream the parent ChangeStream
|
|
580
|
+
* @param {function} callback gets the cursor or error
|
|
581
|
+
* @param {ChangeStreamCursor} [oldCursor] when resuming from an error, carry over options from previous cursor
|
|
582
|
+
*/
|
|
583
|
+
function getCursor(changeStream, callback) {
|
|
584
|
+
if (changeStream.isClosed()) {
|
|
585
|
+
callback(new MongoError('ChangeStream is closed.'));
|
|
586
|
+
return;
|
|
549
587
|
}
|
|
550
588
|
|
|
551
|
-
//
|
|
552
|
-
if (
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
cursor.resumeToken = change._id;
|
|
589
|
+
// if a cursor exists and it is open, return it
|
|
590
|
+
if (changeStream.cursor) {
|
|
591
|
+
callback(undefined, changeStream.cursor);
|
|
592
|
+
return;
|
|
556
593
|
}
|
|
557
594
|
|
|
558
|
-
//
|
|
559
|
-
|
|
560
|
-
|
|
595
|
+
// no cursor, queue callback until topology reconnects
|
|
596
|
+
changeStream[kResumeQueue].push(callback);
|
|
597
|
+
}
|
|
561
598
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
599
|
+
/**
|
|
600
|
+
* Drain the resume queue when a new has become available
|
|
601
|
+
*
|
|
602
|
+
* @param {ChangeStream} changeStream the parent ChangeStream
|
|
603
|
+
* @param {ChangeStreamCursor?} changeStream.cursor the new cursor
|
|
604
|
+
* @param {Error} [err] error getting a new cursor
|
|
605
|
+
*/
|
|
606
|
+
function processResumeQueue(changeStream, err) {
|
|
607
|
+
while (changeStream[kResumeQueue].length) {
|
|
608
|
+
const request = changeStream[kResumeQueue].pop();
|
|
609
|
+
if (changeStream.isClosed() && !err) {
|
|
610
|
+
request(new MongoError('Change Stream is not open.'));
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
request(err, changeStream.cursor);
|
|
614
|
+
}
|
|
566
615
|
}
|
|
567
616
|
|
|
568
617
|
/**
|
package/lib/cmap/connection.js
CHANGED
|
@@ -11,6 +11,8 @@ const wp = require('../core/wireprotocol');
|
|
|
11
11
|
const apm = require('../core/connection/apm');
|
|
12
12
|
const updateSessionFromResponse = require('../core/sessions').updateSessionFromResponse;
|
|
13
13
|
const uuidV4 = require('../core/utils').uuidV4;
|
|
14
|
+
const now = require('../utils').now;
|
|
15
|
+
const calculateDurationInMs = require('../utils').calculateDurationInMs;
|
|
14
16
|
|
|
15
17
|
const kStream = Symbol('stream');
|
|
16
18
|
const kQueue = Symbol('queue');
|
|
@@ -30,6 +32,8 @@ class Connection extends EventEmitter {
|
|
|
30
32
|
this.address = streamIdentifier(stream);
|
|
31
33
|
this.bson = options.bson;
|
|
32
34
|
this.socketTimeout = typeof options.socketTimeout === 'number' ? options.socketTimeout : 360000;
|
|
35
|
+
this.host = options.host || 'localhost';
|
|
36
|
+
this.port = options.port || 27017;
|
|
33
37
|
this.monitorCommands =
|
|
34
38
|
typeof options.monitorCommands === 'boolean' ? options.monitorCommands : false;
|
|
35
39
|
this.closed = false;
|
|
@@ -37,7 +41,7 @@ class Connection extends EventEmitter {
|
|
|
37
41
|
|
|
38
42
|
this[kDescription] = new StreamDescription(this.address, options);
|
|
39
43
|
this[kGeneration] = options.generation;
|
|
40
|
-
this[kLastUseTime] =
|
|
44
|
+
this[kLastUseTime] = now();
|
|
41
45
|
|
|
42
46
|
// retain a reference to an `AutoEncrypter` if present
|
|
43
47
|
if (options.autoEncrypter) {
|
|
@@ -108,7 +112,7 @@ class Connection extends EventEmitter {
|
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
get idleTime() {
|
|
111
|
-
return
|
|
115
|
+
return calculateDurationInMs(this[kLastUseTime]);
|
|
112
116
|
}
|
|
113
117
|
|
|
114
118
|
get clusterTime() {
|
|
@@ -120,7 +124,7 @@ class Connection extends EventEmitter {
|
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
markAvailable() {
|
|
123
|
-
this[kLastUseTime] =
|
|
127
|
+
this[kLastUseTime] = now();
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
destroy(options, callback) {
|
|
@@ -326,7 +330,7 @@ function write(command, options, callback) {
|
|
|
326
330
|
if (this.monitorCommands) {
|
|
327
331
|
this.emit('commandStarted', new apm.CommandStartedEvent(this, command));
|
|
328
332
|
|
|
329
|
-
operationDescription.started =
|
|
333
|
+
operationDescription.started = now();
|
|
330
334
|
operationDescription.cb = (err, reply) => {
|
|
331
335
|
if (err) {
|
|
332
336
|
this.emit(
|