-
-
Notifications
You must be signed in to change notification settings - Fork 4k
Description
Prerequisites
- I have written a descriptive issue title
- I have searched existing issues to ensure the bug has not already been reported
Mongoose version
8.16.3
Node.js version
20.x
MongoDB server version
7.0.15
Typescript version (if applicable)
No response
Description
Calling mongoose.disconnect() or mongoose.connection.close() when there's another connection created via useDb() causes the current mongoose instance to be marked as force closed and then becomes completely unusable.
Steps to Reproduce
Consider the following code:
import mongoose from 'mongoose';
const schema = new mongoose.Schema({
name: String,
age: Number,
});
const User = mongoose.model('User', schema);
mongoose.connect('mongodb://localhost:27017/mydatabase');
const anotherConnection = mongoose.connection.useDb('anotherDatabase');
console.log(await User.create({ name: 'Alice', age: 30 }));
await mongoose.connection.close()
await mongoose.connect('mongodb://localhost:27017/mydatabase');
console.log(await User.findOne({ name: 'Alice', age: 30 }));The last call to findOne() errors with the following:
/private/tmp/mongoose-force-connection/node_modules/mongoose/lib/drivers/node-mongodb-native/collection.js:115
const error = new MongooseError('Connection was force closed');
^
MongooseError: Connection was force closed
at NativeCollection.<computed> [as findOne] (/private/tmp/mongoose-force-connection/node_modules/mongoose/lib/drivers/node-mongodb-native/collection.js:115:21)
at model.Query._findOne (/private/tmp/mongoose-force-connection/node_modules/mongoose/lib/query.js:2687:45)
at model.Query.exec (/private/tmp/mongoose-force-connection/node_modules/mongoose/lib/query.js:4604:80)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async <anonymous> (/private/tmp/mongoose-force-connection/index.ts:17:13)
Node.js v20.19.0The issue is also present when using mongoose.disconnect() instead of mongoose.connection.close().
I spent some time on Friday following the code through and I've figured out why this is happening: it is related to useDb() and the internal creation of the otherDbs storage. To help with debugging this, I've added the following console.log statement here in the code:
console.log('this.otherDbs', this.otherDbs.map(db => db.name));- When we call connection.close(), it hits this code path:
Lines 1203 to 1226 in 0a6a199
| Connection.prototype.close = async function close(force) { | |
| if (typeof force === 'function' || (arguments.length === 2 && typeof arguments[1] === 'function')) { | |
| throw new MongooseError('Connection.prototype.close() no longer accepts a callback'); | |
| } | |
| if (force != null && typeof force === 'object') { | |
| this.$wasForceClosed = !!force.force; | |
| } else { | |
| this.$wasForceClosed = !!force; | |
| } | |
| if (this._lastHeartbeatAt != null) { | |
| this._lastHeartbeatAt = null; | |
| } | |
| for (const model of Object.values(this.models)) { | |
| // If manually disconnecting, make sure to clear each model's `$init` | |
| // promise, so Mongoose knows to re-run `init()` in case the | |
| // connection is re-opened. See gh-12047. | |
| delete model.$init; | |
| } | |
| return this._close(force, false); | |
| }; |
- This internally calls
this._close():
Line 1236 in 0a6a199
| Connection.prototype._close = async function _close(force, destroy) { |
If you are already connected (which we are), that comes through into here and calls this.onClose():
Line 1264 in 0a6a199
| this.onClose(force); |
- Then
onCloseinterates throughthis.otherDbsand calls close again:
Line 1328 in 0a6a199
| this._destroyCalled ? db.destroy({ force: force, skipCloseClient: true }) : db.close({ force: force, skipCloseClient: true }); |
This time with the following arguments:
db.close({ force: force, skipCloseClient: true })Log output is this, showing otherDbs as the second connection:
this.otherDbs [ 'second-database' ]This loops us back around to step 1 again, then when we get to step 3, we get this log output:
this.otherDbs [ 'first-database' ]I believe this to be the source of the bug. connection.close() is getting called 3 times in total, the 3rd and final time is being marked as a force close. The calls to close look like this:
// This is the only one from my code
mongoose.connection.close()
// The first time `onClose`, force: undefined because it wasn't passed to `close()` originally
// The `db` instance here is the `second-database` connection
db.close({ force: undefined, skipCloseClient: true })
// The second time `onClose`, force value is nested due to the previous call
// The `db` instance here is the `first-database` connection again
db.close({ force: { force: undefined, skipCloseClient: true }, skipCloseClient: true })You can see the force property from the 2nd call is getting passed through and nested on the 3rd. It's this final call that is causing this conditional to be true:
Lines 1208 to 1210 in 0a6a199
| if (force != null && typeof force === 'object') { | |
| this.$wasForceClosed = !!force.force; | |
| } else { |
Which finally sets this.$wasForceClosed = true. Any subsequent attempt to use mongoose methods then hit this line in the driver saying that "Connection was force closed":
| const error = new MongooseError('Connection was force closed'); |
Expected Behavior
The above code should work as expected, $wasForceClosed should not be getting set to true when it is not provided to mongoose.connection.close().
I have found a workaround which is to provide noListener: true as an option to the useDb() call:
const anotherConnection = mongoose.connection.useDb('second-database', { noListener: true });This in effect just means the second database connection never gets saved into otherDbs at all:
mongoose/lib/drivers/node-mongodb-native/connection.js
Lines 121 to 124 in 0a6a199
| // push onto the otherDbs stack, this is used when state changes | |
| if (options.noListener !== true) { | |
| this.otherDbs.push(newConn); | |
| } |
This works for my case, but this seems like a bug in mongoose.