From 4de34735c26ba4698109f5b38a5b13a0a85f409f Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Mon, 3 Mar 2025 10:19:31 -0500 Subject: [PATCH 01/18] test(NODE-6626): test resources are cleaned up --- .../node-specific/client_close.test.ts | 235 ++++++++++++++++-- 1 file changed, 209 insertions(+), 26 deletions(-) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index d9b7683cb02..7331c06da78 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -1,9 +1,12 @@ /* eslint-disable @typescript-eslint/no-empty-function */ +import { expect } from 'chai'; + import { getCSFLEKMSProviders } from '../../csfle-kms-providers'; +import { type Collection, type FindCursor, type MongoClient } from '../../mongodb'; import { type TestConfiguration } from '../../tools/runner/config'; import { runScriptAndGetProcessInfo } from './resource_tracking_script_builder'; -describe.skip('MongoClient.close() Integration', () => { +describe('MongoClient.close() Integration', () => { // note: these tests are set-up in accordance of the resource ownership tree let config: TestConfiguration; @@ -14,7 +17,7 @@ describe.skip('MongoClient.close() Integration', () => { describe('Node.js resource: TLS File read', () => { describe('when client is connecting and reads an infinite TLS file', () => { - it('the file read is interrupted by client.close()', async function () { + it.skip('the file read is interrupted by client.close()', async function () { await runScriptAndGetProcessInfo( 'tls-file-read', config, @@ -50,7 +53,7 @@ describe.skip('MongoClient.close() Integration', () => { }); describe('when MongoClientAuthProviders is instantiated and token file read hangs', () => { - it('the file read is interrupted by client.close()', async () => { + it.skip('the file read is interrupted by client.close()', async () => { await runScriptAndGetProcessInfo( 'token-file-read', config, @@ -77,8 +80,7 @@ describe.skip('MongoClient.close() Integration', () => { describe('Node.js resource: Server Selection Timer', () => { describe('after a Topology is created through client.connect()', () => { const metadata: MongoDBMetadataUI = { requires: { topology: 'replicaset' } }; - - it('server selection timers are cleaned up by client.close()', metadata, async () => { + it.skip('server selection timers are cleaned up by client.close()', metadata, async () => { const run = async function ({ MongoClient, uri, expect, sleep, mongodb, getTimerCount }) { const serverSelectionTimeoutMS = 2222; const client = new MongoClient(uri, { @@ -117,7 +119,7 @@ describe.skip('MongoClient.close() Integration', () => { describe('MonitorInterval', () => { describe('Node.js resource: Timer', () => { describe('after a new monitor is made', () => { - it( + it.skip( 'monitor interval timer is cleaned up by client.close()', metadata, async function () { @@ -150,7 +152,7 @@ describe.skip('MongoClient.close() Integration', () => { }); describe('after a heartbeat fails', () => { - it( + it.skip( 'the new monitor interval timer is cleaned up by client.close()', metadata, async () => { @@ -160,7 +162,6 @@ describe.skip('MongoClient.close() Integration', () => { const willBeHeartbeatFailed = once(client, 'serverHeartbeatFailed'); client.connect(); await willBeHeartbeatFailed; - function getMonitorTimer(servers) { for (const [, server] of servers) { return server?.monitor.monitorId.timerId; @@ -183,7 +184,7 @@ describe.skip('MongoClient.close() Integration', () => { describe('Monitoring Connection', () => { describe('Node.js resource: Socket', () => { - it('no sockets remain after client.close()', metadata, async function () { + it.skip('no sockets remain after client.close()', metadata, async function () { const run = async function ({ MongoClient, uri, expect, getSocketEndpoints }) { const client = new MongoClient(uri); await client.connect(); @@ -211,7 +212,7 @@ describe.skip('MongoClient.close() Integration', () => { describe('RTT Pinger', () => { describe('Node.js resource: Timer', () => { describe('after entering monitor streaming mode ', () => { - it( + it.skip( 'the rtt pinger timer is cleaned up by client.close()', metadata, async function () { @@ -247,8 +248,8 @@ describe.skip('MongoClient.close() Integration', () => { describe('Connection', () => { describe('Node.js resource: Socket', () => { describe('when rtt monitoring is turned on', () => { - it('no sockets remain after client.close()', metadata, async () => { - const run = async ({ MongoClient, uri, expect, getSockets, once, log }) => { + it.skip('no sockets remain after client.close()', metadata, async () => { + const run = async ({ MongoClient, uri, expect, getSockets, once }) => { const heartbeatFrequencyMS = 500; const client = new MongoClient(uri, { serverMonitoringMode: 'stream', @@ -265,7 +266,6 @@ describe.skip('MongoClient.close() Integration', () => { while (heartbeatOccurredSet.size < servers.size) { const ev = await once(client, 'serverHeartbeatSucceeded'); - log({ ev: ev[0] }); heartbeatOccurredSet.add(ev[0].connectionId); } @@ -281,8 +281,6 @@ describe.skip('MongoClient.close() Integration', () => { // close the client await client.close(); - - log({ socketsAfterClose: getSockets() }); // upon close, assert rttPinger sockets are cleaned up const activeSocketsAfterClose = activeSocketsAfterHeartbeat(); expect(activeSocketsAfterClose).to.have.lengthOf(0); @@ -299,7 +297,7 @@ describe.skip('MongoClient.close() Integration', () => { describe('ConnectionPool', () => { describe('Node.js resource: minPoolSize timer', () => { describe('after new connection pool is created', () => { - it('the minPoolSize timer is cleaned up by client.close()', async function () { + it.skip('the minPoolSize timer is cleaned up by client.close()', async function () { const run = async function ({ MongoClient, uri, expect, getTimerCount }) { const client = new MongoClient(uri, { minPoolSize: 1 }); let minPoolSizeTimerCreated = false; @@ -357,7 +355,7 @@ describe.skip('MongoClient.close() Integration', () => { await utilClient.close(); }); - it('the wait queue timer is cleaned up by client.close()', async function () { + it.skip('the wait queue timer is cleaned up by client.close()', async function () { const run = async function ({ MongoClient, uri, expect, getTimerCount, once }) { const waitQueueTimeoutMS = 1515; @@ -399,7 +397,7 @@ describe.skip('MongoClient.close() Integration', () => { describe('Connection', () => { describe('Node.js resource: Socket', () => { describe('after a minPoolSize has been set on the ConnectionPool', () => { - it('no sockets remain after client.close()', async function () { + it.skip('no sockets remain after client.close()', async function () { const run = async function ({ MongoClient, uri, expect, getSockets }) { // assert no sockets to start with expect(getSockets()).to.have.lengthOf(0); @@ -431,13 +429,17 @@ describe.skip('MongoClient.close() Integration', () => { const metadata: MongoDBMetadataUI = { requires: { topology: 'sharded' } }; describe('after SRVPoller is created', () => { - it('timers are cleaned up by client.close()', metadata, async () => { + it.skip('timers are cleaned up by client.close()', metadata, async () => { const run = async function ({ MongoClient, expect, getTimerCount }) { const SRV_CONNECTION_STRING = `mongodb+srv://test1.test.build.10gen.cc`; + // 27018 localhost.test.build.10gen.cc. // 27017 localhost.test.build.10gen.cc. - const client = new MongoClient(SRV_CONNECTION_STRING); + const client = new MongoClient(SRV_CONNECTION_STRING, { + serverSelectionTimeoutMS: 2000, + tls: false + }); await client.connect(); // the current expected behavior is that _timeout is set to undefined until SRV polling starts // then _timeout is set to undefined again when SRV polling stops @@ -453,29 +455,143 @@ describe.skip('MongoClient.close() Integration', () => { }); describe('ClientSession (Implicit)', () => { + let idleSessionsBeforeClose; + let idleSessionsAfterClose; + let client; + let utilClient; + let session; + + const metadata: MongoDBMetadataUI = { + requires: { + topology: ['replicaset', 'sharded'], + mongodb: '>=4.2' + } + }; + + beforeEach(async function () { + client = this.configuration.newClient(); + utilClient = this.configuration.newClient(); + await client.connect(); + await client + .db('db') + .collection('collection') + ?.drop() + .catch(() => null); + const collection = await client.db('db').createCollection('collection'); + session = client.startSession({ explicit: false }); + session.startTransaction(); + await collection.insertOne({ x: 1 }, { session }); + + const opBefore = await utilClient.db().admin().command({ currentOp: 1 }); + idleSessionsBeforeClose = opBefore.inprog.filter(s => s.type === 'idleSession'); + + await client.close(); + + const opAfter = await utilClient.db().admin().command({ currentOp: 1 }); + idleSessionsAfterClose = opAfter.inprog.filter(s => s.type === 'idleSession'); + + await utilClient.close(); + }); + + afterEach(async function () { + await utilClient?.close(); + await session?.endSession(); + await client?.close(); + }); + describe('Server resource: LSID/ServerSession', () => { describe('after a clientSession is implicitly created and used', () => { - it.skip('the server-side ServerSession is cleaned up by client.close()', async function () {}); + it( + 'the server-side ServerSession is cleaned up by client.close()', + metadata, + async function () { + expect(idleSessionsBeforeClose).to.not.be.empty; + expect(idleSessionsAfterClose).to.be.empty; + } + ); }); }); describe('Server resource: Transactions', () => { describe('after a clientSession is implicitly created and used', () => { - it.skip('the server-side transaction is cleaned up by client.close()', async function () {}); + it( + 'the server-side transaction is cleaned up by client.close()', + metadata, + async function () { + expect(idleSessionsBeforeClose[0].transaction.txnNumber).to.not.null; + expect(idleSessionsAfterClose).to.be.empty; + } + ); }); }); }); describe('ClientSession (Explicit)', () => { + let idleSessionsBeforeClose; + let idleSessionsAfterClose; + let client; + let utilClient; + let session; + + const metadata: MongoDBMetadataUI = { + requires: { + topology: ['replicaset', 'sharded'], + mongodb: '>=4.2' + } + }; + + beforeEach(async function () { + client = this.configuration.newClient(); + utilClient = this.configuration.newClient(); + await client.connect(); + await client + .db('db') + .collection('collection') + ?.drop() + .catch(() => null); + const collection = await client.db('db').createCollection('collection'); + session = client.startSession(); + session.startTransaction(); + await collection.insertOne({ x: 1 }, { session }); + + const opBefore = await utilClient.db().admin().command({ currentOp: 1 }); + idleSessionsBeforeClose = opBefore.inprog.filter(s => s.type === 'idleSession'); + + await client.close(); + + const opAfter = await utilClient.db().admin().command({ currentOp: 1 }); + idleSessionsAfterClose = opAfter.inprog.filter(s => s.type === 'idleSession'); + }); + + afterEach(async function () { + await utilClient?.close(); + await session?.endSession(); + await client?.close(); + }); + describe('Server resource: LSID/ServerSession', () => { describe('after a clientSession is created and used', () => { - it.skip('the server-side ServerSession is cleaned up by client.close()', async function () {}); + it( + 'the server-side ServerSession is cleaned up by client.close()', + metadata, + async function () { + expect(idleSessionsBeforeClose).to.not.be.empty; + expect(idleSessionsAfterClose).to.be.empty; + } + ); }); }); describe('Server resource: Transactions', () => { describe('after a clientSession is created and used', () => { - it.skip('the server-side transaction is cleaned up by client.close()', async function () {}); + it( + 'the server-side transaction is cleaned up by client.close()', + metadata, + async function () { + expect(idleSessionsBeforeClose[0].transaction.txnNumber).to.not.null; + expect(idleSessionsAfterClose).to.be.empty; + } + ); }); }); }); @@ -491,7 +607,7 @@ describe.skip('MongoClient.close() Integration', () => { describe('KMS Request', () => { describe('Node.js resource: TLS file read', () => { describe('when KMSRequest reads an infinite TLS file', () => { - it('the file read is interrupted by client.close()', metadata, async () => { + it.skip('the file read is interrupted by client.close()', metadata, async () => { await runScriptAndGetProcessInfo( 'tls-file-read-auto-encryption', config, @@ -584,8 +700,75 @@ describe.skip('MongoClient.close() Integration', () => { }); describe('Server resource: Cursor', () => { + const metadata: MongoDBMetadataUI = { + requires: { + mongodb: '>=4.2.0' + } + }; + describe('after cursors are created', () => { - it.skip('all active server-side cursors are closed by client.close()', async function () {}); + let client: MongoClient; + let coll: Collection; + let cursor: FindCursor; + let utilClient: MongoClient; + + beforeEach(async function () { + client = this.configuration.newClient(); + utilClient = this.configuration.newClient(); + await client.connect(); + await client + .db('db') + .collection('coll') + ?.drop() + .catch(() => null); + coll = await client + .db('db') + .createCollection('coll', { capped: true, size: 1_000, max: 4 }); + await coll.insertMany([{ a: 1 }, { b: 2 }, { c: 3 }]); + }); + + afterEach(async function () { + await utilClient?.close(); + await client?.close(); + await cursor?.close(); + }); + + it( + 'all active server-side cursors are closed by client.close()', + metadata, + async function () { + const getCursors = async () => { + const res = await utilClient + .db() + .admin() + .command({ + aggregate: 1, + cursor: {}, + pipeline: [{ $currentOp: { idleCursors: true } }] + }); + return res.cursor.firstBatch.filter( + r => r.type === 'idleCursor' || (r.type === 'op' && r.desc === 'getMore') + ); + }; + + cursor = coll.find( + {}, + { + tailable: true, + awaitData: true + } + ); + await cursor.next(); + + // assert creation + expect(await getCursors()).to.not.be.empty; + + await client.close(); + + // assert clean-up + expect(await getCursors()).to.be.empty; + } + ); }); }); }); From 6c5a45088a31ff5dbd835a01689f93d2c49505f2 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Mon, 3 Mar 2025 11:44:10 -0500 Subject: [PATCH 02/18] chore: undefined error fix --- .../node-specific/client_close.test.ts | 8 +++-- .../resource_tracking_script_builder.ts | 34 ++++++++++++------- .../fixtures/process_resource_script.in.js | 7 ++-- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 7331c06da78..392bb91bde5 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -65,10 +65,11 @@ describe('MongoClient.close() Integration', () => { authMechanism: 'MONGODB-OIDC' }; const client = new MongoClient(uri, options); - client.connect(); + const connectPromise = client.connect(); expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); await client.close(); expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'); + await connectPromise; } ); }); @@ -80,6 +81,7 @@ describe('MongoClient.close() Integration', () => { describe('Node.js resource: Server Selection Timer', () => { describe('after a Topology is created through client.connect()', () => { const metadata: MongoDBMetadataUI = { requires: { topology: 'replicaset' } }; + it.skip('server selection timers are cleaned up by client.close()', metadata, async () => { const run = async function ({ MongoClient, uri, expect, sleep, mongodb, getTimerCount }) { const serverSelectionTimeoutMS = 2222; @@ -160,7 +162,7 @@ describe('MongoClient.close() Integration', () => { const heartbeatFrequencyMS = 2000; const client = new MongoClient('mongodb://fakeUri', { heartbeatFrequencyMS }); const willBeHeartbeatFailed = once(client, 'serverHeartbeatFailed'); - client.connect(); + const connectPromise = client.connect(); await willBeHeartbeatFailed; function getMonitorTimer(servers) { for (const [, server] of servers) { @@ -174,6 +176,8 @@ describe('MongoClient.close() Integration', () => { expect(getMonitorTimer(servers)).to.not.exist; expect(getTimerCount()).to.equal(0); + + await connectPromise; }; await runScriptAndGetProcessInfo('timer-heartbeat-failed-monitor', config, run); } diff --git a/test/integration/node-specific/resource_tracking_script_builder.ts b/test/integration/node-specific/resource_tracking_script_builder.ts index 066fb9fad1e..314d243892c 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -174,10 +174,16 @@ export async function runScriptAndGetProcessInfo( REPORT_RESOURCE_SCRIPT_PATH, func ); - await writeFile(scriptName, scriptContent, { encoding: 'utf8' }); - const logFile = name + '.logs.txt'; + const logFile = name + '.logs.txt'; const stdErrFile = 'err.out'; + + await unlink(scriptName).catch(() => null); + await unlink(logFile).catch(() => null); + await unlink(stdErrFile).catch(() => null); + + await writeFile(scriptName, scriptContent, { encoding: 'utf8' }); + const script = spawn(process.execPath, [scriptName], { stdio: ['ignore', 'ignore', openSync(stdErrFile, 'w')] }); @@ -185,16 +191,15 @@ export async function runScriptAndGetProcessInfo( const willClose = once(script, 'close'); // make sure the process ended - const [exitCode] = await willClose; + const [exitCode] = (await willClose) as [number]; // format messages from child process as an object const messages = (await readFile(logFile, 'utf-8')) .trim() .split('\n') - .map(line => JSON.parse(line)) - .reduce((acc, curr) => ({ ...acc, ...curr }), {}); + .map(line => JSON.parse(line)); - const stdErrSize = await readFile(stdErrFile, { encoding: 'utf8' }); + const stdErr = await readFile(stdErrFile, { encoding: 'utf8' }); // delete temporary files await unlink(scriptName); @@ -203,18 +208,23 @@ export async function runScriptAndGetProcessInfo( // assertions about exit status if (exitCode) { + const { error } = messages.find(m => m.error != null); + expect(error).to.exist; const assertionError = new AssertionError( - messages.error?.message + '\n\t' + JSON.stringify(messages.error?.resources, undefined, 2) + error.message + '\n\t' + JSON.stringify(error.resources, undefined, 2) ); - assertionError.stack = messages.error?.stack + new Error().stack.slice('Error'.length); + assertionError.stack = error.stack + new Error().stack.slice('Error'.length); throw assertionError; } // assertions about resource status - expect(messages.beforeExitHappened).to.be.true; - expect(messages.newResources.libuvResources).to.be.empty; - expect(messages.newResources.activeResources).to.be.empty; + const { beforeExitHappened } = messages.find(m => 'beforeExitHappened' in m); + const { newResources } = messages.find(m => 'newResources' in m); + + expect(beforeExitHappened).to.be.true; + expect(newResources.libuvResources).to.be.empty; + expect(newResources.activeResources).to.be.empty; // assertion about error output - expect(stdErrSize).to.be.empty; + expect(stdErr).to.be.empty; } diff --git a/test/tools/fixtures/process_resource_script.in.js b/test/tools/fixtures/process_resource_script.in.js index c2886af9a77..31def062cb2 100644 --- a/test/tools/fixtures/process_resource_script.in.js +++ b/test/tools/fixtures/process_resource_script.in.js @@ -1,7 +1,7 @@ 'use strict'; /* eslint-disable no-undef */ -/* eslint-disable no-unused-vars */ + const driverPath = DRIVER_SOURCE_PATH; const func = FUNCTION_STRING; const scriptName = SCRIPT_NAME_STRING; @@ -48,7 +48,6 @@ function getNewLibuvResourceArray() { * @param {LibuvResource} resource */ function isNewLibuvResource(resource) { - const serverType = ['tcp', 'udp']; return ( !originalReportAddresses.includes(resource.address) && resource.is_referenced // if a resource is unreferenced, it's not keeping the event loop open ); @@ -128,10 +127,10 @@ async function main() { } main() - .then(() => {}) + .then(() => null) .catch(e => { log({ error: { message: e.message, stack: e.stack, resources: getNewResources() } }); - process.exit(1); + process.exit(2); }); setTimeout(() => { From 4ccbfd36a6a083a20b8f385810ec4aa83a8b6fa4 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Mon, 3 Mar 2025 13:15:03 -0500 Subject: [PATCH 03/18] chore: fix leaky cursors tests --- .../node-specific/abort_signal.test.ts | 22 +++++++++++++++- .../node-specific/auto_connect.test.ts | 25 ++++++++++++++----- .../node-specific/client_close.test.ts | 8 +----- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/test/integration/node-specific/abort_signal.test.ts b/test/integration/node-specific/abort_signal.test.ts index a7527479382..ea298c0ca71 100644 --- a/test/integration/node-specific/abort_signal.test.ts +++ b/test/integration/node-specific/abort_signal.test.ts @@ -16,6 +16,7 @@ import { FindCursor, ListCollectionsCursor, type Log, + Long, type MongoClient, MongoServerError, promiseWithResolvers, @@ -52,9 +53,11 @@ describe('AbortSignal support', () => { let db: Db; let collection: Collection<{ a: number; ssn: string }>; const logs: Log[] = []; + const cursors = []; beforeEach(async function () { logs.length = 0; + cursors.length = 0; client = this.configuration.newClient( {}, @@ -70,17 +73,34 @@ describe('AbortSignal support', () => { await client.connect(); db = client.db('abortSignal'); collection = db.collection('support'); + + client.on('commandSucceeded', ev => { + if ( + ev.commandName === 'find' || + ev.commandName === 'aggregate' || + ev.commandName === 'listCollections' + ) { + const cursorId = Long.isLong(ev.reply.cursor.id) + ? ev.reply.cursor.id + : Long.fromNumber(ev.reply.cursor.id); + + if (!Long.ZERO.equals(cursorId)) { + cursors.push(cursorId); + } + } + }); }); afterEach(async function () { logs.length = 0; const utilClient = this.configuration.newClient(); try { + await utilClient.db('abortSignal').command({ killCursors: 'support', cursors }); await utilClient.db('abortSignal').collection('support').deleteMany({}); } finally { await utilClient.close(); + await client?.close(); } - await client?.close(); }); function testCursor(cursorName: string, constructor: any) { diff --git a/test/integration/node-specific/auto_connect.test.ts b/test/integration/node-specific/auto_connect.test.ts index 3e56b69fbef..e7a199952f8 100644 --- a/test/integration/node-specific/auto_connect.test.ts +++ b/test/integration/node-specific/auto_connect.test.ts @@ -7,6 +7,7 @@ import { type ChangeStream, ClientSession, type Collection, + Long, MongoClient, MongoNotConnectedError, ProfilingLevel, @@ -19,7 +20,7 @@ describe('When executing an operation for the first time', () => { let client: MongoClient; beforeEach('create client', async function () { - client = this.configuration.newClient(); + client = this.configuration.newClient({}, { monitorCommands: true }); }); beforeEach('create test namespace', async function () { @@ -212,26 +213,38 @@ describe('When executing an operation for the first time', () => { let changeCausingCollection: Collection; let collection: Collection; let cs: ChangeStream; + const cursors = []; beforeEach(async function () { + cursors.length = 0; + if (this.configuration.topologyType === TopologyType.Single) { return; } changeCausingClient = this.configuration.newClient(); - await changeCausingClient + changeCausingCollection = await changeCausingClient .db('auto-connect-change') .createCollection('auto-connect') .catch(() => null); - changeCausingCollection = changeCausingClient - .db('auto-connect-change') - .collection('auto-connect'); - collection = client.db('auto-connect-change').collection('auto-connect'); cs = collection.watch(); + + client.on('commandSucceeded', ev => { + if (ev.commandName === 'aggregate') { + const cursorId = Long.isLong(ev.reply.cursor.id) + ? ev.reply.cursor.id + : Long.fromNumber(ev.reply.cursor.id); + + if (!Long.ZERO.equals(cursorId)) { + cursors.push(cursorId); + } + } + }); }); afterEach(async function () { + await client.db('auto-connect-change').command({ killCursors: 'auto-connect', cursors }); await changeCausingClient?.close(); await cs?.close(); }); diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 392bb91bde5..e2eca58a18d 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -755,13 +755,7 @@ describe('MongoClient.close() Integration', () => { ); }; - cursor = coll.find( - {}, - { - tailable: true, - awaitData: true - } - ); + cursor = coll.find({}, { batchSize: 1 }); await cursor.next(); // assert creation From 43159ff8a10074465ccba0362b390417a18cc519 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Mon, 3 Mar 2025 14:40:50 -0500 Subject: [PATCH 04/18] chore: fix cursor clean up --- test/integration/node-specific/abort_signal.test.ts | 5 ++++- test/integration/node-specific/auto_connect.test.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/test/integration/node-specific/abort_signal.test.ts b/test/integration/node-specific/abort_signal.test.ts index ea298c0ca71..6ce4ea3d6e6 100644 --- a/test/integration/node-specific/abort_signal.test.ts +++ b/test/integration/node-specific/abort_signal.test.ts @@ -95,7 +95,10 @@ describe('AbortSignal support', () => { logs.length = 0; const utilClient = this.configuration.newClient(); try { - await utilClient.db('abortSignal').command({ killCursors: 'support', cursors }); + if (cursors.length) { + await utilClient.db('abortSignal').command({ killCursors: 'support', cursors }); + cursors.length = 0; + } await utilClient.db('abortSignal').collection('support').deleteMany({}); } finally { await utilClient.close(); diff --git a/test/integration/node-specific/auto_connect.test.ts b/test/integration/node-specific/auto_connect.test.ts index e7a199952f8..968bb439275 100644 --- a/test/integration/node-specific/auto_connect.test.ts +++ b/test/integration/node-specific/auto_connect.test.ts @@ -244,7 +244,10 @@ describe('When executing an operation for the first time', () => { }); afterEach(async function () { - await client.db('auto-connect-change').command({ killCursors: 'auto-connect', cursors }); + if (cursors.length) { + await client.db('auto-connect-change').command({ killCursors: 'auto-connect', cursors }); + cursors.length = 0; + } await changeCausingClient?.close(); await cs?.close(); }); From dc3b10e730d1821ad00bc11601507b15adc37d29 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Mon, 3 Mar 2025 16:55:10 -0500 Subject: [PATCH 05/18] fix --- test/integration/node-specific/auto_connect.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/integration/node-specific/auto_connect.test.ts b/test/integration/node-specific/auto_connect.test.ts index 968bb439275..5fd84f93591 100644 --- a/test/integration/node-specific/auto_connect.test.ts +++ b/test/integration/node-specific/auto_connect.test.ts @@ -227,6 +227,10 @@ describe('When executing an operation for the first time', () => { .createCollection('auto-connect') .catch(() => null); + changeCausingCollection = changeCausingClient + .db('auto-connect-change') + .collection('auto-connect'); + collection = client.db('auto-connect-change').collection('auto-connect'); cs = collection.watch(); From 2a6bcd7998cb7d70ed7a3d123113b1319ca23a39 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Mon, 3 Mar 2025 18:05:19 -0500 Subject: [PATCH 06/18] sharded clusters have an internal oplog cursor --- .../node-specific/client_close.test.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index e2eca58a18d..978f171cd8b 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -742,17 +742,20 @@ describe('MongoClient.close() Integration', () => { metadata, async function () { const getCursors = async () => { - const res = await utilClient - .db() - .admin() - .command({ + const cursors = await utilClient + .db('admin') + .runCursorCommand({ aggregate: 1, cursor: {}, pipeline: [{ $currentOp: { idleCursors: true } }] - }); - return res.cursor.firstBatch.filter( - r => r.type === 'idleCursor' || (r.type === 'op' && r.desc === 'getMore') - ); + }) + .toArray(); + + return [ + ...cursors.filter(c => c.type === 'idleCursor'), // all idle cursors + ...cursors.filter(c => c.type === 'op' && c.desc === 'getMore'), // all running getMores + ...cursors.filter(c => c.ns !== 'local.oplog.rs') // no internal oplog cursors (sharded clusters) + ]; }; cursor = coll.find({}, { batchSize: 1 }); From eb7e1b1a1e8db166fac45e164c4725452298b96d Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 4 Mar 2025 09:09:19 -0500 Subject: [PATCH 07/18] chore: fix filtering --- .../node-specific/client_close.test.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 978f171cd8b..db58a7c8084 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -744,18 +744,14 @@ describe('MongoClient.close() Integration', () => { const getCursors = async () => { const cursors = await utilClient .db('admin') - .runCursorCommand({ - aggregate: 1, - cursor: {}, - pipeline: [{ $currentOp: { idleCursors: true } }] - }) + .aggregate([{ $currentOp: { idleCursors: true } }]) .toArray(); - return [ - ...cursors.filter(c => c.type === 'idleCursor'), // all idle cursors - ...cursors.filter(c => c.type === 'op' && c.desc === 'getMore'), // all running getMores - ...cursors.filter(c => c.ns !== 'local.oplog.rs') // no internal oplog cursors (sharded clusters) - ]; + return cursors.filter( + c => + c.ns !== 'local.oplog.rs' && + (c.type === 'idleCursor' || (c.type === 'op' && c.desc === 'getMore')) + ); // all idle cursors }; cursor = coll.find({}, { batchSize: 1 }); From 428589aa13e36a40cce73cbf022b5e2e72c21ab7 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 7 Mar 2025 13:47:51 -0500 Subject: [PATCH 08/18] chore: comment on server selection --- test/integration/node-specific/client_close.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index db58a7c8084..54476845329 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -441,8 +441,8 @@ describe('MongoClient.close() Integration', () => { // 27017 localhost.test.build.10gen.cc. const client = new MongoClient(SRV_CONNECTION_STRING, { - serverSelectionTimeoutMS: 2000, - tls: false + serverSelectionTimeoutMS: 2000, // if something changes make this test fail faster than 30s (connect() will reject) + tls: false // srv automatically sets tls to true, so we have to set it to false here. }); await client.connect(); // the current expected behavior is that _timeout is set to undefined until SRV polling starts From ec8930d8ad572be0fe34664ba894b67a08c474f7 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 7 Mar 2025 13:48:12 -0500 Subject: [PATCH 09/18] collection always exists --- test/integration/node-specific/client_close.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 54476845329..52eb03432ce 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -479,7 +479,7 @@ describe('MongoClient.close() Integration', () => { await client .db('db') .collection('collection') - ?.drop() + .drop() .catch(() => null); const collection = await client.db('db').createCollection('collection'); session = client.startSession({ explicit: false }); From 4c80bb4c422ed8bc94ff3d5af070b81011c56cda Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 7 Mar 2025 13:50:41 -0500 Subject: [PATCH 10/18] collection always exists pt2 --- test/integration/node-specific/client_close.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 52eb03432ce..6c34104c5fc 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -551,7 +551,7 @@ describe('MongoClient.close() Integration', () => { await client .db('db') .collection('collection') - ?.drop() + .drop() .catch(() => null); const collection = await client.db('db').createCollection('collection'); session = client.startSession(); From 8eee5b717076af1c5196477ebbe1864b8a6e3dc1 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 7 Mar 2025 14:03:14 -0500 Subject: [PATCH 11/18] Remove implicit session test --- .../node-specific/client_close.test.ts | 74 +------------------ 1 file changed, 1 insertion(+), 73 deletions(-) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 6c34104c5fc..8ab0ff3144e 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -1,5 +1,5 @@ -/* eslint-disable @typescript-eslint/no-empty-function */ import { expect } from 'chai'; +import { once } from 'events'; import { getCSFLEKMSProviders } from '../../csfle-kms-providers'; import { type Collection, type FindCursor, type MongoClient } from '../../mongodb'; @@ -458,78 +458,6 @@ describe('MongoClient.close() Integration', () => { }); }); - describe('ClientSession (Implicit)', () => { - let idleSessionsBeforeClose; - let idleSessionsAfterClose; - let client; - let utilClient; - let session; - - const metadata: MongoDBMetadataUI = { - requires: { - topology: ['replicaset', 'sharded'], - mongodb: '>=4.2' - } - }; - - beforeEach(async function () { - client = this.configuration.newClient(); - utilClient = this.configuration.newClient(); - await client.connect(); - await client - .db('db') - .collection('collection') - .drop() - .catch(() => null); - const collection = await client.db('db').createCollection('collection'); - session = client.startSession({ explicit: false }); - session.startTransaction(); - await collection.insertOne({ x: 1 }, { session }); - - const opBefore = await utilClient.db().admin().command({ currentOp: 1 }); - idleSessionsBeforeClose = opBefore.inprog.filter(s => s.type === 'idleSession'); - - await client.close(); - - const opAfter = await utilClient.db().admin().command({ currentOp: 1 }); - idleSessionsAfterClose = opAfter.inprog.filter(s => s.type === 'idleSession'); - - await utilClient.close(); - }); - - afterEach(async function () { - await utilClient?.close(); - await session?.endSession(); - await client?.close(); - }); - - describe('Server resource: LSID/ServerSession', () => { - describe('after a clientSession is implicitly created and used', () => { - it( - 'the server-side ServerSession is cleaned up by client.close()', - metadata, - async function () { - expect(idleSessionsBeforeClose).to.not.be.empty; - expect(idleSessionsAfterClose).to.be.empty; - } - ); - }); - }); - - describe('Server resource: Transactions', () => { - describe('after a clientSession is implicitly created and used', () => { - it( - 'the server-side transaction is cleaned up by client.close()', - metadata, - async function () { - expect(idleSessionsBeforeClose[0].transaction.txnNumber).to.not.null; - expect(idleSessionsAfterClose).to.be.empty; - } - ); - }); - }); - }); - describe('ClientSession (Explicit)', () => { let idleSessionsBeforeClose; let idleSessionsAfterClose; From 88d1a9ebb602c36a6db5ddbf4fd66a2b7a3ab6bb Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 7 Mar 2025 14:05:20 -0500 Subject: [PATCH 12/18] collection always exists pt3 --- test/integration/node-specific/client_close.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 8ab0ff3144e..f680fd3ded6 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -651,7 +651,7 @@ describe('MongoClient.close() Integration', () => { await client .db('db') .collection('coll') - ?.drop() + .drop() .catch(() => null); coll = await client .db('db') From 7ee694a045eb4c6febcd03cd0d26c99d57ee8cd6 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 7 Mar 2025 14:06:43 -0500 Subject: [PATCH 13/18] remove metadata and remove collection options --- .../node-specific/client_close.test.ts | 56 ++++++++----------- 1 file changed, 22 insertions(+), 34 deletions(-) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index f680fd3ded6..fe38cf4a836 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -632,12 +632,6 @@ describe('MongoClient.close() Integration', () => { }); describe('Server resource: Cursor', () => { - const metadata: MongoDBMetadataUI = { - requires: { - mongodb: '>=4.2.0' - } - }; - describe('after cursors are created', () => { let client: MongoClient; let coll: Collection; @@ -653,9 +647,7 @@ describe('MongoClient.close() Integration', () => { .collection('coll') .drop() .catch(() => null); - coll = await client - .db('db') - .createCollection('coll', { capped: true, size: 1_000, max: 4 }); + coll = await client.db('db').createCollection('coll'); await coll.insertMany([{ a: 1 }, { b: 2 }, { c: 3 }]); }); @@ -665,35 +657,31 @@ describe('MongoClient.close() Integration', () => { await cursor?.close(); }); - it( - 'all active server-side cursors are closed by client.close()', - metadata, - async function () { - const getCursors = async () => { - const cursors = await utilClient - .db('admin') - .aggregate([{ $currentOp: { idleCursors: true } }]) - .toArray(); - - return cursors.filter( - c => - c.ns !== 'local.oplog.rs' && - (c.type === 'idleCursor' || (c.type === 'op' && c.desc === 'getMore')) - ); // all idle cursors - }; + it('all active server-side cursors are closed by client.close()', async function () { + const getCursors = async () => { + const cursors = await utilClient + .db('admin') + .aggregate([{ $currentOp: { idleCursors: true } }]) + .toArray(); + + return cursors.filter( + c => + c.ns !== 'local.oplog.rs' && + (c.type === 'idleCursor' || (c.type === 'op' && c.desc === 'getMore')) + ); // all idle cursors + }; - cursor = coll.find({}, { batchSize: 1 }); - await cursor.next(); + cursor = coll.find({}, { batchSize: 1 }); + await cursor.next(); - // assert creation - expect(await getCursors()).to.not.be.empty; + // assert creation + expect(await getCursors()).to.not.be.empty; - await client.close(); + await client.close(); - // assert clean-up - expect(await getCursors()).to.be.empty; - } - ); + // assert clean-up + expect(await getCursors()).to.be.empty; + }); }); }); }); From 26c2bdc0b208974d01a34af1e12e9cba525ed580 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 7 Mar 2025 14:10:26 -0500 Subject: [PATCH 14/18] add precondition message --- .../node-specific/resource_tracking_script_builder.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/integration/node-specific/resource_tracking_script_builder.ts b/test/integration/node-specific/resource_tracking_script_builder.ts index 314d243892c..375613d6157 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -209,7 +209,8 @@ export async function runScriptAndGetProcessInfo( // assertions about exit status if (exitCode) { const { error } = messages.find(m => m.error != null); - expect(error).to.exist; + expect(error, 'test script exited with non-zero exit code but did not report an error.').to + .exist; const assertionError = new AssertionError( error.message + '\n\t' + JSON.stringify(error.resources, undefined, 2) ); From 97225334c2a23f329e9e714d161822cbde8eaeee Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 7 Mar 2025 14:41:30 -0500 Subject: [PATCH 15/18] chore: put back version filter --- .../node-specific/client_close.test.ts | 57 +++++++++++-------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index fe38cf4a836..dfe58714a3d 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -1,5 +1,4 @@ import { expect } from 'chai'; -import { once } from 'events'; import { getCSFLEKMSProviders } from '../../csfle-kms-providers'; import { type Collection, type FindCursor, type MongoClient } from '../../mongodb'; @@ -626,13 +625,19 @@ describe('MongoClient.close() Integration', () => { }); describe('Node.js resource: Socket', () => { - it.skip('no sockets remain after client.close()', metadata, async () => {}); + it.skip('no sockets remain after client.close()', metadata, async () => null); }); }); }); describe('Server resource: Cursor', () => { - describe('after cursors are created', () => { + const metadata: MongoDBMetadataUI = { + requires: { + mongodb: '>=4.2.0' // MongoServerError: Unrecognized option 'idleCursors' in $currentOp stage. on 4.0 + } + }; + + describe('after cursors are created', metadata, () => { let client: MongoClient; let coll: Collection; let cursor: FindCursor; @@ -657,31 +662,35 @@ describe('MongoClient.close() Integration', () => { await cursor?.close(); }); - it('all active server-side cursors are closed by client.close()', async function () { - const getCursors = async () => { - const cursors = await utilClient - .db('admin') - .aggregate([{ $currentOp: { idleCursors: true } }]) - .toArray(); - - return cursors.filter( - c => - c.ns !== 'local.oplog.rs' && - (c.type === 'idleCursor' || (c.type === 'op' && c.desc === 'getMore')) - ); // all idle cursors - }; + it( + 'all active server-side cursors are closed by client.close()', + metadata, + async function () { + const getCursors = async () => { + const cursors = await utilClient + .db('admin') + .aggregate([{ $currentOp: { idleCursors: true } }]) + .toArray(); + + return cursors.filter( + c => + c.ns !== 'local.oplog.rs' && + (c.type === 'idleCursor' || (c.type === 'op' && c.desc === 'getMore')) + ); // all idle cursors + }; - cursor = coll.find({}, { batchSize: 1 }); - await cursor.next(); + cursor = coll.find({}, { batchSize: 1 }); + await cursor.next(); - // assert creation - expect(await getCursors()).to.not.be.empty; + // assert creation + expect(await getCursors()).to.not.be.empty; - await client.close(); + await client.close(); - // assert clean-up - expect(await getCursors()).to.be.empty; - }); + // assert clean-up + expect(await getCursors()).to.be.empty; + } + ); }); }); }); From f4a4798ec3b44b04d8b9733e2dd0c7a24a19dfdd Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Mon, 10 Mar 2025 14:15:38 -0400 Subject: [PATCH 16/18] chore: filter on ns --- .../node-specific/abort_signal.test.ts | 25 +------------------ .../node-specific/auto_connect.test.ts | 24 ++---------------- .../node-specific/client_close.test.ts | 12 +++------ 3 files changed, 7 insertions(+), 54 deletions(-) diff --git a/test/integration/node-specific/abort_signal.test.ts b/test/integration/node-specific/abort_signal.test.ts index 6ce4ea3d6e6..a7527479382 100644 --- a/test/integration/node-specific/abort_signal.test.ts +++ b/test/integration/node-specific/abort_signal.test.ts @@ -16,7 +16,6 @@ import { FindCursor, ListCollectionsCursor, type Log, - Long, type MongoClient, MongoServerError, promiseWithResolvers, @@ -53,11 +52,9 @@ describe('AbortSignal support', () => { let db: Db; let collection: Collection<{ a: number; ssn: string }>; const logs: Log[] = []; - const cursors = []; beforeEach(async function () { logs.length = 0; - cursors.length = 0; client = this.configuration.newClient( {}, @@ -73,37 +70,17 @@ describe('AbortSignal support', () => { await client.connect(); db = client.db('abortSignal'); collection = db.collection('support'); - - client.on('commandSucceeded', ev => { - if ( - ev.commandName === 'find' || - ev.commandName === 'aggregate' || - ev.commandName === 'listCollections' - ) { - const cursorId = Long.isLong(ev.reply.cursor.id) - ? ev.reply.cursor.id - : Long.fromNumber(ev.reply.cursor.id); - - if (!Long.ZERO.equals(cursorId)) { - cursors.push(cursorId); - } - } - }); }); afterEach(async function () { logs.length = 0; const utilClient = this.configuration.newClient(); try { - if (cursors.length) { - await utilClient.db('abortSignal').command({ killCursors: 'support', cursors }); - cursors.length = 0; - } await utilClient.db('abortSignal').collection('support').deleteMany({}); } finally { await utilClient.close(); - await client?.close(); } + await client?.close(); }); function testCursor(cursorName: string, constructor: any) { diff --git a/test/integration/node-specific/auto_connect.test.ts b/test/integration/node-specific/auto_connect.test.ts index 5fd84f93591..3e56b69fbef 100644 --- a/test/integration/node-specific/auto_connect.test.ts +++ b/test/integration/node-specific/auto_connect.test.ts @@ -7,7 +7,6 @@ import { type ChangeStream, ClientSession, type Collection, - Long, MongoClient, MongoNotConnectedError, ProfilingLevel, @@ -20,7 +19,7 @@ describe('When executing an operation for the first time', () => { let client: MongoClient; beforeEach('create client', async function () { - client = this.configuration.newClient({}, { monitorCommands: true }); + client = this.configuration.newClient(); }); beforeEach('create test namespace', async function () { @@ -213,16 +212,13 @@ describe('When executing an operation for the first time', () => { let changeCausingCollection: Collection; let collection: Collection; let cs: ChangeStream; - const cursors = []; beforeEach(async function () { - cursors.length = 0; - if (this.configuration.topologyType === TopologyType.Single) { return; } changeCausingClient = this.configuration.newClient(); - changeCausingCollection = await changeCausingClient + await changeCausingClient .db('auto-connect-change') .createCollection('auto-connect') .catch(() => null); @@ -233,25 +229,9 @@ describe('When executing an operation for the first time', () => { collection = client.db('auto-connect-change').collection('auto-connect'); cs = collection.watch(); - - client.on('commandSucceeded', ev => { - if (ev.commandName === 'aggregate') { - const cursorId = Long.isLong(ev.reply.cursor.id) - ? ev.reply.cursor.id - : Long.fromNumber(ev.reply.cursor.id); - - if (!Long.ZERO.equals(cursorId)) { - cursors.push(cursorId); - } - } - }); }); afterEach(async function () { - if (cursors.length) { - await client.db('auto-connect-change').command({ killCursors: 'auto-connect', cursors }); - cursors.length = 0; - } await changeCausingClient?.close(); await cs?.close(); }); diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index dfe58714a3d..d34340c267d 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -648,11 +648,11 @@ describe('MongoClient.close() Integration', () => { utilClient = this.configuration.newClient(); await client.connect(); await client - .db('db') - .collection('coll') + .db('close_db') + .collection('close_coll') .drop() .catch(() => null); - coll = await client.db('db').createCollection('coll'); + coll = await client.db('close_db').createCollection('close_coll'); await coll.insertMany([{ a: 1 }, { b: 2 }, { c: 3 }]); }); @@ -672,11 +672,7 @@ describe('MongoClient.close() Integration', () => { .aggregate([{ $currentOp: { idleCursors: true } }]) .toArray(); - return cursors.filter( - c => - c.ns !== 'local.oplog.rs' && - (c.type === 'idleCursor' || (c.type === 'op' && c.desc === 'getMore')) - ); // all idle cursors + return cursors.filter(c => c.ns === 'close_db.close_coll'); }; cursor = coll.find({}, { batchSize: 1 }); From 8022a20a12d2f7b48f22edc607906a4e280526d6 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Mon, 10 Mar 2025 14:35:05 -0400 Subject: [PATCH 17/18] chore: add implicit session client coverage --- .../node-specific/client_close.test.ts | 148 ++++++++++++------ 1 file changed, 100 insertions(+), 48 deletions(-) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index d34340c267d..533ec1f13b2 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -1,25 +1,20 @@ +import * as events from 'node:events'; + import { expect } from 'chai'; import { getCSFLEKMSProviders } from '../../csfle-kms-providers'; import { type Collection, type FindCursor, type MongoClient } from '../../mongodb'; -import { type TestConfiguration } from '../../tools/runner/config'; import { runScriptAndGetProcessInfo } from './resource_tracking_script_builder'; describe('MongoClient.close() Integration', () => { // note: these tests are set-up in accordance of the resource ownership tree - let config: TestConfiguration; - - beforeEach(function () { - config = this.configuration; - }); - describe('Node.js resource: TLS File read', () => { describe('when client is connecting and reads an infinite TLS file', () => { it.skip('the file read is interrupted by client.close()', async function () { await runScriptAndGetProcessInfo( 'tls-file-read', - config, + this.configuration, async function run({ MongoClient, uri, expect }) { const infiniteFile = '/dev/zero'; const client = new MongoClient(uri, { tls: true, tlsCertificateKeyFile: infiniteFile }); @@ -52,10 +47,10 @@ describe('MongoClient.close() Integration', () => { }); describe('when MongoClientAuthProviders is instantiated and token file read hangs', () => { - it.skip('the file read is interrupted by client.close()', async () => { + it.skip('the file read is interrupted by client.close()', async function () { await runScriptAndGetProcessInfo( 'token-file-read', - config, + this.configuration, async function run({ MongoClient, uri, expect }) { const infiniteFile = '/dev/zero'; process.env.OIDC_TOKEN_FILE = infiniteFile; @@ -81,30 +76,41 @@ describe('MongoClient.close() Integration', () => { describe('after a Topology is created through client.connect()', () => { const metadata: MongoDBMetadataUI = { requires: { topology: 'replicaset' } }; - it.skip('server selection timers are cleaned up by client.close()', metadata, async () => { - const run = async function ({ MongoClient, uri, expect, sleep, mongodb, getTimerCount }) { - const serverSelectionTimeoutMS = 2222; - const client = new MongoClient(uri, { - minPoolSize: 1, - serverSelectionTimeoutMS, - readPreference: new mongodb.ReadPreference('secondary', [ - { something: 'that does not exist' } - ]) - }); - const insertPromise = client.db('db').collection('collection').insertOne({ x: 1 }); + it.skip( + 'server selection timers are cleaned up by client.close()', + metadata, + async function () { + const run = async function ({ + MongoClient, + uri, + expect, + sleep, + mongodb, + getTimerCount + }) { + const serverSelectionTimeoutMS = 2222; + const client = new MongoClient(uri, { + minPoolSize: 1, + serverSelectionTimeoutMS, + readPreference: new mongodb.ReadPreference('secondary', [ + { something: 'that does not exist' } + ]) + }); + const insertPromise = client.db('db').collection('collection').insertOne({ x: 1 }); - // don't allow entire server selection timer to elapse to ensure close is called mid-timeout - await sleep(serverSelectionTimeoutMS / 2); + // don't allow entire server selection timer to elapse to ensure close is called mid-timeout + await sleep(serverSelectionTimeoutMS / 2); - expect(getTimerCount()).to.not.equal(0); - await client.close(); - expect(getTimerCount()).to.equal(0); + expect(getTimerCount()).to.not.equal(0); + await client.close(); + expect(getTimerCount()).to.equal(0); - const err = await insertPromise.catch(e => e); - expect(err).to.be.instanceOf(mongodb.MongoTopologyClosedError); - }; - await runScriptAndGetProcessInfo('timer-server-selection', config, run); - }); + const err = await insertPromise.catch(e => e); + expect(err).to.be.instanceOf(mongodb.MongoTopologyClosedError); + }; + await runScriptAndGetProcessInfo('timer-server-selection', this.configuration, run); + } + ); }); }); @@ -147,7 +153,11 @@ describe('MongoClient.close() Integration', () => { expect(getTimerCount()).to.equal(0); }; - await runScriptAndGetProcessInfo('timer-monitor-interval', config, run); + await runScriptAndGetProcessInfo( + 'timer-monitor-interval', + this.configuration, + run + ); } ); }); @@ -156,7 +166,7 @@ describe('MongoClient.close() Integration', () => { it.skip( 'the new monitor interval timer is cleaned up by client.close()', metadata, - async () => { + async function () { const run = async function ({ MongoClient, expect, getTimerCount, once }) { const heartbeatFrequencyMS = 2000; const client = new MongoClient('mongodb://fakeUri', { heartbeatFrequencyMS }); @@ -178,7 +188,11 @@ describe('MongoClient.close() Integration', () => { await connectPromise; }; - await runScriptAndGetProcessInfo('timer-heartbeat-failed-monitor', config, run); + await runScriptAndGetProcessInfo( + 'timer-heartbeat-failed-monitor', + this.configuration, + run + ); } ); }); @@ -207,7 +221,11 @@ describe('MongoClient.close() Integration', () => { expect(getSocketEndpoints()).to.not.deep.include({ host, port }); } }; - await runScriptAndGetProcessInfo('socket-connection-monitoring', config, run); + await runScriptAndGetProcessInfo( + 'socket-connection-monitoring', + this.configuration, + run + ); }); }); }); @@ -242,7 +260,7 @@ describe('MongoClient.close() Integration', () => { expect(getTimerCount()).to.equal(0); }; - await runScriptAndGetProcessInfo('timer-rtt-monitor', config, run); + await runScriptAndGetProcessInfo('timer-rtt-monitor', this.configuration, run); } ); }); @@ -251,7 +269,7 @@ describe('MongoClient.close() Integration', () => { describe('Connection', () => { describe('Node.js resource: Socket', () => { describe('when rtt monitoring is turned on', () => { - it.skip('no sockets remain after client.close()', metadata, async () => { + it.skip('no sockets remain after client.close()', metadata, async function () { const run = async ({ MongoClient, uri, expect, getSockets, once }) => { const heartbeatFrequencyMS = 500; const client = new MongoClient(uri, { @@ -289,7 +307,11 @@ describe('MongoClient.close() Integration', () => { expect(activeSocketsAfterClose).to.have.lengthOf(0); }; - await runScriptAndGetProcessInfo('socket-connection-rtt-monitoring', config, run); + await runScriptAndGetProcessInfo( + 'socket-connection-rtt-monitoring', + this.configuration, + run + ); }); }); }); @@ -323,7 +345,7 @@ describe('MongoClient.close() Integration', () => { expect(getMinPoolSizeTimer(servers)).to.not.exist; expect(getTimerCount()).to.equal(0); }; - await runScriptAndGetProcessInfo('timer-min-pool-size', config, run); + await runScriptAndGetProcessInfo('timer-min-pool-size', this.configuration, run); }); }); }); @@ -334,8 +356,8 @@ describe('MongoClient.close() Integration', () => { const waitQueueTimeoutMS = 1515; beforeEach(async function () { - // configure failPoint - utilClient = this.configuration.newClient(); + // this.configurationure failPoint + utilClient = this.this.configurationuration.newClient(); await utilClient.connect(); const failPoint = { configureFailPoint: 'failCommand', @@ -392,7 +414,7 @@ describe('MongoClient.close() Integration', () => { 'Timed out while checking out a connection from connection pool' ); }; - await runScriptAndGetProcessInfo('timer-check-out', config, run); + await runScriptAndGetProcessInfo('timer-check-out', this.configuration, run); }); }); }); @@ -418,7 +440,7 @@ describe('MongoClient.close() Integration', () => { expect(getSockets()).to.have.lengthOf(0); }; - await runScriptAndGetProcessInfo('socket-minPoolSize', config, run); + await runScriptAndGetProcessInfo('socket-minPoolSize', this.configuration, run); }); }); }); @@ -432,7 +454,7 @@ describe('MongoClient.close() Integration', () => { const metadata: MongoDBMetadataUI = { requires: { topology: 'sharded' } }; describe('after SRVPoller is created', () => { - it.skip('timers are cleaned up by client.close()', metadata, async () => { + it.skip('timers are cleaned up by client.close()', metadata, async function () { const run = async function ({ MongoClient, expect, getTimerCount }) { const SRV_CONNECTION_STRING = `mongodb+srv://test1.test.build.10gen.cc`; @@ -450,13 +472,43 @@ describe('MongoClient.close() Integration', () => { await client.close(); expect(getTimerCount()).to.equal(0); }; - await runScriptAndGetProcessInfo('timer-srv-poller', config, run); + await runScriptAndGetProcessInfo('timer-srv-poller', this.configuration, run); }); }); }); }); }); + describe('ClientSession (Implicit)', () => { + let client: MongoClient; + + beforeEach(async function () { + client = this.configuration.newClient({}, { monitorCommands: true }); + }); + + afterEach(async function () { + await client.close(); + }); + + describe('when MongoClient.close is called', function () { + it('sends an endSessions command', async function () { + await client.db('a').collection('a').insertOne({ a: 1 }); + await client.db('a').collection('a').insertOne({ a: 1 }); + await client.db('a').collection('a').insertOne({ a: 1 }); + const endSessionsStarted = events.once(client, 'commandStarted'); + const willEndSessions = events.once(client, 'commandSucceeded'); + + await client.close(); + + const [startedEv] = await endSessionsStarted; + expect(startedEv).to.have.nested.property('command.endSessions').that.has.lengthOf(1); + + const [commandEv] = await willEndSessions; + expect(commandEv).to.have.property('commandName', 'endSessions'); + }); + }); + }); + describe('ClientSession (Explicit)', () => { let idleSessionsBeforeClose; let idleSessionsAfterClose; @@ -538,10 +590,10 @@ describe('MongoClient.close() Integration', () => { describe('KMS Request', () => { describe('Node.js resource: TLS file read', () => { describe('when KMSRequest reads an infinite TLS file', () => { - it.skip('the file read is interrupted by client.close()', metadata, async () => { + it.skip('the file read is interrupted by client.close()', metadata, async function () { await runScriptAndGetProcessInfo( 'tls-file-read-auto-encryption', - config, + this.configuration, async function run({ MongoClient, uri, expect, mongodb }) { const infiniteFile = '/dev/zero'; @@ -666,7 +718,7 @@ describe('MongoClient.close() Integration', () => { 'all active server-side cursors are closed by client.close()', metadata, async function () { - const getCursors = async () => { + const getCursors = async function () { const cursors = await utilClient .db('admin') .aggregate([{ $currentOp: { idleCursors: true } }]) From 4c1d06c46de0b898f5945959d3724376d4e8127b Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 11 Mar 2025 15:24:54 -0400 Subject: [PATCH 18/18] fix: typo --- test/integration/node-specific/client_close.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 533ec1f13b2..c894f8d2614 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -356,8 +356,7 @@ describe('MongoClient.close() Integration', () => { const waitQueueTimeoutMS = 1515; beforeEach(async function () { - // this.configurationure failPoint - utilClient = this.this.configurationuration.newClient(); + utilClient = this.configuration.newClient(); await utilClient.connect(); const failPoint = { configureFailPoint: 'failCommand',