Skip to content

Commit 13872ee

Browse files
authored
Merge pull request #14813 from Automattic/vkarpov15/mongodb-68
feat: upgrade mongodb -> 6.8.0, handle throwing error on closed cursor in Mongoose
2 parents 8180a73 + a725a75 commit 13872ee

File tree

8 files changed

+93
-33
lines changed

8 files changed

+93
-33
lines changed

lib/cursor/changeStream.js

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,18 @@ class ChangeStream extends EventEmitter {
3333
);
3434
}
3535

36-
// This wrapper is necessary because of buffering.
37-
changeStreamThunk((err, driverChangeStream) => {
38-
if (err != null) {
39-
this.emit('error', err);
40-
return;
41-
}
36+
this.$driverChangeStreamPromise = new Promise((resolve, reject) => {
37+
// This wrapper is necessary because of buffering.
38+
changeStreamThunk((err, driverChangeStream) => {
39+
if (err != null) {
40+
this.emit('error', err);
41+
return reject(err);
42+
}
4243

43-
this.driverChangeStream = driverChangeStream;
44-
this.emit('ready');
44+
this.driverChangeStream = driverChangeStream;
45+
this.emit('ready');
46+
resolve();
47+
});
4548
});
4649
}
4750

@@ -53,20 +56,23 @@ class ChangeStream extends EventEmitter {
5356
this.bindedEvents = true;
5457

5558
if (this.driverChangeStream == null) {
56-
this.once('ready', () => {
57-
this.driverChangeStream.on('close', () => {
58-
this.closed = true;
59-
});
59+
this.$driverChangeStreamPromise.then(
60+
() => {
61+
this.driverChangeStream.on('close', () => {
62+
this.closed = true;
63+
});
6064

61-
driverChangeStreamEvents.forEach(ev => {
62-
this.driverChangeStream.on(ev, data => {
63-
if (data != null && data.fullDocument != null && this.options && this.options.hydrate) {
64-
data.fullDocument = this.options.model.hydrate(data.fullDocument);
65-
}
66-
this.emit(ev, data);
65+
driverChangeStreamEvents.forEach(ev => {
66+
this.driverChangeStream.on(ev, data => {
67+
if (data != null && data.fullDocument != null && this.options && this.options.hydrate) {
68+
data.fullDocument = this.options.model.hydrate(data.fullDocument);
69+
}
70+
this.emit(ev, data);
71+
});
6772
});
68-
});
69-
});
73+
},
74+
() => {} // No need to register events if opening change stream failed
75+
);
7076

7177
return;
7278
}
@@ -142,8 +148,12 @@ class ChangeStream extends EventEmitter {
142148
this.closed = true;
143149
if (this.driverChangeStream) {
144150
return this.driverChangeStream.close();
151+
} else {
152+
return this.$driverChangeStreamPromise.then(
153+
() => this.driverChangeStream.close(),
154+
() => {} // No need to close if opening the change stream failed
155+
);
145156
}
146-
return Promise.resolve();
147157
}
148158
}
149159

lib/cursor/queryCursor.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ function QueryCursor(query) {
4343
this.cursor = null;
4444
this.skipped = false;
4545
this.query = query;
46+
this._closed = false;
4647
const model = query.model;
4748
this._mongooseOptions = {};
4849
this._transforms = [];
@@ -229,6 +230,7 @@ QueryCursor.prototype.close = async function close() {
229230
}
230231
try {
231232
await this.cursor.close();
233+
this._closed = true;
232234
this.emit('close');
233235
} catch (error) {
234236
this.listeners('error').length > 0 && this.emit('error', error);
@@ -266,6 +268,9 @@ QueryCursor.prototype.next = async function next() {
266268
if (typeof arguments[0] === 'function') {
267269
throw new MongooseError('QueryCursor.prototype.next() no longer accepts a callback');
268270
}
271+
if (this._closed) {
272+
throw new MongooseError('Cannot call `next()` on a closed cursor');
273+
}
269274
return new Promise((resolve, reject) => {
270275
_next(this, function(error, doc) {
271276
if (error) {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"dependencies": {
2222
"bson": "^6.7.0",
2323
"kareem": "2.6.3",
24-
"mongodb": "6.7.0",
24+
"mongodb": "6.8.0",
2525
"mpath": "0.9.0",
2626
"mquery": "5.0.0",
2727
"ms": "2.1.3",

scripts/tsc-diagnostics-check.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
const fs = require('fs');
44

55
const stdin = fs.readFileSync(0).toString('utf8');
6-
const maxInstantiations = isNaN(process.argv[2]) ? 127500 : parseInt(process.argv[2], 10);
6+
const maxInstantiations = isNaN(process.argv[2]) ? 135000 : parseInt(process.argv[2], 10);
77

88
console.log(stdin);
99

test/connection.test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,8 @@ describe('connections:', function() {
10321032
await nextChange;
10331033
assert.equal(changes.length, 1);
10341034
assert.equal(changes[0].operationType, 'insert');
1035+
1036+
await changeStream.close();
10351037
await conn.close();
10361038
});
10371039

test/model.test.js

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const sinon = require('sinon');
77
const start = require('./common');
88

99
const assert = require('assert');
10+
const { once } = require('events');
1011
const random = require('./util').random;
1112
const util = require('./util');
1213

@@ -3508,6 +3509,9 @@ describe('Model', function() {
35083509
}
35093510
changeStream.removeListener('change', listener);
35103511
listener = null;
3512+
// Change stream may still emit "MongoAPIError: ChangeStream is closed" because change stream
3513+
// may still poll after close.
3514+
changeStream.on('error', () => {});
35113515
changeStream.close();
35123516
changeStream = null;
35133517
});
@@ -3560,14 +3564,21 @@ describe('Model', function() {
35603564
it('fullDocument (gh-11936)', async function() {
35613565
const MyModel = db.model('Test', new Schema({ name: String }));
35623566

3567+
const doc = await MyModel.create({ name: 'Ned Stark' });
35633568
const changeStream = await MyModel.watch([], {
35643569
fullDocument: 'updateLookup',
35653570
hydrate: true
35663571
});
3572+
await changeStream.$driverChangeStreamPromise;
35673573

3568-
const doc = await MyModel.create({ name: 'Ned Stark' });
3569-
3570-
const p = changeStream.next();
3574+
const p = new Promise((resolve) => {
3575+
changeStream.once('change', change => {
3576+
resolve(change);
3577+
});
3578+
});
3579+
// Need to wait for resume token to be set after the event listener,
3580+
// otherwise change stream might not pick up the update.
3581+
await once(changeStream.driverChangeStream, 'resumeTokenChanged');
35713582
await MyModel.updateOne({ _id: doc._id }, { name: 'Tony Stark' });
35723583

35733584
const changeData = await p;
@@ -3576,22 +3587,31 @@ describe('Model', function() {
35763587
doc._id.toHexString());
35773588
assert.ok(changeData.fullDocument.$__);
35783589
assert.equal(changeData.fullDocument.get('name'), 'Tony Stark');
3590+
3591+
await changeStream.close();
35793592
});
35803593

35813594
it('fullDocument with immediate watcher and hydrate (gh-14049)', async function() {
35823595
const MyModel = db.model('Test', new Schema({ name: String }));
35833596

35843597
const doc = await MyModel.create({ name: 'Ned Stark' });
35853598

3599+
let changeStream = null;
35863600
const p = new Promise((resolve) => {
3587-
MyModel.watch([], {
3601+
changeStream = MyModel.watch([], {
35883602
fullDocument: 'updateLookup',
35893603
hydrate: true
3590-
}).on('change', change => {
3604+
});
3605+
3606+
changeStream.on('change', change => {
35913607
resolve(change);
35923608
});
35933609
});
35943610

3611+
// Need to wait for cursor to be initialized and for resume token to
3612+
// be set, otherwise change stream might not pick up the update.
3613+
await changeStream.$driverChangeStreamPromise;
3614+
await once(changeStream.driverChangeStream, 'resumeTokenChanged');
35953615
await MyModel.updateOne({ _id: doc._id }, { name: 'Tony Stark' });
35963616

35973617
const changeData = await p;
@@ -3600,6 +3620,8 @@ describe('Model', function() {
36003620
doc._id.toHexString());
36013621
assert.ok(changeData.fullDocument.$__);
36023622
assert.equal(changeData.fullDocument.get('name'), 'Tony Stark');
3623+
3624+
await changeStream.close();
36033625
});
36043626

36053627
it('respects discriminators (gh-11007)', async function() {
@@ -3639,6 +3661,9 @@ describe('Model', function() {
36393661
assert.equal(changeData.operationType, 'insert');
36403662
assert.equal(changeData.fullDocument.name, 'Ned Stark');
36413663

3664+
// Change stream may still emit "MongoAPIError: ChangeStream is closed" because change stream
3665+
// may still poll after close.
3666+
changeStream.on('error', () => {});
36423667
await changeStream.close();
36433668
await db.close();
36443669
});
@@ -3654,11 +3679,16 @@ describe('Model', function() {
36543679
setTimeout(resolve, 500, false);
36553680
});
36563681

3657-
changeStream.close();
3658-
await db;
3682+
// Change stream may still emit "MongoAPIError: ChangeStream is closed" because change stream
3683+
// may still poll after close.
3684+
changeStream.on('error', () => {});
3685+
3686+
const close = changeStream.close();
3687+
await db.asPromise();
36593688
const readyCalled = await ready;
36603689
assert.strictEqual(readyCalled, false);
36613690

3691+
await close;
36623692
await db.close();
36633693
});
36643694

@@ -3675,6 +3705,10 @@ describe('Model', function() {
36753705

36763706
await MyModel.create({ name: 'Hodor' });
36773707

3708+
// Change stream may still emit "MongoAPIError: ChangeStream is closed" because change stream
3709+
// may still poll after close.
3710+
changeStream.on('error', () => {});
3711+
36783712
changeStream.close();
36793713
const closedData = await closed;
36803714
assert.strictEqual(closedData, true);

test/model.watch.test.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,22 @@ describe('model: watch: ', function() {
3737
const changeData = await changed;
3838
assert.equal(changeData.operationType, 'insert');
3939
assert.equal(changeData.fullDocument.name, 'Ned Stark');
40+
await changeStream.close();
4041
});
4142

4243
it('watch() close() prevents buffered watch op from running (gh-7022)', async function() {
4344
const MyModel = db.model('Test', new Schema({}));
4445
const changeStream = MyModel.watch();
45-
const ready = new global.Promise(resolve => {
46+
const ready = new Promise(resolve => {
4647
changeStream.once('data', () => {
4748
resolve(true);
4849
});
4950
setTimeout(resolve, 500, false);
5051
});
5152

53+
// Change stream may still emit "MongoAPIError: ChangeStream is closed" because change stream
54+
// may still poll after close.
55+
changeStream.on('error', () => {});
5256
const close = changeStream.close();
5357
await db.asPromise();
5458
const readyCalled = await ready;
@@ -64,12 +68,16 @@ describe('model: watch: ', function() {
6468
await MyModel.init();
6569

6670
const changeStream = MyModel.watch();
67-
const closed = new global.Promise(resolve => {
71+
const closed = new Promise(resolve => {
6872
changeStream.once('close', () => resolve(true));
6973
});
7074

7175
await MyModel.create({ name: 'Hodor' });
7276

77+
// Change stream may still emit "MongoAPIError: ChangeStream is closed" because change stream
78+
// may still poll after close.
79+
changeStream.on('error', () => {});
80+
7381
await changeStream.close();
7482

7583
const closedData = await closed;

test/query.cursor.test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,8 @@ describe('QueryCursor', function() {
415415
await cursor.next();
416416
assert.ok(false);
417417
} catch (error) {
418-
assert.equal(error.name, 'MongoCursorExhaustedError');
418+
assert.equal(error.name, 'MongooseError');
419+
assert.ok(error.message.includes('closed cursor'), error.message);
419420
}
420421
});
421422
});

0 commit comments

Comments
 (0)