Skip to content

Commit eb836bb

Browse files
clemclxnbbeekenbaileympearson
authored
fix(NODE-5106): prevent multiple mongo client connect()s from leaking topology (#3596)
Co-authored-by: Neal Beeken <neal.beeken@mongodb.com> Co-authored-by: Bailey Pearson <bailey.pearson@mongodb.com>
1 parent 2647b61 commit eb836bb

File tree

2 files changed

+83
-0
lines changed

2 files changed

+83
-0
lines changed

src/mongo_client.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,8 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> {
318318
topology?: Topology;
319319
/** @internal */
320320
readonly mongoLogger: MongoLogger;
321+
/** @internal */
322+
private connectionLock?: Promise<this>;
321323

322324
/**
323325
* The consolidate, parsed, transformed and merged options.
@@ -405,6 +407,28 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> {
405407
* @see docs.mongodb.org/manual/reference/connection-string/
406408
*/
407409
async connect(): Promise<this> {
410+
if (this.connectionLock) {
411+
return this.connectionLock;
412+
}
413+
414+
try {
415+
this.connectionLock = this._connect();
416+
await this.connectionLock;
417+
} finally {
418+
// release
419+
this.connectionLock = undefined;
420+
}
421+
422+
return this;
423+
}
424+
425+
/**
426+
* Create a topology to open the connection, must be locked to avoid topology leaks in concurrency scenario.
427+
* Locking is enforced by the connect method.
428+
*
429+
* @internal
430+
*/
431+
private async _connect(): Promise<this> {
408432
if (this.topology && this.topology.isConnected()) {
409433
return this;
410434
}

test/integration/node-specific/mongo_client.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,65 @@ describe('class MongoClient', function () {
517517
);
518518
});
519519

520+
context('concurrent #connect()', () => {
521+
let client: MongoClient;
522+
let topologyOpenEvents;
523+
524+
/** Keep track number of call to client connect to close as many as connect (otherwise leak_checker hook will failed) */
525+
let clientConnectCounter: number;
526+
527+
/**
528+
* Wrap the connect method of the client to keep track
529+
* of number of times connect is called
530+
*/
531+
async function clientConnect() {
532+
if (!client) {
533+
return;
534+
}
535+
clientConnectCounter++;
536+
return client.connect();
537+
}
538+
539+
beforeEach(async function () {
540+
client = this.configuration.newClient();
541+
topologyOpenEvents = [];
542+
clientConnectCounter = 0;
543+
client.on('open', event => topologyOpenEvents.push(event));
544+
});
545+
546+
afterEach(async function () {
547+
// close `clientConnectCounter` times
548+
const clientClosePromises = Array.from({ length: clientConnectCounter }, () =>
549+
client.close()
550+
);
551+
await Promise.all(clientClosePromises);
552+
});
553+
554+
it('parallel client connect calls only create one topology', async function () {
555+
await Promise.all([clientConnect(), clientConnect(), clientConnect()]);
556+
557+
expect(topologyOpenEvents).to.have.lengthOf(1);
558+
expect(client.topology?.isConnected()).to.be.true;
559+
});
560+
561+
it('when connect rejects lock is released regardless', async function () {
562+
const internalConnectStub = sinon.stub(client, '_connect' as keyof MongoClient);
563+
internalConnectStub.onFirstCall().rejects(new Error('cannot connect'));
564+
565+
// first call rejected to simulate a connection failure
566+
const error = await clientConnect().catch(error => error);
567+
expect(error).to.match(/cannot connect/);
568+
569+
internalConnectStub.restore();
570+
571+
// second call should connect
572+
await clientConnect();
573+
574+
expect(topologyOpenEvents).to.have.lengthOf(1);
575+
expect(client.topology?.isConnected()).to.be.true;
576+
});
577+
});
578+
520579
context('#close()', () => {
521580
let client: MongoClient;
522581
let db: Db;

0 commit comments

Comments
 (0)