diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 474edc44f70..fde87add710 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -594,20 +594,13 @@ export class MongoClient extends TypedEventEmitter implements } /** - * Connect to MongoDB using a url + * An optional method to verify a typical set of preconditions before using a MongoClient. + * For detailed information about the connect process see the MongoClient.connect static method documentation. * - * @remarks - * Calling `connect` is optional since the first operation you perform will call `connect` if it's needed. - * `timeoutMS` will bound the time any operation can take before throwing a timeout error. - * However, when the operation being run is automatically connecting your `MongoClient` the `timeoutMS` will not apply to the time taken to connect the MongoClient. - * This means the time to setup the `MongoClient` does not count against `timeoutMS`. - * If you are using `timeoutMS` we recommend connecting your client explicitly in advance of any operation to avoid this inconsistent execution time. - * - * @remarks - * The driver will look up corresponding SRV and TXT records if the connection string starts with `mongodb+srv://`. - * If those look ups throw a DNS Timeout error, the driver will retry the look up once. + * @param url - The MongoDB connection string (supports `mongodb://` and `mongodb+srv://` schemes) + * @param options - Optional configuration options for the client * - * @see docs.mongodb.org/manual/reference/connection-string/ + * @see https://www.mongodb.com/docs/manual/reference/connection-string/ */ async connect(): Promise { if (this.connectionLock) { @@ -868,21 +861,35 @@ export class MongoClient extends TypedEventEmitter implements } /** - * Connect to MongoDB using a url + * Creates a new MongoClient instance and immediately connects it to MongoDB. + * This convenience method combines `new MongoClient(url, options)` and `client.connect()` in a single step. + * + * Connect can be helpful to detect configuration issues early by validating: + * - **DNS Resolution**: Verifies that SRV records and hostnames in the connection string resolve DNS entries + * - **Network Connectivity**: Confirms that host addresses are reachable and ports are open + * - **TLS Configuration**: Validates SSL/TLS certificates, CA files, and encryption settings are correct + * - **Authentication**: Verifies that provided credentials are valid + * - **Server Compatibility**: Ensures the MongoDB server version is supported by this driver version + * - **Load Balancer Setup**: For load-balanced deployments, confirms the service is properly configured + * + * @returns A promise that resolves to the same MongoClient instance once connected * * @remarks - * Calling `connect` is optional since the first operation you perform will call `connect` if it's needed. - * `timeoutMS` will bound the time any operation can take before throwing a timeout error. - * However, when the operation being run is automatically connecting your `MongoClient` the `timeoutMS` will not apply to the time taken to connect the MongoClient. - * This means the time to setup the `MongoClient` does not count against `timeoutMS`. - * If you are using `timeoutMS` we recommend connecting your client explicitly in advance of any operation to avoid this inconsistent execution time. + * **Connection is Optional:** Calling `connect` is optional since any operation method (`find`, `insertOne`, etc.) + * will automatically perform these same validation steps if the client is not already connected. + * However, explicitly calling `connect` can make sense for: + * - **Fail-fast Error Detection**: Non-transient connection issues (hostname unresolved, port refused connection) are discovered immediately rather than during your first operation + * - **Predictable Performance**: Eliminates first connection overhead from your first database operation * * @remarks - * The programmatically provided options take precedence over the URI options. + * **Connection Pooling Impact:** Calling `connect` will populate the connection pool with one connection + * to a server selected by the client's configured `readPreference` (defaults to primary). * * @remarks - * The driver will look up corresponding SRV and TXT records if the connection string starts with `mongodb+srv://`. - * If those look ups throw a DNS Timeout error, the driver will retry the look up once. + * **Timeout Behavior:** When using `timeoutMS`, the connection establishment time does not count against + * the timeout for subsequent operations. This means `connect` runs without a `timeoutMS` limit, while + * your database operations will still respect the configured timeout. If you need predictable operation + * timing with `timeoutMS`, call `connect` explicitly before performing operations. * * @see https://www.mongodb.com/docs/manual/reference/connection-string/ */ diff --git a/src/operations/execute_operation.ts b/src/operations/execute_operation.ts index 61d943e6424..0e1e74867d1 100644 --- a/src/operations/execute_operation.ts +++ b/src/operations/execute_operation.ts @@ -81,8 +81,6 @@ export async function executeOperation< session = client.startSession({ owner, explicit: false }); } else if (session.hasEnded) { throw new MongoExpiredSessionError('Use of expired sessions is not permitted'); - } else if (session.snapshotEnabled && !topology.capabilities.supportsSnapshotReads) { - throw new MongoCompatibilityError('Snapshot reads require MongoDB 5.0 or later'); } else if (session.client !== client) { throw new MongoInvalidArgumentError('ClientSession must be from the same MongoClient'); } @@ -206,6 +204,10 @@ async function tryOperation { }; try { - const server = await this.selectServer( - readPreferenceServerSelector(readPreference), - selectServerOptions - ); - const skipPingOnConnect = this.s.options.__skipPingOnConnect === true; - if (!skipPingOnConnect && this.s.credentials) { + if (!skipPingOnConnect) { + const server = await this.selectServer( + readPreferenceServerSelector(readPreference), + selectServerOptions + ); const connection = await server.pool.checkOut({ timeoutContext: timeoutContext }); server.pool.checkIn(connection); - stateTransition(this, STATE_CONNECTED); - this.emit(Topology.OPEN, this); - this.emit(Topology.CONNECT, this); - - return this; } stateTransition(this, STATE_CONNECTED); diff --git a/test/integration/initial-dns-seedlist-discovery/initial_dns_seedlist_discovery.prose.test.ts b/test/integration/initial-dns-seedlist-discovery/initial_dns_seedlist_discovery.prose.test.ts index a4fba77f068..3f931c019c6 100644 --- a/test/integration/initial-dns-seedlist-discovery/initial_dns_seedlist_discovery.prose.test.ts +++ b/test/integration/initial-dns-seedlist-discovery/initial_dns_seedlist_discovery.prose.test.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import * as dns from 'dns'; import * as sinon from 'sinon'; -import { MongoAPIError, Server, ServerDescription, Topology } from '../../mongodb'; +import { ConnectionPool, MongoAPIError, Server, ServerDescription, Topology } from '../../mongodb'; import { topologyWithPlaceholderClient } from '../../tools/utils'; describe('Initial DNS Seedlist Discovery (Prose Tests)', () => { @@ -41,6 +41,14 @@ describe('Initial DNS Seedlist Discovery (Prose Tests)', () => { {} as any ); }); + + sinon.stub(ConnectionPool.prototype, 'checkOut').callsFake(async function () { + return {}; + }); + + sinon.stub(ConnectionPool.prototype, 'checkIn').callsFake(function () { + return; + }); }); afterEach(async function () { diff --git a/test/integration/node-specific/abort_signal.test.ts b/test/integration/node-specific/abort_signal.test.ts index c128156f2e2..85ef4e69b3b 100644 --- a/test/integration/node-specific/abort_signal.test.ts +++ b/test/integration/node-specific/abort_signal.test.ts @@ -18,10 +18,12 @@ import { type Log, type MongoClient, MongoServerError, + MongoServerSelectionError, promiseWithResolvers, ReadPreference, setDifference, - StateMachine + StateMachine, + Topology } from '../../mongodb'; import { clearFailPoint, @@ -612,7 +614,6 @@ describe('AbortSignal support', () => { let client: MongoClient; let db: Db; let collection: Collection<{ a: number; ssn: string }>; - const logs: Log[] = []; let connectStarted; let controller: AbortController; let signal: AbortSignal; @@ -620,27 +621,15 @@ describe('AbortSignal support', () => { describe('when connect succeeds', () => { beforeEach(async function () { - logs.length = 0; - const promise = promiseWithResolvers(); connectStarted = promise.promise; - client = this.configuration.newClient( - {}, - { - mongodbLogComponentSeverities: { serverSelection: 'debug' }, - mongodbLogPath: { - write: log => { - if (log.c === 'serverSelection' && log.operation === 'handshake') { - controller.abort(); - promise.resolve(); - } - logs.push(log); - } - }, - serverSelectionTimeoutMS: 1000 - } - ); + client = this.configuration.newClient({}, { serverSelectionTimeoutMS: 1000 }); + + client.once('open', () => { + controller.abort(); + promise.resolve(); + }); db = client.db('abortSignal'); collection = db.collection('support'); @@ -651,7 +640,6 @@ describe('AbortSignal support', () => { }); afterEach(async function () { - logs.length = 0; await client?.close(); }); @@ -667,22 +655,18 @@ describe('AbortSignal support', () => { describe('when connect fails', () => { beforeEach(async function () { - logs.length = 0; - const promise = promiseWithResolvers(); connectStarted = promise.promise; + const selectServerStub = sinon + .stub(Topology.prototype, 'selectServer') + .callsFake(async function (...args) { + controller.abort(); + promise.resolve(); + return selectServerStub.wrappedMethod.call(this, ...args); + }); + client = this.configuration.newClient('mongodb://iLoveJavaScript', { - mongodbLogComponentSeverities: { serverSelection: 'debug' }, - mongodbLogPath: { - write: log => { - if (log.c === 'serverSelection' && log.operation === 'handshake') { - controller.abort(); - promise.resolve(); - } - logs.push(log); - } - }, serverSelectionTimeoutMS: 200, maxPoolSize: 1 }); @@ -696,18 +680,23 @@ describe('AbortSignal support', () => { }); afterEach(async function () { - logs.length = 0; + sinon.restore(); await client?.close(); }); - it('escapes auto connect without interrupting it', async () => { + it('server selection error is thrown before reaching signal abort state check', async () => { const toArray = cursor.toArray().catch(error => error); await connectStarted; - expect(await toArray).to.be.instanceOf(DOMException); + const findError = await toArray; + expect(findError).to.be.instanceOf(MongoServerSelectionError); + if (process.platform !== 'win32') { + // linux / mac, unix in general will have this errno set, + // which is generally helpful if this is kept elevated in the error message + expect(findError).to.match(/ENOTFOUND/); + } await sleep(500); expect(client.topology).to.exist; expect(client.topology.description).to.have.property('type', 'Unknown'); - expect(findLast(logs, l => l.message.includes('Server selection failed'))).to.exist; }); }); }); diff --git a/test/integration/node-specific/auto_connect.test.ts b/test/integration/node-specific/auto_connect.test.ts index f0850049632..a8330578ed2 100644 --- a/test/integration/node-specific/auto_connect.test.ts +++ b/test/integration/node-specific/auto_connect.test.ts @@ -9,6 +9,7 @@ import { type Collection, MongoClient, MongoNotConnectedError, + MongoOperationTimeoutError, ProfilingLevel, Topology, TopologyType @@ -862,12 +863,17 @@ describe('When executing an operation for the first time', () => { sinon.restore(); }); - it('client.connect() takes as long as selectServer is delayed for and does not throw a timeout error', async function () { + it('client.connect() takes as long as selectServer is delayed for and throws a timeout error', async function () { const start = performance.now(); expect(client.topology).to.not.exist; // make sure not connected. - const res = await client.db().collection('test').insertOne({ a: 1 }, { timeoutMS: 500 }); // auto-connect + const error = await client + .db() + .collection('test') + .insertOne({ a: 1 }, { timeoutMS: 500 }) + .catch(error => error); const end = performance.now(); - expect(res).to.have.property('acknowledged', true); + expect(error).to.be.instanceOf(MongoOperationTimeoutError); + expect(error).to.match(/Timed out during server selection/); expect(end - start).to.be.within(1000, 1500); // timeoutMS is 1000, did not apply. }); } diff --git a/test/integration/node-specific/mongo_client.test.ts b/test/integration/node-specific/mongo_client.test.ts index 9e0394013cd..cbc0f3a7753 100644 --- a/test/integration/node-specific/mongo_client.test.ts +++ b/test/integration/node-specific/mongo_client.test.ts @@ -12,6 +12,7 @@ import { Db, getTopology, MongoClient, + MongoNetworkError, MongoNotConnectedError, MongoServerSelectionError, ReadPreference, @@ -333,6 +334,28 @@ describe('class MongoClient', function () { } }); }); + + it('throws ENOTFOUND error when connecting to non-existent host with no auth and loadBalanced=true', async function () { + const configuration = this.configuration; + const client = configuration.newClient( + 'mongodb://iLoveJavaScript:27017/test?loadBalanced=true', + { serverSelectionTimeoutMS: 100 } + ); + + const error = await client.connect().catch(error => error); + expect(error).to.be.instanceOf(MongoNetworkError); // not server selection like other topologies + expect(error.message).to.match(/ENOTFOUND/); + }); + + it('throws an error when srv is not a real record', async function () { + const client = this.configuration.newClient('mongodb+srv://iLoveJavaScript/test', { + serverSelectionTimeoutMS: 100 + }); + + const error = await client.connect().catch(error => error); + expect(error).to.be.instanceOf(Error); + expect(error.message).to.match(/ENOTFOUND/); + }); }); it('Should correctly pass through appname', { @@ -600,15 +623,13 @@ describe('class MongoClient', function () { ); it( - 'does not checkout connection when authentication is disabled', + 'checks out connection to confirm connectivity even when authentication is disabled', { requires: { auth: 'disabled' } }, async function () { - const checkoutStartedEvents = []; - client.on('connectionCheckOutStarted', event => { - checkoutStartedEvents.push(event); - }); + const checkoutStarted = once(client, 'connectionCheckOutStarted'); await client.connect(); - expect(checkoutStartedEvents).to.be.empty; + const checkout = await checkoutStarted; + expect(checkout).to.exist; expect(client).to.have.property('topology').that.is.instanceOf(Topology); } ); @@ -700,6 +721,7 @@ describe('class MongoClient', function () { expect(result).to.be.instanceOf(MongoServerSelectionError); expect(client).to.be.instanceOf(MongoClient); expect(client).to.have.property('topology').that.is.instanceOf(Topology); + await client.close(); } ); }); diff --git a/test/integration/server-selection/server_selection.spec.test.ts b/test/integration/server-selection/server_selection.spec.test.ts index 280c157de12..63b03d17a77 100644 --- a/test/integration/server-selection/server_selection.spec.test.ts +++ b/test/integration/server-selection/server_selection.spec.test.ts @@ -3,18 +3,19 @@ import * as path from 'path'; import { loadSpecTests } from '../../spec'; import { runUnifiedSuite } from '../../tools/unified-spec-runner/runner'; -describe.skip('Server Selection Unified Tests (Spec)', function () { +describe('Server Selection Unified Tests (Spec)', function () { const tests = loadSpecTests(path.join('server-selection', 'logging')); runUnifiedSuite(tests, test => { if ( [ 'Failed bulkWrite operation: log messages have operationIds', - 'Successful bulkWrite operation: log messages have operationIds' + 'Failed client bulkWrite operation: log messages have operationIds', + 'Successful bulkWrite operation: log messages have operationIds', + 'Successful client bulkWrite operation: log messages have operationIds' ].includes(test.description) ) { return 'not applicable: operationId not supported'; } return false; }); -}).skipReason = - 'TODO: unskip these tests - NODE-2471 (ping on connect) and NODE-5774 (duplicate server selection for bulkWrite and other wrapper operations'; +}); diff --git a/test/integration/server-selection/server_selection.test.ts b/test/integration/server-selection/server_selection.test.ts deleted file mode 100644 index 8d58285a5cd..00000000000 --- a/test/integration/server-selection/server_selection.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { loadSpecTests } from '../../spec'; -import { runUnifiedSuite } from '../../tools/unified-spec-runner/runner'; - -describe('Server Selection Unified Tests (Node Driver)', function () { - /* TODO(NODE-5774) duplicate server selection for bulkWrite and other wrapper operations - * Remove once the actual unified tests (test/spec/server-selection/logging) are passing - */ - const clonedAndAlteredSpecTests = loadSpecTests( - '../integration/server-selection/unified-server-selection-node-specs-logging' - ); - runUnifiedSuite(clonedAndAlteredSpecTests); -}); diff --git a/test/integration/server-selection/unified-server-selection-node-specs-logging/load-balanced.json b/test/integration/server-selection/unified-server-selection-node-specs-logging/load-balanced.json deleted file mode 100644 index 125766ad1e4..00000000000 --- a/test/integration/server-selection/unified-server-selection-node-specs-logging/load-balanced.json +++ /dev/null @@ -1,135 +0,0 @@ -{ - "description": "load-balanced-logging-node-driver", - "schemaVersion": "1.13", - "runOnRequirements": [ - { - "topologies": [ - "load-balanced" - ] - } - ], - "createEntities": [ - { - "client": { - "id": "client", - "uriOptions": { - "heartbeatFrequencyMS": 500 - }, - "observeLogMessages": { - "serverSelection": "debug" - }, - "observeEvents": [ - "serverDescriptionChangedEvent" - ] - } - }, - { - "database": { - "id": "database", - "client": "client", - "databaseName": "logging-tests" - } - }, - { - "collection": { - "id": "collection", - "database": "database", - "collectionName": "server-selection" - } - } - ], - "tests": [ - { - "description": "A successful operation - load balanced cluster", - "operations": [ - { - "name": "waitForEvent", - "object": "testRunner", - "arguments": { - "client": "client", - "event": { - "serverDescriptionChangedEvent": { - "newDescription": { - "type": "LoadBalancer" - } - } - }, - "count": 1 - } - }, - { - "name": "insertOne", - "object": "collection", - "arguments": { - "document": { - "x": 1 - } - } - } - ], - "expectLogMessages": [ - { - "client": "client", - "messages": [ - { - "level": "debug", - "component": "serverSelection", - "data": { - "message": "Server selection started", - "selector": { - "$$exists": true - }, - "operation": "handshake", - "topologyDescription": { - "$$exists": true - } - } - }, - { - "level": "debug", - "component": "serverSelection", - "data": { - "message": "Server selection succeeded", - "selector": { - "$$exists": true - }, - "operation": "handshake", - "topologyDescription": { - "$$exists": true - } - } - }, - { - "level": "debug", - "component": "serverSelection", - "data": { - "message": "Server selection started", - "selector": { - "$$exists": true - }, - "operation": "insert", - "topologyDescription": { - "$$exists": true - } - } - }, - { - "level": "debug", - "component": "serverSelection", - "data": { - "message": "Server selection succeeded", - "selector": { - "$$exists": true - }, - "operation": "insert", - "topologyDescription": { - "$$exists": true - } - } - } - ] - } - ] - } - ] -} \ No newline at end of file diff --git a/test/integration/server-selection/unified-server-selection-node-specs-logging/replica-set.json b/test/integration/server-selection/unified-server-selection-node-specs-logging/replica-set.json deleted file mode 100644 index b462f3b80e3..00000000000 --- a/test/integration/server-selection/unified-server-selection-node-specs-logging/replica-set.json +++ /dev/null @@ -1,194 +0,0 @@ -{ - "description": "replica-set-logging-node-driver", - "schemaVersion": "1.14", - "runOnRequirements": [ - { - "topologies": [ - "replicaset" - ] - } - ], - "createEntities": [ - { - "client": { - "id": "client", - "uriOptions": { - "retryWrites": false, - "heartbeatFrequencyMS": 500, - "serverSelectionTimeoutMS": 2000 - }, - "observeLogMessages": { - "serverSelection": "debug" - }, - "observeEvents": [ - "serverDescriptionChangedEvent", - "topologyDescriptionChangedEvent" - ] - } - }, - { - "database": { - "id": "database", - "client": "client", - "databaseName": "logging-tests" - } - }, - { - "collection": { - "id": "collection", - "database": "database", - "collectionName": "server-selection" - } - }, - { - "client": { - "id": "failPointClient" - } - }, - { - "collection": { - "id": "unsatisfiableRPColl", - "database": "database", - "collectionName": "unsatisfiableRPColl", - "collectionOptions": { - "readPreference": { - "mode": "secondary", - "tagSets": [ - { - "nonexistenttag": "a" - } - ] - } - } - } - } - ], - "tests": [ - { - "description": "A successful operation", - "operations": [ - { - "name": "waitForEvent", - "object": "testRunner", - "arguments": { - "client": "client", - "event": { - "topologyDescriptionChangedEvent": {} - }, - "count": 4 - } - }, - { - "name": "insertOne", - "object": "collection", - "arguments": { - "document": { - "x": 1 - } - } - } - ], - "expectLogMessages": [ - { - "client": "client", - "messages": [ - { - "level": "debug", - "component": "serverSelection", - "data": { - "message": "Server selection started", - "selector": { - "$$exists": true - }, - "operation": "handshake", - "topologyDescription": { - "$$exists": true - } - } - }, - { - "level": "info", - "component": "serverSelection", - "data": { - "message": "Waiting for suitable server to become available", - "selector": { - "$$exists": true - }, - "operation": "handshake", - "topologyDescription": { - "$$exists": true - }, - "remainingTimeMS": { - "$$type": [ - "int", - "long" - ] - } - } - }, - { - "level": "debug", - "component": "serverSelection", - "data": { - "message": "Server selection succeeded", - "selector": { - "$$exists": true - }, - "operation": "handshake", - "topologyDescription": { - "$$exists": true - }, - "serverHost": { - "$$type": "string" - }, - "serverPort": { - "$$type": [ - "int", - "long" - ] - } - } - }, - { - "level": "debug", - "component": "serverSelection", - "data": { - "message": "Server selection started", - "selector": { - "$$exists": true - }, - "operation": "insert", - "topologyDescription": { - "$$exists": true - } - } - }, - { - "level": "debug", - "component": "serverSelection", - "data": { - "message": "Server selection succeeded", - "selector": { - "$$exists": true - }, - "operation": "insert", - "topologyDescription": { - "$$exists": true - }, - "serverHost": { - "$$type": "string" - }, - "serverPort": { - "$$type": [ - "int", - "long" - ] - } - } - } - ] - } - ] - } - ] -} diff --git a/test/integration/server-selection/unified-server-selection-node-specs-logging/sharded.json b/test/integration/server-selection/unified-server-selection-node-specs-logging/sharded.json deleted file mode 100644 index 345fc84bc00..00000000000 --- a/test/integration/server-selection/unified-server-selection-node-specs-logging/sharded.json +++ /dev/null @@ -1,352 +0,0 @@ -{ - "description": "sharded-logging-node-driver", - - "schemaVersion": "1.14", - "runOnRequirements": [ - { - "topologies": [ - "sharded" - ] - } - ], - "createEntities": [ - { - "client": { - "id": "client", - "uriOptions": { - "retryWrites": false, - "heartbeatFrequencyMS": 500, - "appName": "loggingClient", - "serverSelectionTimeoutMS": 2000 - }, - "observeLogMessages": { - "serverSelection": "debug" - }, - "observeEvents": [ - "serverDescriptionChangedEvent", - "topologyDescriptionChangedEvent" - ], - "useMultipleMongoses": false - } - }, - { - "database": { - "id": "database", - "client": "client", - "databaseName": "logging-tests" - } - }, - { - "collection": { - "id": "collection", - "database": "database", - "collectionName": "server-selection" - } - }, - { - "client": { - "id": "failPointClient", - "useMultipleMongoses": false - } - } - ], - "tests": [ - { - "description": "A successful operation", - "operations": [ - { - "name": "waitForEvent", - "object": "testRunner", - "arguments": { - "client": "client", - "event": { - "topologyDescriptionChangedEvent": {} - }, - "count": 2 - } - }, - { - "name": "insertOne", - "object": "collection", - "arguments": { - "document": { - "x": 1 - } - } - } - ], - "expectLogMessages": [ - { - "client": "client", - "messages": [ - { - "level": "debug", - "component": "serverSelection", - "data": { - "message": "Server selection started", - "selector": { - "$$exists": true - }, - "operation": "handshake", - "topologyDescription": { - "$$exists": true - } - } - }, - { - "level": "info", - "component": "serverSelection", - "data": { - "message": "Waiting for suitable server to become available", - "selector": { - "$$exists": true - }, - "operation": "handshake", - "topologyDescription": { - "$$exists": true - }, - "remainingTimeMS": { - "$$type": [ - "int", - "long" - ] - } - } - }, - { - "level": "debug", - "component": "serverSelection", - "data": { - "message": "Server selection succeeded", - "selector": { - "$$exists": true - }, - "operation": "handshake", - "topologyDescription": { - "$$exists": true - }, - "serverHost": { - "$$type": "string" - }, - "serverPort": { - "$$type": [ - "int", - "long" - ] - } - } - }, - { - "level": "debug", - "component": "serverSelection", - "data": { - "message": "Server selection started", - "selector": { - "$$exists": true - }, - "operation": "insert", - "topologyDescription": { - "$$exists": true - } - } - }, - { - "level": "debug", - "component": "serverSelection", - "data": { - "message": "Server selection succeeded", - "selector": { - "$$exists": true - }, - "operation": "insert", - "topologyDescription": { - "$$exists": true - }, - "serverHost": { - "$$type": "string" - }, - "serverPort": { - "$$type": [ - "int", - "long" - ] - } - } - } - ] - } - ] - }, - { - "description": "Failure due to unreachable server", - "runOnRequirements": [ - { - "minServerVersion": "4.4" - } - ], - "operations": [ - { - "name": "failPoint", - "object": "testRunner", - "arguments": { - "client": "failPointClient", - "failPoint": { - "configureFailPoint": "failCommand", - "mode": "alwaysOn", - "data": { - "failCommands": [ - "hello", - "ismaster" - ], - "appName": "loggingClient", - "closeConnection": true - } - } - } - }, - { - "name": "waitForEvent", - "object": "testRunner", - "arguments": { - "client": "client", - "event": { - "serverDescriptionChangedEvent": { - "newDescription": { - "type": "Unknown" - } - } - }, - "count": 1 - } - }, - { - "name": "insertOne", - "object": "collection", - "arguments": { - "document": { - "x": 1 - } - }, - "expectError": { - "isClientError": true - } - } - ], - "expectLogMessages": [ - { - "client": "client", - "messages": [ - { - "level": "debug", - "component": "serverSelection", - "data": { - "message": "Server selection started", - "selector": { - "$$exists": true - }, - "operation": "handshake", - "topologyDescription": { - "$$exists": true - } - } - }, - { - "level": "info", - "component": "serverSelection", - "data": { - "message": "Waiting for suitable server to become available", - "selector": { - "$$exists": true - }, - "operation": "handshake", - "topologyDescription": { - "$$exists": true - }, - "remainingTimeMS": { - "$$type": [ - "int", - "long" - ] - } - } - }, - { - "level": "debug", - "component": "serverSelection", - "data": { - "message": "Server selection succeeded", - "selector": { - "$$exists": true - }, - "operation": "handshake", - "topologyDescription": { - "$$exists": true - }, - "serverHost": { - "$$type": "string" - }, - "serverPort": { - "$$type": [ - "int", - "long" - ] - } - } - }, - { - "level": "debug", - "component": "serverSelection", - "data": { - "message": "Server selection started", - "selector": { - "$$exists": true - }, - "operation": "insert", - "topologyDescription": { - "$$exists": true - } - } - }, - { - "level": "info", - "component": "serverSelection", - "data": { - "message": "Waiting for suitable server to become available", - "selector": { - "$$exists": true - }, - "operation": "insert", - "topologyDescription": { - "$$exists": true - }, - "remainingTimeMS": { - "$$type": [ - "int", - "long" - ] - } - } - }, - { - "level": "debug", - "component": "serverSelection", - "data": { - "message": "Server selection failed", - "selector": { - "$$exists": true - }, - "operation": "insert", - "topologyDescription": { - "$$exists": true - }, - "failure": { - "$$exists": true - } - } - } - ] - } - ] - } - ] -} diff --git a/test/integration/server-selection/unified-server-selection-node-specs-logging/standalone.json b/test/integration/server-selection/unified-server-selection-node-specs-logging/standalone.json deleted file mode 100644 index 2b5ee88338b..00000000000 --- a/test/integration/server-selection/unified-server-selection-node-specs-logging/standalone.json +++ /dev/null @@ -1,349 +0,0 @@ -{ - "description": "standalone-logging-node-driver", - "schemaVersion": "1.14", - "runOnRequirements": [ - { - "topologies": [ - "single" - ] - } - ], - "createEntities": [ - { - "client": { - "id": "client", - "uriOptions": { - "retryWrites": false, - "heartbeatFrequencyMS": 500, - "appName": "loggingClient", - "serverSelectionTimeoutMS": 2000 - }, - "observeLogMessages": { - "serverSelection": "debug" - }, - "observeEvents": [ - "serverDescriptionChangedEvent", - "topologyDescriptionChangedEvent" - ] - } - }, - { - "database": { - "id": "database", - "client": "client", - "databaseName": "logging-tests" - } - }, - { - "collection": { - "id": "collection", - "database": "database", - "collectionName": "server-selection" - } - }, - { - "client": { - "id": "failPointClient" - } - } - ], - "tests": [ - { - "description": "A successful operation", - "operations": [ - { - "name": "waitForEvent", - "object": "testRunner", - "arguments": { - "client": "client", - "event": { - "topologyDescriptionChangedEvent": {} - }, - "count": 2 - } - }, - { - "name": "insertOne", - "object": "collection", - "arguments": { - "document": { - "x": 1 - } - } - } - ], - "expectLogMessages": [ - { - "client": "client", - "messages": [ - { - "level": "debug", - "component": "serverSelection", - "data": { - "message": "Server selection started", - "selector": { - "$$exists": true - }, - "operation": "handshake", - "topologyDescription": { - "$$exists": true - } - } - }, - { - "level": "info", - "component": "serverSelection", - "data": { - "message": "Waiting for suitable server to become available", - "selector": { - "$$exists": true - }, - "operation": "handshake", - "topologyDescription": { - "$$exists": true - }, - "remainingTimeMS": { - "$$type": [ - "int", - "long" - ] - } - } - }, - { - "level": "debug", - "component": "serverSelection", - "data": { - "message": "Server selection succeeded", - "selector": { - "$$exists": true - }, - "operation": "handshake", - "topologyDescription": { - "$$exists": true - }, - "serverHost": { - "$$type": "string" - }, - "serverPort": { - "$$type": [ - "int", - "long" - ] - } - } - }, - { - "level": "debug", - "component": "serverSelection", - "data": { - "message": "Server selection started", - "selector": { - "$$exists": true - }, - "operation": "insert", - "topologyDescription": { - "$$exists": true - } - } - }, - { - "level": "debug", - "component": "serverSelection", - "data": { - "message": "Server selection succeeded", - "selector": { - "$$exists": true - }, - "operation": "insert", - "topologyDescription": { - "$$exists": true - }, - "serverHost": { - "$$type": "string" - }, - "serverPort": { - "$$type": [ - "int", - "long" - ] - } - } - } - ] - } - ] - }, - { - "description": "Failure due to unreachable server", - "runOnRequirements": [ - { - "minServerVersion": "4.4" - } - ], - "operations": [ - { - "name": "failPoint", - "object": "testRunner", - "arguments": { - "client": "failPointClient", - "failPoint": { - "configureFailPoint": "failCommand", - "mode": "alwaysOn", - "data": { - "failCommands": [ - "hello", - "ismaster" - ], - "appName": "loggingClient", - "closeConnection": true - } - } - } - }, - { - "name": "waitForEvent", - "object": "testRunner", - "arguments": { - "client": "client", - "event": { - "serverDescriptionChangedEvent": { - "newDescription": { - "type": "Unknown" - } - } - }, - "count": 1 - } - }, - { - "name": "insertOne", - "object": "collection", - "arguments": { - "document": { - "x": 1 - } - }, - "expectError": { - "isClientError": true - } - } - ], - "expectLogMessages": [ - { - "client": "client", - "messages": [ - { - "level": "debug", - "component": "serverSelection", - "data": { - "message": "Server selection started", - "selector": { - "$$exists": true - }, - "operation": "handshake", - "topologyDescription": { - "$$exists": true - } - } - }, - { - "level": "info", - "component": "serverSelection", - "data": { - "message": "Waiting for suitable server to become available", - "selector": { - "$$exists": true - }, - "operation": "handshake", - "topologyDescription": { - "$$exists": true - }, - "remainingTimeMS": { - "$$type": [ - "int", - "long" - ] - } - } - }, - { - "level": "debug", - "component": "serverSelection", - "data": { - "message": "Server selection succeeded", - "selector": { - "$$exists": true - }, - "operation": "handshake", - "topologyDescription": { - "$$exists": true - }, - "serverHost": { - "$$type": "string" - }, - "serverPort": { - "$$type": [ - "int", - "long" - ] - } - } - }, - { - "level": "debug", - "component": "serverSelection", - "data": { - "message": "Server selection started", - "selector": { - "$$exists": true - }, - "operation": "insert", - "topologyDescription": { - "$$exists": true - } - } - }, - { - "level": "info", - "component": "serverSelection", - "data": { - "message": "Waiting for suitable server to become available", - "selector": { - "$$exists": true - }, - "operation": "insert", - "topologyDescription": { - "$$exists": true - }, - "remainingTimeMS": { - "$$type": [ - "int", - "long" - ] - } - } - }, - { - "level": "debug", - "component": "serverSelection", - "data": { - "message": "Server selection failed", - "selector": { - "$$exists": true - }, - "operation": "insert", - "topologyDescription": { - "$$exists": true - }, - "failure": { - "$$exists": true - } - } - } - ] - } - ] - } - ] -} diff --git a/test/tools/unified-spec-runner/schema.ts b/test/tools/unified-spec-runner/schema.ts index 8ec850979fa..b0ba6abed99 100644 --- a/test/tools/unified-spec-runner/schema.ts +++ b/test/tools/unified-spec-runner/schema.ts @@ -4,7 +4,6 @@ import type { MongoLoggableComponent, ObjectId, ReadConcernLevel, - ReadPreferenceMode, ServerApiVersion, SeverityLevel, TagSet, @@ -259,16 +258,19 @@ export interface ServerApi { strict?: boolean; deprecationErrors?: boolean; } + +export type UnifiedReadPreference = { + mode: string; + tagSets?: TagSet[]; + maxStalenessSeconds?: number; + hedge?: { enabled: boolean }; +}; + export interface CollectionOrDatabaseOptions { readConcern?: { level: ReadConcernLevel; }; - readPreference?: { - mode: ReadPreferenceMode; - maxStalenessSeconds: number; - tags: TagSet[]; - hedge: { enabled: boolean }; - }; + readPreference?: UnifiedReadPreference; writeConcern?: { w: W; wtimeoutMS: number; diff --git a/test/tools/unified-spec-runner/unified-utils.ts b/test/tools/unified-spec-runner/unified-utils.ts index 1b599a7ed9d..b56ae9bb00a 100644 --- a/test/tools/unified-spec-runner/unified-utils.ts +++ b/test/tools/unified-spec-runner/unified-utils.ts @@ -12,6 +12,8 @@ import { type Document, getMongoDBClientEncryption, type MongoClient, + ReadPreference, + ReadPreferenceMode, ReturnDocument } from '../../mongodb'; import type { CmapEvent, CommandEvent, EntitiesMap, SdamEvent } from './entities'; @@ -23,7 +25,8 @@ import type { ExpectedEventsForClient, KMSProvidersEntity, RunOnRequirement, - StringOrPlaceholder + StringOrPlaceholder, + UnifiedReadPreference } from './schema'; const ENABLE_UNIFIED_TEST_LOGGING = false; @@ -190,16 +193,42 @@ export function patchVersion(version: string): string { return `${major}.${minor ?? 0}.${patch ?? 0}`; } +export function makeNodeReadPrefFromUnified( + readPreferenceFromTest: UnifiedReadPreference +): ReadPreference { + const validModes = Object.values(ReadPreferenceMode); + const testModeLowered = readPreferenceFromTest.mode?.toLowerCase(); + const mode = validModes.find(m => m.toLowerCase() === testModeLowered); + expect(mode, 'test file defines unsupported readPreference mode').to.not.be.null; + + return ReadPreference.fromOptions({ + readPreference: { + mode, + tags: readPreferenceFromTest.tagSets, + maxStalenessSeconds: readPreferenceFromTest.maxStalenessSeconds, + hedge: readPreferenceFromTest.hedge + } + }); +} + export function patchDbOptions(options: CollectionOrDatabaseOptions = {}): DbOptions { - // TODO - return { ...options } as DbOptions; + // @ts-expect-error: there is incompatibilities between unified options and node options, but it mostly works as is. See the readPref fixing below. + const dbOptions: DbOptions = { ...options }; + if (dbOptions.readPreference) { + dbOptions.readPreference = makeNodeReadPrefFromUnified(options.readPreference); + } + return dbOptions; } export function patchCollectionOptions( options: CollectionOrDatabaseOptions = {} ): CollectionOptions { - // TODO - return { ...options } as CollectionOptions; + // @ts-expect-error: there is incompatibilities between unified options and node options, but it mostly works as is. See the readPref fixing below. + const collectionOptions: CollectionOptions = { ...options }; + if (collectionOptions.readPreference) { + collectionOptions.readPreference = makeNodeReadPrefFromUnified(options.readPreference); + } + return collectionOptions; } export function translateOptions(options: Document): Document { diff --git a/test/tools/utils.ts b/test/tools/utils.ts index 79d918a6897..ca6740f2716 100644 --- a/test/tools/utils.ts +++ b/test/tools/utils.ts @@ -281,10 +281,11 @@ export const sorted = (iterable: Iterable, how: (a: T, b: T) => 0 | 1 | -1 * changes*/ export function topologyWithPlaceholderClient( seeds: string | string[] | HostAddress | HostAddress[], - options: Partial + options: Partial, + clientOptions?: MongoClientOptions ): Topology { return new Topology( - new MongoClient('mongodb://iLoveJavaScript'), + new MongoClient('mongodb://iLoveJavaScript', clientOptions), seeds, options as TopologyOptions ); diff --git a/test/unit/assorted/server_discovery_and_monitoring.spec.test.ts b/test/unit/assorted/server_discovery_and_monitoring.spec.test.ts index 62a545417bb..5d726206c21 100644 --- a/test/unit/assorted/server_discovery_and_monitoring.spec.test.ts +++ b/test/unit/assorted/server_discovery_and_monitoring.spec.test.ts @@ -215,7 +215,14 @@ describe('Server Discovery and Monitoring (spec)', function () { .callsFake(async function (_selector, _options) { topologySelectServers.restore(); - const fakeServer = { s: { state: 'connected' }, removeListener: () => true }; + const fakeServer = { + s: { state: 'connected' }, + removeListener: () => true, + pool: { + checkOut: async () => ({}), + checkIn: () => undefined + } + }; return fakeServer; }); }); diff --git a/test/unit/assorted/server_discovery_and_monitoring.test.ts b/test/unit/assorted/server_discovery_and_monitoring.test.ts index 2f6e312594f..e5fcf598e81 100644 --- a/test/unit/assorted/server_discovery_and_monitoring.test.ts +++ b/test/unit/assorted/server_discovery_and_monitoring.test.ts @@ -31,7 +31,14 @@ describe('Server Discovery and Monitoring', function () { .callsFake(async function (_selector, _options) { topologySelectServer.restore(); - const fakeServer = { s: { state: 'connected' }, removeListener: () => true }; + const fakeServer = { + s: { state: 'connected' }, + removeListener: () => true, + pool: { + checkOut: async () => ({}), + checkIn: () => undefined + } + }; return fakeServer; }); diff --git a/test/unit/assorted/server_selection_spec_helper.js b/test/unit/assorted/server_selection_spec_helper.js index 040d8fbdf10..55b890a6646 100644 --- a/test/unit/assorted/server_selection_spec_helper.js +++ b/test/unit/assorted/server_selection_spec_helper.js @@ -106,7 +106,14 @@ export async function executeServerSelectionTest(testDefinition) { .callsFake(async function () { topologySelectServers.restore(); - const fakeServer = { s: { state: 'connected' }, removeListener: () => {} }; + const fakeServer = { + s: { state: 'connected' }, + removeListener: () => true, + pool: { + checkOut: async () => ({}), + checkIn: () => undefined + } + }; return fakeServer; }); diff --git a/test/unit/sdam/topology.test.ts b/test/unit/sdam/topology.test.ts index 8fac7f52ee9..1b0e257e27e 100644 --- a/test/unit/sdam/topology.test.ts +++ b/test/unit/sdam/topology.test.ts @@ -25,6 +25,7 @@ import { Topology } from '../../../src/sdam/topology'; import { TopologyDescription } from '../../../src/sdam/topology_description'; import { TimeoutContext } from '../../../src/timeout'; import { isHello, ns } from '../../../src/utils'; +import { ConnectionPool } from '../../mongodb'; import * as mock from '../../tools/mongodb-mock/index'; import { topologyWithPlaceholderClient } from '../../tools/utils'; @@ -444,31 +445,23 @@ describe('Topology (unit)', function () { describe('selectServer()', function () { it('should schedule monitoring if no suitable server is found', async function () { - const topology = topologyWithPlaceholderClient('someserver:27019', {}); + const topology = topologyWithPlaceholderClient( + 'someserver:27019', + {}, + { serverSelectionTimeoutMS: 10 } + ); const requestCheck = sinon.stub(Server.prototype, 'requestCheck'); - // satisfy the initial connect, then restore the original method - const selectServer = sinon - .stub(Topology.prototype, 'selectServer') - .callsFake(async function () { - const server = Array.from(this.s.servers.values())[0]; - selectServer.restore(); - return server; - }); - sinon.stub(Server.prototype, 'connect').callsFake(function () { this.s.state = 'connected'; this.emit('connect'); return; }); - await topology.connect(); - const err = await topology - .selectServer(ReadPreference.secondary, { serverSelectionTimeoutMS: 1000 }) - .then( - () => null, - e => e - ); + const err = await topology.connect().then( + () => null, + e => e + ); expect(err).to.match(/Server selection timed out/); expect(err).to.have.property('reason'); // When server is created `connect` is called on the monitor. When server selection @@ -516,12 +509,20 @@ describe('Topology (unit)', function () { this.emit('connect'); }); + sinon.stub(ConnectionPool.prototype, 'checkOut').callsFake(async function () { + return {}; + }); + + sinon.stub(ConnectionPool.prototype, 'checkIn').callsFake(function (_) { + return; + }); + const toSelect = 10; let completed = 0; // methodology: // - perform 9 server selections, a few with a selector that throws an error // - ensure each selection immediately returns an empty result (gated by a boolean) - // guaranteeing tha the queue will be full before the last selection + // guaranteeing that the queue will be full before the last selection // - make one last selection, but ensure that all selections are no longer blocked from // returning their value // - verify that 10 callbacks were called