Skip to content

Commit c5f74ab

Browse files
nbbeekendurran
andauthored
feat(NODE-7223): run checkout on connect regardless of credentials (#4715)
Co-authored-by: Durran Jordan <[email protected]>
1 parent 897cbae commit c5f74ab

File tree

9 files changed

+104
-77
lines changed

9 files changed

+104
-77
lines changed

src/mongo_client.ts

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -569,20 +569,13 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> implements
569569
}
570570

571571
/**
572-
* Connect to MongoDB using a url
572+
* An optional method to verify a handful of assumptions that are generally useful at application boot-time before using a MongoClient.
573+
* For detailed information about the connect process see the MongoClient.connect static method documentation.
573574
*
574-
* @remarks
575-
* Calling `connect` is optional since the first operation you perform will call `connect` if it's needed.
576-
* `timeoutMS` will bound the time any operation can take before throwing a timeout error.
577-
* However, when the operation being run is automatically connecting your `MongoClient` the `timeoutMS` will not apply to the time taken to connect the MongoClient.
578-
* This means the time to setup the `MongoClient` does not count against `timeoutMS`.
579-
* If you are using `timeoutMS` we recommend connecting your client explicitly in advance of any operation to avoid this inconsistent execution time.
580-
*
581-
* @remarks
582-
* The driver will look up corresponding SRV and TXT records if the connection string starts with `mongodb+srv://`.
583-
* If those look ups throw a DNS Timeout error, the driver will retry the look up once.
575+
* @param url - The MongoDB connection string (supports `mongodb://` and `mongodb+srv://` schemes)
576+
* @param options - Optional configuration options for the client
584577
*
585-
* @see docs.mongodb.org/manual/reference/connection-string/
578+
* @see https://www.mongodb.com/docs/manual/reference/connection-string/
586579
*/
587580
async connect(): Promise<this> {
588581
if (this.connectionLock) {
@@ -843,21 +836,35 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> implements
843836
}
844837

845838
/**
846-
* Connect to MongoDB using a url
839+
* Creates a new MongoClient instance and immediately connects it to MongoDB.
840+
* This convenience method combines `new MongoClient(url, options)` and `client.connect()` in a single step.
841+
*
842+
* Connect can be helpful to detect configuration issues early by validating:
843+
* - **DNS Resolution**: Verifies that SRV records and hostnames in the connection string resolve DNS entries
844+
* - **Network Connectivity**: Confirms that host addresses are reachable and ports are open
845+
* - **TLS Configuration**: Validates SSL/TLS certificates, CA files, and encryption settings are correct
846+
* - **Authentication**: Verifies that provided credentials are valid
847+
* - **Server Compatibility**: Ensures the MongoDB server version is supported by this driver version
848+
* - **Load Balancer Setup**: For load-balanced deployments, confirms the service is properly configured
849+
*
850+
* @returns A promise that resolves to the same MongoClient instance once connected
847851
*
848852
* @remarks
849-
* Calling `connect` is optional since the first operation you perform will call `connect` if it's needed.
850-
* `timeoutMS` will bound the time any operation can take before throwing a timeout error.
851-
* However, when the operation being run is automatically connecting your `MongoClient` the `timeoutMS` will not apply to the time taken to connect the MongoClient.
852-
* This means the time to setup the `MongoClient` does not count against `timeoutMS`.
853-
* If you are using `timeoutMS` we recommend connecting your client explicitly in advance of any operation to avoid this inconsistent execution time.
853+
* **Connection is Optional:** Calling `connect` is optional since any operation method (`find`, `insertOne`, etc.)
854+
* will automatically perform these same validation steps if the client is not already connected.
855+
* However, explicitly calling `connect` can make sense for:
856+
* - **Fail-fast Error Detection**: Non-transient connection issues (hostname unresolved, port refused connection) are discovered immediately rather than during your first operation
857+
* - **Predictable Performance**: Eliminates first connection overhead from your first database operation
854858
*
855859
* @remarks
856-
* The programmatically provided options take precedence over the URI options.
860+
* **Connection Pooling Impact:** Calling `connect` will populate the connection pool with one connection
861+
* to a server selected by the client's configured `readPreference` (defaults to primary).
857862
*
858863
* @remarks
859-
* The driver will look up corresponding SRV and TXT records if the connection string starts with `mongodb+srv://`.
860-
* If those look ups throw a DNS Timeout error, the driver will retry the look up once.
864+
* **Timeout Behavior:** When using `timeoutMS`, the connection establishment time does not count against
865+
* the timeout for subsequent operations. This means `connect` runs without a `timeoutMS` limit, while
866+
* your database operations will still respect the configured timeout. If you need predictable operation
867+
* timing with `timeoutMS`, call `connect` explicitly before performing operations.
861868
*
862869
* @see https://www.mongodb.com/docs/manual/reference/connection-string/
863870
*/

src/sdam/topology.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ export class Topology extends TypedEventEmitter<TopologyEvents> {
465465
);
466466

467467
const skipPingOnConnect = this.s.options.__skipPingOnConnect === true;
468-
if (!skipPingOnConnect && this.s.credentials) {
468+
if (!skipPingOnConnect) {
469469
const connection = await server.pool.checkOut({ timeoutContext: timeoutContext });
470470
server.pool.checkIn(connection);
471471
stateTransition(this, STATE_CONNECTED);

test/integration/initial-dns-seedlist-discovery/initial_dns_seedlist_discovery.prose.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { expect } from 'chai';
22
import * as dns from 'dns';
33
import * as sinon from 'sinon';
44

5-
import { MongoAPIError, Server, ServerDescription, Topology } from '../../mongodb';
5+
import { ConnectionPool, MongoAPIError, Server, ServerDescription, Topology } from '../../mongodb';
66
import { topologyWithPlaceholderClient } from '../../tools/utils';
77

88
describe('Initial DNS Seedlist Discovery (Prose Tests)', () => {
@@ -41,6 +41,14 @@ describe('Initial DNS Seedlist Discovery (Prose Tests)', () => {
4141
{} as any
4242
);
4343
});
44+
45+
sinon.stub(ConnectionPool.prototype, 'checkOut').callsFake(async function () {
46+
return {};
47+
});
48+
49+
sinon.stub(ConnectionPool.prototype, 'checkIn').callsFake(function () {
50+
return;
51+
});
4452
});
4553

4654
afterEach(async function () {

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

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
type CommandSucceededEvent,
1111
Db,
1212
MongoClient,
13+
MongoNetworkError,
1314
MongoNotConnectedError,
1415
MongoServerSelectionError,
1516
ReadPreference
@@ -315,6 +316,28 @@ describe('class MongoClient', function () {
315316
}
316317
});
317318
});
319+
320+
it('throws ENOTFOUND error when connecting to non-existent host with no auth and loadBalanced=true', async function () {
321+
const configuration = this.configuration;
322+
const client = configuration.newClient(
323+
'mongodb://iLoveJavaScript:27017/test?loadBalanced=true',
324+
{ serverSelectionTimeoutMS: 100 }
325+
);
326+
327+
const error = await client.connect().catch(error => error);
328+
expect(error).to.be.instanceOf(MongoNetworkError); // not server selection like other topologies
329+
expect(error.message).to.match(/ENOTFOUND/);
330+
});
331+
332+
it('throws an error when srv is not a real record', async function () {
333+
const client = this.configuration.newClient('mongodb+srv://iLoveJavaScript/test', {
334+
serverSelectionTimeoutMS: 100
335+
});
336+
337+
const error = await client.connect().catch(error => error);
338+
expect(error).to.be.instanceOf(Error);
339+
expect(error.message).to.match(/ENOTFOUND/);
340+
});
318341
});
319342

