Skip to content

Commit 2005e21

Browse files
committed
feat(NODE-7223): run checkout on connect regardless of credentials
1 parent 28f0152 commit 2005e21

File tree

19 files changed

+186
-1158
lines changed

19 files changed

+186
-1158
lines changed

src/mongo_client.ts

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

596596
/**
597-
* Connect to MongoDB using a url
597+
* An optional method to verify a typical set of preconditions before using a MongoClient.
598+
* For detailed information about the connect process see the MongoClient.connect static method documentation.
598599
*
599-
* @remarks
600-
* Calling `connect` is optional since the first operation you perform will call `connect` if it's needed.
601-
* `timeoutMS` will bound the time any operation can take before throwing a timeout error.
602-
* However, when the operation being run is automatically connecting your `MongoClient` the `timeoutMS` will not apply to the time taken to connect the MongoClient.
603-
* This means the time to setup the `MongoClient` does not count against `timeoutMS`.
604-
* If you are using `timeoutMS` we recommend connecting your client explicitly in advance of any operation to avoid this inconsistent execution time.
605-
*
606-
* @remarks
607-
* The driver will look up corresponding SRV and TXT records if the connection string starts with `mongodb+srv://`.
608-
* If those look ups throw a DNS Timeout error, the driver will retry the look up once.
600+
* @param url - The MongoDB connection string (supports `mongodb://` and `mongodb+srv://` schemes)
601+
* @param options - Optional configuration options for the client
609602
*
610-
* @see docs.mongodb.org/manual/reference/connection-string/
603+
* @see https://www.mongodb.com/docs/manual/reference/connection-string/
611604
*/
612605
async connect(): Promise<this> {
613606
if (this.connectionLock) {
@@ -868,21 +861,35 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> implements
868861
}
869862

870863
/**
871-
* Connect to MongoDB using a url
864+
* Creates a new MongoClient instance and immediately connects it to MongoDB.
865+
* This convenience method combines `new MongoClient(url, options)` and `client.connect()` in a single step.
866+
*
867+
* Connect can be helpful to detect configuration issues early by validating:
868+
* - **DNS Resolution**: Verifies that SRV records and hostnames in the connection string resolve DNS entries
869+
* - **Network Connectivity**: Confirms that host addresses are reachable and ports are open
870+
* - **TLS Configuration**: Validates SSL/TLS certificates, CA files, and encryption settings are correct
871+
* - **Authentication**: Verifies that provided credentials are valid
872+
* - **Server Compatibility**: Ensures the MongoDB server version is supported by this driver version
873+
* - **Load Balancer Setup**: For load-balanced deployments, confirms the service is properly configured
874+
*
875+
* @returns A promise that resolves to the same MongoClient instance once connected
872876
*
873877
* @remarks
874-
* Calling `connect` is optional since the first operation you perform will call `connect` if it's needed.
875-
* `timeoutMS` will bound the time any operation can take before throwing a timeout error.
876-
* However, when the operation being run is automatically connecting your `MongoClient` the `timeoutMS` will not apply to the time taken to connect the MongoClient.
877-
* This means the time to setup the `MongoClient` does not count against `timeoutMS`.
878-
* If you are using `timeoutMS` we recommend connecting your client explicitly in advance of any operation to avoid this inconsistent execution time.
878+
* **Connection is Optional:** Calling `connect` is optional since any operation method (`find`, `insertOne`, etc.)
879+
* will automatically perform these same validation steps if the client is not already connected.
880+
* However, explicitly calling `connect` can make sense for:
881+
* - **Fail-fast Error Detection**: Non-transient connection issues (hostname unresolved, port refused connection) are discovered immediately rather than during your first operation
882+
* - **Predictable Performance**: Eliminates first connection overhead from your first database operation
879883
*
880884
* @remarks
881-
* The programmatically provided options take precedence over the URI options.
885+
* **Connection Pooling Impact:** Calling `connect` will populate the connection pool with one connection
886+
* to a server selected by the client's configured `readPreference` (defaults to primary).
882887
*
883888
* @remarks
884-
* The driver will look up corresponding SRV and TXT records if the connection string starts with `mongodb+srv://`.
885-
* If those look ups throw a DNS Timeout error, the driver will retry the look up once.
889+
* **Timeout Behavior:** When using `timeoutMS`, the connection establishment time does not count against
890+
* the timeout for subsequent operations. This means `connect` runs without a `timeoutMS` limit, while
891+
* your database operations will still respect the configured timeout. If you need predictable operation
892+
* timing with `timeoutMS`, call `connect` explicitly before performing operations.
886893
*
887894
* @see https://www.mongodb.com/docs/manual/reference/connection-string/
888895
*/

src/operations/execute_operation.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,6 @@ export async function executeOperation<
8181
session = client.startSession({ owner, explicit: false });
8282
} else if (session.hasEnded) {
8383
throw new MongoExpiredSessionError('Use of expired sessions is not permitted');
84-
} else if (session.snapshotEnabled && !topology.capabilities.supportsSnapshotReads) {
85-
throw new MongoCompatibilityError('Snapshot reads require MongoDB 5.0 or later');
8684
} else if (session.client !== client) {
8785
throw new MongoInvalidArgumentError('ClientSession must be from the same MongoClient');
8886
}
@@ -206,6 +204,10 @@ async function tryOperation<T extends AbstractOperation, TResult = ResultTypeFro
206204
signal: operation.options.signal
207205
});
208206

