diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts new file mode 100644 index 00000000000..839900e8efe --- /dev/null +++ b/test/integration/node-specific/client_close.test.ts @@ -0,0 +1,157 @@ +import { TestConfiguration } from '../../tools/runner/config'; +import { runScriptAndReturnResourceInfo } from './resource_tracking_script_builder'; + +describe.only('client.close() Integration', () => { + let config: TestConfiguration; + beforeEach(function () { + config = this.configuration; + }); + + describe('MongoClient', () => { + describe('when client is being instantiated and reads a long docker file', () => { + // our docker env detection uses fs.access which will not be aborted until after it runs + // fs.access does not support abort signals + it('the file read is not interrupted by client.close', () => { + }); + }); + describe('when client is connecting and reads a TLS long file', () => { + it('the file read is interrupted by client.close', () => { + }); + }); + }); + + describe('MongoClientAuthProviders', () => { + describe('when MongoClientAuthProviders is instantiated', () => { + it('the token cache is cleaned up by client.close', () => { + }); + }); + }); + + describe('Topology', () => { + describe('after a Topology is explicitly created', () => { + it('timers are cleaned up by client.close()', () => { + + }); + }); + describe('after a Topology is created through client.connect()', () => { + it('timers are cleaned up by client.close()', () => { + + }); + }); + }); + + describe('SRVPoller', () => { + describe('after SRVPoller is explicitly created', () => { + it('timers are cleaned up by client.close()', () => { + + }); + }); + + // SRVPoller is implicitly created after an SRV string's topology transitions to sharded + describe('after SRVPoller is implicitly created', () => { + it('timers are cleaned up by client.close()', () => { + + }); + }); + }); + + describe('ClientSession', () => { + describe('after a clientSession is created', () => { + it('the server-side ServerSession and transaction are cleaned up by client.close()', () => { + + }); + }); + }); + + describe('StateMachine', () => { + describe('when FLE is enabled and the client has made a KMS request', () => { + it('no sockets remain after client.close', () => { + + }); + it('no server-side connection threads remain after client.close', () => { + + }); + describe('when the TLS file read hangs', () => { + it('the file read is interrupted by client.close', () => { + + }); + }); + }); + }); + + describe('ConnectionPool', () => { + describe('after new connection pool is created', () => { + it('minPoolSize timer is cleaned up by client.close()', () => { + + }); + }); + }); + + describe('MonitorInterval', () => { + describe('after a new monitor is made', () => { + it('monitor interval timer is cleaned up by client.close()', () => { + + }); + }); + + describe('after a heartbeat fails', () => { + it('monitor interval timer is cleaned up by client.close()', () => { + + }); + }); + }); + + describe('RTTPinger', () => { + describe('after helloReply has a topologyVersion defined fails', () => { + it('rtt pinger timer is cleaned up by client.close()', () => { + + }); + }); + }); + + describe('Connection', () => { + describe('when connection monitoring is turned on', () => { + it('no sockets remain after client.close', () => { + + }); + it('no server-side connection threads remain after client.close', () => { + + }); + }); + + describe('when rtt monitoring is turned on', () => { + it('no sockets remain after client.close', () => { + + }); + it('no server-side connection threads remain after client.close', () => { + + }); + }); + + describe('after a connection is checked out', () => { + it('no sockets remain after client.close', () => { + + }); + it('no server-side connection threads remain after client.close', () => { + + }); + }); + + describe('after a minPoolSize has been set on the ConnectionPool', () => { + it('no sockets remain after client.close', () => { + + }); + it('no server-side connection threads remain after client.close', () => { + + }); + }); + }); + + describe('Cursor', () => { + describe('after cursors are created', () => { + it('all active server-side cursors are closed by client.close()', () => { + + }); + }); + }); +}); diff --git a/test/integration/node-specific/resource_clean_up.test.ts b/test/integration/node-specific/resource_clean_up.test.ts index e370986a264..9e021b65790 100644 --- a/test/integration/node-specific/resource_clean_up.test.ts +++ b/test/integration/node-specific/resource_clean_up.test.ts @@ -3,7 +3,7 @@ import * as v8 from 'node:v8'; import { expect } from 'chai'; import { sleep } from '../../tools/utils'; -import { runScript } from './resource_tracking_script_builder'; +import { runScriptAndReturnHeapInfo } from './resource_tracking_script_builder'; /** * This 5MB range is selected arbitrarily and should likely be raised if failures are seen intermittently. @@ -38,7 +38,7 @@ describe('Driver Resources', () => { return; } try { - const res = await runScript( + const res = await runScriptAndReturnHeapInfo( 'no_resource_leak_connect_close', this.configuration, async function run({ MongoClient, uri }) { diff --git a/test/integration/node-specific/resource_tracking_script_builder.ts b/test/integration/node-specific/resource_tracking_script_builder.ts index f7cfb764230..1a0376eddb7 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -15,22 +15,26 @@ export type ResourceTestFunction = (options: { iteration: number; }) => Promise; -const RESOURCE_SCRIPT_PATH = path.resolve(__dirname, '../../tools/fixtures/resource_script.in.js'); +const HEAP_RESOURCE_SCRIPT_PATH = path.resolve(__dirname, '../../tools/fixtures/resource_script.in.js'); +const REPORT_RESOURCE_SCRIPT_PATH = path.resolve(__dirname, '../../tools/fixtures/close_resource_script.in.js'); const DRIVER_SRC_PATH = JSON.stringify(path.resolve(__dirname, '../../../lib')); export async function testScriptFactory( name: string, uri: string, - iterations: number, - func: ResourceTestFunction + resourceScriptPath: string, + func: ResourceTestFunction, + iterations?: number, ) { - let resourceScript = await readFile(RESOURCE_SCRIPT_PATH, { encoding: 'utf8' }); + let resourceScript = await readFile(resourceScriptPath, { encoding: 'utf8' }); resourceScript = resourceScript.replace('DRIVER_SOURCE_PATH', DRIVER_SRC_PATH); resourceScript = resourceScript.replace('FUNCTION_STRING', `(${func.toString()})`); resourceScript = resourceScript.replace('NAME_STRING', JSON.stringify(name)); resourceScript = resourceScript.replace('URI_STRING', JSON.stringify(uri)); - resourceScript = resourceScript.replace('ITERATIONS_STRING', `${iterations}`); + if (resourceScriptPath === HEAP_RESOURCE_SCRIPT_PATH) { + resourceScript = resourceScript.replace('ITERATIONS_STRING', `${iterations}`); + } return resourceScript; } @@ -57,7 +61,7 @@ export async function testScriptFactory( * @param options - settings for the script * @throws Error - if the process exits with failure */ -export async function runScript( +export async function runScriptAndReturnHeapInfo( name: string, config: TestConfiguration, func: ResourceTestFunction, @@ -66,7 +70,7 @@ export async function runScript( const scriptName = `${name}.cjs`; const heapsnapshotFile = `${name}.heapsnapshot.json`; - const scriptContent = await testScriptFactory(name, config.url(), iterations, func); + const scriptContent = await testScriptFactory(name, config.url(), HEAP_RESOURCE_SCRIPT_PATH, func, iterations); await writeFile(scriptName, scriptContent, { encoding: 'utf8' }); const processDiedController = new AbortController(); @@ -106,3 +110,34 @@ export async function runScript( heap }; } + +export async function runScriptAndReturnResourceInfo( + name: string, + config: TestConfiguration, + func: ResourceTestFunction +) { + + const scriptName = `scripts/${name}.cjs`; + const scriptContent = await testScriptFactory(name, config.url(), REPORT_RESOURCE_SCRIPT_PATH, func); + await writeFile(scriptName, scriptContent, { encoding: 'utf8' }); + + const processDiedController = new AbortController(); + const script = fork(name); + + // Interrupt our awaiting of messages if the process crashed + script.once('close', exitCode => { + if (exitCode !== 0) { + processDiedController.abort(new Error(`process exited with: ${exitCode}`)); + } + }); + + const willClose = once(script, 'close'); + + // make sure the process ended + const [exitCode] = await willClose; + expect(exitCode, 'process should have exited with zero').to.equal(0); + + const messages = on(script, 'message', { signal: processDiedController.signal }); + const report = await messages.next(); + return report; +} diff --git a/test/tools/fixtures/close_resource_script.in.js b/test/tools/fixtures/close_resource_script.in.js new file mode 100644 index 00000000000..84f2ab431aa --- /dev/null +++ b/test/tools/fixtures/close_resource_script.in.js @@ -0,0 +1,34 @@ +'use strict'; + +/* eslint-disable no-undef */ + +const driverPath = DRIVER_SOURCE_PATH; +const func = FUNCTION_STRING; +const name = NAME_STRING; +const uri = URI_STRING; +const iterations = ITERATIONS_STRING; + +const { MongoClient } = require(driverPath); +const process = require('node:process'); +const v8 = require('node:v8'); +const util = require('node:util'); +const timers = require('node:timers'); + +const run = func; + +async function main() { + process.on('beforeExit', (code) => { + process.send({beforeExit: true}); + }); + await run({ MongoClient, uri, iteration }); + const report = process.report.getReport(); + process.send({ report }); +} + +main() + .then(() => { + process.exit(0); + }) + .catch(() => { + process.exit(1); + });