320343
it('Should correctly pass through appname', async function () {
@@ -528,31 +551,13 @@ describe('class MongoClient', function () {
528551
await client.close();
529552
});
530553

531-
it(
532-
'creates topology and checks out connection when auth is enabled',
533-
{ requires: { auth: 'enabled' } },
534-
async function () {
535-
const checkoutStarted = once(client, 'connectionCheckOutStarted');
536-
await client.connect();
537-
const checkout = await checkoutStarted;
538-
expect(checkout).to.exist;
539-
expect(client).to.have.property('topology').that.is.instanceOf(Topology);
540-
}
541-
);
542-
543-
it(
544-
'does not checkout connection when authentication is disabled',
545-
{ requires: { auth: 'disabled' } },
546-
async function () {
547-
const checkoutStartedEvents = [];
548-
client.on('connectionCheckOutStarted', event => {
549-
checkoutStartedEvents.push(event);
550-
});
551-
await client.connect();
552-
expect(checkoutStartedEvents).to.be.empty;
553-
expect(client).to.have.property('topology').that.is.instanceOf(Topology);
554-
}
555-
);
554+
it('creates topology and checks out connection', async function () {
555+
const checkoutStarted = once(client, 'connectionCheckOutStarted');
556+
await client.connect();
557+
const checkout = await checkoutStarted;
558+
expect(checkout).to.exist;
559+
expect(client).to.have.property('topology').that.is.instanceOf(Topology);
560+
});
556561

557562
it(
558563
'permits operations to be run after connect is called',

test/tools/utils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,17 @@ export async function makeMultiResponseBatchModelArray(
463463
return models;
464464
}
465465

466+
export function fakeServer() {
467+
return {
468+
s: { state: 'connected' },
469+
removeListener: () => true,
470+
pool: {
471+
checkOut: async () => ({}),
472+
checkIn: () => undefined
473+
}
474+
};
475+
}
476+
466477
/**
467478
* A utility to measure the duration of an async function. This is intended to be used for CSOT
468479
* testing, where we expect to timeout within a certain threshold and want to measure the duration

test/unit/assorted/server_discovery_and_monitoring.spec.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import { Server } from '../../../src/sdam/server';
3838
import { ServerDescription, type TopologyVersion } from '../../../src/sdam/server_description';
3939
import { Topology } from '../../../src/sdam/topology';
4040
import { isRecord, ns, squashError } from '../../../src/utils';
41-
import { ejson } from '../../tools/utils';
41+
import { ejson, fakeServer } from '../../tools/utils';
4242

4343
const SDAM_EVENT_CLASSES = {
4444
ServerDescriptionChangedEvent,
@@ -214,9 +214,7 @@ describe('Server Discovery and Monitoring (spec)', function () {
214214
.stub(Topology.prototype, 'selectServer')
215215
.callsFake(async function (_selector, _options) {
216216
topologySelectServers.restore();
217-
218-
const fakeServer = { s: { state: 'connected' }, removeListener: () => true };
219-
return fakeServer;
217+
return fakeServer();
220218
});
221219
});
222220

test/unit/assorted/server_discovery_and_monitoring.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Server } from '../../../src/sdam/server';
88
import { ServerDescription } from '../../../src/sdam/server_description';
99
import { Topology } from '../../../src/sdam/topology';
1010
import { type TopologyDescription } from '../../../src/sdam/topology_description';
11+
import { fakeServer } from '../../tools/utils';
1112

1213
describe('Server Discovery and Monitoring', function () {
1314
let serverConnect: sinon.SinonStub;
@@ -30,9 +31,7 @@ describe('Server Discovery and Monitoring', function () {
3031
.stub(Topology.prototype, 'selectServer')
3132
.callsFake(async function (_selector, _options) {
3233
topologySelectServer.restore();
33-
34-
const fakeServer = { s: { state: 'connected' }, removeListener: () => true };
35-
return fakeServer;
34+
return fakeServer();
3635
});
3736

3837
events = [];

test/unit/assorted/server_selection_spec_helper.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const ServerSelectors = require('../../../src/sdam/server_selection');
99

1010
const sinon = require('sinon');
1111
const { expect } = require('chai');
12-
const { topologyWithPlaceholderClient } = require('../../tools/utils');
12+
const { fakeServer, topologyWithPlaceholderClient } = require('../../tools/utils');
1313

1414
export function serverDescriptionFromDefinition(definition, hosts) {
1515
hosts = hosts || [];
@@ -105,9 +105,7 @@ export async function executeServerSelectionTest(testDefinition) {
105105
.stub(Topology.prototype, 'selectServer')
106106
.callsFake(async function () {
107107
topologySelectServers.restore();
108-
109-
const fakeServer = { s: { state: 'connected' }, removeListener: () => {} };
110-
return fakeServer;
108+
return fakeServer();
111109
});
112110

113111
await topology.connect();

test/unit/sdam/topology.test.ts

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { Topology } from '../../../src/sdam/topology';
2525
import { TopologyDescription } from '../../../src/sdam/topology_description';
2626
import { TimeoutContext } from '../../../src/timeout';
2727
import { isHello, ns } from '../../../src/utils';
28+
import { ConnectionPool } from '../../mongodb';
2829
import * as mock from '../../tools/mongodb-mock/index';
2930
import { topologyWithPlaceholderClient } from '../../tools/utils';
3031

@@ -445,31 +446,23 @@ describe('Topology (unit)', function () {
445446

446447
describe('selectServer()', function () {
447448
it('should schedule monitoring if no suitable server is found', async function () {
448-
const topology = topologyWithPlaceholderClient('someserver:27019', {});
449+
const topology = topologyWithPlaceholderClient(
450+
'someserver:27019',
451+
{},
452+
{ serverSelectionTimeoutMS: 10 }
453+
);
449454
const requestCheck = sinon.stub(Server.prototype, 'requestCheck');
450455

451-
// satisfy the initial connect, then restore the original method
452-
const selectServer = sinon
453-
.stub(Topology.prototype, 'selectServer')
454-
.callsFake(async function () {
455-
const server = Array.from(this.s.servers.values())[0];
456-
selectServer.restore();
457-
return server;
458-
});
459-
460456
sinon.stub(Server.prototype, 'connect').callsFake(function () {
461457
this.s.state = 'connected';
462458
this.emit('connect');
463459
return;
464460
});
465461

466-
await topology.connect();
467-
const err = await topology
468-
.selectServer(ReadPreference.secondary, { serverSelectionTimeoutMS: 1000 })
469-
.then(
470-
() => null,
471-
e => e
472-
);
462+
const err = await topology.connect().then(
463+
() => null,
464+
e => e
465+
);
473466
expect(err).to.match(/Server selection timed out/);
474467
expect(err).to.have.property('reason');
475468
// When server is created `connect` is called on the monitor. When server selection
@@ -517,12 +510,20 @@ describe('Topology (unit)', function () {
517510
this.emit('connect');
518511
});
519512

513+
sinon.stub(ConnectionPool.prototype, 'checkOut').callsFake(async function () {
514+
return {};
515+
});
516+
517+
sinon.stub(ConnectionPool.prototype, 'checkIn').callsFake(function (_) {
518+
return;
519+
});
520+
520521
const toSelect = 10;
521522
let completed = 0;
522523
// methodology:
523524
// - perform 9 server selections, a few with a selector that throws an error
524525
// - ensure each selection immediately returns an empty result (gated by a boolean)
525-
// guaranteeing tha the queue will be full before the last selection
526+
// guaranteeing that the queue will be full before the last selection
526527
// - make one last selection, but ensure that all selections are no longer blocked from
527528
// returning their value
528529
// - verify that 10 callbacks were called

0 commit comments

Comments
 (0)