Skip to content

Commit ed65746

Browse files
committed
fix(NODE-5106): refactor connect locking and add test
1 parent b139f17 commit ed65746

File tree

2 files changed

+103
-44
lines changed

2 files changed

+103
-44
lines changed

src/mongo_client.ts

Lines changed: 53 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -407,63 +407,72 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> {
407407
* @see docs.mongodb.org/manual/reference/connection-string/
408408
*/
409409
async connect(): Promise<this> {
410-
if (this.topology && this.topology.isConnected()) {
411-
return this;
412-
}
413-
414410
if (this.connectionLock) {
415411
return this.connectionLock;
416412
}
417413

418-
this.connectionLock = (async () => {
419-
const options = this[kOptions];
414+
try {
415+
this.connectionLock = this._connect();
416+
await this.connectionLock;
417+
} finally {
418+
// release
419+
this.connectionLock = undefined;
420+
}
420421

421-
if (typeof options.srvHost === 'string') {
422-
const hosts = await resolveSRVRecord(options);
422+
return this;
423+
}
423424

424-
for (const [index, host] of hosts.entries()) {
425-
options.hosts[index] = host;
426-
}
427-
}
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+
* When decorators available put implementation back to original connect methods
430+
* and enforce locking via a dedicated decorator.
431+
* @see https://github.com/mongodb/node-mongodb-native/pull/3596#discussion_r1134211500
432+
*/
433+
private async _connect(): Promise<this> {
434+
if (this.topology && this.topology.isConnected()) {
435+
return this;
436+
}
428437

429-
const topology = new Topology(options.hosts, options);
430-
// Events can be emitted before initialization is complete so we have to
431-
// save the reference to the topology on the client ASAP if the event handlers need to access it
432-
this.topology = topology;
433-
topology.client = this;
438+
const options = this[kOptions];
434439

435-
topology.once(Topology.OPEN, () => this.emit('open', this));
440+
if (typeof options.srvHost === 'string') {
441+
const hosts = await resolveSRVRecord(options);
436442

437-
for (const event of MONGO_CLIENT_EVENTS) {
438-
topology.on(event, (...args: any[]) => this.emit(event, ...(args as any)));
443+
for (const [index, host] of hosts.entries()) {
444+
options.hosts[index] = host;
439445
}
446+
}
440447

441-
const topologyConnect = async () => {
442-
try {
443-
await promisify(callback => topology.connect(options, callback))();
444-
} catch (error) {
445-
topology.close({ force: true });
446-
throw error;
447-
}
448-
};
449-
450-
if (this.autoEncrypter) {
451-
const initAutoEncrypter = promisify(callback => this.autoEncrypter?.init(callback));
452-
await initAutoEncrypter();
453-
await topologyConnect();
454-
await options.encrypter.connectInternalClient();
455-
} else {
456-
await topologyConnect();
457-
}
448+
const topology = new Topology(options.hosts, options);
449+
// Events can be emitted before initialization is complete so we have to
450+
// save the reference to the topology on the client ASAP if the event handlers need to access it
451+
this.topology = topology;
452+
topology.client = this;
458453

459-
return this;
460-
})();
454+
topology.once(Topology.OPEN, () => this.emit('open', this));
461455

462-
try {
463-
await this.connectionLock;
464-
} finally {
465-
// release
466-
this.connectionLock = undefined;
456+
for (const event of MONGO_CLIENT_EVENTS) {
457+
topology.on(event, (...args: any[]) => this.emit(event, ...(args as any)));
458+
}
459+
460+
const topologyConnect = async () => {
461+
try {
462+
await promisify(callback => topology.connect(options, callback))();
463+
} catch (error) {
464+
topology.close({ force: true });
465+
throw error;
466+
}
467+
};
468+
469+
if (this.autoEncrypter) {
470+
const initAutoEncrypter = promisify(callback => this.autoEncrypter?.init(callback));
471+
await initAutoEncrypter();
472+
await topologyConnect();
473+
await options.encrypter.connectInternalClient();
474+
} else {
475+
await topologyConnect();
467476
}
468477

469478
return this;

test/integration/mongo_client.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { expect } from 'chai';
2+
import * as sinon from 'sinon';
3+
4+
import { MongoClient } from '../../src';
5+
6+
describe('MongoClient', () => {
7+
let client: MongoClient;
8+
let topologyOpenEvents;
9+
10+
beforeEach(async function () {
11+
client = this.configuration.newClient();
12+
topologyOpenEvents = [];
13+
client.on('open', event => topologyOpenEvents.push(event));
14+
});
15+
16+
afterEach(async function () {
17+
await client.close();
18+
});
19+
20+
it('Concurrents client connect correctly locked (only one topology created)', async function () {
21+
await Promise.all([client.connect(), client.connect(), client.connect()]);
22+
23+
expect(topologyOpenEvents).to.have.lengthOf(1);
24+
expect(client.topology?.isConnected()).to.be.true;
25+
});
26+
27+
it('Failed client connect must properly release lock', async function () {
28+
const internalConnectStub = sinon.stub(client, '_connect' as keyof MongoClient);
29+
internalConnectStub.onFirstCall().rejects();
30+
31+
// first call rejected to simulate a connection failure
32+
try {
33+
await client.connect();
34+
} catch (err) {
35+
expect(err).to.exist;
36+
}
37+
38+
internalConnectStub.restore();
39+
40+
// second call should connect
41+
try {
42+
await client.connect();
43+
} catch (err) {
44+
expect.fail(`client connect throwed unexpected error`);
45+
}
46+
47+
expect(topologyOpenEvents).to.have.lengthOf(1);
48+
expect(client.topology?.isConnected()).to.be.true;
49+
});
50+
});

0 commit comments

Comments
 (0)