207+
if (session?.snapshotEnabled && !topology.capabilities.supportsSnapshotReads) {
208+
throw new MongoCompatibilityError('Snapshot reads require MongoDB 5.0 or later');
209+
}
210+
209211
const hasReadAspect = operation.hasAspect(Aspect.READ_OPERATION);
210212
const hasWriteAspect = operation.hasAspect(Aspect.WRITE_OPERATION);
211213
const inTransaction = session?.inTransaction() ?? false;

src/sdam/topology.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -464,20 +464,14 @@ export class Topology extends TypedEventEmitter<TopologyEvents> {
464464
};
465465

466466
try {
467-
const server = await this.selectServer(
468-
readPreferenceServerSelector(readPreference),
469-
selectServerOptions
470-
);
471-
472467
const skipPingOnConnect = this.s.options.__skipPingOnConnect === true;
473-
if (!skipPingOnConnect && this.s.credentials) {
468+
if (!skipPingOnConnect) {
469+
const server = await this.selectServer(
470+
readPreferenceServerSelector(readPreference),
471+
selectServerOptions
472+
);
474473
const connection = await server.pool.checkOut({ timeoutContext: timeoutContext });
475474
server.pool.checkIn(connection);
476-
stateTransition(this, STATE_CONNECTED);
477-
this.emit(Topology.OPEN, this);
478-
this.emit(Topology.CONNECT, this);
479-
480-
return this;
481475
}
482476

483477
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/abort_signal.test.ts

Lines changed: 22 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ import {
1818
type Log,
1919
type MongoClient,
2020
MongoServerError,
21+
MongoServerSelectionError,
2122
promiseWithResolvers,
2223
ReadPreference,
2324
setDifference,
24-
StateMachine
25+
StateMachine,
26+
Topology
2527
} from '../../mongodb';
2628
import {
2729
clearFailPoint,
@@ -612,35 +614,22 @@ describe('AbortSignal support', () => {
612614
let client: MongoClient;
613615
let db: Db;
614616
let collection: Collection<{ a: number; ssn: string }>;
615-
const logs: Log[] = [];
616617
let connectStarted;
617618
let controller: AbortController;
618619
let signal: AbortSignal;
619620
let cursor: AbstractCursor<{ a: number }>;
620621

621622
describe('when connect succeeds', () => {
622623
beforeEach(async function () {
623-
logs.length = 0;
624-
625624
const promise = promiseWithResolvers<void>();
626625
connectStarted = promise.promise;
627626

628-
client = this.configuration.newClient(
629-
{},
630-
{
631-
mongodbLogComponentSeverities: { serverSelection: 'debug' },
632-
mongodbLogPath: {
633-
write: log => {
634-
if (log.c === 'serverSelection' && log.operation === 'handshake') {
635-
controller.abort();
636-
promise.resolve();
637-
}
638-
logs.push(log);
639-
}
640-
},
641-
serverSelectionTimeoutMS: 1000
642-
}
643-
);
627+
client = this.configuration.newClient({}, { serverSelectionTimeoutMS: 1000 });
628+
629+
client.once('open', () => {
630+
controller.abort();
631+
promise.resolve();
632+
});
644633
db = client.db('abortSignal');
645634
collection = db.collection('support');
646635

@@ -651,7 +640,6 @@ describe('AbortSignal support', () => {
651640
});
652641

653642
afterEach(async function () {
654-
logs.length = 0;
655643
await client?.close();
656644
});
657645

@@ -667,22 +655,18 @@ describe('AbortSignal support', () => {
667655

668656
describe('when connect fails', () => {
669657
beforeEach(async function () {
670-
logs.length = 0;
671-
672658
const promise = promiseWithResolvers<void>();
673659
connectStarted = promise.promise;
674660

661+
const selectServerStub = sinon
662+
.stub(Topology.prototype, 'selectServer')
663+
.callsFake(async function (...args) {
664+
controller.abort();
665+
promise.resolve();
666+
return selectServerStub.wrappedMethod.call(this, ...args);
667+
});
668+
675669
client = this.configuration.newClient('mongodb://iLoveJavaScript', {
676-
mongodbLogComponentSeverities: { serverSelection: 'debug' },
677-
mongodbLogPath: {
678-
write: log => {
679-
if (log.c === 'serverSelection' && log.operation === 'handshake') {
680-
controller.abort();
681-
promise.resolve();
682-
}
683-
logs.push(log);
684-
}
685-
},
686670
serverSelectionTimeoutMS: 200,
687671
maxPoolSize: 1
688672
});
@@ -696,18 +680,19 @@ describe('AbortSignal support', () => {
696680
});
697681

698682
afterEach(async function () {
699-
logs.length = 0;
683+
sinon.restore();
700684
await client?.close();
701685
});
702686

703-
it('escapes auto connect without interrupting it', async () => {
687+
it('server selection error is thrown before reaching signal abort state check', async () => {
704688
const toArray = cursor.toArray().catch(error => error);
705689
await connectStarted;
706-
expect(await toArray).to.be.instanceOf(DOMException);
690+
const findError = await toArray;
691+
expect(findError).to.be.instanceOf(MongoServerSelectionError);
692+
expect(findError).to.match(/ENOTFOUND/);
707693
await sleep(500);
708694
expect(client.topology).to.exist;
709695
expect(client.topology.description).to.have.property('type', 'Unknown');
710-
expect(findLast(logs, l => l.message.includes('Server selection failed'))).to.exist;
711696
});
712697
});
713698
});

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

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
Db,
1313
getTopology,
1414
MongoClient,
15+
MongoNetworkError,
1516
MongoNotConnectedError,
1617
MongoServerSelectionError,
1718
ReadPreference,
@@ -333,6 +334,28 @@ describe('class MongoClient', function () {
333334
}
334335
});
335336
});
337+
338+
it('throws ENOTFOUND error when connecting to non-existent host with no auth and loadBalanced=true', async function () {
339+
const configuration = this.configuration;
340+
const client = configuration.newClient(
341+
'mongodb://iLoveJavaScript:27017/test?loadBalanced=true',
342+
{ serverSelectionTimeoutMS: 100 }
343+
);
344+
345+
const error = await client.connect().catch(error => error);
346+
expect(error).to.be.instanceOf(MongoNetworkError); // not server selection like other topologies
347+
expect(error.message).to.match(/ENOTFOUND/);
348+
});
349+
350+
it('throws an error when srv is not a real record', async function () {
351+
const client = this.configuration.newClient('mongodb+srv://iLoveJavaScript/test', {
352+
serverSelectionTimeoutMS: 100
353+
});
354+
355+
const error = await client.connect().catch(error => error);
356+
expect(error).to.be.instanceOf(Error);
357+
expect(error.message).to.match(/ENOTFOUND/);
358+
});
336359
});
337360

338361
it('Should correctly pass through appname', {
@@ -600,15 +623,13 @@ describe('class MongoClient', function () {
600623
);
601624

602625
it(
603-
'does not checkout connection when authentication is disabled',
626+
'checks out connection to confirm connectivity even when authentication is disabled',
604627
{ requires: { auth: 'disabled' } },
605628
async function () {
606-
const checkoutStartedEvents = [];
607-
client.on('connectionCheckOutStarted', event => {
608-
checkoutStartedEvents.push(event);
609-
});
629+
const checkoutStarted = once(client, 'connectionCheckOutStarted');
610630
await client.connect();
611-
expect(checkoutStartedEvents).to.be.empty;
631+
const checkout = await checkoutStarted;
632+
expect(checkout).to.exist;
612633
expect(client).to.have.property('topology').that.is.instanceOf(Topology);
613634
}
614635
);

test/integration/server-selection/server_selection.spec.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as path from 'path';
33
import { loadSpecTests } from '../../spec';
44
import { runUnifiedSuite } from '../../tools/unified-spec-runner/runner';
55

6-
describe.skip('Server Selection Unified Tests (Spec)', function () {
6+
describe('Server Selection Unified Tests (Spec)', function () {
77
const tests = loadSpecTests(path.join('server-selection', 'logging'));
88
runUnifiedSuite(tests, test => {
99
if (
@@ -16,5 +16,4 @@ describe.skip('Server Selection Unified Tests (Spec)', function () {
1616
}
1717
return false;
1818
});
19-
}).skipReason =
20-
'TODO: unskip these tests - NODE-2471 (ping on connect) and NODE-5774 (duplicate server selection for bulkWrite and other wrapper operations';
19+
});

test/integration/server-selection/server_selection.test.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)