Skip to content

Commit c44c225

Browse files
clemclxnbbeekenbaileympearson
committed
fix(NODE-5106): prevent multiple mongo client connect()s from leaking topology (#3596)
Co-authored-by: Neal Beeken <[email protected]> Co-authored-by: Bailey Pearson <[email protected]>
1 parent 261199f commit c44c225

File tree

2 files changed

+61
-0
lines changed

2 files changed

+61
-0
lines changed

Diff for: src/mongo_client.ts

+2
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,8 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> {
343343
topology?: Topology;
344344
/** @internal */
345345
readonly mongoLogger: MongoLogger;
346+
/** @internal */
347+
private connectionLock?: Promise<this>;
346348

347349
/**
348350
* The consolidate, parsed, transformed and merged options.

Diff for: test/integration/node-specific/mongo_client.test.ts

+59
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,65 @@ describe('class MongoClient', function () {
516516
);
517517
});
518518

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

0 commit comments

Comments
 (0)