From 0af55e9d5a6a0934e23f0f2523dff6ee667068c8 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Mon, 16 Dec 2024 14:19:54 -0500 Subject: [PATCH 01/43] skeleton --- .../client-close/client_close.test.ts | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 test/integration/client-close/client_close.test.ts diff --git a/test/integration/client-close/client_close.test.ts b/test/integration/client-close/client_close.test.ts new file mode 100644 index 0000000000..646008514f --- /dev/null +++ b/test/integration/client-close/client_close.test.ts @@ -0,0 +1,137 @@ +import { + MongoClient, +} from '../../mongodb'; + +/* +async function runWithProcessAndCheck(_fn) { + start process + Run fn in process + Assert no resources + Close process +} +*/ + +describe.only('client.close() Resource Management Integration tests', () => { + let client: MongoClient; + beforeEach(function () { + client = this.configuration.newClient(); + }); + + describe('File System', () => { + context('when client is closed', () => { + context('after client is connected', () => { + it('the TLS file access is cleaned up', () => { + + }); + }); + context('after client is created ', () => { + // 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 .docker file access is cleaned up', () => { + + }); + }); + + context('when FLE is enabled', () => { + context('after client has made a KMS request', () => { + it('the TLS file access is cleaned up', () => { + + }); + }); + }); + }); + }); + + describe('Connection Creation and Socket Lifetime', () => { + context('when client is closed', () => { + context('after client is connected', () => { + it('the socket is cleaned up', () => { + + }); + }); + + context('after a connection is checked out', () => { + it('the socket is cleaned up', () => { + + }); + }); + + context('after a minPoolSize has been set on the ConnectionPool', () => { + it('the socket is cleaned up', () => { + + }); + }); + + context('when connection monitoring is turned on', () => { + it('the socket is cleaned up', () => { + + }); + }); + + context('when rtt monitoring is turned on', () => { + it('the socket is cleaned up', () => { + + }); + }); + + context('when FLE is enabled', () => { + context('after client has made a KMS request', () => { + it('the socket is cleaned up', () => { + + }); + }); + }); + }); + }); + + describe('Timers', () => { + context('when client is closed', () => { + context('after SRVPoller is explicitly created', () => { + it('timers are cleaned up', () => { + + }); + }); + + // SRVPoller is implicitly created after an SRV string's topology transitions to sharded + context('after SRVPoller is implicitly created', () => { + it('timers are cleaned up', () => { + + }); + }); + + context('after new connection pool is created', () => { + it('minPoolSize timer is cleaned up', () => { + + }); + }); + + context('after a new monitor is made', () => { + it('monitor interval timer is cleaned up', () => { + + }); + }); + + context('after a heartbeat fails', () => { + it('monitor interval timer is cleaned up', () => { + + }); + }); + + context('after helloReply has a topologyVersion defined fails', () => { + it('rtt pinger timer is cleaned up', () => { + + }); + }); + }); + }); + + describe('Cursor Clean-up', () => { + context('when client is closed', () => { + context('after cursors are created', () => { + it('closes all active cursors', () => { + + }); + }); + }); + }); +}); From 4798bc2e7752b78e035aee58b52f809d78b3d65b Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Mon, 16 Dec 2024 16:29:12 -0500 Subject: [PATCH 02/43] skeleton updates --- .../client-close/client_close.test.ts | 194 ++++++++++++------ .../fixtures/close_resource_script.in.js | 43 ++++ 2 files changed, 170 insertions(+), 67 deletions(-) create mode 100644 test/tools/fixtures/close_resource_script.in.js diff --git a/test/integration/client-close/client_close.test.ts b/test/integration/client-close/client_close.test.ts index 646008514f..1c63c39598 100644 --- a/test/integration/client-close/client_close.test.ts +++ b/test/integration/client-close/client_close.test.ts @@ -1,136 +1,196 @@ +import { fork } from 'child_process'; import { MongoClient, } from '../../mongodb'; +import { on, once } from 'node:events'; +import { readFile, unlink, writeFile } from 'node:fs/promises'; +import { TestConfiguration } from '../../tools/runner/config'; +import { expect } from 'chai'; +import { StringOrPlaceholder } from '../../tools/unified-spec-runner/schema'; /* -async function runWithProcessAndCheck(_fn) { - start process - Run fn in process - Assert no resources - Close process -} +export async function testScriptFactory( + name: string, + uri: string, + iterations: number, + func: Function +) { + let resourceScript = await readFile(RESOURCE_SCRIPT_PATH, { 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}`); + + return resourceScript; +} + +export async function runScriptAndReturnResourceInfo( + name: string, + config: TestConfiguration, + func: Function +) { + + const pathName = `scripts/${name}.cjs`; + const scriptContent = await testScriptFactory(name, config.url(), func); + await writeFile(name, func.toString(), { encoding: 'utf8' }); + + const processDiedController = new AbortController(); + const script = fork(name, { execArgv: ['--expose-gc'] }); + + // 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); + + return process.report.getReport().libuv; +} */ -describe.only('client.close() Resource Management Integration tests', () => { +describe.only('client.close() Integration', () => { let client: MongoClient; + let config: TestConfiguration; beforeEach(function () { + config = this.configuration; client = this.configuration.newClient(); }); describe('File System', () => { - context('when client is closed', () => { - context('after client is connected', () => { - it('the TLS file access is cleaned up', () => { + describe('when client is connected and reading a TLS long file', () => { + it('the file read is interrupted by client.close', () => { - }); }); - context('after client is created ', () => { - // 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 .docker file access is cleaned up', () => { - - }); + }); + describe('when client is created and reading 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', () => { }); + }); - context('when FLE is enabled', () => { - context('after client has made a KMS request', () => { - it('the TLS file access is cleaned up', () => { + describe('when FLE is enabled and the client has made a KMS request that is reading a long TLS file', () => { + it('the file read is interrupted by client.close', () => { - }); - }); }); }); }); describe('Connection Creation and Socket Lifetime', () => { - context('when client is closed', () => { - context('after client is connected', () => { - it('the socket is cleaned up', () => { + describe('after client is connected', () => { + it('no sockets remain after client.close', () => { - }); }); + it('no server-side connection threads remain after client.close', () => { - context('after a connection is checked out', () => { - it('the socket is cleaned up', () => { + }); + }); + + describe('after a connection is checked out', () => { + it('no sockets remain after client.close', () => { + + }); + it('no server-side connection threads remain after client.close', () => { - }); }); + }); - context('after a minPoolSize has been set on the ConnectionPool', () => { - it('the socket is cleaned up', () => { + 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('when connection monitoring is turned on', () => { + it('no sockets remain after client.close', () => { - context('when connection monitoring is turned on', () => { - it('the socket is cleaned up', () => { + }); + it('no server-side connection threads remain after client.close', () => { - }); }); + }); - context('when rtt monitoring is turned on', () => { - it('the socket is cleaned up', () => { + describe('when rtt monitoring is turned on', () => { + it('no sockets remain after client.close', () => { - }); }); + it('no server-side connection threads remain after client.close', () => { + + }); + }); - context('when FLE is enabled', () => { - context('after client has made a KMS request', () => { - it('the socket is cleaned up', () => { + 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('Timers', () => { - context('when client is closed', () => { - context('after SRVPoller is explicitly created', () => { - it('timers are cleaned up', () => { + 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 - context('after SRVPoller is implicitly created', () => { - it('timers are cleaned up', () => { + // 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()', () => { - }); }); + }); - context('after new connection pool is created', () => { - it('minPoolSize timer is cleaned up', () => { + describe('after new connection pool is created', () => { + it('minPoolSize timer is cleaned up by client.close()', () => { - }); }); + }); - context('after a new monitor is made', () => { - it('monitor interval timer is cleaned up', () => { + describe('after a new monitor is made', () => { + it('monitor interval timer is cleaned up by client.close()', () => { - }); }); + }); - context('after a heartbeat fails', () => { - it('monitor interval timer is cleaned up', () => { + describe('after a heartbeat fails', () => { + it('monitor interval timer is cleaned up by client.close()', () => { - }); }); + }); - context('after helloReply has a topologyVersion defined fails', () => { - it('rtt pinger timer is cleaned up', () => { + describe('after helloReply has a topologyVersion defined fails', () => { + it('rtt pinger timer is cleaned up by client.close()', () => { - }); }); }); }); describe('Cursor Clean-up', () => { - context('when client is closed', () => { - context('after cursors are created', () => { - it('closes all active cursors', () => { + describe('after cursors are created', () => { + it('all active server-side cursors are closed by client.close()', () => { + + }); + }); + }); + + describe('Sessions', () => { + describe('after a clientSession is created', () => { + it('the server-side ServerSession is cleaned up by client.close()', () => { - }); }); }); }); 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 0000000000..920382bff7 --- /dev/null +++ b/test/tools/fixtures/close_resource_script.in.js @@ -0,0 +1,43 @@ +'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 sleep = util.promisify(timers.setTimeout); + +const run = func; + +const MB = (2 ** 10) ** 2; + +async function main() { + for (let iteration = 0; iteration < iterations; iteration++) { + await run({ MongoClient, uri, iteration }); + global.gc(); + } + + global.gc(); + // Sleep b/c maybe gc will run + await sleep(100); + global.gc(); + + process.send({ process.report.getReport()}); +} + +main() + .then(() => { + process.exit(0); + }) + .catch(() => { + process.exit(1); + }); From 8adca00d718295283723561177a0c06c63550678 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Tue, 17 Dec 2024 11:27:38 -0500 Subject: [PATCH 03/43] refactor for table --- .../client_close.test.ts | 184 +++++++----------- .../node-specific/resource_clean_up.test.ts | 4 +- .../resource_tracking_script_builder.ts | 49 ++++- .../fixtures/close_resource_script.in.js | 21 +- 4 files changed, 122 insertions(+), 136 deletions(-) rename test/integration/{client-close => node-specific}/client_close.test.ts (53%) diff --git a/test/integration/client-close/client_close.test.ts b/test/integration/node-specific/client_close.test.ts similarity index 53% rename from test/integration/client-close/client_close.test.ts rename to test/integration/node-specific/client_close.test.ts index 1c63c39598..839900e8ef 100644 --- a/test/integration/client-close/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -1,135 +1,69 @@ -import { fork } from 'child_process'; -import { - MongoClient, -} from '../../mongodb'; -import { on, once } from 'node:events'; -import { readFile, unlink, writeFile } from 'node:fs/promises'; import { TestConfiguration } from '../../tools/runner/config'; -import { expect } from 'chai'; -import { StringOrPlaceholder } from '../../tools/unified-spec-runner/schema'; - -/* -export async function testScriptFactory( - name: string, - uri: string, - iterations: number, - func: Function -) { - let resourceScript = await readFile(RESOURCE_SCRIPT_PATH, { 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}`); - - return resourceScript; -} - -export async function runScriptAndReturnResourceInfo( - name: string, - config: TestConfiguration, - func: Function -) { - - const pathName = `scripts/${name}.cjs`; - const scriptContent = await testScriptFactory(name, config.url(), func); - await writeFile(name, func.toString(), { encoding: 'utf8' }); - - const processDiedController = new AbortController(); - const script = fork(name, { execArgv: ['--expose-gc'] }); - - // 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); - - return process.report.getReport().libuv; -} -*/ +import { runScriptAndReturnResourceInfo } from './resource_tracking_script_builder'; describe.only('client.close() Integration', () => { - let client: MongoClient; let config: TestConfiguration; beforeEach(function () { config = this.configuration; - client = this.configuration.newClient(); }); - describe('File System', () => { - describe('when client is connected and reading a TLS long file', () => { - it('the file read is interrupted by client.close', () => { - - }); - }); - describe('when client is created and reading a long docker file', () => { + 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 FLE is enabled and the client has made a KMS request that is reading a long TLS file', () => { + describe('when client is connecting and reads a TLS long file', () => { it('the file read is interrupted by client.close', () => { - }); }); }); - describe('Connection Creation and Socket Lifetime', () => { - describe('after client is connected', () => { - it('no sockets remain after client.close', () => { - - }); - it('no server-side connection threads remain after client.close', () => { - + describe('MongoClientAuthProviders', () => { + describe('when MongoClientAuthProviders is instantiated', () => { + it('the token cache is cleaned up by client.close', () => { }); }); + }); - describe('after a connection is checked out', () => { - it('no sockets remain after client.close', () => { + describe('Topology', () => { + describe('after a Topology is explicitly created', () => { + it('timers are cleaned up by client.close()', () => { }); - it('no server-side connection threads remain after client.close', () => { + }); + describe('after a Topology is created through client.connect()', () => { + it('timers are cleaned up by 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('SRVPoller', () => { + describe('after SRVPoller is explicitly created', () => { + it('timers are cleaned up by client.close()', () => { }); }); - describe('when connection monitoring is turned on', () => { - it('no sockets remain after client.close', () => { - - }); - it('no server-side connection threads remain after 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('when rtt monitoring is turned on', () => { - it('no sockets remain after client.close', () => { - - }); - it('no server-side connection threads remain after 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', () => { @@ -137,29 +71,23 @@ describe.only('client.close() Integration', () => { it('no server-side connection threads remain after client.close', () => { }); - }); - }); - - describe('Timers', () => { - 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('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()', () => { @@ -171,7 +99,9 @@ describe.only('client.close() Integration', () => { }); }); + }); + describe('RTTPinger', () => { describe('after helloReply has a topologyVersion defined fails', () => { it('rtt pinger timer is cleaned up by client.close()', () => { @@ -179,17 +109,47 @@ describe.only('client.close() Integration', () => { }); }); - describe('Cursor Clean-up', () => { - describe('after cursors are created', () => { - it('all active server-side cursors are closed 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('Sessions', () => { - describe('after a clientSession is created', () => { - it('the server-side ServerSession is cleaned up by 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 e370986a26..9e021b6579 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 f7cfb76423..1a0376eddb 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 index 920382bff7..84f2ab431a 100644 --- a/test/tools/fixtures/close_resource_script.in.js +++ b/test/tools/fixtures/close_resource_script.in.js @@ -14,24 +14,15 @@ const v8 = require('node:v8'); const util = require('node:util'); const timers = require('node:timers'); -const sleep = util.promisify(timers.setTimeout); - const run = func; -const MB = (2 ** 10) ** 2; - async function main() { - for (let iteration = 0; iteration < iterations; iteration++) { - await run({ MongoClient, uri, iteration }); - global.gc(); - } - - global.gc(); - // Sleep b/c maybe gc will run - await sleep(100); - global.gc(); - - process.send({ process.report.getReport()}); + process.on('beforeExit', (code) => { + process.send({beforeExit: true}); + }); + await run({ MongoClient, uri, iteration }); + const report = process.report.getReport(); + process.send({ report }); } main() From 3412bb1bc31bbbdde2ada3229847017a5e86886f Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Tue, 17 Dec 2024 17:54:12 -0500 Subject: [PATCH 04/43] boilerplate execution --- no_resource_leak_connect_close.cjs | 55 +++++++++++++++++++ .../node-specific/client_close.test.ts | 52 +++++++++--------- .../resource_tracking_script_builder.ts | 18 ++++-- .../fixtures/close_resource_script.in.js | 10 ++-- topology-clean-up.cjs | 38 +++++++++++++ topology.cjs | 37 +++++++++++++ 6 files changed, 174 insertions(+), 36 deletions(-) create mode 100644 no_resource_leak_connect_close.cjs create mode 100644 topology-clean-up.cjs create mode 100644 topology.cjs diff --git a/no_resource_leak_connect_close.cjs b/no_resource_leak_connect_close.cjs new file mode 100644 index 0000000000..f7ba3bd6dd --- /dev/null +++ b/no_resource_leak_connect_close.cjs @@ -0,0 +1,55 @@ +'use strict'; + +/* eslint-disable no-undef */ + +const driverPath = "/Users/aditi.khare/Desktop/node-mongodb-native/lib"; +const func = (async function run({ MongoClient, uri }) { + const mongoClient = new MongoClient(uri, { minPoolSize: 100 }); + await mongoClient.connect(); + // Any operations will reproduce the issue found in v5.0.0/v4.13.0 + // it would seem the MessageStream has to be used? + await mongoClient.db().command({ ping: 1 }); + await mongoClient.close(); + }); +const name = "no_resource_leak_connect_close"; +const uri = "mongodb://bob:pwd123@localhost:31000/integration_tests?replicaSet=rs&authSource=admin"; +const iterations = 100; + +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 sleep = util.promisify(timers.setTimeout); + +const run = func; + +const MB = (2 ** 10) ** 2; + +async function main() { + const startingMemoryUsed = process.memoryUsage().heapUsed / MB; + process.send({ startingMemoryUsed }); + + for (let iteration = 0; iteration < iterations; iteration++) { + await run({ MongoClient, uri, iteration }); + global.gc(); + } + + global.gc(); + // Sleep b/c maybe gc will run + await sleep(100); + global.gc(); + + const endingMemoryUsed = process.memoryUsage().heapUsed / MB; + process.send({ endingMemoryUsed }); + v8.writeHeapSnapshot(`${name}.heapsnapshot.json`); +} + +main() + .then(() => { + process.exit(0); + }) + .catch(() => { + process.exit(1); + }); diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 839900e8ef..01301f7ef5 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -1,7 +1,8 @@ +import { expect } from 'chai'; import { TestConfiguration } from '../../tools/runner/config'; import { runScriptAndReturnResourceInfo } from './resource_tracking_script_builder'; -describe.only('client.close() Integration', () => { +describe.skip('client.close() Integration', () => { let config: TestConfiguration; beforeEach(function () { config = this.configuration; @@ -21,34 +22,31 @@ describe.only('client.close() Integration', () => { }); describe('MongoClientAuthProviders', () => { - describe('when MongoClientAuthProviders is instantiated', () => { - it('the token cache is cleaned up by client.close', () => { + describe('when MongoClientAuthProviders is instantiated and token file read hangs', () => { + it('the file read is interrupted 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()', () => { - + it('server selection timers are cleaned up by client.close()', async () => { + await runScriptAndReturnResourceInfo( + 'topology-clean-up', + config, + async function run({ MongoClient, uri }) { + const client = new MongoClient(uri); + await client.connect(); + await 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', () => { + describe('after SRVPoller is created', () => { it('timers are cleaned up by client.close()', () => { }); @@ -56,9 +54,9 @@ describe.only('client.close() Integration', () => { }); describe('ClientSession', () => { - describe('after a clientSession is created', () => { + describe('after a clientSession is created and used', () => { it('the server-side ServerSession and transaction are cleaned up by client.close()', () => { - + // must send a command to the server }); }); }); @@ -67,9 +65,6 @@ describe.only('client.close() Integration', () => { 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', () => { @@ -79,6 +74,10 @@ describe.only('client.close() Integration', () => { }); }); + describe('Server', () => { + + }); + describe('ConnectionPool', () => { describe('after new connection pool is created', () => { it('minPoolSize timer is cleaned up by client.close()', () => { @@ -95,22 +94,23 @@ describe.only('client.close() Integration', () => { }); describe('after a heartbeat fails', () => { - it('monitor interval timer is cleaned up by client.close()', () => { + it('the new 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('after entering monitor streaming mode ', () => { + it('the rtt pinger timer is cleaned up by client.close()', () => { + // helloReply has a topologyVersion defined }); }); }); describe('Connection', () => { describe('when connection monitoring is turned on', () => { + // connection monitoring is by default turned on - with the exception of load-balanced mode it('no sockets remain after client.close', () => { }); diff --git a/test/integration/node-specific/resource_tracking_script_builder.ts b/test/integration/node-specific/resource_tracking_script_builder.ts index 1a0376eddb..6a2bda1584 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -117,12 +117,12 @@ export async function runScriptAndReturnResourceInfo( func: ResourceTestFunction ) { - const scriptName = `scripts/${name}.cjs`; + const scriptName = `${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); + const script = fork(scriptName); // Interrupt our awaiting of messages if the process crashed script.once('close', exitCode => { @@ -133,11 +133,19 @@ export async function runScriptAndReturnResourceInfo( const willClose = once(script, 'close'); + const messages = on(script, 'message', { signal: processDiedController.signal }); + const report = await messages.next(); + let { finalReport, originalReport } = report.value[0]; + + // const beforeExit = await messages.next(); + // make sure the process ended const [exitCode] = await willClose; + + // assertions about exit status 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; + // assertions about clean-up + expect(originalReport.length).to.equal(finalReport.length); + // expect(beforeExitEventHappened).to.be.true; } diff --git a/test/tools/fixtures/close_resource_script.in.js b/test/tools/fixtures/close_resource_script.in.js index 84f2ab431a..1a0b0265ff 100644 --- a/test/tools/fixtures/close_resource_script.in.js +++ b/test/tools/fixtures/close_resource_script.in.js @@ -6,7 +6,6 @@ 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'); @@ -18,11 +17,12 @@ const run = func; async function main() { process.on('beforeExit', (code) => { - process.send({beforeExit: true}); + console.log('Process beforeExit event with code: ', code); }); - await run({ MongoClient, uri, iteration }); - const report = process.report.getReport(); - process.send({ report }); + const originalReport = process.report.getReport().libuv; + await run({ MongoClient, uri }); + const finalReport = process.report.getReport().libuv; + process.send({originalReport, finalReport}); } main() diff --git a/topology-clean-up.cjs b/topology-clean-up.cjs new file mode 100644 index 0000000000..7581450ded --- /dev/null +++ b/topology-clean-up.cjs @@ -0,0 +1,38 @@ +'use strict'; + +/* eslint-disable no-undef */ + +const driverPath = "/Users/aditi.khare/Desktop/node-mongodb-native/lib"; +const func = (async function run({ MongoClient, uri }) { + const client = new MongoClient(uri); + await client.connect(); + await client.close(); + }); +const name = "topology-clean-up"; +const uri = "mongodb://bob:pwd123@localhost:31000/integration_tests?replicaSet=rs&authSource=admin"; + +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) => { + console.log('Process beforeExit event with code: ', code); + }); + const originalReport = process.report.getReport().libuv; + await run({ MongoClient, uri }); + const finalReport = process.report.getReport().libuv; + process.send({originalReport, finalReport}); +} + +main() + .then(() => { + process.exit(0); + }) + .catch(() => { + process.exit(1); + }); diff --git a/topology.cjs b/topology.cjs new file mode 100644 index 0000000000..fdf1953f41 --- /dev/null +++ b/topology.cjs @@ -0,0 +1,37 @@ +'use strict'; + +/* eslint-disable no-undef */ + +const driverPath = "/Users/aditi.khare/Desktop/node-mongodb-native/lib"; +const func = (async function run({ MongoClient, uri }) { + const mongoClient = new MongoClient(uri); + await mongoClient.connect(); + }); +const name = "topology"; +const uri = "mongodb://bob:pwd123@localhost:31000/integration_tests?replicaSet=rs&authSource=admin"; +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); + }); From 1531becc0b0c6dea728ec9a3a3e4584b3ba5c160 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Tue, 17 Dec 2024 17:55:25 -0500 Subject: [PATCH 05/43] fix --- topology-clean-up.cjs | 38 -------------------------------------- topology.cjs | 37 ------------------------------------- 2 files changed, 75 deletions(-) delete mode 100644 topology-clean-up.cjs delete mode 100644 topology.cjs diff --git a/topology-clean-up.cjs b/topology-clean-up.cjs deleted file mode 100644 index 7581450ded..0000000000 --- a/topology-clean-up.cjs +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -/* eslint-disable no-undef */ - -const driverPath = "/Users/aditi.khare/Desktop/node-mongodb-native/lib"; -const func = (async function run({ MongoClient, uri }) { - const client = new MongoClient(uri); - await client.connect(); - await client.close(); - }); -const name = "topology-clean-up"; -const uri = "mongodb://bob:pwd123@localhost:31000/integration_tests?replicaSet=rs&authSource=admin"; - -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) => { - console.log('Process beforeExit event with code: ', code); - }); - const originalReport = process.report.getReport().libuv; - await run({ MongoClient, uri }); - const finalReport = process.report.getReport().libuv; - process.send({originalReport, finalReport}); -} - -main() - .then(() => { - process.exit(0); - }) - .catch(() => { - process.exit(1); - }); diff --git a/topology.cjs b/topology.cjs deleted file mode 100644 index fdf1953f41..0000000000 --- a/topology.cjs +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -/* eslint-disable no-undef */ - -const driverPath = "/Users/aditi.khare/Desktop/node-mongodb-native/lib"; -const func = (async function run({ MongoClient, uri }) { - const mongoClient = new MongoClient(uri); - await mongoClient.connect(); - }); -const name = "topology"; -const uri = "mongodb://bob:pwd123@localhost:31000/integration_tests?replicaSet=rs&authSource=admin"; -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); - }); From 4200f3f2d5040a7183786907c2ea2ed759511c4a Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Wed, 18 Dec 2024 16:55:22 -0500 Subject: [PATCH 06/43] preliminary tests finished --- .../node-specific/client_close.test.ts | 107 ++++++++++++------ .../resource_tracking_script_builder.ts | 52 +++++++-- ...cript.in.js => heap_resource_script.in.js} | 0 ...pt.in.js => process_resource_script.in.js} | 6 +- 4 files changed, 121 insertions(+), 44 deletions(-) rename test/tools/fixtures/{resource_script.in.js => heap_resource_script.in.js} (100%) rename test/tools/fixtures/{close_resource_script.in.js => process_resource_script.in.js} (82%) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 01301f7ef5..9bc2b354ab 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -1,8 +1,8 @@ -import { expect } from 'chai'; +import sinon = require('sinon'); import { TestConfiguration } from '../../tools/runner/config'; -import { runScriptAndReturnResourceInfo } from './resource_tracking_script_builder'; +import { runScriptAndGetProcessInfo } from './resource_tracking_script_builder'; -describe.skip('client.close() Integration', () => { +describe('client.close() Integration', () => { let config: TestConfiguration; beforeEach(function () { config = this.configuration; @@ -12,27 +12,39 @@ describe.skip('client.close() Integration', () => { 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', () => { + it.only('the file read is not interrupted by client.close()', async () => { + await runScriptAndGetProcessInfo( + 'docker-read', + config, + async function run({ MongoClient, uri }) { + const dockerPath = '.dockerenv'; + sinon.stub(fs, 'access').callsFake(async () => await sleep(5000)); + await fs.writeFile('.dockerenv', '', { encoding: 'utf8' }); + const client = new MongoClient(uri);; + await client.close(); + unlink(dockerPath); + }); }); }); describe('when client is connecting and reads a TLS long file', () => { - it('the file read is interrupted by client.close', () => { + it('the file read is interrupted by client.close()', async () => { + }); }); }); describe('MongoClientAuthProviders', () => { describe('when MongoClientAuthProviders is instantiated and token file read hangs', () => { - it('the file read is interrupted by client.close', () => { + it('the file read is interrupted by client.close()', async () => { }); }); }); - describe('Topology', () => { + describe.only('Topology', () => { describe('after a Topology is created through client.connect()', () => { it('server selection timers are cleaned up by client.close()', async () => { - await runScriptAndReturnResourceInfo( - 'topology-clean-up', + await runScriptAndGetProcessInfo( + 'server-selection-timers', config, async function run({ MongoClient, uri }) { const client = new MongoClient(uri); @@ -45,42 +57,58 @@ describe.skip('client.close() Integration', () => { }); describe('SRVPoller', () => { - // SRVPoller is implicitly created after an SRV string's topology transitions to sharded + // TODO: only non-LB mode describe('after SRVPoller is created', () => { - it('timers are cleaned up by client.close()', () => { - + it('timers are cleaned up by client.close()', async () => { + await runScriptAndGetProcessInfo( + 'srv-poller', + config, + async function run({ MongoClient, uri }) { + const client = new MongoClient(uri); + await client.connect(); + await client.close(); + } + ); }); }); }); describe('ClientSession', () => { describe('after a clientSession is created and used', () => { - it('the server-side ServerSession and transaction are cleaned up by client.close()', () => { - // must send a command to the server + it('the server-side ServerSession and transaction are cleaned up by client.close()', async () => { + await runScriptAndGetProcessInfo( + 'client-session', + config, + async function run({ MongoClient, uri }) { + const client = new MongoClient(uri); + await client.connect(); + const session = client.startSession(); + session.startTransaction(); + client.db('db').collection('coll').insertOne({ a: 1 }); + await session.endSession(); + await 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 sockets remain after client.close()', async () => { }); describe('when the TLS file read hangs', () => { - it('the file read is interrupted by client.close', () => { + it('the file read is interrupted by client.close()', async () => { }); }); }); }); - describe('Server', () => { - - }); - describe('ConnectionPool', () => { describe('after new connection pool is created', () => { - it('minPoolSize timer is cleaned up by client.close()', () => { + it('minPoolSize timer is cleaned up by client.close()', async () => { }); }); @@ -88,13 +116,13 @@ describe.skip('client.close() Integration', () => { describe('MonitorInterval', () => { describe('after a new monitor is made', () => { - it('monitor interval timer is cleaned up by client.close()', () => { + it('monitor interval timer is cleaned up by client.close()', async () => { }); }); describe('after a heartbeat fails', () => { - it('the new monitor interval timer is cleaned up by client.close()', () => { + it('the new monitor interval timer is cleaned up by client.close()', async () => { }); }); @@ -102,7 +130,7 @@ describe.skip('client.close() Integration', () => { describe('RTTPinger', () => { describe('after entering monitor streaming mode ', () => { - it('the rtt pinger timer is cleaned up by client.close()', () => { + it('the rtt pinger timer is cleaned up by client.close()', async () => { // helloReply has a topologyVersion defined }); }); @@ -111,37 +139,46 @@ describe.skip('client.close() Integration', () => { describe('Connection', () => { describe('when connection monitoring is turned on', () => { // connection monitoring is by default turned on - with the exception of load-balanced mode - it('no sockets remain after client.close', () => { - - }); - it('no server-side connection threads remain after client.close', () => { + it('no sockets remain after client.close()', async () => { + // TODO: skip for LB mode + await runScriptAndGetProcessInfo( + 'connection-monitoring', + config, + async function run({ MongoClient, uri }) { + const client = new MongoClient(uri); + await client.connect(); + await client.close(); + } + ); + }); + it('no server-side connection threads remain after client.close()', async () => { }); }); describe('when rtt monitoring is turned on', () => { - it('no sockets remain after client.close', () => { + it('no sockets remain after client.close()', async () => { }); - it('no server-side connection threads remain after client.close', () => { + it('no server-side connection threads remain after client.close()', async () => { }); }); describe('after a connection is checked out', () => { - it('no sockets remain after client.close', () => { + it('no sockets remain after client.close()', async () => { }); - it('no server-side connection threads remain after client.close', () => { + it('no server-side connection threads remain after client.close()', async () => { }); }); describe('after a minPoolSize has been set on the ConnectionPool', () => { - it('no sockets remain after client.close', () => { + it('no sockets remain after client.close()', async () => { }); - it('no server-side connection threads remain after client.close', () => { + it('no server-side connection threads remain after client.close()', async () => { }); }); @@ -149,7 +186,7 @@ describe.skip('client.close() Integration', () => { describe('Cursor', () => { describe('after cursors are created', () => { - it('all active server-side cursors are closed by client.close()', () => { + it('all active server-side cursors are closed by client.close()', async () => { }); }); diff --git a/test/integration/node-specific/resource_tracking_script_builder.ts b/test/integration/node-specific/resource_tracking_script_builder.ts index 6a2bda1584..2437edaac1 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -8,13 +8,21 @@ import { parseSnapshot } from 'v8-heapsnapshot'; import { type MongoClient } from '../../mongodb'; import { type TestConfiguration } from '../../tools/runner/config'; +import { sleep } from '../../tools/utils'; -export type ResourceTestFunction = (options: { +export type ResourceTestFunction = HeapResourceTestFunction | ProcessResourceTestFunction; + +export type HeapResourceTestFunction = (options: { MongoClient: typeof MongoClient; uri: string; iteration: number; }) => Promise; +export type ProcessResourceTestFunction = (options: { + MongoClient: typeof MongoClient; + uri: string; +}) => Promise; + 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')); @@ -40,7 +48,8 @@ export async function testScriptFactory( } /** - * A helper for running arbitrary MongoDB Driver scripting code in a resource information collecting script + * A helper for running arbitrary MongoDB Driver scripting code in a resource information collecting script. + * This script uses heap data to collect resource information. * * **The provided function is run in an isolated Node.js process** * @@ -64,7 +73,7 @@ export async function testScriptFactory( export async function runScriptAndReturnHeapInfo( name: string, config: TestConfiguration, - func: ResourceTestFunction, + func: HeapResourceTestFunction, { iterations = 100 } = {} ) { const scriptName = `${name}.cjs`; @@ -111,10 +120,31 @@ export async function runScriptAndReturnHeapInfo( }; } -export async function runScriptAndReturnResourceInfo( + +/** + * A helper for running arbitrary MongoDB Driver scripting code in a resource information collecting script. + * This script uses info from node:process to collect resource information. + * + * **The provided function is run in an isolated Node.js process** + * + * A user of this function will likely need to familiarize themselves with the surrounding scripting, but briefly: + * - Every MongoClient you construct should have an asyncResource attached to it like so: + * ```js + * mongoClient.asyncResource = new this.async_hooks.AsyncResource('MongoClient'); + * ``` + * - You can perform any number of operations and connects/closes of MongoClients + * - This function performs assertions that at the end of the provided function, the js event loop has been exhausted + * + * @param name - the name of the script, this defines the name of the file, it will be cleaned up if the function returns successfully + * @param config - `this.configuration` from your mocha config + * @param func - your javascript function, you can write it inline! this will stringify the function, use the references on the `this` context to get typechecking + * @param options - settings for the script + * @throws Error - if the process exits with failure or if the process' resources are not cleaned up by the provided function. + */ +export async function runScriptAndGetProcessInfo( name: string, config: TestConfiguration, - func: ResourceTestFunction + func: ProcessResourceTestFunction ) { const scriptName = `${name}.cjs`; @@ -137,15 +167,23 @@ export async function runScriptAndReturnResourceInfo( const report = await messages.next(); let { finalReport, originalReport } = report.value[0]; - // const beforeExit = await messages.next(); + const nullishSleepFn = async function () { + await sleep(5000); + return null; + } + + // in the event the beforeExit event doesn't fire, set the event value to null after waiting 5 seconds + const beforeExitEvent = await Promise.race([messages.next(), nullishSleepFn()]); // make sure the process ended const [exitCode] = await willClose; + await unlink(scriptName); + // assertions about exit status expect(exitCode, 'process should have exited with zero').to.equal(0); // assertions about clean-up expect(originalReport.length).to.equal(finalReport.length); - // expect(beforeExitEventHappened).to.be.true; + expect(beforeExitEvent).to.not.be.null; } diff --git a/test/tools/fixtures/resource_script.in.js b/test/tools/fixtures/heap_resource_script.in.js similarity index 100% rename from test/tools/fixtures/resource_script.in.js rename to test/tools/fixtures/heap_resource_script.in.js diff --git a/test/tools/fixtures/close_resource_script.in.js b/test/tools/fixtures/process_resource_script.in.js similarity index 82% rename from test/tools/fixtures/close_resource_script.in.js rename to test/tools/fixtures/process_resource_script.in.js index 1a0b0265ff..10805eeb62 100644 --- a/test/tools/fixtures/close_resource_script.in.js +++ b/test/tools/fixtures/process_resource_script.in.js @@ -12,15 +12,17 @@ const process = require('node:process'); const v8 = require('node:v8'); const util = require('node:util'); const timers = require('node:timers'); +const fs = require('node:fs'); +const sinon = require('sinon'); const run = func; async function main() { process.on('beforeExit', (code) => { - console.log('Process beforeExit event with code: ', code); + process.send({ beforeExitCode: code }); }); const originalReport = process.report.getReport().libuv; - await run({ MongoClient, uri }); + await run({ MongoClient, uri, fs, sinon }); const finalReport = process.report.getReport().libuv; process.send({originalReport, finalReport}); } From 321ef713e2b05291cfd376a62e2c5815e07865ef Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Wed, 18 Dec 2024 16:56:52 -0500 Subject: [PATCH 07/43] remove misc file --- no_resource_leak_connect_close.cjs | 55 ------------------------------ 1 file changed, 55 deletions(-) delete mode 100644 no_resource_leak_connect_close.cjs diff --git a/no_resource_leak_connect_close.cjs b/no_resource_leak_connect_close.cjs deleted file mode 100644 index f7ba3bd6dd..0000000000 --- a/no_resource_leak_connect_close.cjs +++ /dev/null @@ -1,55 +0,0 @@ -'use strict'; - -/* eslint-disable no-undef */ - -const driverPath = "/Users/aditi.khare/Desktop/node-mongodb-native/lib"; -const func = (async function run({ MongoClient, uri }) { - const mongoClient = new MongoClient(uri, { minPoolSize: 100 }); - await mongoClient.connect(); - // Any operations will reproduce the issue found in v5.0.0/v4.13.0 - // it would seem the MessageStream has to be used? - await mongoClient.db().command({ ping: 1 }); - await mongoClient.close(); - }); -const name = "no_resource_leak_connect_close"; -const uri = "mongodb://bob:pwd123@localhost:31000/integration_tests?replicaSet=rs&authSource=admin"; -const iterations = 100; - -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 sleep = util.promisify(timers.setTimeout); - -const run = func; - -const MB = (2 ** 10) ** 2; - -async function main() { - const startingMemoryUsed = process.memoryUsage().heapUsed / MB; - process.send({ startingMemoryUsed }); - - for (let iteration = 0; iteration < iterations; iteration++) { - await run({ MongoClient, uri, iteration }); - global.gc(); - } - - global.gc(); - // Sleep b/c maybe gc will run - await sleep(100); - global.gc(); - - const endingMemoryUsed = process.memoryUsage().heapUsed / MB; - process.send({ endingMemoryUsed }); - v8.writeHeapSnapshot(`${name}.heapsnapshot.json`); -} - -main() - .then(() => { - process.exit(0); - }) - .catch(() => { - process.exit(1); - }); From 1503084246e9e24bbe4707b4e1091dc078b44263 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Thu, 19 Dec 2024 11:39:15 -0500 Subject: [PATCH 08/43] temp for screen-share --- test/tools/fixtures/process_resource_script.in.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/tools/fixtures/process_resource_script.in.js b/test/tools/fixtures/process_resource_script.in.js index 10805eeb62..e4a1873cf8 100644 --- a/test/tools/fixtures/process_resource_script.in.js +++ b/test/tools/fixtures/process_resource_script.in.js @@ -22,9 +22,11 @@ async function main() { process.send({ beforeExitCode: code }); }); const originalReport = process.report.getReport().libuv; - await run({ MongoClient, uri, fs, sinon }); + const mongoClient = await run({ MongoClient, uri, fs, sinon }); + const intermediateReport = process.report.getReport().libuv; + await mongoClient.close(); const finalReport = process.report.getReport().libuv; - process.send({originalReport, finalReport}); + process.send({originalReport, intermediateReport, finalReport}); } main() From a128974a2438edb9d5e4cfaed09c12e58eef29e5 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Thu, 19 Dec 2024 11:43:39 -0500 Subject: [PATCH 09/43] lint --- .../node-specific/client_close.test.ts | 203 ++++++++---------- .../resource_tracking_script_builder.ts | 33 ++- .../fixtures/process_resource_script.in.js | 4 +- 3 files changed, 117 insertions(+), 123 deletions(-) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 9bc2b354ab..880b67d446 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -1,194 +1,173 @@ import sinon = require('sinon'); -import { TestConfiguration } from '../../tools/runner/config'; +import { type TestConfiguration } from '../../tools/runner/config'; import { runScriptAndGetProcessInfo } from './resource_tracking_script_builder'; describe('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.only('the file read is not interrupted by client.close()', async () => { - await runScriptAndGetProcessInfo( - 'docker-read', - config, - async function run({ MongoClient, uri }) { - const dockerPath = '.dockerenv'; - sinon.stub(fs, 'access').callsFake(async () => await sleep(5000)); - await fs.writeFile('.dockerenv', '', { encoding: 'utf8' }); - const client = new MongoClient(uri);; - await client.close(); - unlink(dockerPath); - }); - }); + // our docker env detection uses fs.access which will not be aborted until after it runs + // fs.access does not support abort signals + it.only('the file read is not interrupted by client.close()', async () => { + await runScriptAndGetProcessInfo( + 'docker-read', + config, + async function run({ MongoClient, uri }) { + const dockerPath = '.dockerenv'; + sinon.stub(fs, 'access').callsFake(async () => await sleep(5000)); + await fs.writeFile('.dockerenv', '', { encoding: 'utf8' }); + const client = new MongoClient(uri); + await client.close(); + unlink(dockerPath); + } + ); + }); }); - describe('when client is connecting and reads a TLS long file', () => { - it('the file read is interrupted by client.close()', async () => { - }); + describe('when client is connecting and reads a TLS long file', () => { + it('the file read is interrupted by client.close()', async () => {}); }); }); describe('MongoClientAuthProviders', () => { describe('when MongoClientAuthProviders is instantiated and token file read hangs', () => { - it('the file read is interrupted by client.close()', async () => { - }); + it('the file read is interrupted by client.close()', async () => {}); }); }); - describe.only('Topology', () => { + describe('Topology', () => { describe('after a Topology is created through client.connect()', () => { - it('server selection timers are cleaned up by client.close()', async () => { - await runScriptAndGetProcessInfo( - 'server-selection-timers', - config, - async function run({ MongoClient, uri }) { - const client = new MongoClient(uri); - await client.connect(); - await client.close(); - } - ); - }); + it('server selection timers are cleaned up by client.close()', async () => { + await runScriptAndGetProcessInfo( + 'server-selection-timers', + config, + async function run({ MongoClient, uri }) { + const client = new MongoClient(uri); + await client.connect(); + await client.close(); + } + ); + }); }); }); describe('SRVPoller', () => { // TODO: only non-LB mode describe('after SRVPoller is created', () => { - it('timers are cleaned up by client.close()', async () => { - await runScriptAndGetProcessInfo( - 'srv-poller', - config, - async function run({ MongoClient, uri }) { - const client = new MongoClient(uri); - await client.connect(); - await client.close(); - } - ); - }); + it('timers are cleaned up by client.close()', async () => { + await runScriptAndGetProcessInfo( + 'srv-poller', + config, + async function run({ MongoClient, uri }) { + const client = new MongoClient(uri); + await client.connect(); + await client.close(); + } + ); + }); }); }); describe('ClientSession', () => { describe('after a clientSession is created and used', () => { - it('the server-side ServerSession and transaction are cleaned up by client.close()', async () => { - await runScriptAndGetProcessInfo( - 'client-session', - config, - async function run({ MongoClient, uri }) { - const client = new MongoClient(uri); - await client.connect(); - const session = client.startSession(); - session.startTransaction(); - client.db('db').collection('coll').insertOne({ a: 1 }); - await session.endSession(); - await client.close(); - } - ); - }); + it('the server-side ServerSession and transaction are cleaned up by client.close()', async () => { + await runScriptAndGetProcessInfo( + 'client-session', + config, + async function run({ MongoClient, uri }) { + const client = new MongoClient(uri); + await client.connect(); + const session = client.startSession(); + session.startTransaction(); + client.db('db').collection('coll').insertOne({ a: 1 }); + await session.endSession(); + await client.close(); + } + ); + }); }); }); describe('StateMachine', () => { describe('when FLE is enabled and the client has made a KMS request', () => { - it('no sockets remain after client.close()', async () => { + it('no sockets remain after client.close()', async () => {}); - }); - describe('when the TLS file read hangs', () => { - it('the file read is interrupted by client.close()', async () => { - - }); - }); + describe('when the TLS file read hangs', () => { + it('the file read is interrupted by client.close()', async () => {}); + }); }); }); describe('ConnectionPool', () => { describe('after new connection pool is created', () => { - it('minPoolSize timer is cleaned up by client.close()', async () => { - - }); + it('minPoolSize timer is cleaned up by client.close()', async () => {}); }); }); describe('MonitorInterval', () => { describe('after a new monitor is made', () => { - it('monitor interval timer is cleaned up by client.close()', async () => { - - }); + it('monitor interval timer is cleaned up by client.close()', async () => {}); }); describe('after a heartbeat fails', () => { - it('the new monitor interval timer is cleaned up by client.close()', async () => { - - }); + it('the new monitor interval timer is cleaned up by client.close()', async () => {}); }); }); describe('RTTPinger', () => { describe('after entering monitor streaming mode ', () => { - it('the rtt pinger timer is cleaned up by client.close()', async () => { - // helloReply has a topologyVersion defined - }); + it('the rtt pinger timer is cleaned up by client.close()', async () => { + // helloReply has a topologyVersion defined + }); }); }); describe('Connection', () => { describe('when connection monitoring is turned on', () => { - // connection monitoring is by default turned on - with the exception of load-balanced mode - it('no sockets remain after client.close()', async () => { - // TODO: skip for LB mode - await runScriptAndGetProcessInfo( - 'connection-monitoring', - config, - async function run({ MongoClient, uri }) { - const client = new MongoClient(uri); - await client.connect(); - await client.close(); - } - ); - }); - it('no server-side connection threads remain after client.close()', async () => { - - }); + // connection monitoring is by default turned on - with the exception of load-balanced mode + it('no sockets remain after client.close()', async () => { + // TODO: skip for LB mode + await runScriptAndGetProcessInfo( + 'connection-monitoring', + config, + async function run({ MongoClient, uri }) { + const client = new MongoClient(uri); + await client.connect(); + await client.close(); + } + ); + }); + + it('no server-side connection threads remain after client.close()', async () => {}); }); describe('when rtt monitoring is turned on', () => { - it('no sockets remain after client.close()', async () => { - - }); - it('no server-side connection threads remain after client.close()', async () => { + it('no sockets remain after client.close()', async () => {}); - }); + it('no server-side connection threads remain after client.close()', async () => {}); }); describe('after a connection is checked out', () => { - it('no sockets remain after client.close()', async () => { - - }); - it('no server-side connection threads remain after client.close()', async () => { + it('no sockets remain after client.close()', async () => {}); - }); + it('no server-side connection threads remain after client.close()', async () => {}); }); describe('after a minPoolSize has been set on the ConnectionPool', () => { - it('no sockets remain after client.close()', async () => { + it('no sockets remain after client.close()', async () => {}); - }); - it('no server-side connection threads remain after client.close()', async () => { - - }); + it('no server-side connection threads remain after client.close()', async () => {}); }); }); describe('Cursor', () => { describe('after cursors are created', () => { - it('all active server-side cursors are closed by client.close()', async () => { - - }); + it('all active server-side cursors are closed by client.close()', async () => {}); }); }); }); diff --git a/test/integration/node-specific/resource_tracking_script_builder.ts b/test/integration/node-specific/resource_tracking_script_builder.ts index 2437edaac1..9bf9fd6464 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -23,8 +23,14 @@ export type ProcessResourceTestFunction = (options: { uri: string; }) => Promise; -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 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( @@ -32,7 +38,7 @@ export async function testScriptFactory( uri: string, resourceScriptPath: string, func: ResourceTestFunction, - iterations?: number, + iterations?: number ) { let resourceScript = await readFile(resourceScriptPath, { encoding: 'utf8' }); @@ -79,7 +85,13 @@ export async function runScriptAndReturnHeapInfo( const scriptName = `${name}.cjs`; const heapsnapshotFile = `${name}.heapsnapshot.json`; - const scriptContent = await testScriptFactory(name, config.url(), HEAP_RESOURCE_SCRIPT_PATH, func, iterations); + const scriptContent = await testScriptFactory( + name, + config.url(), + HEAP_RESOURCE_SCRIPT_PATH, + func, + iterations + ); await writeFile(scriptName, scriptContent, { encoding: 'utf8' }); const processDiedController = new AbortController(); @@ -120,7 +132,6 @@ export async function runScriptAndReturnHeapInfo( }; } - /** * A helper for running arbitrary MongoDB Driver scripting code in a resource information collecting script. * This script uses info from node:process to collect resource information. @@ -146,9 +157,13 @@ export async function runScriptAndGetProcessInfo( config: TestConfiguration, func: ProcessResourceTestFunction ) { - const scriptName = `${name}.cjs`; - const scriptContent = await testScriptFactory(name, config.url(), REPORT_RESOURCE_SCRIPT_PATH, func); + const scriptContent = await testScriptFactory( + name, + config.url(), + REPORT_RESOURCE_SCRIPT_PATH, + func + ); await writeFile(scriptName, scriptContent, { encoding: 'utf8' }); const processDiedController = new AbortController(); @@ -165,12 +180,12 @@ export async function runScriptAndGetProcessInfo( const messages = on(script, 'message', { signal: processDiedController.signal }); const report = await messages.next(); - let { finalReport, originalReport } = report.value[0]; + const { finalReport, originalReport } = report.value[0]; const nullishSleepFn = async function () { await sleep(5000); return null; - } + }; // in the event the beforeExit event doesn't fire, set the event value to null after waiting 5 seconds const beforeExitEvent = await Promise.race([messages.next(), nullishSleepFn()]); diff --git a/test/tools/fixtures/process_resource_script.in.js b/test/tools/fixtures/process_resource_script.in.js index e4a1873cf8..58f30ece00 100644 --- a/test/tools/fixtures/process_resource_script.in.js +++ b/test/tools/fixtures/process_resource_script.in.js @@ -18,7 +18,7 @@ const sinon = require('sinon'); const run = func; async function main() { - process.on('beforeExit', (code) => { + process.on('beforeExit', code => { process.send({ beforeExitCode: code }); }); const originalReport = process.report.getReport().libuv; @@ -26,7 +26,7 @@ async function main() { const intermediateReport = process.report.getReport().libuv; await mongoClient.close(); const finalReport = process.report.getReport().libuv; - process.send({originalReport, intermediateReport, finalReport}); + process.send({ originalReport, intermediateReport, finalReport }); } main() From 4e55deef80558af0ef389c314f898a984e6497b5 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Thu, 19 Dec 2024 17:34:19 -0500 Subject: [PATCH 10/43] boilerplate fully working --- .../node-specific/client_close.test.ts | 27 ++++++++------- .../resource_tracking_script_builder.ts | 33 ++++++++----------- .../fixtures/process_resource_script.in.js | 31 ++++++++++++----- 3 files changed, 51 insertions(+), 40 deletions(-) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 880b67d446..bcf1ee7ec8 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -13,7 +13,7 @@ describe('client.close() Integration', () => { 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.only('the file read is not interrupted by client.close()', async () => { + it('the file read is not interrupted by client.close()', async () => { await runScriptAndGetProcessInfo( 'docker-read', config, @@ -30,7 +30,13 @@ describe('client.close() Integration', () => { }); describe('when client is connecting and reads a TLS long file', () => { - it('the file read is interrupted by client.close()', async () => {}); + it.only('the file read is interrupted by client.close()', async () => { + await runScriptAndGetProcessInfo( + 'docker-read', + config, + async function run({ MongoClient, uri }) {} + ); + }); }); }); @@ -75,20 +81,19 @@ describe('client.close() Integration', () => { describe('ClientSession', () => { describe('after a clientSession is created and used', () => { - it('the server-side ServerSession and transaction are cleaned up by client.close()', async () => { - await runScriptAndGetProcessInfo( - 'client-session', - config, - async function run({ MongoClient, uri }) { - const client = new MongoClient(uri); + it('the server-side ServerSession and transaction are cleaned up by client.close()', async function () { + const client = this.configuration.newClient(); await client.connect(); const session = client.startSession(); session.startTransaction(); - client.db('db').collection('coll').insertOne({ a: 1 }); + await client.db('db').collection('coll').insertOne({ a: 1 }, { session }); + + // assert server-side session exists + await session.endSession(); await client.close(); - } - ); + + // assert command was sent to server to end server side session }); }); }); diff --git a/test/integration/node-specific/resource_tracking_script_builder.ts b/test/integration/node-specific/resource_tracking_script_builder.ts index 9bf9fd6464..e2e3b8dc09 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -1,14 +1,14 @@ -import { fork } from 'node:child_process'; +import { fork, spawn } from 'node:child_process'; import { on, once } from 'node:events'; import { readFile, unlink, writeFile } from 'node:fs/promises'; -import * as path from 'node:path'; +import * as fs from 'node:fs'; import { expect } from 'chai'; import { parseSnapshot } from 'v8-heapsnapshot'; import { type MongoClient } from '../../mongodb'; import { type TestConfiguration } from '../../tools/runner/config'; -import { sleep } from '../../tools/utils'; +import path = require('node:path'); export type ResourceTestFunction = HeapResourceTestFunction | ProcessResourceTestFunction; @@ -29,7 +29,7 @@ const HEAP_RESOURCE_SCRIPT_PATH = path.resolve( ); const REPORT_RESOURCE_SCRIPT_PATH = path.resolve( __dirname, - '../../tools/fixtures/close_resource_script.in.js' + '../../tools/fixtures/process_resource_script.in.js' ); const DRIVER_SRC_PATH = JSON.stringify(path.resolve(__dirname, '../../../lib')); @@ -165,9 +165,10 @@ export async function runScriptAndGetProcessInfo( func ); await writeFile(scriptName, scriptContent, { encoding: 'utf8' }); + const logFile = 'logs.txt'; const processDiedController = new AbortController(); - const script = fork(scriptName); + const script = spawn(process.argv[0], [scriptName], { stdio: ['ignore', 'ignore', 'ignore'] }); // Interrupt our awaiting of messages if the process crashed script.once('close', exitCode => { @@ -178,27 +179,19 @@ export async function runScriptAndGetProcessInfo( const willClose = once(script, 'close'); - const messages = on(script, 'message', { signal: processDiedController.signal }); - const report = await messages.next(); - const { finalReport, originalReport } = report.value[0]; - - const nullishSleepFn = async function () { - await sleep(5000); - return null; - }; - - // in the event the beforeExit event doesn't fire, set the event value to null after waiting 5 seconds - const beforeExitEvent = await Promise.race([messages.next(), nullishSleepFn()]); - // make sure the process ended const [exitCode] = await willClose; + const formattedLogRead = '{' + fs.readFileSync(logFile, 'utf-8').slice(0, -3) + '}'; + const messages = JSON.parse(formattedLogRead); + await unlink(scriptName); + await unlink('logs.txt'); // assertions about exit status expect(exitCode, 'process should have exited with zero').to.equal(0); - // assertions about clean-up - expect(originalReport.length).to.equal(finalReport.length); - expect(beforeExitEvent).to.not.be.null; + // assertions about resource status + expect(messages.beforeExitHappened).to.be.true; + expect(messages.newResources).to.be.empty; } diff --git a/test/tools/fixtures/process_resource_script.in.js b/test/tools/fixtures/process_resource_script.in.js index 58f30ece00..1ecfeb963b 100644 --- a/test/tools/fixtures/process_resource_script.in.js +++ b/test/tools/fixtures/process_resource_script.in.js @@ -14,25 +14,38 @@ const util = require('node:util'); const timers = require('node:timers'); const fs = require('node:fs'); const sinon = require('sinon'); +let originalReport; +const logFile = 'logs.txt'; const run = func; +// Returns an array containing new the resources th +function getNewResourceArray() { + let currReport = process.report.getReport().libuv; + const originalReportAddresses = originalReport.map(resource => resource.address); + currReport = currReport.filter(resource =>!originalReportAddresses.includes(resource.address)); + return currReport; +} + +function log(message) { + // remove outer parentheses for easier parsing + const messageToLog = JSON.stringify(message).slice(1, -1) + ', \n' + fs.writeFileSync(logFile, messageToLog, { flag: 'a' }); +} + async function main() { + originalReport = process.report.getReport().libuv; + console.log('please'); process.on('beforeExit', code => { - process.send({ beforeExitCode: code }); + log({ beforeExitHappened: true }); }); - const originalReport = process.report.getReport().libuv; - const mongoClient = await run({ MongoClient, uri, fs, sinon }); - const intermediateReport = process.report.getReport().libuv; - await mongoClient.close(); - const finalReport = process.report.getReport().libuv; - process.send({ originalReport, intermediateReport, finalReport }); + log({newResources: getNewResourceArray()}); } main() .then(() => { - process.exit(0); + log({ exitCode: 0 }); }) .catch(() => { - process.exit(1); + log({ exitCode: 1 }); }); From f56008864d27739e1d69ee6e2997bcde1b26bf6d Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Fri, 20 Dec 2024 14:35:30 -0500 Subject: [PATCH 11/43] TLS test case running --- logs.txt | 114 ++++++++++++++++++ src/mongo_client.ts | 1 + .../node-specific/client_close.test.ts | 46 ++++--- .../resource_tracking_script_builder.ts | 8 +- .../fixtures/process_resource_script.in.js | 24 ++-- tls-file-read.cjs | 67 ++++++++++ 6 files changed, 231 insertions(+), 29 deletions(-) create mode 100644 logs.txt create mode 100644 tls-file-read.cjs diff --git a/logs.txt b/logs.txt new file mode 100644 index 0000000000..d96369f371 --- /dev/null +++ b/logs.txt @@ -0,0 +1,114 @@ +"resources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000129e17e40","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"resources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000108504160","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"resources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000015401a1d0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000013d8098d0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"resources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000011873bfe0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000158167f00","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000150e475b0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000108205870","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000015c013ee0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000137a083b0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000108915c80","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000011a2051c0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000012470fb80","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000146744fd0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x00000001546117f0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000141e3f280","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000010a608670","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000105d09a30","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000013160fbb0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000138221de0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000013be715c0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000144741480","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x00000001456769d0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000147b08430","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000011d70ad80","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000015b841df0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000128009560","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"newResources":[], +"exitCode":0, +"beforeExitHappened":true, +"newResources":[], +"exitCode":0, +"beforeExitHappened":true, +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000013084b1f0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"exitCode":99, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000105e0f410","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"exitCode":99, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000013cf08270","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"exitCode":99, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000154f0d460","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000012df1f0e0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x00000001207090f0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x00000001063076c0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"exitCode":99, +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000012f60cb30","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, +"exitCode":99, diff --git a/src/mongo_client.ts b/src/mongo_client.ts index dea069cc94..31fb13140e 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -555,6 +555,7 @@ export class MongoClient extends TypedEventEmitter implements options.crl ??= await fs.readFile(options.tlsCRLFile); } if (typeof options.tlsCertificateKeyFile === 'string') { + console.log('here'); if (!options.key || !options.cert) { const contents = await fs.readFile(options.tlsCertificateKeyFile); options.key ??= contents; diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index bcf1ee7ec8..f56f76550b 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -1,8 +1,9 @@ -import sinon = require('sinon'); +const process = require('node:process'); +import { expect } from 'chai'; import { type TestConfiguration } from '../../tools/runner/config'; import { runScriptAndGetProcessInfo } from './resource_tracking_script_builder'; -describe('client.close() Integration', () => { +describe.skip('client.close() Integration', () => { let config: TestConfiguration; beforeEach(function () { @@ -18,23 +19,33 @@ describe('client.close() Integration', () => { 'docker-read', config, async function run({ MongoClient, uri }) { - const dockerPath = '.dockerenv'; + /* const dockerPath = '.dockerenv'; sinon.stub(fs, 'access').callsFake(async () => await sleep(5000)); await fs.writeFile('.dockerenv', '', { encoding: 'utf8' }); const client = new MongoClient(uri); await client.close(); - unlink(dockerPath); + unlink(dockerPath); */ } ); }); }); - describe('when client is connecting and reads a TLS long file', () => { - it.only('the file read is interrupted by client.close()', async () => { + describe('when client is connecting and reads an infinite TLS file', () => { + it('the file read is interrupted by client.close()', async function () { await runScriptAndGetProcessInfo( - 'docker-read', + 'tls-file-read', config, - async function run({ MongoClient, uri }) {} + async function run({ MongoClient, uri }) { + const devZeroFilePath = '/dev/zero'; + const client = new MongoClient(uri, { tlsCertificateKeyFile: devZeroFilePath }); + client.connect(); + log({ ActiveResources: process.getActiveResourcesInfo() }); + chai.expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); + await client.close(); + setTimeout(() => + chai.expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'), + 1000); + } ); }); }); @@ -82,18 +93,19 @@ describe('client.close() Integration', () => { describe('ClientSession', () => { describe('after a clientSession is created and used', () => { it('the server-side ServerSession and transaction are cleaned up by client.close()', async function () { - const client = this.configuration.newClient(); - await client.connect(); - const session = client.startSession(); - session.startTransaction(); - await client.db('db').collection('coll').insertOne({ a: 1 }, { session }); + const client = this.configuration.newClient(); + await client.connect(); + const session = client.startSession(); + session.startTransaction(); + await client.db('db').collection('coll').insertOne({ a: 1 }, { session }); - // assert server-side session exists + // assert server-side session exists + expect(session.serverSession).to.exist; - await session.endSession(); - await client.close(); + await session.endSession(); + await client.close(); - // assert command was sent to server to end server side session + // assert command was sent to server to end server side session }); }); }); diff --git a/test/integration/node-specific/resource_tracking_script_builder.ts b/test/integration/node-specific/resource_tracking_script_builder.ts index e2e3b8dc09..f09d7a41c9 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -1,14 +1,14 @@ import { fork, spawn } from 'node:child_process'; import { on, once } from 'node:events'; -import { readFile, unlink, writeFile } from 'node:fs/promises'; import * as fs from 'node:fs'; +import { readFile, unlink, writeFile } from 'node:fs/promises'; +import * as path from 'node:path'; import { expect } from 'chai'; import { parseSnapshot } from 'v8-heapsnapshot'; import { type MongoClient } from '../../mongodb'; import { type TestConfiguration } from '../../tools/runner/config'; -import path = require('node:path'); export type ResourceTestFunction = HeapResourceTestFunction | ProcessResourceTestFunction; @@ -185,8 +185,8 @@ export async function runScriptAndGetProcessInfo( const formattedLogRead = '{' + fs.readFileSync(logFile, 'utf-8').slice(0, -3) + '}'; const messages = JSON.parse(formattedLogRead); - await unlink(scriptName); - await unlink('logs.txt'); + //await unlink(scriptName); + //await unlink(logFile); // assertions about exit status expect(exitCode, 'process should have exited with zero').to.equal(0); diff --git a/test/tools/fixtures/process_resource_script.in.js b/test/tools/fixtures/process_resource_script.in.js index 1ecfeb963b..6ddc7337c8 100644 --- a/test/tools/fixtures/process_resource_script.in.js +++ b/test/tools/fixtures/process_resource_script.in.js @@ -1,7 +1,6 @@ 'use strict'; /* eslint-disable no-undef */ - const driverPath = DRIVER_SOURCE_PATH; const func = FUNCTION_STRING; const name = NAME_STRING; @@ -13,33 +12,34 @@ const v8 = require('node:v8'); const util = require('node:util'); const timers = require('node:timers'); const fs = require('node:fs'); -const sinon = require('sinon'); +const chai = require('chai'); + let originalReport; const logFile = 'logs.txt'; const run = func; -// Returns an array containing new the resources th +// Returns an array containing new the resources created after script start function getNewResourceArray() { let currReport = process.report.getReport().libuv; const originalReportAddresses = originalReport.map(resource => resource.address); - currReport = currReport.filter(resource =>!originalReportAddresses.includes(resource.address)); + currReport = currReport.filter(resource => !originalReportAddresses.includes(resource.address)); return currReport; } function log(message) { // remove outer parentheses for easier parsing - const messageToLog = JSON.stringify(message).slice(1, -1) + ', \n' + const messageToLog = JSON.stringify(message).slice(1, -1) + ', \n'; fs.writeFileSync(logFile, messageToLog, { flag: 'a' }); } async function main() { originalReport = process.report.getReport().libuv; - console.log('please'); - process.on('beforeExit', code => { + process.on('beforeExit', () => { log({ beforeExitHappened: true }); }); - log({newResources: getNewResourceArray()}); + run({ MongoClient, uri }); + log({ newResources: getNewResourceArray() }); } main() @@ -49,3 +49,11 @@ main() .catch(() => { log({ exitCode: 1 }); }); + +setTimeout(() => { + // this means something was in the event loop such that it hung for more than 10 seconds + // so we kill the process + log({ exitCode : 99 }); + process.exit(99); + // using `unref` will ensure this setTimeout call is not a resource / does not keep the event loop running +}, 10000).unref(); diff --git a/tls-file-read.cjs b/tls-file-read.cjs new file mode 100644 index 0000000000..ba7f356750 --- /dev/null +++ b/tls-file-read.cjs @@ -0,0 +1,67 @@ +'use strict'; + +/* eslint-disable no-undef */ +const driverPath = "/Users/aditi.khare/Desktop/node-mongodb-native/lib"; +const func = (async function run({ MongoClient, uri }) { + const devZeroFilePath = '/dev/zero'; + const client = new MongoClient(uri, { tlsCertificateKeyFile: devZeroFilePath }); + client.connect(); + log({ ActiveResources: process.getActiveResourcesInfo() }); + chai.expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); + // await client.close(); + setTimeout(() => chai.expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'), 1000); + }); +const name = "tls-file-read"; +const uri = "mongodb://bob:pwd123@localhost:31000/integration_tests?replicaSet=rs&authSource=admin"; + +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 fs = require('node:fs'); +const chai = require('chai'); + +let originalReport; +const logFile = 'logs.txt'; + +const run = func; + +// Returns an array containing new the resources created after script start +function getNewResourceArray() { + let currReport = process.report.getReport().libuv; + const originalReportAddresses = originalReport.map(resource => resource.address); + currReport = currReport.filter(resource => !originalReportAddresses.includes(resource.address)); + return currReport; +} + +function log(message) { + // remove outer parentheses for easier parsing + const messageToLog = JSON.stringify(message).slice(1, -1) + ', \n'; + fs.writeFileSync(logFile, messageToLog, { flag: 'a' }); +} + +async function main() { + originalReport = process.report.getReport().libuv; + process.on('beforeExit', () => { + log({ beforeExitHappened: true }); + }); + run({ MongoClient, uri }); + log({ newResources: getNewResourceArray() }); +} + +main() + .then(() => { + log({ exitCode: 0 }); + }) + .catch(() => { + log({ exitCode: 1 }); + }); + +setTimeout(() => { + // this means something was in the event loop such that it hung for more than 10 seconds + // so we kill the process + log({ exitCode : 99 }); + process.exit(99); + // using `unref` will ensure this setTimeout call is not a resource / does not keep the event loop running +}, 10000).unref(); From ef9cc907656e436ac58ef08e7a6a29d3e2eb687e Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Fri, 20 Dec 2024 14:40:57 -0500 Subject: [PATCH 12/43] clea up lint --- logs.txt | 114 ------------------ .../node-specific/client_close.test.ts | 9 +- .../resource_tracking_script_builder.ts | 5 +- .../fixtures/process_resource_script.in.js | 4 +- tls-file-read.cjs | 67 ---------- 5 files changed, 11 insertions(+), 188 deletions(-) delete mode 100644 logs.txt delete mode 100644 tls-file-read.cjs diff --git a/logs.txt b/logs.txt deleted file mode 100644 index d96369f371..0000000000 --- a/logs.txt +++ /dev/null @@ -1,114 +0,0 @@ -"resources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000129e17e40","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"resources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000108504160","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"resources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000015401a1d0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000013d8098d0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"resources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000011873bfe0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000158167f00","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000150e475b0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000108205870","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000015c013ee0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000137a083b0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000108915c80","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000011a2051c0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000012470fb80","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000146744fd0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x00000001546117f0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000141e3f280","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000010a608670","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000105d09a30","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000013160fbb0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000138221de0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000013be715c0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000144741480","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x00000001456769d0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000147b08430","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000011d70ad80","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000015b841df0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000128009560","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"newResources":[], -"exitCode":0, -"beforeExitHappened":true, -"newResources":[], -"exitCode":0, -"beforeExitHappened":true, -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000013084b1f0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"exitCode":99, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000105e0f410","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"exitCode":99, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000013cf08270","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"exitCode":99, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TTYWrap","TTYWrap","TTYWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000154f0d460","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000012df1f0e0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x00000001207090f0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x00000001063076c0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"exitCode":99, -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000012f60cb30","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, -"exitCode":99, diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index f56f76550b..09151b9e11 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -1,5 +1,5 @@ -const process = require('node:process'); import { expect } from 'chai'; + import { type TestConfiguration } from '../../tools/runner/config'; import { runScriptAndGetProcessInfo } from './resource_tracking_script_builder'; @@ -42,9 +42,10 @@ describe.skip('client.close() Integration', () => { log({ ActiveResources: process.getActiveResourcesInfo() }); chai.expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); await client.close(); - setTimeout(() => - chai.expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'), - 1000); + setTimeout( + () => chai.expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'), + 1000 + ); } ); }); diff --git a/test/integration/node-specific/resource_tracking_script_builder.ts b/test/integration/node-specific/resource_tracking_script_builder.ts index f09d7a41c9..ec9382620a 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -185,8 +185,9 @@ export async function runScriptAndGetProcessInfo( const formattedLogRead = '{' + fs.readFileSync(logFile, 'utf-8').slice(0, -3) + '}'; const messages = JSON.parse(formattedLogRead); - //await unlink(scriptName); - //await unlink(logFile); + // delete temporary files + await unlink(scriptName); + await unlink(logFile); // assertions about exit status expect(exitCode, 'process should have exited with zero').to.equal(0); diff --git a/test/tools/fixtures/process_resource_script.in.js b/test/tools/fixtures/process_resource_script.in.js index 6ddc7337c8..3827edc3db 100644 --- a/test/tools/fixtures/process_resource_script.in.js +++ b/test/tools/fixtures/process_resource_script.in.js @@ -1,6 +1,7 @@ 'use strict'; /* eslint-disable no-undef */ +/* eslint-disable no-unused-vars */ const driverPath = DRIVER_SOURCE_PATH; const func = FUNCTION_STRING; const name = NAME_STRING; @@ -13,6 +14,7 @@ const util = require('node:util'); const timers = require('node:timers'); const fs = require('node:fs'); const chai = require('chai'); +const { setTimeout } = require('timers'); let originalReport; const logFile = 'logs.txt'; @@ -53,7 +55,7 @@ main() setTimeout(() => { // this means something was in the event loop such that it hung for more than 10 seconds // so we kill the process - log({ exitCode : 99 }); + log({ exitCode: 99 }); process.exit(99); // using `unref` will ensure this setTimeout call is not a resource / does not keep the event loop running }, 10000).unref(); diff --git a/tls-file-read.cjs b/tls-file-read.cjs deleted file mode 100644 index ba7f356750..0000000000 --- a/tls-file-read.cjs +++ /dev/null @@ -1,67 +0,0 @@ -'use strict'; - -/* eslint-disable no-undef */ -const driverPath = "/Users/aditi.khare/Desktop/node-mongodb-native/lib"; -const func = (async function run({ MongoClient, uri }) { - const devZeroFilePath = '/dev/zero'; - const client = new MongoClient(uri, { tlsCertificateKeyFile: devZeroFilePath }); - client.connect(); - log({ ActiveResources: process.getActiveResourcesInfo() }); - chai.expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); - // await client.close(); - setTimeout(() => chai.expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'), 1000); - }); -const name = "tls-file-read"; -const uri = "mongodb://bob:pwd123@localhost:31000/integration_tests?replicaSet=rs&authSource=admin"; - -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 fs = require('node:fs'); -const chai = require('chai'); - -let originalReport; -const logFile = 'logs.txt'; - -const run = func; - -// Returns an array containing new the resources created after script start -function getNewResourceArray() { - let currReport = process.report.getReport().libuv; - const originalReportAddresses = originalReport.map(resource => resource.address); - currReport = currReport.filter(resource => !originalReportAddresses.includes(resource.address)); - return currReport; -} - -function log(message) { - // remove outer parentheses for easier parsing - const messageToLog = JSON.stringify(message).slice(1, -1) + ', \n'; - fs.writeFileSync(logFile, messageToLog, { flag: 'a' }); -} - -async function main() { - originalReport = process.report.getReport().libuv; - process.on('beforeExit', () => { - log({ beforeExitHappened: true }); - }); - run({ MongoClient, uri }); - log({ newResources: getNewResourceArray() }); -} - -main() - .then(() => { - log({ exitCode: 0 }); - }) - .catch(() => { - log({ exitCode: 1 }); - }); - -setTimeout(() => { - // this means something was in the event loop such that it hung for more than 10 seconds - // so we kill the process - log({ exitCode : 99 }); - process.exit(99); - // using `unref` will ensure this setTimeout call is not a resource / does not keep the event loop running -}, 10000).unref(); From 9ed1528fb38ad24b40a419b2ee725792ab65c220 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Fri, 20 Dec 2024 14:42:18 -0500 Subject: [PATCH 13/43] clean up --- src/mongo_client.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 31fb13140e..dea069cc94 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -555,7 +555,6 @@ export class MongoClient extends TypedEventEmitter implements options.crl ??= await fs.readFile(options.tlsCRLFile); } if (typeof options.tlsCertificateKeyFile === 'string') { - console.log('here'); if (!options.key || !options.cert) { const contents = await fs.readFile(options.tlsCertificateKeyFile); options.key ??= contents; From 1c8e20f2bc188628ead846c1507d3fd33d00e9ab Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Fri, 20 Dec 2024 16:45:55 -0500 Subject: [PATCH 14/43] most of tree reformatting done --- logs.txt | 3 + .../node-specific/client_close.test.ts | 192 +++++++++++++----- .../resource_tracking_script_builder.ts | 2 + .../fixtures/process_resource_script.in.js | 2 +- tls-file-read.cjs | 69 +++++++ 5 files changed, 217 insertions(+), 51 deletions(-) create mode 100644 logs.txt create mode 100644 tls-file-read.cjs diff --git a/logs.txt b/logs.txt new file mode 100644 index 0000000000..6fd61f4334 --- /dev/null +++ b/logs.txt @@ -0,0 +1,3 @@ +"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], +"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000135e04560","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], +"exitCode":0, diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 09151b9e11..fc5e5ffdd7 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -1,19 +1,40 @@ import { expect } from 'chai'; +import * as fs from 'fs'; import { type TestConfiguration } from '../../tools/runner/config'; import { runScriptAndGetProcessInfo } from './resource_tracking_script_builder'; +import { sleep } from '../../tools/utils'; -describe.skip('client.close() Integration', () => { - let config: TestConfiguration; +describe.skip('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('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 + 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 () { + await runScriptAndGetProcessInfo( + 'tls-file-read', + config, + async function run({ MongoClient, uri, log, chai }) { + const devZeroFilePath = '/dev/zero'; + const client = new MongoClient(uri, { tlsCertificateKeyFile: devZeroFilePath }); + client.connect(); + log({ ActiveResources: process.getActiveResourcesInfo() }); + chai.expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); + await client.close(); + chai.expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'); + } + ); + }); + }); + }); + + describe('Node.js resource: .dockerenv file access', () => { + describe('when client is connecting and reads an infinite .dockerenv file', () => { it('the file read is not interrupted by client.close()', async () => { await runScriptAndGetProcessInfo( 'docker-read', @@ -29,35 +50,129 @@ describe.skip('client.close() Integration', () => { ); }); }); + }); - describe('when client is connecting and reads an infinite TLS file', () => { - it('the file read is interrupted by client.close()', async function () { - await runScriptAndGetProcessInfo( - 'tls-file-read', - config, - async function run({ MongoClient, uri }) { - const devZeroFilePath = '/dev/zero'; - const client = new MongoClient(uri, { tlsCertificateKeyFile: devZeroFilePath }); - client.connect(); - log({ ActiveResources: process.getActiveResourcesInfo() }); - chai.expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); - await client.close(); - setTimeout( - () => chai.expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'), - 1000 - ); - } - ); + describe('MongoClientAuthProviders', () => { + describe('Node.js resource: Token file read', () => { + describe('when MongoClientAuthProviders is instantiated and token file read hangs', () => { + it('the file read is interrupted by client.close()', async () => {}); }); }); }); - describe('MongoClientAuthProviders', () => { - describe('when MongoClientAuthProviders is instantiated and token file read hangs', () => { - it('the file read is interrupted by client.close()', async () => {}); + describe('Topology', () => { + describe('Node.js resource: Server Selection Timer', () => { + + }); + + describe('Server', () => { + describe('Monitor', () => { + describe('MonitorInterval', () => { + describe('Node.js resource: Timer', () => { + + }); + }); + + describe('Connection Monitoring', () => { + // connection monitoring is by default turned on - with the exception of load-balanced mode + describe('Node.js resource: Socket', () => { + it('no sockets remain after client.close()', async () => { + // TODO: skip for LB mode + }); + }); + describe('Server resource: connection thread', () => { + + }); + }); + + describe('RTT Pinger', () => { + describe('Node.js resource: Timer', () => { + + }); + describe('Connection', () => { + describe('Node.js resource: Socket', () => { + + }); + describe('Server resource: connection thread', () => { + + }); + }); + }); + }); + + describe('ConnectionPool', () => { + describe('Node.js resource: minPoolSize timer', () => { + + }); + + describe('Node.js resource: checkOut Timer', () => { // waitQueueTimeoutMS + + }); + + describe('Connection', () => { + describe('Node.js resource: Socket', () => { + + }); + describe('Node.js resource: Socket', () => { + + }); + }); + }); + }); + + describe('SrvPoller', () => { + describe('Node.js resource: Timer', () => { + + }); + }); + }); + + describe('ClientSession (Implicit)', () => { + describe('Server resource: LSID/ServerSession', () => { + }); + + describe('Server resource: Transactions', () => { + }); + }); + + describe('ClientSession (Explicit)', () => { + describe('Server resource: LSID/ServerSession', () => { + }); + + describe('Server resource: Transactions', () => { + }); + }); + + describe('AutoEncrypter', () => { + describe('KMS Request', () => { + describe('Node.js resource: TLS file read', () => { + + }); + describe('Node.js resource: Socket', () => { + + }); + }); + }); + + describe('ClientEncryption', () => { + describe('KMS Request', () => { + describe('Node.js resource: TLS file read', () => { + + }); + describe('Node.js resource: Socket', () => { + + }); + }); + }); + + describe('Server resource: Cursor', () => { + describe('after cursors are created', () => { + it('all active server-side cursors are closed by client.close()', async () => {}); }); }); +}); +describe.skip('OLD', () => { describe('Topology', () => { describe('after a Topology is created through client.connect()', () => { it('server selection timers are cleaned up by client.close()', async () => { @@ -146,23 +261,6 @@ describe.skip('client.close() Integration', () => { }); describe('Connection', () => { - describe('when connection monitoring is turned on', () => { - // connection monitoring is by default turned on - with the exception of load-balanced mode - it('no sockets remain after client.close()', async () => { - // TODO: skip for LB mode - await runScriptAndGetProcessInfo( - 'connection-monitoring', - config, - async function run({ MongoClient, uri }) { - const client = new MongoClient(uri); - await client.connect(); - await client.close(); - } - ); - }); - - it('no server-side connection threads remain after client.close()', async () => {}); - }); describe('when rtt monitoring is turned on', () => { it('no sockets remain after client.close()', async () => {}); @@ -182,10 +280,4 @@ describe.skip('client.close() Integration', () => { it('no server-side connection threads remain after client.close()', async () => {}); }); }); - - describe('Cursor', () => { - describe('after cursors are created', () => { - it('all active server-side cursors are closed by client.close()', async () => {}); - }); - }); }); diff --git a/test/integration/node-specific/resource_tracking_script_builder.ts b/test/integration/node-specific/resource_tracking_script_builder.ts index ec9382620a..8821c725ec 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -21,6 +21,8 @@ export type HeapResourceTestFunction = (options: { export type ProcessResourceTestFunction = (options: { MongoClient: typeof MongoClient; uri: string; + log: (out: any) => void; + chai: { expect: Function }; }) => Promise; const HEAP_RESOURCE_SCRIPT_PATH = path.resolve( diff --git a/test/tools/fixtures/process_resource_script.in.js b/test/tools/fixtures/process_resource_script.in.js index 3827edc3db..2eb627b61e 100644 --- a/test/tools/fixtures/process_resource_script.in.js +++ b/test/tools/fixtures/process_resource_script.in.js @@ -40,7 +40,7 @@ async function main() { process.on('beforeExit', () => { log({ beforeExitHappened: true }); }); - run({ MongoClient, uri }); + await run({ MongoClient, uri }); log({ newResources: getNewResourceArray() }); } diff --git a/tls-file-read.cjs b/tls-file-read.cjs new file mode 100644 index 0000000000..ed4638389a --- /dev/null +++ b/tls-file-read.cjs @@ -0,0 +1,69 @@ +'use strict'; + +/* eslint-disable no-undef */ +/* eslint-disable no-unused-vars */ +const driverPath = "/Users/aditi.khare/Desktop/node-mongodb-native/lib"; +const func = (async function run({ MongoClient, uri }) { + const devZeroFilePath = '/dev/zero'; + const client = new MongoClient(uri, { tlsCertificateKeyFile: devZeroFilePath }); + client.connect(); + log({ ActiveResources: process.getActiveResourcesInfo() }); + (0, chai_1.expect)(process.getActiveResourcesInfo()).to.include('FSReqPromise'); + await client.close(); + setTimeout(() => chai.expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'), 1000); + }); +const name = "tls-file-read"; +const uri = "mongodb://bob:pwd123@localhost:31000/integration_tests?replicaSet=rs&authSource=admin"; + +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 fs = require('node:fs'); +const chai = require('chai'); +const { setTimeout } = require('timers'); + +let originalReport; +const logFile = 'logs.txt'; + +const run = func; + +// Returns an array containing new the resources created after script start +function getNewResourceArray() { + let currReport = process.report.getReport().libuv; + const originalReportAddresses = originalReport.map(resource => resource.address); + currReport = currReport.filter(resource => !originalReportAddresses.includes(resource.address)); + return currReport; +} + +function log(message) { + // remove outer parentheses for easier parsing + const messageToLog = JSON.stringify(message).slice(1, -1) + ', \n'; + fs.writeFileSync(logFile, messageToLog, { flag: 'a' }); +} + +async function main() { + originalReport = process.report.getReport().libuv; + process.on('beforeExit', () => { + log({ beforeExitHappened: true }); + }); + run({ MongoClient, uri }); + log({ newResources: getNewResourceArray() }); +} + +main() + .then(() => { + log({ exitCode: 0 }); + }) + .catch(() => { + log({ exitCode: 1 }); + }); + +setTimeout(() => { + // this means something was in the event loop such that it hung for more than 10 seconds + // so we kill the process + log({ exitCode: 99 }); + process.exit(99); + // using `unref` will ensure this setTimeout call is not a resource / does not keep the event loop running +}, 10000).unref(); From 1de9809974e1906d1f9190abf97b93b73a693675 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Fri, 20 Dec 2024 17:30:43 -0500 Subject: [PATCH 15/43] reorganized tests and added in most of neal's suggestions --- logs.txt | 3 - .../node-specific/client_close.test.ts | 237 ++++++++---------- .../resource_tracking_script_builder.ts | 6 +- .../fixtures/process_resource_script.in.js | 5 +- tls-file-read.cjs | 69 ----- 5 files changed, 113 insertions(+), 207 deletions(-) delete mode 100644 logs.txt delete mode 100644 tls-file-read.cjs diff --git a/logs.txt b/logs.txt deleted file mode 100644 index 6fd61f4334..0000000000 --- a/logs.txt +++ /dev/null @@ -1,3 +0,0 @@ -"ActiveResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"], -"newResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000135e04560","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}], -"exitCode":0, diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index fc5e5ffdd7..76503475f8 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -1,9 +1,7 @@ import { expect } from 'chai'; -import * as fs from 'fs'; import { type TestConfiguration } from '../../tools/runner/config'; import { runScriptAndGetProcessInfo } from './resource_tracking_script_builder'; -import { sleep } from '../../tools/utils'; describe.skip('MongoClient.close() Integration', () => { // note: these tests are set-up in accordance of the resource ownership tree @@ -62,39 +60,70 @@ describe.skip('MongoClient.close() Integration', () => { describe('Topology', () => { describe('Node.js resource: Server Selection Timer', () => { - + describe('after a Topology is created through client.connect()', () => { + it('server selection timers are cleaned up by client.close()', async () => { + await runScriptAndGetProcessInfo( + 'server-selection-timers', + config, + async function run({ MongoClient, uri }) { + const client = new MongoClient(uri); + client.connect(); + await client.close(); + } + ); + }); + }); }); describe('Server', () => { describe('Monitor', () => { describe('MonitorInterval', () => { describe('Node.js resource: Timer', () => { + describe('after a new monitor is made', () => { + it('monitor interval timer is cleaned up by client.close()', async () => {}); + }); + describe('after a heartbeat fails', () => { + it('the new monitor interval timer is cleaned up by client.close()', async () => {}); + }); }); }); describe('Connection Monitoring', () => { // connection monitoring is by default turned on - with the exception of load-balanced mode describe('Node.js resource: Socket', () => { - it('no sockets remain after client.close()', async () => { - // TODO: skip for LB mode + it('no sockets remain after client.close()', async () => { + // TODO: skip for LB mode }); }); - describe('Server resource: connection thread', () => { + describe('Server resource: connection thread', () => { + it('no connection threads remain after client.close()', async () => { + // TODO: skip for LB mode + }); }); }); describe('RTT Pinger', () => { describe('Node.js resource: Timer', () => { - + describe('after entering monitor streaming mode ', () => { + it('the rtt pinger timer is cleaned up by client.close()', async () => { + // helloReply has a topologyVersion defined + }); + }); }); + describe('Connection', () => { describe('Node.js resource: Socket', () => { - + describe('when rtt monitoring is turned on', () => { + it('no sockets remain after client.close()', async () => {}); + }); }); - describe('Server resource: connection thread', () => { + describe('Server resource: connection thread', () => { + describe('when rtt monitoring is turned on', () => { + it('no server-side connection threads remain after client.close()', async () => {}); + }); }); }); }); @@ -102,19 +131,37 @@ 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 () => {}); + }); }); - describe('Node.js resource: checkOut Timer', () => { // waitQueueTimeoutMS - + describe('Node.js resource: checkOut Timer', () => { + // waitQueueTimeoutMS + describe('after new connection pool is created', () => { + it('the wait queue timer is cleaned up by client.close()', async () => {}); + }); }); describe('Connection', () => { describe('Node.js resource: Socket', () => { + describe('after a connection is checked out', () => { + it('no sockets remain after client.close()', async () => {}); + }); + describe('after a minPoolSize has been set on the ConnectionPool', () => { + it('no sockets remain after client.close()', async () => {}); + }); }); - describe('Node.js resource: Socket', () => { + describe('Server-side resource: Connection thread', () => { + describe('after a connection is checked out', () => { + it('no connection threads remain after client.close()', async () => {}); + }); + + describe('after a minPoolSize has been set on the ConnectionPool', () => { + it('no connection threads remain after client.close()', async () => {}); + }); }); }); }); @@ -122,34 +169,75 @@ describe.skip('MongoClient.close() Integration', () => { describe('SrvPoller', () => { describe('Node.js resource: Timer', () => { - + describe('after SRVPoller is created', () => { + it('timers are cleaned up by client.close()', async () => { + await runScriptAndGetProcessInfo( + 'srv-poller', + config, + async function run({ MongoClient, uri }) { + const client = new MongoClient(uri); + await client.connect(); + await client.close(); + } + ); + }); + }); }); }); }); describe('ClientSession (Implicit)', () => { 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()', async function () {}); + }); }); describe('Server resource: Transactions', () => { + describe('after a clientSession is implicitly created and used', () => { + it('the server-side transaction is cleaned up by client.close()', async function () {}); + }); }); }); describe('ClientSession (Explicit)', () => { describe('Server resource: LSID/ServerSession', () => { + describe('after a clientSession is created and used', () => { + it('the server-side ServerSession is cleaned up by client.close()', async function () {}); + }); }); describe('Server resource: Transactions', () => { + describe('after a clientSession is created and used', () => { + it('the server-side transaction is cleaned up by client.close()', async function () { + const client = this.configuration.newClient(); + await client.connect(); + const session = client.startSession(); + session.startTransaction(); + await client.db('db').collection('coll').insertOne({ a: 1 }, { session }); + + // assert server-side session exists + expect(session.serverSession).to.exist; + + await session.endSession(); + await client.close(); + + // assert command was sent to server to end server side session + }); + }); }); }); describe('AutoEncrypter', () => { describe('KMS Request', () => { describe('Node.js resource: TLS file read', () => { - + describe('when KMSRequest reads an infinite TLS file read', () => { + it('the file read is interrupted by client.close()', async () => {}); + }); }); - describe('Node.js resource: Socket', () => { + describe('Node.js resource: Socket', () => { + it('no sockets remain after client.close()', async () => {}); }); }); }); @@ -157,10 +245,13 @@ describe.skip('MongoClient.close() Integration', () => { describe('ClientEncryption', () => { describe('KMS Request', () => { describe('Node.js resource: TLS file read', () => { - + describe('when KMSRequest reads an infinite TLS file read', () => { + it('the file read is interrupted by client.close()', async () => {}); + }); }); - describe('Node.js resource: Socket', () => { + describe('Node.js resource: Socket', () => { + it('no sockets remain after client.close()', async () => {}); }); }); }); @@ -171,113 +262,3 @@ describe.skip('MongoClient.close() Integration', () => { }); }); }); - -describe.skip('OLD', () => { - describe('Topology', () => { - describe('after a Topology is created through client.connect()', () => { - it('server selection timers are cleaned up by client.close()', async () => { - await runScriptAndGetProcessInfo( - 'server-selection-timers', - config, - async function run({ MongoClient, uri }) { - const client = new MongoClient(uri); - await client.connect(); - await client.close(); - } - ); - }); - }); - }); - - describe('SRVPoller', () => { - // TODO: only non-LB mode - describe('after SRVPoller is created', () => { - it('timers are cleaned up by client.close()', async () => { - await runScriptAndGetProcessInfo( - 'srv-poller', - config, - async function run({ MongoClient, uri }) { - const client = new MongoClient(uri); - await client.connect(); - await client.close(); - } - ); - }); - }); - }); - - describe('ClientSession', () => { - describe('after a clientSession is created and used', () => { - it('the server-side ServerSession and transaction are cleaned up by client.close()', async function () { - const client = this.configuration.newClient(); - await client.connect(); - const session = client.startSession(); - session.startTransaction(); - await client.db('db').collection('coll').insertOne({ a: 1 }, { session }); - - // assert server-side session exists - expect(session.serverSession).to.exist; - - await session.endSession(); - await client.close(); - - // assert command was sent to server to end server side session - }); - }); - }); - - describe('StateMachine', () => { - describe('when FLE is enabled and the client has made a KMS request', () => { - it('no sockets remain after client.close()', async () => {}); - - describe('when the TLS file read hangs', () => { - it('the file read is interrupted by client.close()', async () => {}); - }); - }); - }); - - describe('ConnectionPool', () => { - describe('after new connection pool is created', () => { - it('minPoolSize timer is cleaned up by client.close()', async () => {}); - }); - }); - - describe('MonitorInterval', () => { - describe('after a new monitor is made', () => { - it('monitor interval timer is cleaned up by client.close()', async () => {}); - }); - - describe('after a heartbeat fails', () => { - it('the new monitor interval timer is cleaned up by client.close()', async () => {}); - }); - }); - - describe('RTTPinger', () => { - describe('after entering monitor streaming mode ', () => { - it('the rtt pinger timer is cleaned up by client.close()', async () => { - // helloReply has a topologyVersion defined - }); - }); - }); - - describe('Connection', () => { - - describe('when rtt monitoring is turned on', () => { - it('no sockets remain after client.close()', async () => {}); - - it('no server-side connection threads remain after client.close()', async () => {}); - }); - - describe('after a connection is checked out', () => { - it('no sockets remain after client.close()', async () => {}); - - it('no server-side connection threads remain after client.close()', async () => {}); - }); - - describe('after a minPoolSize has been set on the ConnectionPool', () => { - it('no sockets remain after client.close()', async () => {}); - - it('no server-side connection threads remain after client.close()', async () => {}); - }); - }); -}); diff --git a/test/integration/node-specific/resource_tracking_script_builder.ts b/test/integration/node-specific/resource_tracking_script_builder.ts index 8821c725ec..8db7a8cf33 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -22,7 +22,7 @@ export type ProcessResourceTestFunction = (options: { MongoClient: typeof MongoClient; uri: string; log: (out: any) => void; - chai: { expect: Function }; + chai: { expect: object }; }) => Promise; const HEAP_RESOURCE_SCRIPT_PATH = path.resolve( @@ -48,9 +48,7 @@ export async function testScriptFactory( resourceScript = resourceScript.replace('FUNCTION_STRING', `(${func.toString()})`); resourceScript = resourceScript.replace('NAME_STRING', JSON.stringify(name)); resourceScript = resourceScript.replace('URI_STRING', JSON.stringify(uri)); - if (resourceScriptPath === HEAP_RESOURCE_SCRIPT_PATH) { - resourceScript = resourceScript.replace('ITERATIONS_STRING', `${iterations}`); - } + resourceScript = resourceScript.replace('ITERATIONS_STRING', `${iterations}`); return resourceScript; } diff --git a/test/tools/fixtures/process_resource_script.in.js b/test/tools/fixtures/process_resource_script.in.js index 2eb627b61e..d13de6a137 100644 --- a/test/tools/fixtures/process_resource_script.in.js +++ b/test/tools/fixtures/process_resource_script.in.js @@ -9,7 +9,6 @@ const uri = URI_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 fs = require('node:fs'); @@ -48,8 +47,8 @@ main() .then(() => { log({ exitCode: 0 }); }) - .catch(() => { - log({ exitCode: 1 }); + .catch((e) => { + log({ exitCode: 1, error: util.inspect(e) }); }); setTimeout(() => { diff --git a/tls-file-read.cjs b/tls-file-read.cjs deleted file mode 100644 index ed4638389a..0000000000 --- a/tls-file-read.cjs +++ /dev/null @@ -1,69 +0,0 @@ -'use strict'; - -/* eslint-disable no-undef */ -/* eslint-disable no-unused-vars */ -const driverPath = "/Users/aditi.khare/Desktop/node-mongodb-native/lib"; -const func = (async function run({ MongoClient, uri }) { - const devZeroFilePath = '/dev/zero'; - const client = new MongoClient(uri, { tlsCertificateKeyFile: devZeroFilePath }); - client.connect(); - log({ ActiveResources: process.getActiveResourcesInfo() }); - (0, chai_1.expect)(process.getActiveResourcesInfo()).to.include('FSReqPromise'); - await client.close(); - setTimeout(() => chai.expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'), 1000); - }); -const name = "tls-file-read"; -const uri = "mongodb://bob:pwd123@localhost:31000/integration_tests?replicaSet=rs&authSource=admin"; - -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 fs = require('node:fs'); -const chai = require('chai'); -const { setTimeout } = require('timers'); - -let originalReport; -const logFile = 'logs.txt'; - -const run = func; - -// Returns an array containing new the resources created after script start -function getNewResourceArray() { - let currReport = process.report.getReport().libuv; - const originalReportAddresses = originalReport.map(resource => resource.address); - currReport = currReport.filter(resource => !originalReportAddresses.includes(resource.address)); - return currReport; -} - -function log(message) { - // remove outer parentheses for easier parsing - const messageToLog = JSON.stringify(message).slice(1, -1) + ', \n'; - fs.writeFileSync(logFile, messageToLog, { flag: 'a' }); -} - -async function main() { - originalReport = process.report.getReport().libuv; - process.on('beforeExit', () => { - log({ beforeExitHappened: true }); - }); - run({ MongoClient, uri }); - log({ newResources: getNewResourceArray() }); -} - -main() - .then(() => { - log({ exitCode: 0 }); - }) - .catch(() => { - log({ exitCode: 1 }); - }); - -setTimeout(() => { - // this means something was in the event loop such that it hung for more than 10 seconds - // so we kill the process - log({ exitCode: 99 }); - process.exit(99); - // using `unref` will ensure this setTimeout call is not a resource / does not keep the event loop running -}, 10000).unref(); From e77405f7e105bda7a2aa4de77d6a9b0364502c45 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Mon, 30 Dec 2024 11:18:30 -0500 Subject: [PATCH 16/43] TLS test cases and socket test cases --- logs.txt | 4 + socket-connection-monitoring.cjs | 84 ++++ .../node-specific/client_close.test.ts | 373 ++++++++++++++++-- .../resource_tracking_script_builder.ts | 12 +- .../fixtures/process_resource_script.in.js | 17 +- 5 files changed, 440 insertions(+), 50 deletions(-) create mode 100644 logs.txt create mode 100644 socket-connection-monitoring.cjs diff --git a/logs.txt b/logs.txt new file mode 100644 index 0000000000..de362fc5de --- /dev/null +++ b/logs.txt @@ -0,0 +1,4 @@ +"report":[{"host":"localhost","port":27017},{"host":"localhost","port":27017}], +"newLibuvResources":[], +"exitCode":0, +"beforeExitHappened":true, diff --git a/socket-connection-monitoring.cjs b/socket-connection-monitoring.cjs new file mode 100644 index 0000000000..380497ba11 --- /dev/null +++ b/socket-connection-monitoring.cjs @@ -0,0 +1,84 @@ +'use strict'; + +/* eslint-disable no-undef */ +/* eslint-disable no-unused-vars */ +const driverPath = "/Users/aditi.khare/Desktop/node-mongodb-native/lib"; +const func = (async function run({ MongoClient, uri, log, chai }) { + const client = new MongoClient(uri, { serverMonitoringMode: 'auto' }); + await client.connect(); + // returns all active tcp endpoints + const connectionMonitoringReport = () => process.report.getReport().libuv.filter(r => r.type === 'tcp' && r.is_active).map(r => r.remoteEndpoint); + log({ report: connectionMonitoringReport() }); + // assert socket creation + const servers = client.topology?.s.servers; + for (const server of servers) { + let { host, port } = server[1].s.description.hostAddress; + // regardless of if its active the socket should be gone from the libuv report + chai.expect(connectionMonitoringReport()).to.deep.include({ host, port }); + } + await client.close(); + // assert socket destruction + for (const server of servers) { + let { host, port } = server[1].s.description.hostAddress; + chai.expect(connectionMonitoringReport()).to.not.deep.include({ host, port }); + } + }); +const name = "socket-connection-monitoring"; +const uri = "mongodb://bob:pwd123@localhost:27017/integration_tests?authSource=admin"; + +const { MongoClient, ClientEncryption, BSON } = require(driverPath); +const process = require('node:process'); +const util = require('node:util'); +const timers = require('node:timers'); +const fs = require('node:fs'); +const chai = require('chai'); +const { setTimeout } = require('timers'); + +let originalReport; +const logFile = 'logs.txt'; + +const run = func; +const serverType = ['tcp', 'udp']; + +// Returns an array containing new the resources created after script start +function getNewLibuvResourceArray() { + let currReport = process.report.getReport().libuv; + const originalReportAddresses = originalReport.map(resource => resource.address); + currReport = currReport.filter(resource => + !originalReportAddresses.includes(resource.address) && + resource.is_referenced && // if a resource is unreferenced, it's not keeping the event loop open + (!serverType.includes(resource.type) || resource.is_active) +); + return currReport; +} + +function log(message) { + // remove outer parentheses for easier parsing + const messageToLog = JSON.stringify(message).slice(1, -1) + ', \n'; + fs.writeFileSync(logFile, messageToLog, { flag: 'a' }); +} + +async function main() { + originalReport = process.report.getReport().libuv; + process.on('beforeExit', () => { + log({ beforeExitHappened: true }); + }); + await run({ MongoClient, uri, log, chai, ClientEncryption, BSON }); + log({ newLibuvResources: getNewLibuvResourceArray() }); +} + +main() + .then(() => { + log({ exitCode: 0 }); + }) + .catch(e => { + log({ exitCode: 1, error: util.inspect(e) }); + }); + +setTimeout(() => { + // this means something was in the event loop such that it hung for more than 10 seconds + // so we kill the process + log({ exitCode: 99 }); + process.exit(99); + // using `unref` will ensure this setTimeout call is not a resource / does not keep the event loop running +}, 10000).unref(); diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 76503475f8..0fa4af88c0 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -1,12 +1,12 @@ import { expect } from 'chai'; - 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; + beforeEach(function () { config = this.configuration; }); @@ -18,8 +18,8 @@ describe.skip('MongoClient.close() Integration', () => { 'tls-file-read', config, async function run({ MongoClient, uri, log, chai }) { - const devZeroFilePath = '/dev/zero'; - const client = new MongoClient(uri, { tlsCertificateKeyFile: devZeroFilePath }); + const infiniteFile = '/dev/zero'; + const client = new MongoClient(uri, { tlsCertificateKeyFile: infiniteFile }); client.connect(); log({ ActiveResources: process.getActiveResourcesInfo() }); chai.expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); @@ -33,17 +33,19 @@ describe.skip('MongoClient.close() Integration', () => { describe('Node.js resource: .dockerenv file access', () => { describe('when client is connecting and reads an infinite .dockerenv file', () => { - it('the file read is not interrupted by client.close()', async () => { + it('the file read is not interrupted by client.close()', async function () { await runScriptAndGetProcessInfo( - 'docker-read', + 'docker-file-access', config, - async function run({ MongoClient, uri }) { - /* const dockerPath = '.dockerenv'; - sinon.stub(fs, 'access').callsFake(async () => await sleep(5000)); - await fs.writeFile('.dockerenv', '', { encoding: 'utf8' }); - const client = new MongoClient(uri); + async function run({ MongoClient, uri, log, chai }) { + // TODO: unsure how to make a /.dockerenv fs access read hang + const client = this.configuration.newClient(); + client.connect(); + // assert resource exists + chai.expect(process.getActiveResourcesInfo()).to.contain('FSReqPromise'); await client.close(); - unlink(dockerPath); */ + // assert resource still exists + chai.expect(process.getActiveResourcesInfo()).to.contain('FSReqPromise'); } ); }); @@ -53,7 +55,29 @@ describe.skip('MongoClient.close() Integration', () => { describe('MongoClientAuthProviders', () => { describe('Node.js resource: Token file read', () => { describe('when MongoClientAuthProviders is instantiated and token file read hangs', () => { - it('the file read is interrupted by client.close()', async () => {}); + it('the file read is interrupted by client.close()', async () => { + await runScriptAndGetProcessInfo( + 'token-file-read', + config, + async function run({ MongoClient, uri, log, chai }) { + const infiniteFile = '/dev/zero'; + log({ ActiveResources: process.getActiveResourcesInfo() }); + + // speculative authentication call to getToken() during initial handshake + const client = new MongoClient(uri, { + authMechanismProperties: { TOKEN_RESOURCE: infiniteFile } + }); + client.connect(); + + log({ ActiveResources: process.getActiveResourcesInfo() }); + + chai.expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); + await client.close(); + + chai.expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'); + } + ); + }); }); }); }); @@ -77,6 +101,13 @@ describe.skip('MongoClient.close() Integration', () => { describe('Server', () => { describe('Monitor', () => { + // connection monitoring is by default turned on - with the exception of load-balanced mode + const metadata: MongoDBMetadataUI = { + requires: { + topology: ['single', 'replicaset', 'sharded'] + } + }; + describe('MonitorInterval', () => { describe('Node.js resource: Timer', () => { describe('after a new monitor is made', () => { @@ -90,15 +121,42 @@ describe.skip('MongoClient.close() Integration', () => { }); describe('Connection Monitoring', () => { - // connection monitoring is by default turned on - with the exception of load-balanced mode describe('Node.js resource: Socket', () => { - it('no sockets remain after client.close()', async () => { - // TODO: skip for LB mode + it.only('no sockets remain after client.close()', metadata, async function () { + await runScriptAndGetProcessInfo( + 'socket-connection-monitoring', + config, + async function run({ MongoClient, uri, log, chai }) { + const client = new MongoClient(uri, { serverMonitoringMode: 'auto' }); + await client.connect(); + + // returns all active tcp endpoints + const connectionMonitoringReport = () => process.report.getReport().libuv.filter(r => r.type === 'tcp' && r.is_active).map(r => r.remoteEndpoint); + + log({report: connectionMonitoringReport() }); + // assert socket creation + const servers = client.topology?.s.servers; + for (const server of servers) { + let { host, port } = server[1].s.description.hostAddress; + // regardless of if its active the socket should be gone from the libuv report + + chai.expect(connectionMonitoringReport()).to.deep.include({ host, port }); + } + + await client.close(); + + // assert socket destruction + for (const server of servers) { + let { host, port } = server[1].s.description.hostAddress; + chai.expect(connectionMonitoringReport()).to.not.deep.include({ host, port }); + } + } + ); }); }); describe('Server resource: connection thread', () => { - it('no connection threads remain after client.close()', async () => { + it('no connection threads remain after client.close()', metadata, async () => { // TODO: skip for LB mode }); }); @@ -116,7 +174,35 @@ 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()', async () => {}); + it('no sockets remain after client.close()', async () => { + await runScriptAndGetProcessInfo( + 'socket-rtt-monitoring', + config, + async function run({ MongoClient, uri, log, chai }) { + const client = new MongoClient(uri); + await client.connect(); + + // returns all active tcp endpoints + const connectionMonitoringReport = () => process.report.getReport().libuv.filter(r => r.type === 'tcp' && r.is_active).map(r => r.remoteEndpoint); + + // assert socket creation + const servers = client.topology?.s.servers; + for (const server of servers) { + let { host, port } = server[1].s.description.hostAddress; + // regardless of if its active the socket should be gone from the libuv report + chai.expect(connectionMonitoringReport()).to.deep.include({ host, port }); + } + + await client.close(); + + // assert socket destruction + for (const server of servers) { + let { host, port } = server[1].s.description.hostAddress; + chai.expect(connectionMonitoringReport()).to.not.deep.include({ host, port }); + } + } + ); + }); }); }); @@ -209,35 +295,157 @@ describe.skip('MongoClient.close() Integration', () => { describe('Server resource: Transactions', () => { describe('after a clientSession is created and used', () => { - it('the server-side transaction is cleaned up by client.close()', async function () { - const client = this.configuration.newClient(); - await client.connect(); - const session = client.startSession(); - session.startTransaction(); - await client.db('db').collection('coll').insertOne({ a: 1 }, { session }); - - // assert server-side session exists - expect(session.serverSession).to.exist; - - await session.endSession(); - await client.close(); - - // assert command was sent to server to end server side session - }); + it('the server-side transaction is cleaned up by client.close()', async function () {}); }); }); }); describe('AutoEncrypter', () => { + const metadata: MongoDBMetadataUI = { + requires: { + mongodb: '>=4.2.0', + clientSideEncryption: true + } + }; + describe('KMS Request', () => { describe('Node.js resource: TLS file read', () => { - describe('when KMSRequest reads an infinite TLS file read', () => { - it('the file read is interrupted by client.close()', async () => {}); + describe('when KMSRequest reads an infinite TLS file', () => { + it('the file read is interrupted by client.close()', async () => { + await runScriptAndGetProcessInfo( + 'tls-file-read', + config, + async function run({ MongoClient, uri, log, chai, ClientEncryption, BSON }) { + const infiniteFile = '/dev/zero'; + + const kmsProviders = BSON.EJSON.parse(process.env.CSFLE_KMS_PROVIDERS); + const masterKey = { + region: 'us-east-1', + key: 'arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0' + }; + const provider = 'aws'; + + const keyVaultClient = new MongoClient(uri); + await keyVaultClient.connect(); + await keyVaultClient.db('keyvault').collection('datakeys'); + + const clientEncryption = new ClientEncryption(keyVaultClient, { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders + }); + const dataKey = await clientEncryption.createDataKey(provider, { masterKey }); + + function getEncryptExtraOptions() { + if ( + typeof process.env.CRYPT_SHARED_LIB_PATH === 'string' && + process.env.CRYPT_SHARED_LIB_PATH.length > 0 + ) { + return { cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH }; + } + return {}; + } + const schemaMap = { + 'db.coll': { + bsonType: 'object', + encryptMetadata: { + keyId: [dataKey] + }, + properties: { + a: { + encrypt: { + bsonType: 'int', + algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Random', + keyId: [dataKey] + } + } + } + } + }; + const encryptionOptions = { + autoEncryption: { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders, + extraOptions: getEncryptExtraOptions(), + schemaMap, + tlsOptions: { aws: { tlsCAFile: infiniteFile } } + } + }; + + const encryptedClient = new MongoClient(uri, encryptionOptions); + await encryptedClient.connect(); + + const insertPromise = encryptedClient + .db('db') + .collection('coll') + .insertOne({ a: 1 }); + + chai.expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); + log({ activeResourcesBeforeClose: process.getActiveResourcesInfo() }); + + await keyVaultClient.close(); + await encryptedClient.close(); + + chai.expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); + log({ activeResourcesAfterClose: process.getActiveResourcesInfo() }); + + const err = await insertPromise.catch(e => e); + chai.expect(err).to.exist; + chai.expect(err.errmsg).to.contain('Error in KMS response'); + } + ); + }); }); }); describe('Node.js resource: Socket', () => { - it('no sockets remain after client.close()', async () => {}); + it('no sockets remain after client.close()', metadata, async () => { + await runScriptAndGetProcessInfo( + 'tls-file-read', + config, + async function run({ MongoClient, uri, log, chai, ClientEncryption, BSON }) { + const kmsProviders = BSON.EJSON.parse(process.env.CSFLE_KMS_PROVIDERS); + const masterKey = { + region: 'us-east-1', + key: 'arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0' + }; + const provider = 'aws'; + + const keyVaultClient = new MongoClient(uri); + await keyVaultClient.connect(); + + await keyVaultClient.db('keyvault').collection('datakeys'); + const clientEncryption = new ClientEncryption(keyVaultClient, { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders + }); + + const socketIdCache = process.report + .getReport() + .libuv.filter(r => r.type === 'tcp') + .map(r => r.address); + log({ socketIdCache }); + + // runs KMS request + const dataKey = await clientEncryption + .createDataKey(provider, { masterKey }) + .catch(e => e); + + const newSocketsBeforeClose = process.report + .getReport() + .libuv.filter(r => !socketIdCache.includes(r.address) && r.type === 'tcp'); + log({ newSocketsBeforeClose }); + chai.expect(newSocketsBeforeClose).to.have.length.gte(1); + + await keyVaultClient.close(); + + const newSocketsAfterClose = process.report + .getReport() + .libuv.filter(r => !socketIdCache.includes(r.address) && r.type === 'tcp'); + log({ newSocketsAfterClose }); + chai.expect(newSocketsAfterClose).to.be.empty; + } + ); + }); }); }); }); @@ -246,19 +454,106 @@ describe.skip('MongoClient.close() Integration', () => { describe('KMS Request', () => { describe('Node.js resource: TLS file read', () => { describe('when KMSRequest reads an infinite TLS file read', () => { - it('the file read is interrupted by client.close()', async () => {}); + it('the file read is interrupted by client.close()', async () => { + await runScriptAndGetProcessInfo( + 'tls-file-read', + config, + async function run({ MongoClient, uri, log, chai, ClientEncryption, BSON }) { + const infiniteFile = '/dev/zero'; + const kmsProviders = BSON.EJSON.parse(process.env.CSFLE_KMS_PROVIDERS); + const masterKey = { + region: 'us-east-1', + key: 'arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0' + }; + const provider = 'aws'; + + const keyVaultClient = new MongoClient(uri); + await keyVaultClient.connect(); + + await keyVaultClient.db('keyvault').collection('datakeys'); + const clientEncryption = new ClientEncryption(keyVaultClient, { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders, + tlsOptions: { aws: { tlsCAFile: infiniteFile } } + }); + + const dataKeyPromise = clientEncryption.createDataKey(provider, { masterKey }); + + chai.expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); + + log({ activeResourcesBeforeClose: process.getActiveResourcesInfo() }); + + await keyVaultClient.close(); + + chai.expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); + + log({ activeResourcesAfterClose: process.getActiveResourcesInfo() }); + + const err = await dataKeyPromise.catch(e => e); + chai.expect(err).to.exist; + chai.expect(err.errmsg).to.contain('Error in KMS response'); + } + ); + }); }); }); describe('Node.js resource: Socket', () => { - it('no sockets remain after client.close()', async () => {}); + it('no sockets remain after client.close()', async () => { + await runScriptAndGetProcessInfo( + 'tls-file-read', + config, + async function run({ MongoClient, uri, log, chai, ClientEncryption, BSON }) { + const kmsProviders = BSON.EJSON.parse(process.env.CSFLE_KMS_PROVIDERS); + const masterKey = { + region: 'us-east-1', + key: 'arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0' + }; + const provider = 'aws'; + + const keyVaultClient = new MongoClient(uri); + await keyVaultClient.connect(); + + await keyVaultClient.db('keyvault').collection('datakeys'); + const clientEncryption = new ClientEncryption(keyVaultClient, { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders + }); + + const socketIdCache = process.report + .getReport() + .libuv.filter(r => r.type === 'tcp') + .map(r => r.address); + log({ socketIdCache }); + + // runs KMS request + const dataKey = await clientEncryption + .createDataKey(provider, { masterKey }) + .catch(e => e); + + const newSocketsBeforeClose = process.report + .getReport() + .libuv.filter(r => !socketIdCache.includes(r.address) && r.type === 'tcp'); + log({ newSocketsBeforeClose }); + chai.expect(newSocketsBeforeClose).to.have.length.gte(1); + + await keyVaultClient.close(); + + const newSocketsAfterClose = process.report + .getReport() + .libuv.filter(r => !socketIdCache.includes(r.address) && r.type === 'tcp'); + log({ newSocketsAfterClose }); + chai.expect(newSocketsAfterClose).to.be.empty; + } + ); + }); }); }); }); describe('Server resource: Cursor', () => { describe('after cursors are created', () => { - it('all active server-side cursors are closed by client.close()', async () => {}); + it('all active server-side cursors are closed by client.close()', async function () {}); }); }); }); diff --git a/test/integration/node-specific/resource_tracking_script_builder.ts b/test/integration/node-specific/resource_tracking_script_builder.ts index 8db7a8cf33..aaa4d34b33 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -7,7 +7,7 @@ import * as path from 'node:path'; import { expect } from 'chai'; import { parseSnapshot } from 'v8-heapsnapshot'; -import { type MongoClient } from '../../mongodb'; +import { type BSON, type ClientEncryption, type MongoClient } from '../../mongodb'; import { type TestConfiguration } from '../../tools/runner/config'; export type ResourceTestFunction = HeapResourceTestFunction | ProcessResourceTestFunction; @@ -22,7 +22,9 @@ export type ProcessResourceTestFunction = (options: { MongoClient: typeof MongoClient; uri: string; log: (out: any) => void; - chai: { expect: object }; + chai: { expect: typeof expect }; + ClientEncryption?: typeof ClientEncryption; + BSON?: typeof BSON; }) => Promise; const HEAP_RESOURCE_SCRIPT_PATH = path.resolve( @@ -186,13 +188,13 @@ export async function runScriptAndGetProcessInfo( const messages = JSON.parse(formattedLogRead); // delete temporary files - await unlink(scriptName); - await unlink(logFile); + // await unlink(scriptName); + // await unlink(logFile); // assertions about exit status expect(exitCode, 'process should have exited with zero').to.equal(0); // assertions about resource status expect(messages.beforeExitHappened).to.be.true; - expect(messages.newResources).to.be.empty; + expect(messages.newLibuvResources).to.be.empty; } diff --git a/test/tools/fixtures/process_resource_script.in.js b/test/tools/fixtures/process_resource_script.in.js index d13de6a137..60049e658c 100644 --- a/test/tools/fixtures/process_resource_script.in.js +++ b/test/tools/fixtures/process_resource_script.in.js @@ -7,7 +7,7 @@ const func = FUNCTION_STRING; const name = NAME_STRING; const uri = URI_STRING; -const { MongoClient } = require(driverPath); +const { MongoClient, ClientEncryption, BSON } = require(driverPath); const process = require('node:process'); const util = require('node:util'); const timers = require('node:timers'); @@ -19,12 +19,17 @@ let originalReport; const logFile = 'logs.txt'; const run = func; +const serverType = ['tcp', 'udp']; // Returns an array containing new the resources created after script start -function getNewResourceArray() { +function getNewLibuvResourceArray() { let currReport = process.report.getReport().libuv; const originalReportAddresses = originalReport.map(resource => resource.address); - currReport = currReport.filter(resource => !originalReportAddresses.includes(resource.address)); + currReport = currReport.filter(resource => + !originalReportAddresses.includes(resource.address) && + resource.is_referenced && // if a resource is unreferenced, it's not keeping the event loop open + (!serverType.includes(resource.type) || resource.is_active) +); return currReport; } @@ -39,15 +44,15 @@ async function main() { process.on('beforeExit', () => { log({ beforeExitHappened: true }); }); - await run({ MongoClient, uri }); - log({ newResources: getNewResourceArray() }); + await run({ MongoClient, uri, log, chai, ClientEncryption, BSON }); + log({ newLibuvResources: getNewLibuvResourceArray() }); } main() .then(() => { log({ exitCode: 0 }); }) - .catch((e) => { + .catch(e => { log({ exitCode: 1, error: util.inspect(e) }); }); From 11aa73c21d204c2019073625a8801dd90914455a Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Mon, 30 Dec 2024 11:36:51 -0500 Subject: [PATCH 17/43] TLS test cases --- logs.txt | 4 - socket-connection-monitoring.cjs | 84 ------- .../node-specific/client_close.test.ts | 215 ++---------------- .../resource_tracking_script_builder.ts | 4 +- .../fixtures/process_resource_script.in.js | 11 +- 5 files changed, 22 insertions(+), 296 deletions(-) delete mode 100644 logs.txt delete mode 100644 socket-connection-monitoring.cjs diff --git a/logs.txt b/logs.txt deleted file mode 100644 index de362fc5de..0000000000 --- a/logs.txt +++ /dev/null @@ -1,4 +0,0 @@ -"report":[{"host":"localhost","port":27017},{"host":"localhost","port":27017}], -"newLibuvResources":[], -"exitCode":0, -"beforeExitHappened":true, diff --git a/socket-connection-monitoring.cjs b/socket-connection-monitoring.cjs deleted file mode 100644 index 380497ba11..0000000000 --- a/socket-connection-monitoring.cjs +++ /dev/null @@ -1,84 +0,0 @@ -'use strict'; - -/* eslint-disable no-undef */ -/* eslint-disable no-unused-vars */ -const driverPath = "/Users/aditi.khare/Desktop/node-mongodb-native/lib"; -const func = (async function run({ MongoClient, uri, log, chai }) { - const client = new MongoClient(uri, { serverMonitoringMode: 'auto' }); - await client.connect(); - // returns all active tcp endpoints - const connectionMonitoringReport = () => process.report.getReport().libuv.filter(r => r.type === 'tcp' && r.is_active).map(r => r.remoteEndpoint); - log({ report: connectionMonitoringReport() }); - // assert socket creation - const servers = client.topology?.s.servers; - for (const server of servers) { - let { host, port } = server[1].s.description.hostAddress; - // regardless of if its active the socket should be gone from the libuv report - chai.expect(connectionMonitoringReport()).to.deep.include({ host, port }); - } - await client.close(); - // assert socket destruction - for (const server of servers) { - let { host, port } = server[1].s.description.hostAddress; - chai.expect(connectionMonitoringReport()).to.not.deep.include({ host, port }); - } - }); -const name = "socket-connection-monitoring"; -const uri = "mongodb://bob:pwd123@localhost:27017/integration_tests?authSource=admin"; - -const { MongoClient, ClientEncryption, BSON } = require(driverPath); -const process = require('node:process'); -const util = require('node:util'); -const timers = require('node:timers'); -const fs = require('node:fs'); -const chai = require('chai'); -const { setTimeout } = require('timers'); - -let originalReport; -const logFile = 'logs.txt'; - -const run = func; -const serverType = ['tcp', 'udp']; - -// Returns an array containing new the resources created after script start -function getNewLibuvResourceArray() { - let currReport = process.report.getReport().libuv; - const originalReportAddresses = originalReport.map(resource => resource.address); - currReport = currReport.filter(resource => - !originalReportAddresses.includes(resource.address) && - resource.is_referenced && // if a resource is unreferenced, it's not keeping the event loop open - (!serverType.includes(resource.type) || resource.is_active) -); - return currReport; -} - -function log(message) { - // remove outer parentheses for easier parsing - const messageToLog = JSON.stringify(message).slice(1, -1) + ', \n'; - fs.writeFileSync(logFile, messageToLog, { flag: 'a' }); -} - -async function main() { - originalReport = process.report.getReport().libuv; - process.on('beforeExit', () => { - log({ beforeExitHappened: true }); - }); - await run({ MongoClient, uri, log, chai, ClientEncryption, BSON }); - log({ newLibuvResources: getNewLibuvResourceArray() }); -} - -main() - .then(() => { - log({ exitCode: 0 }); - }) - .catch(e => { - log({ exitCode: 1, error: util.inspect(e) }); - }); - -setTimeout(() => { - // this means something was in the event loop such that it hung for more than 10 seconds - // so we kill the process - log({ exitCode: 99 }); - process.exit(99); - // using `unref` will ensure this setTimeout call is not a resource / does not keep the event loop running -}, 10000).unref(); diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 0fa4af88c0..95f04252c7 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -1,8 +1,9 @@ -import { expect } from 'chai'; +/* eslint-disable @typescript-eslint/no-empty-function */ + import { type TestConfiguration } from '../../tools/runner/config'; import { runScriptAndGetProcessInfo } from './resource_tracking_script_builder'; -describe('MongoClient.close() Integration', () => { +describe.skip('MongoClient.close() Integration', () => { // note: these tests are set-up in accordance of the resource ownership tree let config: TestConfiguration; @@ -32,23 +33,9 @@ describe('MongoClient.close() Integration', () => { }); describe('Node.js resource: .dockerenv file access', () => { - describe('when client is connecting and reads an infinite .dockerenv file', () => { - it('the file read is not interrupted by client.close()', async function () { - await runScriptAndGetProcessInfo( - 'docker-file-access', - config, - async function run({ MongoClient, uri, log, chai }) { - // TODO: unsure how to make a /.dockerenv fs access read hang - const client = this.configuration.newClient(); - client.connect(); - // assert resource exists - chai.expect(process.getActiveResourcesInfo()).to.contain('FSReqPromise'); - await client.close(); - // assert resource still exists - chai.expect(process.getActiveResourcesInfo()).to.contain('FSReqPromise'); - } - ); - }); + describe('when client is connecting and fs.access stalls while accessing .dockerenv file', () => { + it('the file access is not interrupted by client.close()', async function () {}).skipReason = + 'TODO(NODE-6624): Align Client.Close Test Cases with Finalized Design'; }); }); @@ -63,7 +50,7 @@ describe('MongoClient.close() Integration', () => { const infiniteFile = '/dev/zero'; log({ ActiveResources: process.getActiveResourcesInfo() }); - // speculative authentication call to getToken() during initial handshake + // speculative authentication call to getToken() is during initial handshake const client = new MongoClient(uri, { authMechanismProperties: { TOKEN_RESOURCE: infiniteFile } }); @@ -85,17 +72,7 @@ describe('MongoClient.close() Integration', () => { describe('Topology', () => { describe('Node.js resource: Server Selection Timer', () => { describe('after a Topology is created through client.connect()', () => { - it('server selection timers are cleaned up by client.close()', async () => { - await runScriptAndGetProcessInfo( - 'server-selection-timers', - config, - async function run({ MongoClient, uri }) { - const client = new MongoClient(uri); - client.connect(); - await client.close(); - } - ); - }); + it('server selection timers are cleaned up by client.close()', async () => {}); }); }); @@ -122,43 +99,11 @@ describe('MongoClient.close() Integration', () => { describe('Connection Monitoring', () => { describe('Node.js resource: Socket', () => { - it.only('no sockets remain after client.close()', metadata, async function () { - await runScriptAndGetProcessInfo( - 'socket-connection-monitoring', - config, - async function run({ MongoClient, uri, log, chai }) { - const client = new MongoClient(uri, { serverMonitoringMode: 'auto' }); - await client.connect(); - - // returns all active tcp endpoints - const connectionMonitoringReport = () => process.report.getReport().libuv.filter(r => r.type === 'tcp' && r.is_active).map(r => r.remoteEndpoint); - - log({report: connectionMonitoringReport() }); - // assert socket creation - const servers = client.topology?.s.servers; - for (const server of servers) { - let { host, port } = server[1].s.description.hostAddress; - // regardless of if its active the socket should be gone from the libuv report - - chai.expect(connectionMonitoringReport()).to.deep.include({ host, port }); - } - - await client.close(); - - // assert socket destruction - for (const server of servers) { - let { host, port } = server[1].s.description.hostAddress; - chai.expect(connectionMonitoringReport()).to.not.deep.include({ host, port }); - } - } - ); - }); + it('no sockets remain after client.close()', metadata, async function () {}); }); describe('Server resource: connection thread', () => { - it('no connection threads remain after client.close()', metadata, async () => { - // TODO: skip for LB mode - }); + it('no connection threads remain after client.close()', metadata, async () => {}); }); }); @@ -174,35 +119,7 @@ describe('MongoClient.close() Integration', () => { describe('Connection', () => { describe('Node.js resource: Socket', () => { describe('when rtt monitoring is turned on', () => { - it('no sockets remain after client.close()', async () => { - await runScriptAndGetProcessInfo( - 'socket-rtt-monitoring', - config, - async function run({ MongoClient, uri, log, chai }) { - const client = new MongoClient(uri); - await client.connect(); - - // returns all active tcp endpoints - const connectionMonitoringReport = () => process.report.getReport().libuv.filter(r => r.type === 'tcp' && r.is_active).map(r => r.remoteEndpoint); - - // assert socket creation - const servers = client.topology?.s.servers; - for (const server of servers) { - let { host, port } = server[1].s.description.hostAddress; - // regardless of if its active the socket should be gone from the libuv report - chai.expect(connectionMonitoringReport()).to.deep.include({ host, port }); - } - - await client.close(); - - // assert socket destruction - for (const server of servers) { - let { host, port } = server[1].s.description.hostAddress; - chai.expect(connectionMonitoringReport()).to.not.deep.include({ host, port }); - } - } - ); - }); + it('no sockets remain after client.close()', async () => {}); }); }); @@ -256,17 +173,7 @@ describe('MongoClient.close() Integration', () => { describe('SrvPoller', () => { describe('Node.js resource: Timer', () => { describe('after SRVPoller is created', () => { - it('timers are cleaned up by client.close()', async () => { - await runScriptAndGetProcessInfo( - 'srv-poller', - config, - async function run({ MongoClient, uri }) { - const client = new MongoClient(uri); - await client.connect(); - await client.close(); - } - ); - }); + it('timers are cleaned up by client.close()', async () => {}); }); }); }); @@ -398,54 +305,7 @@ describe('MongoClient.close() Integration', () => { }); describe('Node.js resource: Socket', () => { - it('no sockets remain after client.close()', metadata, async () => { - await runScriptAndGetProcessInfo( - 'tls-file-read', - config, - async function run({ MongoClient, uri, log, chai, ClientEncryption, BSON }) { - const kmsProviders = BSON.EJSON.parse(process.env.CSFLE_KMS_PROVIDERS); - const masterKey = { - region: 'us-east-1', - key: 'arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0' - }; - const provider = 'aws'; - - const keyVaultClient = new MongoClient(uri); - await keyVaultClient.connect(); - - await keyVaultClient.db('keyvault').collection('datakeys'); - const clientEncryption = new ClientEncryption(keyVaultClient, { - keyVaultNamespace: 'keyvault.datakeys', - kmsProviders - }); - - const socketIdCache = process.report - .getReport() - .libuv.filter(r => r.type === 'tcp') - .map(r => r.address); - log({ socketIdCache }); - - // runs KMS request - const dataKey = await clientEncryption - .createDataKey(provider, { masterKey }) - .catch(e => e); - - const newSocketsBeforeClose = process.report - .getReport() - .libuv.filter(r => !socketIdCache.includes(r.address) && r.type === 'tcp'); - log({ newSocketsBeforeClose }); - chai.expect(newSocketsBeforeClose).to.have.length.gte(1); - - await keyVaultClient.close(); - - const newSocketsAfterClose = process.report - .getReport() - .libuv.filter(r => !socketIdCache.includes(r.address) && r.type === 'tcp'); - log({ newSocketsAfterClose }); - chai.expect(newSocketsAfterClose).to.be.empty; - } - ); - }); + it('no sockets remain after client.close()', metadata, async () => {}); }); }); }); @@ -499,54 +359,7 @@ describe('MongoClient.close() Integration', () => { }); describe('Node.js resource: Socket', () => { - it('no sockets remain after client.close()', async () => { - await runScriptAndGetProcessInfo( - 'tls-file-read', - config, - async function run({ MongoClient, uri, log, chai, ClientEncryption, BSON }) { - const kmsProviders = BSON.EJSON.parse(process.env.CSFLE_KMS_PROVIDERS); - const masterKey = { - region: 'us-east-1', - key: 'arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0' - }; - const provider = 'aws'; - - const keyVaultClient = new MongoClient(uri); - await keyVaultClient.connect(); - - await keyVaultClient.db('keyvault').collection('datakeys'); - const clientEncryption = new ClientEncryption(keyVaultClient, { - keyVaultNamespace: 'keyvault.datakeys', - kmsProviders - }); - - const socketIdCache = process.report - .getReport() - .libuv.filter(r => r.type === 'tcp') - .map(r => r.address); - log({ socketIdCache }); - - // runs KMS request - const dataKey = await clientEncryption - .createDataKey(provider, { masterKey }) - .catch(e => e); - - const newSocketsBeforeClose = process.report - .getReport() - .libuv.filter(r => !socketIdCache.includes(r.address) && r.type === 'tcp'); - log({ newSocketsBeforeClose }); - chai.expect(newSocketsBeforeClose).to.have.length.gte(1); - - await keyVaultClient.close(); - - const newSocketsAfterClose = process.report - .getReport() - .libuv.filter(r => !socketIdCache.includes(r.address) && r.type === 'tcp'); - log({ newSocketsAfterClose }); - chai.expect(newSocketsAfterClose).to.be.empty; - } - ); - }); + it('no sockets remain after client.close()', async () => {}); }); }); }); diff --git a/test/integration/node-specific/resource_tracking_script_builder.ts b/test/integration/node-specific/resource_tracking_script_builder.ts index aaa4d34b33..c15e12e77b 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -188,8 +188,8 @@ export async function runScriptAndGetProcessInfo( const messages = JSON.parse(formattedLogRead); // delete temporary files - // await unlink(scriptName); - // await unlink(logFile); + await unlink(scriptName); + await unlink(logFile); // assertions about exit status expect(exitCode, 'process should have exited with zero').to.equal(0); diff --git a/test/tools/fixtures/process_resource_script.in.js b/test/tools/fixtures/process_resource_script.in.js index 60049e658c..4a984af311 100644 --- a/test/tools/fixtures/process_resource_script.in.js +++ b/test/tools/fixtures/process_resource_script.in.js @@ -25,11 +25,12 @@ const serverType = ['tcp', 'udp']; function getNewLibuvResourceArray() { let currReport = process.report.getReport().libuv; const originalReportAddresses = originalReport.map(resource => resource.address); - currReport = currReport.filter(resource => - !originalReportAddresses.includes(resource.address) && - resource.is_referenced && // if a resource is unreferenced, it's not keeping the event loop open - (!serverType.includes(resource.type) || resource.is_active) -); + currReport = currReport.filter( + resource => + !originalReportAddresses.includes(resource.address) && + resource.is_referenced && // if a resource is unreferenced, it's not keeping the event loop open + (!serverType.includes(resource.type) || resource.is_active) + ); return currReport; } From c99579ade75d6079af94685b4af214619da5b6e3 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Mon, 30 Dec 2024 12:00:02 -0500 Subject: [PATCH 18/43] fix message formatting --- logs.txt | 0 .../node-specific/resource_tracking_script_builder.ts | 4 ++-- test/tools/fixtures/process_resource_script.in.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 logs.txt diff --git a/logs.txt b/logs.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/integration/node-specific/resource_tracking_script_builder.ts b/test/integration/node-specific/resource_tracking_script_builder.ts index c15e12e77b..43e2e1efb3 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -184,8 +184,8 @@ export async function runScriptAndGetProcessInfo( // make sure the process ended const [exitCode] = await willClose; - const formattedLogRead = '{' + fs.readFileSync(logFile, 'utf-8').slice(0, -3) + '}'; - const messages = JSON.parse(formattedLogRead); + // 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 }), {}); // delete temporary files await unlink(scriptName); diff --git a/test/tools/fixtures/process_resource_script.in.js b/test/tools/fixtures/process_resource_script.in.js index 4a984af311..e2a6dedf19 100644 --- a/test/tools/fixtures/process_resource_script.in.js +++ b/test/tools/fixtures/process_resource_script.in.js @@ -36,7 +36,7 @@ function getNewLibuvResourceArray() { function log(message) { // remove outer parentheses for easier parsing - const messageToLog = JSON.stringify(message).slice(1, -1) + ', \n'; + const messageToLog = JSON.stringify(message) + ' \n'; fs.writeFileSync(logFile, messageToLog, { flag: 'a' }); } From 47c20db6881287f8c967bd5bae6c973d9b879e42 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Mon, 30 Dec 2024 13:27:36 -0500 Subject: [PATCH 19/43] fix message formatting + test cases naming --- .../node-specific/client_close.test.ts | 40 ++++++++++++------- .../resource_tracking_script_builder.ts | 7 +++- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 95f04252c7..2d9d141bbe 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -41,26 +41,36 @@ describe.skip('MongoClient.close() Integration', () => { describe('MongoClientAuthProviders', () => { describe('Node.js resource: Token file read', () => { + let tokenFileEnvCache; + + beforeEach(function () { + if (process.env.AUTH === 'auth') { + this.currentTest.skipReason = 'OIDC test environment requires auth disabled'; + return this.skip(); + } + tokenFileEnvCache = process.env.OIDC_TOKEN_FILE; + }); + + afterEach(function () { + process.env.OIDC_TOKEN_FILE = tokenFileEnvCache; + }); + describe('when MongoClientAuthProviders is instantiated and token file read hangs', () => { it('the file read is interrupted by client.close()', async () => { await runScriptAndGetProcessInfo( 'token-file-read', config, - async function run({ MongoClient, uri, log, chai }) { + async function run({ MongoClient, uri, chai }) { const infiniteFile = '/dev/zero'; - log({ ActiveResources: process.getActiveResourcesInfo() }); - - // speculative authentication call to getToken() is during initial handshake - const client = new MongoClient(uri, { - authMechanismProperties: { TOKEN_RESOURCE: infiniteFile } - }); + process.env.OIDC_TOKEN_FILE = infiniteFile; + const options = { + authMechanismProperties: { ENVIRONMENT: 'test' }, + authMechanism: 'MONGODB-OIDC' + }; + const client = new MongoClient(uri, options); client.connect(); - - log({ ActiveResources: process.getActiveResourcesInfo() }); - chai.expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); await client.close(); - chai.expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'); } ); @@ -220,7 +230,7 @@ describe.skip('MongoClient.close() Integration', () => { describe('when KMSRequest reads an infinite TLS file', () => { it('the file read is interrupted by client.close()', async () => { await runScriptAndGetProcessInfo( - 'tls-file-read', + 'tls-file-read-auto-encryption', config, async function run({ MongoClient, uri, log, chai, ClientEncryption, BSON }) { const infiniteFile = '/dev/zero'; @@ -292,7 +302,7 @@ describe.skip('MongoClient.close() Integration', () => { await keyVaultClient.close(); await encryptedClient.close(); - chai.expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); + chai.expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'); log({ activeResourcesAfterClose: process.getActiveResourcesInfo() }); const err = await insertPromise.catch(e => e); @@ -316,7 +326,7 @@ describe.skip('MongoClient.close() Integration', () => { describe('when KMSRequest reads an infinite TLS file read', () => { it('the file read is interrupted by client.close()', async () => { await runScriptAndGetProcessInfo( - 'tls-file-read', + 'tls-file-read-client-encryption', config, async function run({ MongoClient, uri, log, chai, ClientEncryption, BSON }) { const infiniteFile = '/dev/zero'; @@ -345,7 +355,7 @@ describe.skip('MongoClient.close() Integration', () => { await keyVaultClient.close(); - chai.expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); + chai.expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'); log({ activeResourcesAfterClose: process.getActiveResourcesInfo() }); diff --git a/test/integration/node-specific/resource_tracking_script_builder.ts b/test/integration/node-specific/resource_tracking_script_builder.ts index 43e2e1efb3..e2d98b2236 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -1,6 +1,5 @@ import { fork, spawn } from 'node:child_process'; import { on, once } from 'node:events'; -import * as fs from 'node:fs'; import { readFile, unlink, writeFile } from 'node:fs/promises'; import * as path from 'node:path'; @@ -185,7 +184,11 @@ export async function runScriptAndGetProcessInfo( const [exitCode] = await willClose; // 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 }), {}); + const messages = (await readFile(logFile, 'utf-8')) + .trim() + .split('\n') + .map(line => JSON.parse(line)) + .reduce((acc, curr) => ({ ...acc, ...curr }), {}); // delete temporary files await unlink(scriptName); From 133b20da7f83f4a727238466b0dc540dc8c64326 Mon Sep 17 00:00:00 2001 From: Aditi Khare <106987683+aditi-khare-mongoDB@users.noreply.github.com> Date: Mon, 30 Dec 2024 13:29:08 -0500 Subject: [PATCH 20/43] Delete logs.txt --- logs.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 logs.txt diff --git a/logs.txt b/logs.txt deleted file mode 100644 index e69de29bb2..0000000000 From 8fd53a4878a9d6824f80b8232ff7d82584557ef7 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Mon, 30 Dec 2024 14:28:48 -0500 Subject: [PATCH 21/43] requested changes: remove log calls, change chai to expect, clarify script vars/functions --- .../node-specific/client_close.test.ts | 39 +++++++-------- .../resource_tracking_script_builder.ts | 24 ++++------ .../fixtures/process_resource_script.in.js | 47 ++++++++++++++----- 3 files changed, 59 insertions(+), 51 deletions(-) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 2d9d141bbe..b49827c89a 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -18,14 +18,13 @@ describe.skip('MongoClient.close() Integration', () => { await runScriptAndGetProcessInfo( 'tls-file-read', config, - async function run({ MongoClient, uri, log, chai }) { + async function run({ MongoClient, uri, expect }) { const infiniteFile = '/dev/zero'; const client = new MongoClient(uri, { tlsCertificateKeyFile: infiniteFile }); client.connect(); - log({ ActiveResources: process.getActiveResourcesInfo() }); - chai.expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); + expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); await client.close(); - chai.expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'); + expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'); } ); }); @@ -60,7 +59,7 @@ describe.skip('MongoClient.close() Integration', () => { await runScriptAndGetProcessInfo( 'token-file-read', config, - async function run({ MongoClient, uri, chai }) { + async function run({ MongoClient, uri, expect }) { const infiniteFile = '/dev/zero'; process.env.OIDC_TOKEN_FILE = infiniteFile; const options = { @@ -69,9 +68,9 @@ describe.skip('MongoClient.close() Integration', () => { }; const client = new MongoClient(uri, options); client.connect(); - chai.expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); + expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); await client.close(); - chai.expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'); + expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'); } ); }); @@ -232,7 +231,7 @@ describe.skip('MongoClient.close() Integration', () => { await runScriptAndGetProcessInfo( 'tls-file-read-auto-encryption', config, - async function run({ MongoClient, uri, log, chai, ClientEncryption, BSON }) { + async function run({ MongoClient, uri, expect, ClientEncryption, BSON }) { const infiniteFile = '/dev/zero'; const kmsProviders = BSON.EJSON.parse(process.env.CSFLE_KMS_PROVIDERS); @@ -296,18 +295,16 @@ describe.skip('MongoClient.close() Integration', () => { .collection('coll') .insertOne({ a: 1 }); - chai.expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); - log({ activeResourcesBeforeClose: process.getActiveResourcesInfo() }); + expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); await keyVaultClient.close(); await encryptedClient.close(); - chai.expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'); - log({ activeResourcesAfterClose: process.getActiveResourcesInfo() }); + expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'); const err = await insertPromise.catch(e => e); - chai.expect(err).to.exist; - chai.expect(err.errmsg).to.contain('Error in KMS response'); + expect(err).to.exist; + expect(err.errmsg).to.contain('Error in KMS response'); } ); }); @@ -328,7 +325,7 @@ describe.skip('MongoClient.close() Integration', () => { await runScriptAndGetProcessInfo( 'tls-file-read-client-encryption', config, - async function run({ MongoClient, uri, log, chai, ClientEncryption, BSON }) { + async function run({ MongoClient, uri, expect, ClientEncryption, BSON }) { const infiniteFile = '/dev/zero'; const kmsProviders = BSON.EJSON.parse(process.env.CSFLE_KMS_PROVIDERS); const masterKey = { @@ -349,19 +346,15 @@ describe.skip('MongoClient.close() Integration', () => { const dataKeyPromise = clientEncryption.createDataKey(provider, { masterKey }); - chai.expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); - - log({ activeResourcesBeforeClose: process.getActiveResourcesInfo() }); + expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); await keyVaultClient.close(); - chai.expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'); - - log({ activeResourcesAfterClose: process.getActiveResourcesInfo() }); + expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'); const err = await dataKeyPromise.catch(e => e); - chai.expect(err).to.exist; - chai.expect(err.errmsg).to.contain('Error in KMS response'); + expect(err).to.exist; + expect(err.errmsg).to.contain('Error in KMS response'); } ); }); diff --git a/test/integration/node-specific/resource_tracking_script_builder.ts b/test/integration/node-specific/resource_tracking_script_builder.ts index e2d98b2236..be8804b041 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -20,8 +20,8 @@ export type HeapResourceTestFunction = (options: { export type ProcessResourceTestFunction = (options: { MongoClient: typeof MongoClient; uri: string; - log: (out: any) => void; - chai: { expect: typeof expect }; + log?: (out: any) => void; + expect: typeof expect; ClientEncryption?: typeof ClientEncryption; BSON?: typeof BSON; }) => Promise; @@ -47,7 +47,7 @@ export async function testScriptFactory( 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('SCRIPT_NAME_STRING', JSON.stringify(name)); resourceScript = resourceScript.replace('URI_STRING', JSON.stringify(uri)); resourceScript = resourceScript.replace('ITERATIONS_STRING', `${iterations}`); @@ -140,11 +140,11 @@ export async function runScriptAndReturnHeapInfo( * **The provided function is run in an isolated Node.js process** * * A user of this function will likely need to familiarize themselves with the surrounding scripting, but briefly: - * - Every MongoClient you construct should have an asyncResource attached to it like so: - * ```js - * mongoClient.asyncResource = new this.async_hooks.AsyncResource('MongoClient'); - * ``` - * - You can perform any number of operations and connects/closes of MongoClients + * - Many MongoClient operations (construction, connection, commands) can result in resources that keep the JS event loop running. + * - Timers + * - Active Sockets + * - File Read Hangs + * * - This function performs assertions that at the end of the provided function, the js event loop has been exhausted * * @param name - the name of the script, this defines the name of the file, it will be cleaned up if the function returns successfully @@ -168,16 +168,8 @@ export async function runScriptAndGetProcessInfo( await writeFile(scriptName, scriptContent, { encoding: 'utf8' }); const logFile = 'logs.txt'; - const processDiedController = new AbortController(); const script = spawn(process.argv[0], [scriptName], { stdio: ['ignore', 'ignore', 'ignore'] }); - // 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 diff --git a/test/tools/fixtures/process_resource_script.in.js b/test/tools/fixtures/process_resource_script.in.js index e2a6dedf19..9e3a65806d 100644 --- a/test/tools/fixtures/process_resource_script.in.js +++ b/test/tools/fixtures/process_resource_script.in.js @@ -4,7 +4,7 @@ /* eslint-disable no-unused-vars */ const driverPath = DRIVER_SOURCE_PATH; const func = FUNCTION_STRING; -const name = NAME_STRING; +const scriptName = SCRIPT_NAME_STRING; const uri = URI_STRING; const { MongoClient, ClientEncryption, BSON } = require(driverPath); @@ -12,28 +12,54 @@ const process = require('node:process'); const util = require('node:util'); const timers = require('node:timers'); const fs = require('node:fs'); -const chai = require('chai'); +const { expect } = require('chai'); const { setTimeout } = require('timers'); let originalReport; const logFile = 'logs.txt'; const run = func; -const serverType = ['tcp', 'udp']; -// Returns an array containing new the resources created after script start +/** + * + * Returns an array containing the new resources created after script started. + * A new resource is something that will keep the event loop running. + * + * In order to be counted as a new resource, a resource MUST: + * - Must NOT share an address with a libuv resource that existed at the start of script + * - Must be referenced. See [here](https://nodejs.org/api/timers.html#timeoutref) for more context. + * - Must NOT be an inactive server + * + * We're using the following tool to track resources: `process.report.getReport().libuv` + * For more context, see documentation for [process.report.getReport()](https://nodejs.org/api/report.html), and [libuv](https://docs.libuv.org/en/v1.x/handle.html). + * + */ function getNewLibuvResourceArray() { let currReport = process.report.getReport().libuv; const originalReportAddresses = originalReport.map(resource => resource.address); - currReport = currReport.filter( - resource => + + /** + * @typedef {Object} LibuvResource + * @property {boolean} is_active Is the resource active? For a socket, this means it is allowing I/O. For a timer, this means a timer is has not expired. + * @property {string} type What is the resource type? For example, 'tcp' | 'timer' | 'udp' | 'tty'... (See more in [docs](https://docs.libuv.org/en/v1.x/handle.html)). + * @property {boolean} is_referenced Is the resource keeping the JS event loop active? + * + * @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 (!serverType.includes(resource.type) || resource.is_active) - ); + ); + } + + currReport = currReport.filter(resource => isNewLibuvResource(resource)); return currReport; } +// A log function for debugging function log(message) { // remove outer parentheses for easier parsing const messageToLog = JSON.stringify(message) + ' \n'; @@ -45,14 +71,12 @@ async function main() { process.on('beforeExit', () => { log({ beforeExitHappened: true }); }); - await run({ MongoClient, uri, log, chai, ClientEncryption, BSON }); + await run({ MongoClient, uri, log, expect, ClientEncryption, BSON }); log({ newLibuvResources: getNewLibuvResourceArray() }); } main() - .then(() => { - log({ exitCode: 0 }); - }) + .then(() => {}) .catch(e => { log({ exitCode: 1, error: util.inspect(e) }); }); @@ -60,7 +84,6 @@ main() setTimeout(() => { // this means something was in the event loop such that it hung for more than 10 seconds // so we kill the process - log({ exitCode: 99 }); process.exit(99); // using `unref` will ensure this setTimeout call is not a resource / does not keep the event loop running }, 10000).unref(); From e60a42b69acedfa1e67cbe4b01c7a8c7f3532ef5 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Mon, 30 Dec 2024 14:32:38 -0500 Subject: [PATCH 22/43] requested changes: additional expectation --- test/integration/node-specific/client_close.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index b49827c89a..771259996e 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -290,6 +290,8 @@ describe.skip('MongoClient.close() Integration', () => { const encryptedClient = new MongoClient(uri, encryptionOptions); await encryptedClient.connect(); + expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'); + const insertPromise = encryptedClient .db('db') .collection('coll') From 13a7f27fa87e77fc2e5d0df330b686c4dabcb002 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Mon, 30 Dec 2024 16:22:16 -0500 Subject: [PATCH 23/43] temp --- .../node-specific/client_close.test.ts | 72 +++++++++++++++++-- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 95f04252c7..034c72f846 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -99,11 +99,41 @@ describe.skip('MongoClient.close() Integration', () => { describe('Connection Monitoring', () => { describe('Node.js resource: Socket', () => { - it('no sockets remain after client.close()', metadata, async function () {}); + it('no sockets remain after client.close()', metadata, async function () { + await runScriptAndGetProcessInfo( + 'socket-connection-monitoring', + config, + async function run({ MongoClient, uri, log, chai }) { + const client = new MongoClient(uri, { serverMonitoringMode: 'auto' }); + await client.connect(); + + // returns all active tcp endpoints + const connectionMonitoringReport = () => process.report.getReport().libuv.filter(r => r.type === 'tcp' && r.is_active).map(r => r.remoteEndpoint); + + log({report: connectionMonitoringReport() }); + // assert socket creation + const servers = client.topology?.s.servers; + for (const server of servers) { + let { host, port } = server[1].s.description.hostAddress; + chai.expect(connectionMonitoringReport()).to.deep.include({ host, port }); + } + + await client.close(); + + // assert socket destruction + for (const server of servers) { + let { host, port } = server[1].s.description.hostAddress; + chai.expect(connectionMonitoringReport()).to.not.deep.include({ host, port }); + } + } + ); + }); }); describe('Server resource: connection thread', () => { - it('no connection threads remain after client.close()', metadata, async () => {}); + it('no connection threads remain after client.close()', metadata, async () => { + + }); }); }); @@ -112,14 +142,48 @@ describe.skip('MongoClient.close() Integration', () => { describe('after entering monitor streaming mode ', () => { it('the rtt pinger timer is cleaned up by client.close()', async () => { // helloReply has a topologyVersion defined - }); + await runScriptAndGetProcessInfo( + 'socket-connection-monitoring', + config, + async function run({ MongoClient, uri, expect }) { + const client = new MongoClient(uri, { serverMonitoringMode: 'stream', minHeartbeatFrequencyMS: 10000 }); + await client.connect(); + }); }); }); describe('Connection', () => { describe('Node.js resource: Socket', () => { describe('when rtt monitoring is turned on', () => { - it('no sockets remain after client.close()', async () => {}); + it('no sockets remain after client.close()', async () => { + await runScriptAndGetProcessInfo( + 'socket-connection-monitoring', + config, + async function run({ MongoClient, uri, log, expect }) { + const client = new MongoClient(uri, { serverMonitoringMode: 'stream', minHeartbeatFrequencyMS: 10000 }); + await client.connect(); + + // returns all active tcp endpoints + const connectionMonitoringReport = () => process.report.getReport().libuv.filter(r => r.type === 'tcp' && r.is_active).map(r => r.remoteEndpoint); + + log({report: connectionMonitoringReport() }); + // assert socket creation + const servers = client.topology?.s.servers; + for (const server of servers) { + let { host, port } = server[1].s.description.hostAddress; + chai.expect(connectionMonitoringReport()).to.deep.include({ host, port }); + } + + await client.close(); + + // assert socket destruction + for (const server of servers) { + let { host, port } = server[1].s.description.hostAddress; + chai.expect(connectionMonitoringReport()).to.not.deep.include({ host, port }); + } + } + ); + }); }); }); From bbdd8fd27a69371b6cd05b1f1ca7303e7a949c99 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Mon, 30 Dec 2024 19:50:11 -0500 Subject: [PATCH 24/43] rttPinger done --- .../node-specific/client_close.test.ts | 124 +++++++++++------- .../resource_tracking_script_builder.ts | 1 + .../fixtures/process_resource_script.in.js | 5 +- 3 files changed, 81 insertions(+), 49 deletions(-) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 5aaa3242c0..9a46288af2 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -3,7 +3,7 @@ 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; @@ -112,37 +112,39 @@ describe.skip('MongoClient.close() Integration', () => { await runScriptAndGetProcessInfo( 'socket-connection-monitoring', config, - async function run({ MongoClient, uri, log, chai }) { - const client = new MongoClient(uri, { serverMonitoringMode: 'auto' }); + async function run({ MongoClient, uri, log, expect }) { + const client = new MongoClient(uri); await client.connect(); // returns all active tcp endpoints - const connectionMonitoringReport = () => process.report.getReport().libuv.filter(r => r.type === 'tcp' && r.is_active).map(r => r.remoteEndpoint); - - log({report: connectionMonitoringReport() }); + const connectionMonitoringReport = () => + process.report + .getReport() + .libuv.filter(r => r.type === 'tcp' && r.is_active) + .map(r => r.remoteEndpoint); + + log({ report: connectionMonitoringReport() }); // assert socket creation const servers = client.topology?.s.servers; for (const server of servers) { - let { host, port } = server[1].s.description.hostAddress; - chai.expect(connectionMonitoringReport()).to.deep.include({ host, port }); + const { host, port } = server[1].s.description.hostAddress; + expect(connectionMonitoringReport()).to.deep.include({ host, port }); } await client.close(); - // assert socket destruction + // assert socket destruction for (const server of servers) { - let { host, port } = server[1].s.description.hostAddress; - chai.expect(connectionMonitoringReport()).to.not.deep.include({ host, port }); + const { host, port } = server[1].s.description.hostAddress; + expect(connectionMonitoringReport()).to.not.deep.include({ host, port }); } - } + } ); }); }); describe('Server resource: connection thread', () => { - it('no connection threads remain after client.close()', metadata, async () => { - - }); + it('no connection threads remain after client.close()', metadata, async () => {}); }); }); @@ -151,13 +153,7 @@ describe.skip('MongoClient.close() Integration', () => { describe('after entering monitor streaming mode ', () => { it('the rtt pinger timer is cleaned up by client.close()', async () => { // helloReply has a topologyVersion defined - await runScriptAndGetProcessInfo( - 'socket-connection-monitoring', - config, - async function run({ MongoClient, uri, expect }) { - const client = new MongoClient(uri, { serverMonitoringMode: 'stream', minHeartbeatFrequencyMS: 10000 }); - await client.connect(); - }); + }); }); }); @@ -165,33 +161,65 @@ describe.skip('MongoClient.close() Integration', () => { describe('Node.js resource: Socket', () => { describe('when rtt monitoring is turned on', () => { it('no sockets remain after client.close()', async () => { + const run = async function({ MongoClient, uri, log, expect, sleep }) { + const heartbeatFrequencyMS = 100; + const client = new MongoClient(uri, { + serverMonitoringMode: 'stream', + heartbeatFrequencyMS + }); + await client.connect(); + + const servers = Array.from(client.topology.s.servers.keys()); + + // a hashmap of + const serversHeartbeatOccurred = servers.reduce( + (acc, hostname) => ({ ...acc, [hostname]: false }), + {} + ); + + const activeSocketsReport = () => + process.report + .getReport() + .libuv.filter(r => r.type === 'tcp' && r.is_active); + + const socketsAddressesBeforeHeartbeat = activeSocketsReport().map( + r => r.address + ); + + const rttSocketReport = () => + activeSocketsReport() + .filter(r => !socketsAddressesBeforeHeartbeat.includes(r.address)) + .map(r => r.remoteEndpoint.host + ':' + r.remoteEndpoint.port); + + client.on('serverHeartbeatSucceeded', async ev => { + // assert creation of rttPinger socket + const newSocketsAfterHeartbeat = rttSocketReport(); + expect(newSocketsAfterHeartbeat).to.deep.contain(ev.connectionId); + + // assert rttPinger socket is connected to a server + expect(serversHeartbeatOccurred.keys()).to.deep.contain(ev.connectionId); + serversHeartbeatOccurred[ev.connectionId] = true; + }); + + // ensure there is enough time for the heartbeatFrequencyMS for the event to occur + await sleep(heartbeatFrequencyMS * 10); + + // all servers should have had a heartbeat event + expect(serversHeartbeatOccurred.values().filter(r => r !== true)).to.be.empty; + + // close the client + await client.close(); + + // upon close, assert rttPinger socket is cleaned up + const newSocketsAfterClose = rttSocketReport(); + expect(newSocketsAfterClose).to.have.length(0); + } + await runScriptAndGetProcessInfo( - 'socket-connection-monitoring', - config, - async function run({ MongoClient, uri, log, expect }) { - const client = new MongoClient(uri, { serverMonitoringMode: 'stream', minHeartbeatFrequencyMS: 10000 }); - await client.connect(); - - // returns all active tcp endpoints - const connectionMonitoringReport = () => process.report.getReport().libuv.filter(r => r.type === 'tcp' && r.is_active).map(r => r.remoteEndpoint); - - log({report: connectionMonitoringReport() }); - // assert socket creation - const servers = client.topology?.s.servers; - for (const server of servers) { - let { host, port } = server[1].s.description.hostAddress; - chai.expect(connectionMonitoringReport()).to.deep.include({ host, port }); - } - - await client.close(); - - // assert socket destruction - for (const server of servers) { - let { host, port } = server[1].s.description.hostAddress; - chai.expect(connectionMonitoringReport()).to.not.deep.include({ host, port }); - } - } - ); + 'socket-connection-monitoring', + 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 be8804b041..95cbda2af1 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -24,6 +24,7 @@ export type ProcessResourceTestFunction = (options: { expect: typeof expect; ClientEncryption?: typeof ClientEncryption; BSON?: typeof BSON; + sleep?: typeof Promise; }) => Promise; const HEAP_RESOURCE_SCRIPT_PATH = path.resolve( diff --git a/test/tools/fixtures/process_resource_script.in.js b/test/tools/fixtures/process_resource_script.in.js index 9e3a65806d..dee48e9142 100644 --- a/test/tools/fixtures/process_resource_script.in.js +++ b/test/tools/fixtures/process_resource_script.in.js @@ -20,6 +20,9 @@ const logFile = 'logs.txt'; const run = func; +const { promisify } = require('node:util'); +const sleep = promisify(setTimeout); + /** * * Returns an array containing the new resources created after script started. @@ -71,7 +74,7 @@ async function main() { process.on('beforeExit', () => { log({ beforeExitHappened: true }); }); - await run({ MongoClient, uri, log, expect, ClientEncryption, BSON }); + await run({ MongoClient, uri, log, expect, ClientEncryption, BSON, sleep }); log({ newLibuvResources: getNewLibuvResourceArray() }); } From 958835d33c27ffe25ab3b39e3e3ef2797aefd23a Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Thu, 2 Jan 2025 13:10:05 -0500 Subject: [PATCH 25/43] initial commit --- .../node-specific/client_close.test.ts | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 9a46288af2..3c5058114a 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -142,10 +142,6 @@ describe('MongoClient.close() Integration', () => { ); }); }); - - describe('Server resource: connection thread', () => { - it('no connection threads remain after client.close()', metadata, async () => {}); - }); }); describe('RTT Pinger', () => { @@ -223,12 +219,6 @@ describe('MongoClient.close() Integration', () => { }); }); }); - - describe('Server resource: connection thread', () => { - describe('when rtt monitoring is turned on', () => { - it('no server-side connection threads remain after client.close()', async () => {}); - }); - }); }); }); }); @@ -257,16 +247,6 @@ describe('MongoClient.close() Integration', () => { it('no sockets remain after client.close()', async () => {}); }); }); - - describe('Server-side resource: Connection thread', () => { - describe('after a connection is checked out', () => { - it('no connection threads remain after client.close()', async () => {}); - }); - - describe('after a minPoolSize has been set on the ConnectionPool', () => { - it('no connection threads remain after client.close()', async () => {}); - }); - }); }); }); }); From d9ed22a859f21fca657270c436c8ade3e65a1f0c Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Thu, 2 Jan 2025 13:30:53 -0500 Subject: [PATCH 26/43] removed resources that we are no longer integration testing - connection thread, docker fs access, client encryption --- .../node-specific/client_close.test.ts | 77 ------------------- 1 file changed, 77 deletions(-) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 771259996e..15f5db9327 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -31,13 +31,6 @@ describe.skip('MongoClient.close() Integration', () => { }); }); - describe('Node.js resource: .dockerenv file access', () => { - describe('when client is connecting and fs.access stalls while accessing .dockerenv file', () => { - it('the file access is not interrupted by client.close()', async function () {}).skipReason = - 'TODO(NODE-6624): Align Client.Close Test Cases with Finalized Design'; - }); - }); - describe('MongoClientAuthProviders', () => { describe('Node.js resource: Token file read', () => { let tokenFileEnvCache; @@ -110,10 +103,6 @@ describe.skip('MongoClient.close() Integration', () => { describe('Node.js resource: Socket', () => { it('no sockets remain after client.close()', metadata, async function () {}); }); - - describe('Server resource: connection thread', () => { - it('no connection threads remain after client.close()', metadata, async () => {}); - }); }); describe('RTT Pinger', () => { @@ -131,12 +120,6 @@ describe.skip('MongoClient.close() Integration', () => { it('no sockets remain after client.close()', async () => {}); }); }); - - describe('Server resource: connection thread', () => { - describe('when rtt monitoring is turned on', () => { - it('no server-side connection threads remain after client.close()', async () => {}); - }); - }); }); }); }); @@ -165,16 +148,6 @@ describe.skip('MongoClient.close() Integration', () => { it('no sockets remain after client.close()', async () => {}); }); }); - - describe('Server-side resource: Connection thread', () => { - describe('after a connection is checked out', () => { - it('no connection threads remain after client.close()', async () => {}); - }); - - describe('after a minPoolSize has been set on the ConnectionPool', () => { - it('no connection threads remain after client.close()', async () => {}); - }); - }); }); }); }); @@ -319,56 +292,6 @@ describe.skip('MongoClient.close() Integration', () => { }); }); - describe('ClientEncryption', () => { - describe('KMS Request', () => { - describe('Node.js resource: TLS file read', () => { - describe('when KMSRequest reads an infinite TLS file read', () => { - it('the file read is interrupted by client.close()', async () => { - await runScriptAndGetProcessInfo( - 'tls-file-read-client-encryption', - config, - async function run({ MongoClient, uri, expect, ClientEncryption, BSON }) { - const infiniteFile = '/dev/zero'; - const kmsProviders = BSON.EJSON.parse(process.env.CSFLE_KMS_PROVIDERS); - const masterKey = { - region: 'us-east-1', - key: 'arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0' - }; - const provider = 'aws'; - - const keyVaultClient = new MongoClient(uri); - await keyVaultClient.connect(); - - await keyVaultClient.db('keyvault').collection('datakeys'); - const clientEncryption = new ClientEncryption(keyVaultClient, { - keyVaultNamespace: 'keyvault.datakeys', - kmsProviders, - tlsOptions: { aws: { tlsCAFile: infiniteFile } } - }); - - const dataKeyPromise = clientEncryption.createDataKey(provider, { masterKey }); - - expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); - - await keyVaultClient.close(); - - expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'); - - const err = await dataKeyPromise.catch(e => e); - expect(err).to.exist; - expect(err.errmsg).to.contain('Error in KMS response'); - } - ); - }); - }); - }); - - describe('Node.js resource: Socket', () => { - it('no sockets remain after client.close()', async () => {}); - }); - }); - }); - describe('Server resource: Cursor', () => { describe('after cursors are created', () => { it('all active server-side cursors are closed by client.close()', async function () {}); From 425cf3f2198838a3fa6d0d307d45538c613e653e Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Thu, 2 Jan 2025 15:17:21 -0500 Subject: [PATCH 27/43] temp --- .../node-specific/client_close.test.ts | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 526b72ec29..8ecfa0470d 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -1,7 +1,10 @@ /* eslint-disable @typescript-eslint/no-empty-function */ +import { expect } from 'chai'; +import { MongoClient } from '../../mongodb'; import { type TestConfiguration } from '../../tools/runner/config'; import { runScriptAndGetProcessInfo } from './resource_tracking_script_builder'; +import { sleep } from '../../tools/utils'; describe('MongoClient.close() Integration', () => { // note: these tests are set-up in accordance of the resource ownership tree @@ -105,7 +108,7 @@ describe('MongoClient.close() Integration', () => { await runScriptAndGetProcessInfo( 'socket-connection-monitoring', config, - async function run({ MongoClient, uri, log, expect }) { + async function run({ MongoClient, uri, expect }) { const client = new MongoClient(uri); await client.connect(); @@ -116,7 +119,6 @@ describe('MongoClient.close() Integration', () => { .libuv.filter(r => r.type === 'tcp' && r.is_active) .map(r => r.remoteEndpoint); - log({ report: connectionMonitoringReport() }); // assert socket creation const servers = client.topology?.s.servers; for (const server of servers) { @@ -150,7 +152,7 @@ describe('MongoClient.close() Integration', () => { describe('Node.js resource: Socket', () => { describe('when rtt monitoring is turned on', () => { it('no sockets remain after client.close()', async () => { - const run = async function({ MongoClient, uri, log, expect, sleep }) { + const run = async function({ MongoClient, uri, expect, sleep }) { const heartbeatFrequencyMS = 100; const client = new MongoClient(uri, { serverMonitoringMode: 'stream', @@ -233,7 +235,20 @@ describe('MongoClient.close() Integration', () => { describe('Connection', () => { describe('Node.js resource: Socket', () => { describe('after a connection is checked out', () => { - it('no sockets remain after client.close()', async () => {}); + it.only('no sockets remain after client.close()', async function () { + const run = async function ({ MongoClient, uri, log, expect}) { + const options = { minPoolSize: 2, heartbeatFrequencyMS: 10000, minHeartbeatFrequencyMS: 10000 }; + const client = new MongoClient(uri, options); + await client.connect(); + const connectionMonitoringReport = () => + process.report + .getReport() + .libuv.filter(r => r.type === 'tcp' && r.is_active); + log('post connect', connectionMonitoringReport()); + await client.close(); + } + await run({ MongoClient, uri: config.uri, log: console.log, expect }); + }); }); describe('after a minPoolSize has been set on the ConnectionPool', () => { From 83b5685ccd9c6630939d45878b9bd3a6adc7bda7 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Thu, 2 Jan 2025 16:30:27 -0500 Subject: [PATCH 28/43] requested changes: add in exitCode message, skip unimplemented tests, fix options --- .../node-specific/client_close.test.ts | 43 ++++++++++--------- .../resource_tracking_script_builder.ts | 8 +++- .../fixtures/process_resource_script.in.js | 4 +- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 15f5db9327..c6bc568ee8 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-empty-function */ - import { type TestConfiguration } from '../../tools/runner/config'; import { runScriptAndGetProcessInfo } from './resource_tracking_script_builder'; @@ -20,12 +19,14 @@ describe.skip('MongoClient.close() Integration', () => { config, async function run({ MongoClient, uri, expect }) { const infiniteFile = '/dev/zero'; - const client = new MongoClient(uri, { tlsCertificateKeyFile: infiniteFile }); - client.connect(); + const client = new MongoClient(uri, { tls: true, tlsCertificateKeyFile: infiniteFile }); + const connectPromise = client.connect(); expect(process.getActiveResourcesInfo()).to.include('FSReqPromise'); await client.close(); expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'); - } + const err = await connectPromise.catch(e => e); + expect(err).to.exist; + } ); }); }); @@ -74,7 +75,7 @@ describe.skip('MongoClient.close() Integration', () => { describe('Topology', () => { describe('Node.js resource: Server Selection Timer', () => { describe('after a Topology is created through client.connect()', () => { - it('server selection timers are cleaned up by client.close()', async () => {}); + it.skip('server selection timers are cleaned up by client.close()', async () => {}); }); }); @@ -90,25 +91,25 @@ describe.skip('MongoClient.close() Integration', () => { describe('MonitorInterval', () => { describe('Node.js resource: Timer', () => { describe('after a new monitor is made', () => { - it('monitor interval timer is cleaned up by client.close()', async () => {}); + it.skip('monitor interval timer is cleaned up by client.close()', async () => {}); }); describe('after a heartbeat fails', () => { - it('the new monitor interval timer is cleaned up by client.close()', async () => {}); + it.skip('the new monitor interval timer is cleaned up by client.close()', async () => {}); }); }); }); describe('Connection Monitoring', () => { 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 () {}); }); }); describe('RTT Pinger', () => { describe('Node.js resource: Timer', () => { describe('after entering monitor streaming mode ', () => { - it('the rtt pinger timer is cleaned up by client.close()', async () => { + it.skip('the rtt pinger timer is cleaned up by client.close()', async () => { // helloReply has a topologyVersion defined }); }); @@ -117,7 +118,7 @@ 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()', async () => {}); + it.skip('no sockets remain after client.close()', async () => {}); }); }); }); @@ -127,25 +128,25 @@ 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 () => {}); + it.skip('the minPoolSize timer is cleaned up by client.close()', async () => {}); }); }); describe('Node.js resource: checkOut Timer', () => { // waitQueueTimeoutMS describe('after new connection pool is created', () => { - it('the wait queue timer is cleaned up by client.close()', async () => {}); + it.skip('the wait queue timer is cleaned up by client.close()', async () => {}); }); }); describe('Connection', () => { describe('Node.js resource: Socket', () => { describe('after a connection is checked out', () => { - it('no sockets remain after client.close()', async () => {}); + it.skip('no sockets remain after client.close()', async () => {}); }); describe('after a minPoolSize has been set on the ConnectionPool', () => { - it('no sockets remain after client.close()', async () => {}); + it.skip('no sockets remain after client.close()', async () => {}); }); }); }); @@ -155,7 +156,7 @@ describe.skip('MongoClient.close() Integration', () => { describe('SrvPoller', () => { describe('Node.js resource: Timer', () => { describe('after SRVPoller is created', () => { - it('timers are cleaned up by client.close()', async () => {}); + it.skip('timers are cleaned up by client.close()', async () => {}); }); }); }); @@ -164,13 +165,13 @@ describe.skip('MongoClient.close() Integration', () => { describe('ClientSession (Implicit)', () => { 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()', async function () {}); + it.skip('the server-side ServerSession is cleaned up by client.close()', async function () {}); }); }); describe('Server resource: Transactions', () => { describe('after a clientSession is implicitly created and used', () => { - it('the server-side transaction is cleaned up by client.close()', async function () {}); + it.skip('the server-side transaction is cleaned up by client.close()', async function () {}); }); }); }); @@ -178,13 +179,13 @@ describe.skip('MongoClient.close() Integration', () => { describe('ClientSession (Explicit)', () => { describe('Server resource: LSID/ServerSession', () => { describe('after a clientSession is created and used', () => { - it('the server-side ServerSession is cleaned up by client.close()', async function () {}); + it.skip('the server-side ServerSession is cleaned up by client.close()', async function () {}); }); }); describe('Server resource: Transactions', () => { describe('after a clientSession is created and used', () => { - it('the server-side transaction is cleaned up by client.close()', async function () {}); + it.skip('the server-side transaction is cleaned up by client.close()', async function () {}); }); }); }); @@ -287,14 +288,14 @@ describe.skip('MongoClient.close() Integration', () => { }); describe('Node.js resource: Socket', () => { - it('no sockets remain after client.close()', metadata, async () => {}); + it.skip('no sockets remain after client.close()', metadata, async () => {}); }); }); }); describe('Server resource: Cursor', () => { describe('after cursors are created', () => { - it('all active server-side cursors are closed by client.close()', async function () {}); + it.skip('all active server-side cursors are closed by client.close()', async function () {}); }); }); }); diff --git a/test/integration/node-specific/resource_tracking_script_builder.ts b/test/integration/node-specific/resource_tracking_script_builder.ts index be8804b041..60ef64b742 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -3,7 +3,7 @@ import { on, once } from 'node:events'; import { readFile, unlink, writeFile } from 'node:fs/promises'; import * as path from 'node:path'; -import { expect } from 'chai'; +import { AssertionError, expect } from 'chai'; import { parseSnapshot } from 'v8-heapsnapshot'; import { type BSON, type ClientEncryption, type MongoClient } from '../../mongodb'; @@ -187,7 +187,11 @@ export async function runScriptAndGetProcessInfo( await unlink(logFile); // assertions about exit status - expect(exitCode, 'process should have exited with zero').to.equal(0); + if (exitCode) { + const assertionError = new AssertionError(messages.error.message); + assertionError.stack = messages.error.stack + new Error().stack.slice('Error'.length); + throw assertionError; + } // assertions about resource status expect(messages.beforeExitHappened).to.be.true; diff --git a/test/tools/fixtures/process_resource_script.in.js b/test/tools/fixtures/process_resource_script.in.js index 9e3a65806d..79c0968054 100644 --- a/test/tools/fixtures/process_resource_script.in.js +++ b/test/tools/fixtures/process_resource_script.in.js @@ -78,12 +78,14 @@ async function main() { main() .then(() => {}) .catch(e => { - log({ exitCode: 1, error: util.inspect(e) }); + log({ exitCode: 1, error: { message: e.message, stack: e.stack } }); }); setTimeout(() => { // this means something was in the event loop such that it hung for more than 10 seconds // so we kill the process + log({ newLibuvResources: getNewLibuvResourceArray() }); + log({ exitCode: 99, error: { message: 'Process timed out: resources remain in the event loop.' } }); process.exit(99); // using `unref` will ensure this setTimeout call is not a resource / does not keep the event loop running }, 10000).unref(); From a82a7fdf71ecd198d3677ec4da5ce73d2c0ebca7 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Thu, 2 Jan 2025 17:17:55 -0500 Subject: [PATCH 29/43] temp --- logs.txt | 15 ++ .../node-specific/client_close.test.ts | 174 ++++++++++-------- .../resource_tracking_script_builder.ts | 2 +- .../fixtures/process_resource_script.in.js | 9 +- 4 files changed, 115 insertions(+), 85 deletions(-) create mode 100644 logs.txt diff --git a/logs.txt b/logs.txt new file mode 100644 index 0000000000..1dc196bb1c --- /dev/null +++ b/logs.txt @@ -0,0 +1,15 @@ +{"monreport":[{"host":"localhost","port":31000},{"host":"localhost","port":31001},{"host":"localhost","port":31002},{"host":"localhost","port":31003},{"host":"localhost","port":31000}]} +{"relevantHostAddresses":[{"host":"localhost","port":31000},{"host":"localhost","port":31000}]} +{"monreport":[{"host":"localhost","port":31000},{"host":"localhost","port":31001},{"host":"localhost","port":31002},{"host":"localhost","port":31003},{"host":"localhost","port":31000}]} +{"relevantHostAddresses":[{"host":"localhost","port":31001}]} +{"exitCode":1,"error":"AssertionError: expected [ { host: 'localhost', port: 31001 } ] to have a length at least 2 but got 1\n at run (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:24:80)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async main (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:103:3) {\n showDiff: true,\n actual: 1,\n expected: 2\n}"} +{"monreport":[{"host":"localhost","port":31000},{"host":"localhost","port":31001},{"host":"localhost","port":31002},{"host":"localhost","port":31003},{"host":"localhost","port":31000}]} +{"relevantHostAddresses":[{"host":"localhost","port":31000},{"host":"localhost","port":31000}]} +{"monreport":[{"host":"localhost","port":31000},{"host":"localhost","port":31001},{"host":"localhost","port":31002},{"host":"localhost","port":31003},{"host":"localhost","port":31000}]} +{"relevantHostAddresses":[{"host":"localhost","port":31001}]} +{"exitCode":1,"error":"AssertionError: expected [ { host: 'localhost', port: 31001 } ] to have a length at least 2 but got 1\n at run (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:24:80)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async main (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:103:3) {\n showDiff: true,\n actual: 1,\n expected: 2\n}"} +{"exitCode":1,"error":"ReferenceError: servers is not defined\n at run (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:28:54)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async main (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:103:3)"} +{"beforeExitHappened":true} +{"monreport":[{"host":"localhost","port":31000},{"host":"localhost","port":31001},{"host":"localhost","port":31002},{"host":"localhost","port":31003},{"host":"localhost","port":31000}]} +{"exitCode":1,"error":"ReferenceError: servers is not defined\n at run (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:29:54)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async main (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:104:3)"} +{"beforeExitHappened":true} diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 8ecfa0470d..f1f89d6e1b 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -1,10 +1,10 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import { expect } from 'chai'; + import { MongoClient } from '../../mongodb'; import { type TestConfiguration } from '../../tools/runner/config'; import { runScriptAndGetProcessInfo } from './resource_tracking_script_builder'; -import { sleep } from '../../tools/utils'; describe('MongoClient.close() Integration', () => { // note: these tests are set-up in accordance of the resource ownership tree @@ -104,11 +104,11 @@ describe('MongoClient.close() Integration', () => { describe('Connection Monitoring', () => { describe('Node.js resource: Socket', () => { - it('no sockets remain after client.close()', metadata, async function () { + it.only('no sockets remain after client.close()', metadata, async function () { await runScriptAndGetProcessInfo( 'socket-connection-monitoring', config, - async function run({ MongoClient, uri, expect }) { + async function run({ MongoClient, uri, log, expect }) { const client = new MongoClient(uri); await client.connect(); @@ -119,12 +119,19 @@ describe('MongoClient.close() Integration', () => { .libuv.filter(r => r.type === 'tcp' && r.is_active) .map(r => r.remoteEndpoint); - // assert socket creation - const servers = client.topology?.s.servers; + log({ monreport: connectionMonitoringReport() }); + // assert socket creation + // there should be two sockets for each server: + // client connection socket + // monitor socket + /* const servers = client.topology?.s.servers; for (const server of servers) { + log({ monreport: connectionMonitoringReport() }); const { host, port } = server[1].s.description.hostAddress; - expect(connectionMonitoringReport()).to.deep.include({ host, port }); - } + const relevantHostAddresses = connectionMonitoringReport().filter(r => r.host === host && r.port === port); + log({ relevantHostAddresses }); + expect(relevantHostAddresses).length.to.be.gte(2); + } */ await client.close(); @@ -152,65 +159,59 @@ describe('MongoClient.close() Integration', () => { describe('Node.js resource: Socket', () => { describe('when rtt monitoring is turned on', () => { it('no sockets remain after client.close()', async () => { - const run = async function({ MongoClient, uri, expect, sleep }) { - const heartbeatFrequencyMS = 100; - const client = new MongoClient(uri, { - serverMonitoringMode: 'stream', - heartbeatFrequencyMS - }); - await client.connect(); - - const servers = Array.from(client.topology.s.servers.keys()); - - // a hashmap of - const serversHeartbeatOccurred = servers.reduce( - (acc, hostname) => ({ ...acc, [hostname]: false }), - {} - ); - - const activeSocketsReport = () => - process.report - .getReport() - .libuv.filter(r => r.type === 'tcp' && r.is_active); - - const socketsAddressesBeforeHeartbeat = activeSocketsReport().map( - r => r.address - ); - - const rttSocketReport = () => - activeSocketsReport() - .filter(r => !socketsAddressesBeforeHeartbeat.includes(r.address)) - .map(r => r.remoteEndpoint.host + ':' + r.remoteEndpoint.port); - - client.on('serverHeartbeatSucceeded', async ev => { - // assert creation of rttPinger socket - const newSocketsAfterHeartbeat = rttSocketReport(); - expect(newSocketsAfterHeartbeat).to.deep.contain(ev.connectionId); - - // assert rttPinger socket is connected to a server - expect(serversHeartbeatOccurred.keys()).to.deep.contain(ev.connectionId); - serversHeartbeatOccurred[ev.connectionId] = true; - }); - - // ensure there is enough time for the heartbeatFrequencyMS for the event to occur - await sleep(heartbeatFrequencyMS * 10); - - // all servers should have had a heartbeat event - expect(serversHeartbeatOccurred.values().filter(r => r !== true)).to.be.empty; - - // close the client - await client.close(); - - // upon close, assert rttPinger socket is cleaned up - const newSocketsAfterClose = rttSocketReport(); - expect(newSocketsAfterClose).to.have.length(0); - } - - await runScriptAndGetProcessInfo( - 'socket-connection-monitoring', - config, - run - ); + const run = async function ({ MongoClient, uri, expect, sleep }) { + const heartbeatFrequencyMS = 100; + const client = new MongoClient(uri, { + serverMonitoringMode: 'stream', + heartbeatFrequencyMS + }); + await client.connect(); + + const servers = Array.from(client.topology.s.servers.keys()); + + // a hashmap of + const serversHeartbeatOccurred = servers.reduce( + (acc, hostname) => ({ ...acc, [hostname]: false }), + {} + ); + + const activeSocketsReport = () => + process.report.getReport().libuv.filter(r => r.type === 'tcp' && r.is_active); + + const socketsAddressesBeforeHeartbeat = activeSocketsReport().map( + r => r.address + ); + + const rttSocketReport = () => + activeSocketsReport() + .filter(r => !socketsAddressesBeforeHeartbeat.includes(r.address)) + .map(r => r.remoteEndpoint.host + ':' + r.remoteEndpoint.port); + + client.on('serverHeartbeatSucceeded', async ev => { + // assert creation of rttPinger socket + const newSocketsAfterHeartbeat = rttSocketReport(); + expect(newSocketsAfterHeartbeat).to.deep.contain(ev.connectionId); + + // assert rttPinger socket is connected to a server + expect(serversHeartbeatOccurred.keys()).to.deep.contain(ev.connectionId); + serversHeartbeatOccurred[ev.connectionId] = true; + }); + + // ensure there is enough time for the heartbeatFrequencyMS for the event to occur + await sleep(heartbeatFrequencyMS * 10); + + // all servers should have had a heartbeat event + expect(serversHeartbeatOccurred.values().filter(r => r !== true)).to.be.empty; + + // close the client + await client.close(); + + // upon close, assert rttPinger socket is cleaned up + const newSocketsAfterClose = rttSocketReport(); + expect(newSocketsAfterClose).to.have.length(0); + }; + + await runScriptAndGetProcessInfo('socket-connection-monitoring', config, run); }); }); }); @@ -235,24 +236,39 @@ describe('MongoClient.close() Integration', () => { describe('Connection', () => { describe('Node.js resource: Socket', () => { describe('after a connection is checked out', () => { - it.only('no sockets remain after client.close()', async function () { - const run = async function ({ MongoClient, uri, log, expect}) { - const options = { minPoolSize: 2, heartbeatFrequencyMS: 10000, minHeartbeatFrequencyMS: 10000 }; + it('no sockets remain after client.close()', async function () { + }); + }); + + describe('after a minPoolSize has been set on the ConnectionPool', () => { + it('no sockets remain after client.close()', async function () { + const run = async function ({ MongoClient, uri, log, expect }) { + log({hi: 1}); + const options = { minPoolSize: 2 }; const client = new MongoClient(uri, options); await client.connect(); const connectionMonitoringReport = () => - process.report - .getReport() - .libuv.filter(r => r.type === 'tcp' && r.is_active); - log('post connect', connectionMonitoringReport()); + process.report.getReport().libuv.filter(r => r.type === 'tcp' && r.is_active).map(r => r.remoteEndpoint); + + // assert socket creation + // there should be three sockets for each server: client connection socket, monitor socket, pool size monitoring socket + const servers = client.topology?.s.servers; + for (const server of servers) { + log({ monreport: connectionMonitoringReport() }); + const { host, port } = server[1].s.description.hostAddress; + const relevantHostAddresses = connectionMonitoringReport().filter(r => r.host === host && r.port === port); + log({relevantHostAddresses}); + expect(relevantHostAddresses).length.to.be.gte(3); + } await client.close(); - } - await run({ MongoClient, uri: config.uri, log: console.log, expect }); - }); - }); + }; - describe('after a minPoolSize has been set on the ConnectionPool', () => { - it('no sockets remain after client.close()', async () => {}); + await runScriptAndGetProcessInfo( + 'socket-minPoolSize', + 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 95cbda2af1..d3ce2ad947 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -185,7 +185,7 @@ export async function runScriptAndGetProcessInfo( // delete temporary files await unlink(scriptName); - await unlink(logFile); + // await unlink(logFile); // assertions about exit status expect(exitCode, 'process should have exited with zero').to.equal(0); diff --git a/test/tools/fixtures/process_resource_script.in.js b/test/tools/fixtures/process_resource_script.in.js index dee48e9142..79c0968054 100644 --- a/test/tools/fixtures/process_resource_script.in.js +++ b/test/tools/fixtures/process_resource_script.in.js @@ -20,9 +20,6 @@ const logFile = 'logs.txt'; const run = func; -const { promisify } = require('node:util'); -const sleep = promisify(setTimeout); - /** * * Returns an array containing the new resources created after script started. @@ -74,19 +71,21 @@ async function main() { process.on('beforeExit', () => { log({ beforeExitHappened: true }); }); - await run({ MongoClient, uri, log, expect, ClientEncryption, BSON, sleep }); + await run({ MongoClient, uri, log, expect, ClientEncryption, BSON }); log({ newLibuvResources: getNewLibuvResourceArray() }); } main() .then(() => {}) .catch(e => { - log({ exitCode: 1, error: util.inspect(e) }); + log({ exitCode: 1, error: { message: e.message, stack: e.stack } }); }); setTimeout(() => { // this means something was in the event loop such that it hung for more than 10 seconds // so we kill the process + log({ newLibuvResources: getNewLibuvResourceArray() }); + log({ exitCode: 99, error: { message: 'Process timed out: resources remain in the event loop.' } }); process.exit(99); // using `unref` will ensure this setTimeout call is not a resource / does not keep the event loop running }, 10000).unref(); From 454859ea7e822679c538dd9239b4e1a6d909fd46 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Thu, 2 Jan 2025 18:21:11 -0500 Subject: [PATCH 30/43] requested changes: fix exitCode --- .../node-specific/client_close.test.ts | 2 +- .../resource_tracking_script_builder.ts | 12 +++--- .../fixtures/process_resource_script.in.js | 37 ++++++++++++++++--- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index c6bc568ee8..7f9a3f11a3 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -26,7 +26,7 @@ describe.skip('MongoClient.close() Integration', () => { expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise'); const err = await connectPromise.catch(e => e); expect(err).to.exist; - } + } ); }); }); diff --git a/test/integration/node-specific/resource_tracking_script_builder.ts b/test/integration/node-specific/resource_tracking_script_builder.ts index 60ef64b742..24f5a5dab4 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -166,7 +166,7 @@ export async function runScriptAndGetProcessInfo( func ); await writeFile(scriptName, scriptContent, { encoding: 'utf8' }); - const logFile = 'logs.txt'; + const logFile = name + '.logs.txt'; const script = spawn(process.argv[0], [scriptName], { stdio: ['ignore', 'ignore', 'ignore'] }); @@ -187,10 +187,12 @@ export async function runScriptAndGetProcessInfo( await unlink(logFile); // assertions about exit status - if (exitCode) { - const assertionError = new AssertionError(messages.error.message); - assertionError.stack = messages.error.stack + new Error().stack.slice('Error'.length); - throw assertionError; + if (exitCode) { + const assertionError = new AssertionError( + messages.error.message + '\n\t' + JSON.stringify(messages.error.resources) + ); + assertionError.stack = messages.error.stack + new Error().stack.slice('Error'.length); + throw assertionError; } // assertions about resource status diff --git a/test/tools/fixtures/process_resource_script.in.js b/test/tools/fixtures/process_resource_script.in.js index 79c0968054..845afc5d9b 100644 --- a/test/tools/fixtures/process_resource_script.in.js +++ b/test/tools/fixtures/process_resource_script.in.js @@ -16,13 +16,13 @@ const { expect } = require('chai'); const { setTimeout } = require('timers'); let originalReport; -const logFile = 'logs.txt'; +const logFile = scriptName + '.logs.txt'; const run = func; /** * - * Returns an array containing the new resources created after script started. + * Returns an array containing the new libuv resources created after script started. * A new resource is something that will keep the event loop running. * * In order to be counted as a new resource, a resource MUST: @@ -59,6 +59,27 @@ function getNewLibuvResourceArray() { return currReport; } +/** + * Returns an object of the new resources created after script started. + * + * + * In order to be counted as a new resource, a resource MUST either: + * - Meet the criteria to be returned by our helper utility `getNewLibuvResourceArray()` + * OR + * - Be returned by `process.getActiveResourcesInfo() + * + * The reason we are using both methods to detect active resources is: + * - `process.report.getReport().libuv` does not detect active requests (such as timers or file reads) accurately + * - `process.getActiveResourcesInfo()` does not contain enough server information we need for our assertions + * + */ +function getNewResources() { + return { + libuvResources: getNewLibuvResourceArray(), + activeResources: process.getActiveResourcesInfo() + }; +} + // A log function for debugging function log(message) { // remove outer parentheses for easier parsing @@ -72,20 +93,24 @@ async function main() { log({ beforeExitHappened: true }); }); await run({ MongoClient, uri, log, expect, ClientEncryption, BSON }); - log({ newLibuvResources: getNewLibuvResourceArray() }); } main() .then(() => {}) .catch(e => { - log({ exitCode: 1, error: { message: e.message, stack: e.stack } }); + log({ error: { message: e.message, stack: e.stack, resources: getNewResources() } }); + process.exit(1); }); setTimeout(() => { // this means something was in the event loop such that it hung for more than 10 seconds // so we kill the process - log({ newLibuvResources: getNewLibuvResourceArray() }); - log({ exitCode: 99, error: { message: 'Process timed out: resources remain in the event loop.' } }); + log({ + error: { + message: 'Process timed out: resources remain in the event loop', + resources: getNewResources() + } + }); process.exit(99); // using `unref` will ensure this setTimeout call is not a resource / does not keep the event loop running }, 10000).unref(); From 56662b8676265d9c18ed0661eac6110a6f8f634d Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Thu, 2 Jan 2025 18:29:35 -0500 Subject: [PATCH 31/43] remove extra file --- logs.txt | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 logs.txt diff --git a/logs.txt b/logs.txt deleted file mode 100644 index 1dc196bb1c..0000000000 --- a/logs.txt +++ /dev/null @@ -1,15 +0,0 @@ -{"monreport":[{"host":"localhost","port":31000},{"host":"localhost","port":31001},{"host":"localhost","port":31002},{"host":"localhost","port":31003},{"host":"localhost","port":31000}]} -{"relevantHostAddresses":[{"host":"localhost","port":31000},{"host":"localhost","port":31000}]} -{"monreport":[{"host":"localhost","port":31000},{"host":"localhost","port":31001},{"host":"localhost","port":31002},{"host":"localhost","port":31003},{"host":"localhost","port":31000}]} -{"relevantHostAddresses":[{"host":"localhost","port":31001}]} -{"exitCode":1,"error":"AssertionError: expected [ { host: 'localhost', port: 31001 } ] to have a length at least 2 but got 1\n at run (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:24:80)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async main (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:103:3) {\n showDiff: true,\n actual: 1,\n expected: 2\n}"} -{"monreport":[{"host":"localhost","port":31000},{"host":"localhost","port":31001},{"host":"localhost","port":31002},{"host":"localhost","port":31003},{"host":"localhost","port":31000}]} -{"relevantHostAddresses":[{"host":"localhost","port":31000},{"host":"localhost","port":31000}]} -{"monreport":[{"host":"localhost","port":31000},{"host":"localhost","port":31001},{"host":"localhost","port":31002},{"host":"localhost","port":31003},{"host":"localhost","port":31000}]} -{"relevantHostAddresses":[{"host":"localhost","port":31001}]} -{"exitCode":1,"error":"AssertionError: expected [ { host: 'localhost', port: 31001 } ] to have a length at least 2 but got 1\n at run (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:24:80)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async main (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:103:3) {\n showDiff: true,\n actual: 1,\n expected: 2\n}"} -{"exitCode":1,"error":"ReferenceError: servers is not defined\n at run (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:28:54)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async main (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:103:3)"} -{"beforeExitHappened":true} -{"monreport":[{"host":"localhost","port":31000},{"host":"localhost","port":31001},{"host":"localhost","port":31002},{"host":"localhost","port":31003},{"host":"localhost","port":31000}]} -{"exitCode":1,"error":"ReferenceError: servers is not defined\n at run (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:29:54)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async main (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:104:3)"} -{"beforeExitHappened":true} From 9ec5b00f290aa14e57fc116484ac63612492a7a5 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Fri, 3 Jan 2025 11:08:47 -0500 Subject: [PATCH 32/43] temp --- socket-connection-monitoring.logs.txt | 30 ++++++++ .../node-specific/client_close.test.ts | 73 ++++++++++--------- 2 files changed, 69 insertions(+), 34 deletions(-) create mode 100644 socket-connection-monitoring.logs.txt diff --git a/socket-connection-monitoring.logs.txt b/socket-connection-monitoring.logs.txt new file mode 100644 index 0000000000..9cd5a20717 --- /dev/null +++ b/socket-connection-monitoring.logs.txt @@ -0,0 +1,30 @@ + +{} +{"relevantHostAddresses":[{"host":"localhost","port":31000},{"host":"localhost","port":31000}]} +{} +{"relevantHostAddresses":[{"host":"localhost","port":31001}]} +{} +{"relevantHostAddresses":[{"host":"localhost","port":31002}]} +{} +{"relevantHostAddresses":[{"host":"localhost","port":31003}]} +{"beforeExitHappened":true} +{"error":{"message":"Converting circular structure to JSON\n --> starting at object with constructor 'MongoClient'\n | property 's' -> object with constructor 'Object'\n | property 'sessionPool' -> object with constructor 'ServerSessionPool'\n --- property 'client' closes the circle","stack":"TypeError: Converting circular structure to JSON\n --> starting at object with constructor 'MongoClient'\n | property 's' -> object with constructor 'Object'\n | property 'sessionPool' -> object with constructor 'ServerSessionPool'\n --- property 'client' closes the circle\n at JSON.stringify ()\n at log (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:112:29)\n at run (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:20:37)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async main (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:121:3)","resources":{"libuvResources":[{"type":"tcp","is_active":true,"is_referenced":true,"address":"0x000000012981e920","localEndpoint":{"host":"localhost","port":64049},"remoteEndpoint":{"host":"localhost","port":31000},"sendBufferSize":146808,"recvBufferSize":406960,"fd":24,"writeQueueSize":0,"readable":true,"writable":true},{"type":"tcp","is_active":true,"is_referenced":true,"address":"0x0000000129820140","localEndpoint":{"host":"localhost","port":64050},"remoteEndpoint":{"host":"localhost","port":31001},"sendBufferSize":146808,"recvBufferSize":406984,"fd":25,"writeQueueSize":0,"readable":true,"writable":true},{"type":"tcp","is_active":true,"is_referenced":true,"address":"0x0000000129820550","localEndpoint":{"host":"localhost","port":64051},"remoteEndpoint":{"host":"localhost","port":31002},"sendBufferSize":146808,"recvBufferSize":406984,"fd":26,"writeQueueSize":0,"readable":true,"writable":true},{"type":"tcp","is_active":true,"is_referenced":true,"address":"0x0000000129820a20","localEndpoint":{"host":"localhost","port":64052},"remoteEndpoint":{"host":"localhost","port":31003},"sendBufferSize":146808,"recvBufferSize":407095,"fd":27,"writeQueueSize":0,"readable":true,"writable":true},{"type":"tcp","is_active":true,"is_referenced":true,"address":"0x0000000129820ef0","localEndpoint":{"host":"localhost","port":64053},"remoteEndpoint":{"host":"localhost","port":31000},"sendBufferSize":146808,"recvBufferSize":406294,"fd":28,"writeQueueSize":0,"readable":true,"writable":true}],"activeResources":["TCPSocketWrap","TCPSocketWrap","TCPSocketWrap","TCPSocketWrap","TCPSocketWrap","Timeout","Timeout","Timeout","Timeout"]}}} +{"error":{"message":"Converting circular structure to JSON\n --> starting at object with constructor 'MongoClient'\n | property 's' -> object with constructor 'Object'\n | property 'sessionPool' -> object with constructor 'ServerSessionPool'\n --- property 'client' closes the circle","stack":"TypeError: Converting circular structure to JSON\n --> starting at object with constructor 'MongoClient'\n | property 's' -> object with constructor 'Object'\n | property 'sessionPool' -> object with constructor 'ServerSessionPool'\n --- property 'client' closes the circle\n at JSON.stringify ()\n at log (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:112:29)\n at run (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:20:37)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async main (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:121:3)","resources":{"libuvResources":[{"type":"tcp","is_active":true,"is_referenced":true,"address":"0x0000000106e1bb10","localEndpoint":{"host":"localhost","port":64068},"remoteEndpoint":{"host":"localhost","port":31000},"sendBufferSize":146808,"recvBufferSize":406960,"fd":24,"writeQueueSize":0,"readable":true,"writable":true},{"type":"tcp","is_active":true,"is_referenced":true,"address":"0x0000000106e1fd30","localEndpoint":{"host":"localhost","port":64069},"remoteEndpoint":{"host":"localhost","port":31001},"sendBufferSize":146808,"recvBufferSize":406984,"fd":27,"writeQueueSize":0,"readable":true,"writable":true},{"type":"tcp","is_active":true,"is_referenced":true,"address":"0x0000000106e20070","localEndpoint":{"host":"localhost","port":64070},"remoteEndpoint":{"host":"localhost","port":31002},"sendBufferSize":146808,"recvBufferSize":406984,"fd":28,"writeQueueSize":0,"readable":true,"writable":true},{"type":"tcp","is_active":true,"is_referenced":true,"address":"0x0000000106e20540","localEndpoint":{"host":"localhost","port":64071},"remoteEndpoint":{"host":"localhost","port":31003},"sendBufferSize":146808,"recvBufferSize":407095,"fd":26,"writeQueueSize":0,"readable":true,"writable":true},{"type":"tcp","is_active":true,"is_referenced":true,"address":"0x0000000106e20a10","localEndpoint":{"host":"localhost","port":64072},"remoteEndpoint":{"host":"localhost","port":31000},"sendBufferSize":146808,"recvBufferSize":406294,"fd":25,"writeQueueSize":0,"readable":true,"writable":true}],"activeResources":["TCPSocketWrap","TCPSocketWrap","TCPSocketWrap","TCPSocketWrap","TCPSocketWrap","Timeout","Timeout","Timeout","Timeout"]}}} +{"server":"[\n 'localhost:31000',\n Server {\n _events: [Object: null prototype] {\n serverHeartbeatStarted: [Function (anonymous)],\n serverHeartbeatSucceeded: [Function (anonymous)],\n serverHeartbeatFailed: [Function (anonymous)],\n commandStarted: [Function (anonymous)],\n commandSucceeded: [Function (anonymous)],\n commandFailed: [Function (anonymous)],\n connectionPoolCreated: [Function (anonymous)],\n connectionPoolReady: [Function (anonymous)],\n connectionPoolCleared: [Function (anonymous)],\n connectionPoolClosed: [Function (anonymous)],\n connectionCreated: [Function (anonymous)],\n connectionReady: [Function (anonymous)],\n connectionClosed: [Function (anonymous)],\n connectionCheckOutStarted: [Function (anonymous)],\n connectionCheckOutFailed: [Function (anonymous)],\n connectionCheckedOut: [Function (anonymous)],\n connectionCheckedIn: [Function (anonymous)],\n descriptionReceived: [Function (anonymous)]\n },\n _eventsCount: 18,\n _maxListeners: undefined,\n serverApi: undefined,\n topology: Topology {\n _events: [Object: null prototype],\n _eventsCount: 26,\n _maxListeners: undefined,\n client: [MongoClient],\n waitQueue: [List],\n s: [Object],\n mongoLogger: undefined,\n component: 'topology',\n connectionLock: undefined,\n [Symbol(kCapture)]: false\n },\n pool: ConnectionPool {\n _events: [Object: null prototype],\n _eventsCount: 15,\n _maxListeners: undefined,\n options: [Object],\n poolState: 'ready',\n server: [Circular *1],\n connections: [List],\n pending: 0,\n checkedOut: Set(0) {},\n minPoolSizeTimer: undefined,\n generation: 0,\n serviceGenerations: Map(0) {},\n connectionCounter: Object [Generator] {},\n cancellationToken: [CancellationToken],\n waitQueue: [List],\n metrics: [ConnectionPoolMetrics],\n processingWaitQueue: false,\n mongoLogger: undefined,\n component: 'connection',\n [Symbol(kCapture)]: false\n },\n s: {\n description: [ServerDescription],\n options: [Object: null prototype],\n state: 'connected',\n operationCount: 0\n },\n monitor: Monitor {\n _events: [Object: null prototype],\n _eventsCount: 4,\n _maxListeners: undefined,\n component: 'topology',\n server: [Circular *1],\n connection: [Connection],\n cancellationToken: [CancellationToken],\n monitorId: [MonitorInterval],\n s: [Object],\n address: 'localhost:31000',\n options: [Object],\n isRunningInFaasEnv: false,\n mongoLogger: undefined,\n rttSampler: [RTTSampler],\n connectOptions: [Object],\n [Symbol(kCapture)]: false\n },\n [Symbol(kCapture)]: false\n }\n]"} +{"relevantHostAddresses":[{"host":"localhost","port":31000},{"host":"localhost","port":31000}]} +{"server":"[\n 'localhost:31001',\n Server {\n _events: [Object: null prototype] {\n serverHeartbeatStarted: [Function (anonymous)],\n serverHeartbeatSucceeded: [Function (anonymous)],\n serverHeartbeatFailed: [Function (anonymous)],\n commandStarted: [Function (anonymous)],\n commandSucceeded: [Function (anonymous)],\n commandFailed: [Function (anonymous)],\n connectionPoolCreated: [Function (anonymous)],\n connectionPoolReady: [Function (anonymous)],\n connectionPoolCleared: [Function (anonymous)],\n connectionPoolClosed: [Function (anonymous)],\n connectionCreated: [Function (anonymous)],\n connectionReady: [Function (anonymous)],\n connectionClosed: [Function (anonymous)],\n connectionCheckOutStarted: [Function (anonymous)],\n connectionCheckOutFailed: [Function (anonymous)],\n connectionCheckedOut: [Function (anonymous)],\n connectionCheckedIn: [Function (anonymous)],\n descriptionReceived: [Function (anonymous)]\n },\n _eventsCount: 18,\n _maxListeners: undefined,\n serverApi: undefined,\n topology: Topology {\n _events: [Object: null prototype],\n _eventsCount: 26,\n _maxListeners: undefined,\n client: [MongoClient],\n waitQueue: [List],\n s: [Object],\n mongoLogger: undefined,\n component: 'topology',\n connectionLock: undefined,\n [Symbol(kCapture)]: false\n },\n pool: ConnectionPool {\n _events: [Object: null prototype],\n _eventsCount: 15,\n _maxListeners: undefined,\n options: [Object],\n poolState: 'ready',\n server: [Circular *1],\n connections: [List],\n pending: 0,\n checkedOut: Set(0) {},\n minPoolSizeTimer: undefined,\n generation: 0,\n serviceGenerations: Map(0) {},\n connectionCounter: Object [Generator] {},\n cancellationToken: [CancellationToken],\n waitQueue: [List],\n metrics: [ConnectionPoolMetrics],\n processingWaitQueue: false,\n mongoLogger: undefined,\n component: 'connection',\n [Symbol(kCapture)]: false\n },\n s: {\n description: [ServerDescription],\n options: [Object: null prototype],\n state: 'connected',\n operationCount: 0\n },\n monitor: Monitor {\n _events: [Object: null prototype],\n _eventsCount: 4,\n _maxListeners: undefined,\n component: 'topology',\n server: [Circular *1],\n connection: [Connection],\n cancellationToken: [CancellationToken],\n monitorId: [MonitorInterval],\n s: [Object],\n address: 'localhost:31001',\n options: [Object],\n isRunningInFaasEnv: false,\n mongoLogger: undefined,\n rttSampler: [RTTSampler],\n connectOptions: [Object],\n [Symbol(kCapture)]: false\n },\n [Symbol(kCapture)]: false\n }\n]"} +{"relevantHostAddresses":[{"host":"localhost","port":31001}]} +{"server":"[\n 'localhost:31002',\n Server {\n _events: [Object: null prototype] {\n serverHeartbeatStarted: [Function (anonymous)],\n serverHeartbeatSucceeded: [Function (anonymous)],\n serverHeartbeatFailed: [Function (anonymous)],\n commandStarted: [Function (anonymous)],\n commandSucceeded: [Function (anonymous)],\n commandFailed: [Function (anonymous)],\n connectionPoolCreated: [Function (anonymous)],\n connectionPoolReady: [Function (anonymous)],\n connectionPoolCleared: [Function (anonymous)],\n connectionPoolClosed: [Function (anonymous)],\n connectionCreated: [Function (anonymous)],\n connectionReady: [Function (anonymous)],\n connectionClosed: [Function (anonymous)],\n connectionCheckOutStarted: [Function (anonymous)],\n connectionCheckOutFailed: [Function (anonymous)],\n connectionCheckedOut: [Function (anonymous)],\n connectionCheckedIn: [Function (anonymous)],\n descriptionReceived: [Function (anonymous)]\n },\n _eventsCount: 18,\n _maxListeners: undefined,\n serverApi: undefined,\n topology: Topology {\n _events: [Object: null prototype],\n _eventsCount: 26,\n _maxListeners: undefined,\n client: [MongoClient],\n waitQueue: [List],\n s: [Object],\n mongoLogger: undefined,\n component: 'topology',\n connectionLock: undefined,\n [Symbol(kCapture)]: false\n },\n pool: ConnectionPool {\n _events: [Object: null prototype],\n _eventsCount: 15,\n _maxListeners: undefined,\n options: [Object],\n poolState: 'ready',\n server: [Circular *1],\n connections: [List],\n pending: 0,\n checkedOut: Set(0) {},\n minPoolSizeTimer: undefined,\n generation: 0,\n serviceGenerations: Map(0) {},\n connectionCounter: Object [Generator] {},\n cancellationToken: [CancellationToken],\n waitQueue: [List],\n metrics: [ConnectionPoolMetrics],\n processingWaitQueue: false,\n mongoLogger: undefined,\n component: 'connection',\n [Symbol(kCapture)]: false\n },\n s: {\n description: [ServerDescription],\n options: [Object: null prototype],\n state: 'connected',\n operationCount: 0\n },\n monitor: Monitor {\n _events: [Object: null prototype],\n _eventsCount: 4,\n _maxListeners: undefined,\n component: 'topology',\n server: [Circular *1],\n connection: [Connection],\n cancellationToken: [CancellationToken],\n monitorId: [MonitorInterval],\n s: [Object],\n address: 'localhost:31002',\n options: [Object],\n isRunningInFaasEnv: false,\n mongoLogger: undefined,\n rttSampler: [RTTSampler],\n connectOptions: [Object],\n [Symbol(kCapture)]: false\n },\n [Symbol(kCapture)]: false\n }\n]"} +{"relevantHostAddresses":[{"host":"localhost","port":31002}]} +{"server":"[\n 'localhost:31003',\n Server {\n _events: [Object: null prototype] {\n serverHeartbeatStarted: [Function (anonymous)],\n serverHeartbeatSucceeded: [Function (anonymous)],\n serverHeartbeatFailed: [Function (anonymous)],\n commandStarted: [Function (anonymous)],\n commandSucceeded: [Function (anonymous)],\n commandFailed: [Function (anonymous)],\n connectionPoolCreated: [Function (anonymous)],\n connectionPoolReady: [Function (anonymous)],\n connectionPoolCleared: [Function (anonymous)],\n connectionPoolClosed: [Function (anonymous)],\n connectionCreated: [Function (anonymous)],\n connectionReady: [Function (anonymous)],\n connectionClosed: [Function (anonymous)],\n connectionCheckOutStarted: [Function (anonymous)],\n connectionCheckOutFailed: [Function (anonymous)],\n connectionCheckedOut: [Function (anonymous)],\n connectionCheckedIn: [Function (anonymous)],\n descriptionReceived: [Function (anonymous)]\n },\n _eventsCount: 18,\n _maxListeners: undefined,\n serverApi: undefined,\n topology: Topology {\n _events: [Object: null prototype],\n _eventsCount: 26,\n _maxListeners: undefined,\n client: [MongoClient],\n waitQueue: [List],\n s: [Object],\n mongoLogger: undefined,\n component: 'topology',\n connectionLock: undefined,\n [Symbol(kCapture)]: false\n },\n pool: ConnectionPool {\n _events: [Object: null prototype],\n _eventsCount: 15,\n _maxListeners: undefined,\n options: [Object],\n poolState: 'paused',\n server: [Circular *1],\n connections: [List],\n pending: 0,\n checkedOut: Set(0) {},\n minPoolSizeTimer: undefined,\n generation: 0,\n serviceGenerations: Map(0) {},\n connectionCounter: Object [Generator] {},\n cancellationToken: [CancellationToken],\n waitQueue: [List],\n metrics: [ConnectionPoolMetrics],\n processingWaitQueue: false,\n mongoLogger: undefined,\n component: 'connection',\n [Symbol(kCapture)]: false\n },\n s: {\n description: [ServerDescription],\n options: [Object: null prototype],\n state: 'connected',\n operationCount: 0\n },\n monitor: Monitor {\n _events: [Object: null prototype],\n _eventsCount: 4,\n _maxListeners: undefined,\n component: 'topology',\n server: [Circular *1],\n connection: [Connection],\n cancellationToken: [CancellationToken],\n monitorId: [MonitorInterval],\n s: [Object],\n address: 'localhost:31003',\n options: [Object],\n isRunningInFaasEnv: false,\n mongoLogger: undefined,\n rttSampler: [RTTSampler],\n connectOptions: [Object],\n [Symbol(kCapture)]: false\n },\n [Symbol(kCapture)]: false\n }\n]"} +{"relevantHostAddresses":[{"host":"localhost","port":31003}]} +{"beforeExitHappened":true} +{"server":"[\n 'localhost:31000',\n Server {\n _events: [Object: null prototype] {\n serverHeartbeatStarted: [Function (anonymous)],\n serverHeartbeatSucceeded: [Function (anonymous)],\n serverHeartbeatFailed: [Function (anonymous)],\n commandStarted: [Function (anonymous)],\n commandSucceeded: [Function (anonymous)],\n commandFailed: [Function (anonymous)],\n connectionPoolCreated: [Function (anonymous)],\n connectionPoolReady: [Function (anonymous)],\n connectionPoolCleared: [Function (anonymous)],\n connectionPoolClosed: [Function (anonymous)],\n connectionCreated: [Function (anonymous)],\n connectionReady: [Function (anonymous)],\n connectionClosed: [Function (anonymous)],\n connectionCheckOutStarted: [Function (anonymous)],\n connectionCheckOutFailed: [Function (anonymous)],\n connectionCheckedOut: [Function (anonymous)],\n connectionCheckedIn: [Function (anonymous)],\n descriptionReceived: [Function (anonymous)]\n },\n _eventsCount: 18,\n _maxListeners: undefined,\n serverApi: undefined,\n topology: Topology {\n _events: [Object: null prototype],\n _eventsCount: 26,\n _maxListeners: undefined,\n client: [MongoClient],\n waitQueue: [List],\n s: [Object],\n mongoLogger: undefined,\n component: 'topology',\n connectionLock: undefined,\n [Symbol(kCapture)]: false\n },\n pool: ConnectionPool {\n _events: [Object: null prototype],\n _eventsCount: 15,\n _maxListeners: undefined,\n options: [Object],\n poolState: 'ready',\n server: [Circular *1],\n connections: [List],\n pending: 0,\n checkedOut: Set(0) {},\n minPoolSizeTimer: undefined,\n generation: 0,\n serviceGenerations: Map(0) {},\n connectionCounter: Object [Generator] {},\n cancellationToken: [CancellationToken],\n waitQueue: [List],\n metrics: [ConnectionPoolMetrics],\n processingWaitQueue: false,\n mongoLogger: undefined,\n component: 'connection',\n [Symbol(kCapture)]: false\n },\n s: {\n description: [ServerDescription],\n options: [Object: null prototype],\n state: 'connected',\n operationCount: 0\n },\n monitor: Monitor {\n _events: [Object: null prototype],\n _eventsCount: 4,\n _maxListeners: undefined,\n component: 'topology',\n server: [Circular *1],\n connection: [Connection],\n cancellationToken: [CancellationToken],\n monitorId: [MonitorInterval],\n s: [Object],\n address: 'localhost:31000',\n options: [Object],\n isRunningInFaasEnv: false,\n mongoLogger: undefined,\n rttSampler: [RTTSampler],\n connectOptions: [Object],\n [Symbol(kCapture)]: false\n },\n [Symbol(kCapture)]: false\n }\n]"} +{"relevantHostAddresses":[{"host":"localhost","port":31000},{"host":"localhost","port":31000}]} +{"server":"[\n 'localhost:31001',\n Server {\n _events: [Object: null prototype] {\n serverHeartbeatStarted: [Function (anonymous)],\n serverHeartbeatSucceeded: [Function (anonymous)],\n serverHeartbeatFailed: [Function (anonymous)],\n commandStarted: [Function (anonymous)],\n commandSucceeded: [Function (anonymous)],\n commandFailed: [Function (anonymous)],\n connectionPoolCreated: [Function (anonymous)],\n connectionPoolReady: [Function (anonymous)],\n connectionPoolCleared: [Function (anonymous)],\n connectionPoolClosed: [Function (anonymous)],\n connectionCreated: [Function (anonymous)],\n connectionReady: [Function (anonymous)],\n connectionClosed: [Function (anonymous)],\n connectionCheckOutStarted: [Function (anonymous)],\n connectionCheckOutFailed: [Function (anonymous)],\n connectionCheckedOut: [Function (anonymous)],\n connectionCheckedIn: [Function (anonymous)],\n descriptionReceived: [Function (anonymous)]\n },\n _eventsCount: 18,\n _maxListeners: undefined,\n serverApi: undefined,\n topology: Topology {\n _events: [Object: null prototype],\n _eventsCount: 26,\n _maxListeners: undefined,\n client: [MongoClient],\n waitQueue: [List],\n s: [Object],\n mongoLogger: undefined,\n component: 'topology',\n connectionLock: undefined,\n [Symbol(kCapture)]: false\n },\n pool: ConnectionPool {\n _events: [Object: null prototype],\n _eventsCount: 15,\n _maxListeners: undefined,\n options: [Object],\n poolState: 'ready',\n server: [Circular *1],\n connections: [List],\n pending: 0,\n checkedOut: Set(0) {},\n minPoolSizeTimer: undefined,\n generation: 0,\n serviceGenerations: Map(0) {},\n connectionCounter: Object [Generator] {},\n cancellationToken: [CancellationToken],\n waitQueue: [List],\n metrics: [ConnectionPoolMetrics],\n processingWaitQueue: false,\n mongoLogger: undefined,\n component: 'connection',\n [Symbol(kCapture)]: false\n },\n s: {\n description: [ServerDescription],\n options: [Object: null prototype],\n state: 'connected',\n operationCount: 0\n },\n monitor: Monitor {\n _events: [Object: null prototype],\n _eventsCount: 4,\n _maxListeners: undefined,\n component: 'topology',\n server: [Circular *1],\n connection: [Connection],\n cancellationToken: [CancellationToken],\n monitorId: [MonitorInterval],\n s: [Object],\n address: 'localhost:31001',\n options: [Object],\n isRunningInFaasEnv: false,\n mongoLogger: undefined,\n rttSampler: [RTTSampler],\n connectOptions: [Object],\n [Symbol(kCapture)]: false\n },\n [Symbol(kCapture)]: false\n }\n]"} +{"relevantHostAddresses":[{"host":"localhost","port":31001}]} +{"server":"[\n 'localhost:31002',\n Server {\n _events: [Object: null prototype] {\n serverHeartbeatStarted: [Function (anonymous)],\n serverHeartbeatSucceeded: [Function (anonymous)],\n serverHeartbeatFailed: [Function (anonymous)],\n commandStarted: [Function (anonymous)],\n commandSucceeded: [Function (anonymous)],\n commandFailed: [Function (anonymous)],\n connectionPoolCreated: [Function (anonymous)],\n connectionPoolReady: [Function (anonymous)],\n connectionPoolCleared: [Function (anonymous)],\n connectionPoolClosed: [Function (anonymous)],\n connectionCreated: [Function (anonymous)],\n connectionReady: [Function (anonymous)],\n connectionClosed: [Function (anonymous)],\n connectionCheckOutStarted: [Function (anonymous)],\n connectionCheckOutFailed: [Function (anonymous)],\n connectionCheckedOut: [Function (anonymous)],\n connectionCheckedIn: [Function (anonymous)],\n descriptionReceived: [Function (anonymous)]\n },\n _eventsCount: 18,\n _maxListeners: undefined,\n serverApi: undefined,\n topology: Topology {\n _events: [Object: null prototype],\n _eventsCount: 26,\n _maxListeners: undefined,\n client: [MongoClient],\n waitQueue: [List],\n s: [Object],\n mongoLogger: undefined,\n component: 'topology',\n connectionLock: undefined,\n [Symbol(kCapture)]: false\n },\n pool: ConnectionPool {\n _events: [Object: null prototype],\n _eventsCount: 15,\n _maxListeners: undefined,\n options: [Object],\n poolState: 'ready',\n server: [Circular *1],\n connections: [List],\n pending: 0,\n checkedOut: Set(0) {},\n minPoolSizeTimer: undefined,\n generation: 0,\n serviceGenerations: Map(0) {},\n connectionCounter: Object [Generator] {},\n cancellationToken: [CancellationToken],\n waitQueue: [List],\n metrics: [ConnectionPoolMetrics],\n processingWaitQueue: false,\n mongoLogger: undefined,\n component: 'connection',\n [Symbol(kCapture)]: false\n },\n s: {\n description: [ServerDescription],\n options: [Object: null prototype],\n state: 'connected',\n operationCount: 0\n },\n monitor: Monitor {\n _events: [Object: null prototype],\n _eventsCount: 4,\n _maxListeners: undefined,\n component: 'topology',\n server: [Circular *1],\n connection: [Connection],\n cancellationToken: [CancellationToken],\n monitorId: [MonitorInterval],\n s: [Object],\n address: 'localhost:31002',\n options: [Object],\n isRunningInFaasEnv: false,\n mongoLogger: undefined,\n rttSampler: [RTTSampler],\n connectOptions: [Object],\n [Symbol(kCapture)]: false\n },\n [Symbol(kCapture)]: false\n }\n]"} +{"relevantHostAddresses":[{"host":"localhost","port":31002}]} +{"server":"[\n 'localhost:31003',\n Server {\n _events: [Object: null prototype] {\n serverHeartbeatStarted: [Function (anonymous)],\n serverHeartbeatSucceeded: [Function (anonymous)],\n serverHeartbeatFailed: [Function (anonymous)],\n commandStarted: [Function (anonymous)],\n commandSucceeded: [Function (anonymous)],\n commandFailed: [Function (anonymous)],\n connectionPoolCreated: [Function (anonymous)],\n connectionPoolReady: [Function (anonymous)],\n connectionPoolCleared: [Function (anonymous)],\n connectionPoolClosed: [Function (anonymous)],\n connectionCreated: [Function (anonymous)],\n connectionReady: [Function (anonymous)],\n connectionClosed: [Function (anonymous)],\n connectionCheckOutStarted: [Function (anonymous)],\n connectionCheckOutFailed: [Function (anonymous)],\n connectionCheckedOut: [Function (anonymous)],\n connectionCheckedIn: [Function (anonymous)],\n descriptionReceived: [Function (anonymous)]\n },\n _eventsCount: 18,\n _maxListeners: undefined,\n serverApi: undefined,\n topology: Topology {\n _events: [Object: null prototype],\n _eventsCount: 26,\n _maxListeners: undefined,\n client: [MongoClient],\n waitQueue: [List],\n s: [Object],\n mongoLogger: undefined,\n component: 'topology',\n connectionLock: undefined,\n [Symbol(kCapture)]: false\n },\n pool: ConnectionPool {\n _events: [Object: null prototype],\n _eventsCount: 15,\n _maxListeners: undefined,\n options: [Object],\n poolState: 'paused',\n server: [Circular *1],\n connections: [List],\n pending: 0,\n checkedOut: Set(0) {},\n minPoolSizeTimer: undefined,\n generation: 0,\n serviceGenerations: Map(0) {},\n connectionCounter: Object [Generator] {},\n cancellationToken: [CancellationToken],\n waitQueue: [List],\n metrics: [ConnectionPoolMetrics],\n processingWaitQueue: false,\n mongoLogger: undefined,\n component: 'connection',\n [Symbol(kCapture)]: false\n },\n s: {\n description: [ServerDescription],\n options: [Object: null prototype],\n state: 'connected',\n operationCount: 0\n },\n monitor: Monitor {\n _events: [Object: null prototype],\n _eventsCount: 4,\n _maxListeners: undefined,\n component: 'topology',\n server: [Circular *1],\n connection: [Connection],\n cancellationToken: [CancellationToken],\n monitorId: [MonitorInterval],\n s: [Object],\n address: 'localhost:31003',\n options: [Object],\n isRunningInFaasEnv: false,\n mongoLogger: undefined,\n rttSampler: [RTTSampler],\n connectOptions: [Object],\n [Symbol(kCapture)]: false\n },\n [Symbol(kCapture)]: false\n }\n]"} +{"relevantHostAddresses":[{"host":"localhost","port":31003}]} +{"beforeExitHappened":true} diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 3289e5c807..f67043c9cb 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -1,4 +1,6 @@ /* eslint-disable @typescript-eslint/no-empty-function */ +import { expect } from 'chai'; +import { MongoClient } from '../../mongodb'; import { type TestConfiguration } from '../../tools/runner/config'; import { runScriptAndGetProcessInfo } from './resource_tracking_script_builder'; @@ -103,43 +105,46 @@ describe('MongoClient.close() Integration', () => { describe('Connection Monitoring', () => { describe('Node.js resource: Socket', () => { it.only('no sockets remain after client.close()', metadata, async function () { - await runScriptAndGetProcessInfo( + const run = async function({ MongoClient, uri, log, expect }) { + const client = new MongoClient(uri); + await client.connect(); + + // returns all active tcp endpoints + const connectionMonitoringReport = () => + process.report + .getReport() + .libuv.filter(r => r.type === 'tcp' && r.is_active) + .map(r => r.remoteEndpoint); + + // assert socket creation + // there should be two sockets for each server: + // client connection socket + // monitor socket + log(connectionMonitoringReport()); + const servers = client.topology?.s.servers; + for (const server of servers) { + const { host, port } = server[1].s.description.hostAddress; + log('MONITOR OF ', host, port, ':\n', server[1].monitor.address); + const relevantHostAddresses = connectionMonitoringReport().filter(r => r.host === host && r.port === port); + //log({ relevantHostAddresses }); + // expect(relevantHostAddresses).length.to.be.gte(2); + } + + await client.close(); + + // assert socket destruction + for (const server of servers) { + const { host, port } = server[1].s.description.hostAddress; + expect(connectionMonitoringReport()).to.not.deep.include({ host, port }); + } + }; + /* await runScriptAndGetProcessInfo( 'socket-connection-monitoring', config, - async function run({ MongoClient, uri, log, expect }) { - const client = new MongoClient(uri); - await client.connect(); - - // returns all active tcp endpoints - const connectionMonitoringReport = () => - process.report - .getReport() - .libuv.filter(r => r.type === 'tcp' && r.is_active) - .map(r => r.remoteEndpoint); + run + ); */ - log({ monreport: connectionMonitoringReport() }); - // assert socket creation - // there should be two sockets for each server: - // client connection socket - // monitor socket - /* const servers = client.topology?.s.servers; - for (const server of servers) { - log({ monreport: connectionMonitoringReport() }); - const { host, port } = server[1].s.description.hostAddress; - const relevantHostAddresses = connectionMonitoringReport().filter(r => r.host === host && r.port === port); - log({ relevantHostAddresses }); - expect(relevantHostAddresses).length.to.be.gte(2); - } */ - - await client.close(); - - // assert socket destruction - for (const server of servers) { - const { host, port } = server[1].s.description.hostAddress; - expect(connectionMonitoringReport()).to.not.deep.include({ host, port }); - } - } - ); + await run({ MongoClient, uri: config.uri, log: console.log, expect }); }); }); }); From 5eb92ee49d2fed3f8f5f2b9be1700d416e2d19c7 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Fri, 3 Jan 2025 11:17:48 -0500 Subject: [PATCH 33/43] neal's requested changes --- .../node-specific/resource_tracking_script_builder.ts | 2 +- 1 file changed, 1 insertion(+), 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 24f5a5dab4..f97b82a911 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -189,7 +189,7 @@ export async function runScriptAndGetProcessInfo( // assertions about exit status if (exitCode) { const assertionError = new AssertionError( - messages.error.message + '\n\t' + JSON.stringify(messages.error.resources) + messages.error.message + '\n\t' + JSON.stringify(messages.error.resources, undefined, 2) ); assertionError.stack = messages.error.stack + new Error().stack.slice('Error'.length); throw assertionError; From 78f787fe0ae2df772b88dcb193fdfff2fa93a1dc Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Sun, 5 Jan 2025 12:20:17 -0500 Subject: [PATCH 34/43] remove extra files --- socket-connection-monitoring.logs.txt | 30 ------------------- .../node-specific/client_close.test.ts | 25 +++++----------- .../resource_tracking_script_builder.ts | 9 +++--- .../fixtures/process_resource_script.in.js | 4 ++- 4 files changed, 16 insertions(+), 52 deletions(-) delete mode 100644 socket-connection-monitoring.logs.txt diff --git a/socket-connection-monitoring.logs.txt b/socket-connection-monitoring.logs.txt deleted file mode 100644 index 9cd5a20717..0000000000 --- a/socket-connection-monitoring.logs.txt +++ /dev/null @@ -1,30 +0,0 @@ - -{} -{"relevantHostAddresses":[{"host":"localhost","port":31000},{"host":"localhost","port":31000}]} -{} -{"relevantHostAddresses":[{"host":"localhost","port":31001}]} -{} -{"relevantHostAddresses":[{"host":"localhost","port":31002}]} -{} -{"relevantHostAddresses":[{"host":"localhost","port":31003}]} -{"beforeExitHappened":true} -{"error":{"message":"Converting circular structure to JSON\n --> starting at object with constructor 'MongoClient'\n | property 's' -> object with constructor 'Object'\n | property 'sessionPool' -> object with constructor 'ServerSessionPool'\n --- property 'client' closes the circle","stack":"TypeError: Converting circular structure to JSON\n --> starting at object with constructor 'MongoClient'\n | property 's' -> object with constructor 'Object'\n | property 'sessionPool' -> object with constructor 'ServerSessionPool'\n --- property 'client' closes the circle\n at JSON.stringify ()\n at log (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:112:29)\n at run (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:20:37)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async main (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:121:3)","resources":{"libuvResources":[{"type":"tcp","is_active":true,"is_referenced":true,"address":"0x000000012981e920","localEndpoint":{"host":"localhost","port":64049},"remoteEndpoint":{"host":"localhost","port":31000},"sendBufferSize":146808,"recvBufferSize":406960,"fd":24,"writeQueueSize":0,"readable":true,"writable":true},{"type":"tcp","is_active":true,"is_referenced":true,"address":"0x0000000129820140","localEndpoint":{"host":"localhost","port":64050},"remoteEndpoint":{"host":"localhost","port":31001},"sendBufferSize":146808,"recvBufferSize":406984,"fd":25,"writeQueueSize":0,"readable":true,"writable":true},{"type":"tcp","is_active":true,"is_referenced":true,"address":"0x0000000129820550","localEndpoint":{"host":"localhost","port":64051},"remoteEndpoint":{"host":"localhost","port":31002},"sendBufferSize":146808,"recvBufferSize":406984,"fd":26,"writeQueueSize":0,"readable":true,"writable":true},{"type":"tcp","is_active":true,"is_referenced":true,"address":"0x0000000129820a20","localEndpoint":{"host":"localhost","port":64052},"remoteEndpoint":{"host":"localhost","port":31003},"sendBufferSize":146808,"recvBufferSize":407095,"fd":27,"writeQueueSize":0,"readable":true,"writable":true},{"type":"tcp","is_active":true,"is_referenced":true,"address":"0x0000000129820ef0","localEndpoint":{"host":"localhost","port":64053},"remoteEndpoint":{"host":"localhost","port":31000},"sendBufferSize":146808,"recvBufferSize":406294,"fd":28,"writeQueueSize":0,"readable":true,"writable":true}],"activeResources":["TCPSocketWrap","TCPSocketWrap","TCPSocketWrap","TCPSocketWrap","TCPSocketWrap","Timeout","Timeout","Timeout","Timeout"]}}} -{"error":{"message":"Converting circular structure to JSON\n --> starting at object with constructor 'MongoClient'\n | property 's' -> object with constructor 'Object'\n | property 'sessionPool' -> object with constructor 'ServerSessionPool'\n --- property 'client' closes the circle","stack":"TypeError: Converting circular structure to JSON\n --> starting at object with constructor 'MongoClient'\n | property 's' -> object with constructor 'Object'\n | property 'sessionPool' -> object with constructor 'ServerSessionPool'\n --- property 'client' closes the circle\n at JSON.stringify ()\n at log (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:112:29)\n at run (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:20:37)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async main (/Users/aditi.khare/Desktop/node-mongodb-native/socket-connection-monitoring.cjs:121:3)","resources":{"libuvResources":[{"type":"tcp","is_active":true,"is_referenced":true,"address":"0x0000000106e1bb10","localEndpoint":{"host":"localhost","port":64068},"remoteEndpoint":{"host":"localhost","port":31000},"sendBufferSize":146808,"recvBufferSize":406960,"fd":24,"writeQueueSize":0,"readable":true,"writable":true},{"type":"tcp","is_active":true,"is_referenced":true,"address":"0x0000000106e1fd30","localEndpoint":{"host":"localhost","port":64069},"remoteEndpoint":{"host":"localhost","port":31001},"sendBufferSize":146808,"recvBufferSize":406984,"fd":27,"writeQueueSize":0,"readable":true,"writable":true},{"type":"tcp","is_active":true,"is_referenced":true,"address":"0x0000000106e20070","localEndpoint":{"host":"localhost","port":64070},"remoteEndpoint":{"host":"localhost","port":31002},"sendBufferSize":146808,"recvBufferSize":406984,"fd":28,"writeQueueSize":0,"readable":true,"writable":true},{"type":"tcp","is_active":true,"is_referenced":true,"address":"0x0000000106e20540","localEndpoint":{"host":"localhost","port":64071},"remoteEndpoint":{"host":"localhost","port":31003},"sendBufferSize":146808,"recvBufferSize":407095,"fd":26,"writeQueueSize":0,"readable":true,"writable":true},{"type":"tcp","is_active":true,"is_referenced":true,"address":"0x0000000106e20a10","localEndpoint":{"host":"localhost","port":64072},"remoteEndpoint":{"host":"localhost","port":31000},"sendBufferSize":146808,"recvBufferSize":406294,"fd":25,"writeQueueSize":0,"readable":true,"writable":true}],"activeResources":["TCPSocketWrap","TCPSocketWrap","TCPSocketWrap","TCPSocketWrap","TCPSocketWrap","Timeout","Timeout","Timeout","Timeout"]}}} -{"server":"[\n 'localhost:31000',\n Server {\n _events: [Object: null prototype] {\n serverHeartbeatStarted: [Function (anonymous)],\n serverHeartbeatSucceeded: [Function (anonymous)],\n serverHeartbeatFailed: [Function (anonymous)],\n commandStarted: [Function (anonymous)],\n commandSucceeded: [Function (anonymous)],\n commandFailed: [Function (anonymous)],\n connectionPoolCreated: [Function (anonymous)],\n connectionPoolReady: [Function (anonymous)],\n connectionPoolCleared: [Function (anonymous)],\n connectionPoolClosed: [Function (anonymous)],\n connectionCreated: [Function (anonymous)],\n connectionReady: [Function (anonymous)],\n connectionClosed: [Function (anonymous)],\n connectionCheckOutStarted: [Function (anonymous)],\n connectionCheckOutFailed: [Function (anonymous)],\n connectionCheckedOut: [Function (anonymous)],\n connectionCheckedIn: [Function (anonymous)],\n descriptionReceived: [Function (anonymous)]\n },\n _eventsCount: 18,\n _maxListeners: undefined,\n serverApi: undefined,\n topology: Topology {\n _events: [Object: null prototype],\n _eventsCount: 26,\n _maxListeners: undefined,\n client: [MongoClient],\n waitQueue: [List],\n s: [Object],\n mongoLogger: undefined,\n component: 'topology',\n connectionLock: undefined,\n [Symbol(kCapture)]: false\n },\n pool: ConnectionPool {\n _events: [Object: null prototype],\n _eventsCount: 15,\n _maxListeners: undefined,\n options: [Object],\n poolState: 'ready',\n server: [Circular *1],\n connections: [List],\n pending: 0,\n checkedOut: Set(0) {},\n minPoolSizeTimer: undefined,\n generation: 0,\n serviceGenerations: Map(0) {},\n connectionCounter: Object [Generator] {},\n cancellationToken: [CancellationToken],\n waitQueue: [List],\n metrics: [ConnectionPoolMetrics],\n processingWaitQueue: false,\n mongoLogger: undefined,\n component: 'connection',\n [Symbol(kCapture)]: false\n },\n s: {\n description: [ServerDescription],\n options: [Object: null prototype],\n state: 'connected',\n operationCount: 0\n },\n monitor: Monitor {\n _events: [Object: null prototype],\n _eventsCount: 4,\n _maxListeners: undefined,\n component: 'topology',\n server: [Circular *1],\n connection: [Connection],\n cancellationToken: [CancellationToken],\n monitorId: [MonitorInterval],\n s: [Object],\n address: 'localhost:31000',\n options: [Object],\n isRunningInFaasEnv: false,\n mongoLogger: undefined,\n rttSampler: [RTTSampler],\n connectOptions: [Object],\n [Symbol(kCapture)]: false\n },\n [Symbol(kCapture)]: false\n }\n]"} -{"relevantHostAddresses":[{"host":"localhost","port":31000},{"host":"localhost","port":31000}]} -{"server":"[\n 'localhost:31001',\n Server {\n _events: [Object: null prototype] {\n serverHeartbeatStarted: [Function (anonymous)],\n serverHeartbeatSucceeded: [Function (anonymous)],\n serverHeartbeatFailed: [Function (anonymous)],\n commandStarted: [Function (anonymous)],\n commandSucceeded: [Function (anonymous)],\n commandFailed: [Function (anonymous)],\n connectionPoolCreated: [Function (anonymous)],\n connectionPoolReady: [Function (anonymous)],\n connectionPoolCleared: [Function (anonymous)],\n connectionPoolClosed: [Function (anonymous)],\n connectionCreated: [Function (anonymous)],\n connectionReady: [Function (anonymous)],\n connectionClosed: [Function (anonymous)],\n connectionCheckOutStarted: [Function (anonymous)],\n connectionCheckOutFailed: [Function (anonymous)],\n connectionCheckedOut: [Function (anonymous)],\n connectionCheckedIn: [Function (anonymous)],\n descriptionReceived: [Function (anonymous)]\n },\n _eventsCount: 18,\n _maxListeners: undefined,\n serverApi: undefined,\n topology: Topology {\n _events: [Object: null prototype],\n _eventsCount: 26,\n _maxListeners: undefined,\n client: [MongoClient],\n waitQueue: [List],\n s: [Object],\n mongoLogger: undefined,\n component: 'topology',\n connectionLock: undefined,\n [Symbol(kCapture)]: false\n },\n pool: ConnectionPool {\n _events: [Object: null prototype],\n _eventsCount: 15,\n _maxListeners: undefined,\n options: [Object],\n poolState: 'ready',\n server: [Circular *1],\n connections: [List],\n pending: 0,\n checkedOut: Set(0) {},\n minPoolSizeTimer: undefined,\n generation: 0,\n serviceGenerations: Map(0) {},\n connectionCounter: Object [Generator] {},\n cancellationToken: [CancellationToken],\n waitQueue: [List],\n metrics: [ConnectionPoolMetrics],\n processingWaitQueue: false,\n mongoLogger: undefined,\n component: 'connection',\n [Symbol(kCapture)]: false\n },\n s: {\n description: [ServerDescription],\n options: [Object: null prototype],\n state: 'connected',\n operationCount: 0\n },\n monitor: Monitor {\n _events: [Object: null prototype],\n _eventsCount: 4,\n _maxListeners: undefined,\n component: 'topology',\n server: [Circular *1],\n connection: [Connection],\n cancellationToken: [CancellationToken],\n monitorId: [MonitorInterval],\n s: [Object],\n address: 'localhost:31001',\n options: [Object],\n isRunningInFaasEnv: false,\n mongoLogger: undefined,\n rttSampler: [RTTSampler],\n connectOptions: [Object],\n [Symbol(kCapture)]: false\n },\n [Symbol(kCapture)]: false\n }\n]"} -{"relevantHostAddresses":[{"host":"localhost","port":31001}]} -{"server":"[\n 'localhost:31002',\n Server {\n _events: [Object: null prototype] {\n serverHeartbeatStarted: [Function (anonymous)],\n serverHeartbeatSucceeded: [Function (anonymous)],\n serverHeartbeatFailed: [Function (anonymous)],\n commandStarted: [Function (anonymous)],\n commandSucceeded: [Function (anonymous)],\n commandFailed: [Function (anonymous)],\n connectionPoolCreated: [Function (anonymous)],\n connectionPoolReady: [Function (anonymous)],\n connectionPoolCleared: [Function (anonymous)],\n connectionPoolClosed: [Function (anonymous)],\n connectionCreated: [Function (anonymous)],\n connectionReady: [Function (anonymous)],\n connectionClosed: [Function (anonymous)],\n connectionCheckOutStarted: [Function (anonymous)],\n connectionCheckOutFailed: [Function (anonymous)],\n connectionCheckedOut: [Function (anonymous)],\n connectionCheckedIn: [Function (anonymous)],\n descriptionReceived: [Function (anonymous)]\n },\n _eventsCount: 18,\n _maxListeners: undefined,\n serverApi: undefined,\n topology: Topology {\n _events: [Object: null prototype],\n _eventsCount: 26,\n _maxListeners: undefined,\n client: [MongoClient],\n waitQueue: [List],\n s: [Object],\n mongoLogger: undefined,\n component: 'topology',\n connectionLock: undefined,\n [Symbol(kCapture)]: false\n },\n pool: ConnectionPool {\n _events: [Object: null prototype],\n _eventsCount: 15,\n _maxListeners: undefined,\n options: [Object],\n poolState: 'ready',\n server: [Circular *1],\n connections: [List],\n pending: 0,\n checkedOut: Set(0) {},\n minPoolSizeTimer: undefined,\n generation: 0,\n serviceGenerations: Map(0) {},\n connectionCounter: Object [Generator] {},\n cancellationToken: [CancellationToken],\n waitQueue: [List],\n metrics: [ConnectionPoolMetrics],\n processingWaitQueue: false,\n mongoLogger: undefined,\n component: 'connection',\n [Symbol(kCapture)]: false\n },\n s: {\n description: [ServerDescription],\n options: [Object: null prototype],\n state: 'connected',\n operationCount: 0\n },\n monitor: Monitor {\n _events: [Object: null prototype],\n _eventsCount: 4,\n _maxListeners: undefined,\n component: 'topology',\n server: [Circular *1],\n connection: [Connection],\n cancellationToken: [CancellationToken],\n monitorId: [MonitorInterval],\n s: [Object],\n address: 'localhost:31002',\n options: [Object],\n isRunningInFaasEnv: false,\n mongoLogger: undefined,\n rttSampler: [RTTSampler],\n connectOptions: [Object],\n [Symbol(kCapture)]: false\n },\n [Symbol(kCapture)]: false\n }\n]"} -{"relevantHostAddresses":[{"host":"localhost","port":31002}]} -{"server":"[\n 'localhost:31003',\n Server {\n _events: [Object: null prototype] {\n serverHeartbeatStarted: [Function (anonymous)],\n serverHeartbeatSucceeded: [Function (anonymous)],\n serverHeartbeatFailed: [Function (anonymous)],\n commandStarted: [Function (anonymous)],\n commandSucceeded: [Function (anonymous)],\n commandFailed: [Function (anonymous)],\n connectionPoolCreated: [Function (anonymous)],\n connectionPoolReady: [Function (anonymous)],\n connectionPoolCleared: [Function (anonymous)],\n connectionPoolClosed: [Function (anonymous)],\n connectionCreated: [Function (anonymous)],\n connectionReady: [Function (anonymous)],\n connectionClosed: [Function (anonymous)],\n connectionCheckOutStarted: [Function (anonymous)],\n connectionCheckOutFailed: [Function (anonymous)],\n connectionCheckedOut: [Function (anonymous)],\n connectionCheckedIn: [Function (anonymous)],\n descriptionReceived: [Function (anonymous)]\n },\n _eventsCount: 18,\n _maxListeners: undefined,\n serverApi: undefined,\n topology: Topology {\n _events: [Object: null prototype],\n _eventsCount: 26,\n _maxListeners: undefined,\n client: [MongoClient],\n waitQueue: [List],\n s: [Object],\n mongoLogger: undefined,\n component: 'topology',\n connectionLock: undefined,\n [Symbol(kCapture)]: false\n },\n pool: ConnectionPool {\n _events: [Object: null prototype],\n _eventsCount: 15,\n _maxListeners: undefined,\n options: [Object],\n poolState: 'paused',\n server: [Circular *1],\n connections: [List],\n pending: 0,\n checkedOut: Set(0) {},\n minPoolSizeTimer: undefined,\n generation: 0,\n serviceGenerations: Map(0) {},\n connectionCounter: Object [Generator] {},\n cancellationToken: [CancellationToken],\n waitQueue: [List],\n metrics: [ConnectionPoolMetrics],\n processingWaitQueue: false,\n mongoLogger: undefined,\n component: 'connection',\n [Symbol(kCapture)]: false\n },\n s: {\n description: [ServerDescription],\n options: [Object: null prototype],\n state: 'connected',\n operationCount: 0\n },\n monitor: Monitor {\n _events: [Object: null prototype],\n _eventsCount: 4,\n _maxListeners: undefined,\n component: 'topology',\n server: [Circular *1],\n connection: [Connection],\n cancellationToken: [CancellationToken],\n monitorId: [MonitorInterval],\n s: [Object],\n address: 'localhost:31003',\n options: [Object],\n isRunningInFaasEnv: false,\n mongoLogger: undefined,\n rttSampler: [RTTSampler],\n connectOptions: [Object],\n [Symbol(kCapture)]: false\n },\n [Symbol(kCapture)]: false\n }\n]"} -{"relevantHostAddresses":[{"host":"localhost","port":31003}]} -{"beforeExitHappened":true} -{"server":"[\n 'localhost:31000',\n Server {\n _events: [Object: null prototype] {\n serverHeartbeatStarted: [Function (anonymous)],\n serverHeartbeatSucceeded: [Function (anonymous)],\n serverHeartbeatFailed: [Function (anonymous)],\n commandStarted: [Function (anonymous)],\n commandSucceeded: [Function (anonymous)],\n commandFailed: [Function (anonymous)],\n connectionPoolCreated: [Function (anonymous)],\n connectionPoolReady: [Function (anonymous)],\n connectionPoolCleared: [Function (anonymous)],\n connectionPoolClosed: [Function (anonymous)],\n connectionCreated: [Function (anonymous)],\n connectionReady: [Function (anonymous)],\n connectionClosed: [Function (anonymous)],\n connectionCheckOutStarted: [Function (anonymous)],\n connectionCheckOutFailed: [Function (anonymous)],\n connectionCheckedOut: [Function (anonymous)],\n connectionCheckedIn: [Function (anonymous)],\n descriptionReceived: [Function (anonymous)]\n },\n _eventsCount: 18,\n _maxListeners: undefined,\n serverApi: undefined,\n topology: Topology {\n _events: [Object: null prototype],\n _eventsCount: 26,\n _maxListeners: undefined,\n client: [MongoClient],\n waitQueue: [List],\n s: [Object],\n mongoLogger: undefined,\n component: 'topology',\n connectionLock: undefined,\n [Symbol(kCapture)]: false\n },\n pool: ConnectionPool {\n _events: [Object: null prototype],\n _eventsCount: 15,\n _maxListeners: undefined,\n options: [Object],\n poolState: 'ready',\n server: [Circular *1],\n connections: [List],\n pending: 0,\n checkedOut: Set(0) {},\n minPoolSizeTimer: undefined,\n generation: 0,\n serviceGenerations: Map(0) {},\n connectionCounter: Object [Generator] {},\n cancellationToken: [CancellationToken],\n waitQueue: [List],\n metrics: [ConnectionPoolMetrics],\n processingWaitQueue: false,\n mongoLogger: undefined,\n component: 'connection',\n [Symbol(kCapture)]: false\n },\n s: {\n description: [ServerDescription],\n options: [Object: null prototype],\n state: 'connected',\n operationCount: 0\n },\n monitor: Monitor {\n _events: [Object: null prototype],\n _eventsCount: 4,\n _maxListeners: undefined,\n component: 'topology',\n server: [Circular *1],\n connection: [Connection],\n cancellationToken: [CancellationToken],\n monitorId: [MonitorInterval],\n s: [Object],\n address: 'localhost:31000',\n options: [Object],\n isRunningInFaasEnv: false,\n mongoLogger: undefined,\n rttSampler: [RTTSampler],\n connectOptions: [Object],\n [Symbol(kCapture)]: false\n },\n [Symbol(kCapture)]: false\n }\n]"} -{"relevantHostAddresses":[{"host":"localhost","port":31000},{"host":"localhost","port":31000}]} -{"server":"[\n 'localhost:31001',\n Server {\n _events: [Object: null prototype] {\n serverHeartbeatStarted: [Function (anonymous)],\n serverHeartbeatSucceeded: [Function (anonymous)],\n serverHeartbeatFailed: [Function (anonymous)],\n commandStarted: [Function (anonymous)],\n commandSucceeded: [Function (anonymous)],\n commandFailed: [Function (anonymous)],\n connectionPoolCreated: [Function (anonymous)],\n connectionPoolReady: [Function (anonymous)],\n connectionPoolCleared: [Function (anonymous)],\n connectionPoolClosed: [Function (anonymous)],\n connectionCreated: [Function (anonymous)],\n connectionReady: [Function (anonymous)],\n connectionClosed: [Function (anonymous)],\n connectionCheckOutStarted: [Function (anonymous)],\n connectionCheckOutFailed: [Function (anonymous)],\n connectionCheckedOut: [Function (anonymous)],\n connectionCheckedIn: [Function (anonymous)],\n descriptionReceived: [Function (anonymous)]\n },\n _eventsCount: 18,\n _maxListeners: undefined,\n serverApi: undefined,\n topology: Topology {\n _events: [Object: null prototype],\n _eventsCount: 26,\n _maxListeners: undefined,\n client: [MongoClient],\n waitQueue: [List],\n s: [Object],\n mongoLogger: undefined,\n component: 'topology',\n connectionLock: undefined,\n [Symbol(kCapture)]: false\n },\n pool: ConnectionPool {\n _events: [Object: null prototype],\n _eventsCount: 15,\n _maxListeners: undefined,\n options: [Object],\n poolState: 'ready',\n server: [Circular *1],\n connections: [List],\n pending: 0,\n checkedOut: Set(0) {},\n minPoolSizeTimer: undefined,\n generation: 0,\n serviceGenerations: Map(0) {},\n connectionCounter: Object [Generator] {},\n cancellationToken: [CancellationToken],\n waitQueue: [List],\n metrics: [ConnectionPoolMetrics],\n processingWaitQueue: false,\n mongoLogger: undefined,\n component: 'connection',\n [Symbol(kCapture)]: false\n },\n s: {\n description: [ServerDescription],\n options: [Object: null prototype],\n state: 'connected',\n operationCount: 0\n },\n monitor: Monitor {\n _events: [Object: null prototype],\n _eventsCount: 4,\n _maxListeners: undefined,\n component: 'topology',\n server: [Circular *1],\n connection: [Connection],\n cancellationToken: [CancellationToken],\n monitorId: [MonitorInterval],\n s: [Object],\n address: 'localhost:31001',\n options: [Object],\n isRunningInFaasEnv: false,\n mongoLogger: undefined,\n rttSampler: [RTTSampler],\n connectOptions: [Object],\n [Symbol(kCapture)]: false\n },\n [Symbol(kCapture)]: false\n }\n]"} -{"relevantHostAddresses":[{"host":"localhost","port":31001}]} -{"server":"[\n 'localhost:31002',\n Server {\n _events: [Object: null prototype] {\n serverHeartbeatStarted: [Function (anonymous)],\n serverHeartbeatSucceeded: [Function (anonymous)],\n serverHeartbeatFailed: [Function (anonymous)],\n commandStarted: [Function (anonymous)],\n commandSucceeded: [Function (anonymous)],\n commandFailed: [Function (anonymous)],\n connectionPoolCreated: [Function (anonymous)],\n connectionPoolReady: [Function (anonymous)],\n connectionPoolCleared: [Function (anonymous)],\n connectionPoolClosed: [Function (anonymous)],\n connectionCreated: [Function (anonymous)],\n connectionReady: [Function (anonymous)],\n connectionClosed: [Function (anonymous)],\n connectionCheckOutStarted: [Function (anonymous)],\n connectionCheckOutFailed: [Function (anonymous)],\n connectionCheckedOut: [Function (anonymous)],\n connectionCheckedIn: [Function (anonymous)],\n descriptionReceived: [Function (anonymous)]\n },\n _eventsCount: 18,\n _maxListeners: undefined,\n serverApi: undefined,\n topology: Topology {\n _events: [Object: null prototype],\n _eventsCount: 26,\n _maxListeners: undefined,\n client: [MongoClient],\n waitQueue: [List],\n s: [Object],\n mongoLogger: undefined,\n component: 'topology',\n connectionLock: undefined,\n [Symbol(kCapture)]: false\n },\n pool: ConnectionPool {\n _events: [Object: null prototype],\n _eventsCount: 15,\n _maxListeners: undefined,\n options: [Object],\n poolState: 'ready',\n server: [Circular *1],\n connections: [List],\n pending: 0,\n checkedOut: Set(0) {},\n minPoolSizeTimer: undefined,\n generation: 0,\n serviceGenerations: Map(0) {},\n connectionCounter: Object [Generator] {},\n cancellationToken: [CancellationToken],\n waitQueue: [List],\n metrics: [ConnectionPoolMetrics],\n processingWaitQueue: false,\n mongoLogger: undefined,\n component: 'connection',\n [Symbol(kCapture)]: false\n },\n s: {\n description: [ServerDescription],\n options: [Object: null prototype],\n state: 'connected',\n operationCount: 0\n },\n monitor: Monitor {\n _events: [Object: null prototype],\n _eventsCount: 4,\n _maxListeners: undefined,\n component: 'topology',\n server: [Circular *1],\n connection: [Connection],\n cancellationToken: [CancellationToken],\n monitorId: [MonitorInterval],\n s: [Object],\n address: 'localhost:31002',\n options: [Object],\n isRunningInFaasEnv: false,\n mongoLogger: undefined,\n rttSampler: [RTTSampler],\n connectOptions: [Object],\n [Symbol(kCapture)]: false\n },\n [Symbol(kCapture)]: false\n }\n]"} -{"relevantHostAddresses":[{"host":"localhost","port":31002}]} -{"server":"[\n 'localhost:31003',\n Server {\n _events: [Object: null prototype] {\n serverHeartbeatStarted: [Function (anonymous)],\n serverHeartbeatSucceeded: [Function (anonymous)],\n serverHeartbeatFailed: [Function (anonymous)],\n commandStarted: [Function (anonymous)],\n commandSucceeded: [Function (anonymous)],\n commandFailed: [Function (anonymous)],\n connectionPoolCreated: [Function (anonymous)],\n connectionPoolReady: [Function (anonymous)],\n connectionPoolCleared: [Function (anonymous)],\n connectionPoolClosed: [Function (anonymous)],\n connectionCreated: [Function (anonymous)],\n connectionReady: [Function (anonymous)],\n connectionClosed: [Function (anonymous)],\n connectionCheckOutStarted: [Function (anonymous)],\n connectionCheckOutFailed: [Function (anonymous)],\n connectionCheckedOut: [Function (anonymous)],\n connectionCheckedIn: [Function (anonymous)],\n descriptionReceived: [Function (anonymous)]\n },\n _eventsCount: 18,\n _maxListeners: undefined,\n serverApi: undefined,\n topology: Topology {\n _events: [Object: null prototype],\n _eventsCount: 26,\n _maxListeners: undefined,\n client: [MongoClient],\n waitQueue: [List],\n s: [Object],\n mongoLogger: undefined,\n component: 'topology',\n connectionLock: undefined,\n [Symbol(kCapture)]: false\n },\n pool: ConnectionPool {\n _events: [Object: null prototype],\n _eventsCount: 15,\n _maxListeners: undefined,\n options: [Object],\n poolState: 'paused',\n server: [Circular *1],\n connections: [List],\n pending: 0,\n checkedOut: Set(0) {},\n minPoolSizeTimer: undefined,\n generation: 0,\n serviceGenerations: Map(0) {},\n connectionCounter: Object [Generator] {},\n cancellationToken: [CancellationToken],\n waitQueue: [List],\n metrics: [ConnectionPoolMetrics],\n processingWaitQueue: false,\n mongoLogger: undefined,\n component: 'connection',\n [Symbol(kCapture)]: false\n },\n s: {\n description: [ServerDescription],\n options: [Object: null prototype],\n state: 'connected',\n operationCount: 0\n },\n monitor: Monitor {\n _events: [Object: null prototype],\n _eventsCount: 4,\n _maxListeners: undefined,\n component: 'topology',\n server: [Circular *1],\n connection: [Connection],\n cancellationToken: [CancellationToken],\n monitorId: [MonitorInterval],\n s: [Object],\n address: 'localhost:31003',\n options: [Object],\n isRunningInFaasEnv: false,\n mongoLogger: undefined,\n rttSampler: [RTTSampler],\n connectOptions: [Object],\n [Symbol(kCapture)]: false\n },\n [Symbol(kCapture)]: false\n }\n]"} -{"relevantHostAddresses":[{"host":"localhost","port":31003}]} -{"beforeExitHappened":true} diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index f67043c9cb..d4784e6e38 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -4,7 +4,7 @@ import { MongoClient } from '../../mongodb'; import { type TestConfiguration } from '../../tools/runner/config'; import { runScriptAndGetProcessInfo } from './resource_tracking_script_builder'; -describe('MongoClient.close() Integration', () => { +describe.only('MongoClient.close() Integration', () => { // note: these tests are set-up in accordance of the resource ownership tree let config: TestConfiguration; @@ -104,8 +104,8 @@ describe('MongoClient.close() Integration', () => { describe('Connection Monitoring', () => { describe('Node.js resource: Socket', () => { - it.only('no sockets remain after client.close()', metadata, async function () { - const run = async function({ MongoClient, uri, log, expect }) { + it('no sockets remain after client.close()', metadata, async function () { + const run = async function({ MongoClient, uri, expect }) { const client = new MongoClient(uri); await client.connect(); @@ -117,17 +117,10 @@ describe('MongoClient.close() Integration', () => { .map(r => r.remoteEndpoint); // assert socket creation - // there should be two sockets for each server: - // client connection socket - // monitor socket - log(connectionMonitoringReport()); const servers = client.topology?.s.servers; for (const server of servers) { const { host, port } = server[1].s.description.hostAddress; - log('MONITOR OF ', host, port, ':\n', server[1].monitor.address); - const relevantHostAddresses = connectionMonitoringReport().filter(r => r.host === host && r.port === port); - //log({ relevantHostAddresses }); - // expect(relevantHostAddresses).length.to.be.gte(2); + expect(connectionMonitoringReport()).to.deep.include({host, port}); } await client.close(); @@ -138,13 +131,11 @@ describe('MongoClient.close() Integration', () => { expect(connectionMonitoringReport()).to.not.deep.include({ host, port }); } }; - /* await runScriptAndGetProcessInfo( + await runScriptAndGetProcessInfo( 'socket-connection-monitoring', config, run - ); */ - - await run({ MongoClient, uri: config.uri, log: console.log, expect }); + ); }); }); }); @@ -162,7 +153,7 @@ describe('MongoClient.close() Integration', () => { describe('Node.js resource: Socket', () => { describe('when rtt monitoring is turned on', () => { it('no sockets remain after client.close()', async () => { - const run = async function ({ MongoClient, uri, expect, sleep }) { + const run = async ({ MongoClient, uri, expect, sleep }) => { const heartbeatFrequencyMS = 100; const client = new MongoClient(uri, { serverMonitoringMode: 'stream', @@ -214,7 +205,7 @@ describe('MongoClient.close() Integration', () => { expect(newSocketsAfterClose).to.have.length(0); }; - await runScriptAndGetProcessInfo('socket-connection-monitoring', config, run); + await runScriptAndGetProcessInfo('socket-connection-rtt-monitoring', 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 a8ea733183..5d0a4ebc43 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -8,6 +8,7 @@ import { parseSnapshot } from 'v8-heapsnapshot'; import { type BSON, type ClientEncryption, type MongoClient } from '../../mongodb'; import { type TestConfiguration } from '../../tools/runner/config'; +import { sleep } from '../../tools/utils'; export type ResourceTestFunction = HeapResourceTestFunction | ProcessResourceTestFunction; @@ -24,7 +25,7 @@ export type ProcessResourceTestFunction = (options: { expect: typeof expect; ClientEncryption?: typeof ClientEncryption; BSON?: typeof BSON; - sleep?: typeof Promise; + sleep?: typeof sleep; }) => Promise; const HEAP_RESOURCE_SCRIPT_PATH = path.resolve( @@ -185,12 +186,12 @@ export async function runScriptAndGetProcessInfo( // delete temporary files await unlink(scriptName); - // await unlink(logFile); + await unlink(logFile); // assertions about exit status if (exitCode) { const assertionError = new AssertionError( - messages.error.message + '\n\t' + JSON.stringify(messages.error.resources) + messages.error.message + '\n\t' + JSON.stringify(messages.error.resources, undefined, 2) ); assertionError.stack = messages.error.stack + new Error().stack.slice('Error'.length); throw assertionError; @@ -198,5 +199,5 @@ export async function runScriptAndGetProcessInfo( // assertions about resource status expect(messages.beforeExitHappened).to.be.true; - expect(messages.newLibuvResources).to.be.empty; + expect(messages.newResources).to.be.empty; } diff --git a/test/tools/fixtures/process_resource_script.in.js b/test/tools/fixtures/process_resource_script.in.js index 845afc5d9b..ebf3dd6f86 100644 --- a/test/tools/fixtures/process_resource_script.in.js +++ b/test/tools/fixtures/process_resource_script.in.js @@ -17,6 +17,7 @@ const { setTimeout } = require('timers'); let originalReport; const logFile = scriptName + '.logs.txt'; +const sleep = promisify(setTimeout); const run = func; @@ -92,7 +93,8 @@ async function main() { process.on('beforeExit', () => { log({ beforeExitHappened: true }); }); - await run({ MongoClient, uri, log, expect, ClientEncryption, BSON }); + await run({ MongoClient, uri, log, expect, ClientEncryption, BSON, sleep }); + log({ newResources: getNewResources() }); } main() From 77fadbbc4aff8b255788943bd2ba35dc3607e341 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Sun, 5 Jan 2025 12:24:17 -0500 Subject: [PATCH 35/43] re-add assertion --- .../node-specific/resource_tracking_script_builder.ts | 2 +- test/tools/fixtures/process_resource_script.in.js | 1 + 2 files 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 f97b82a911..7094ad862b 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -197,5 +197,5 @@ export async function runScriptAndGetProcessInfo( // assertions about resource status expect(messages.beforeExitHappened).to.be.true; - expect(messages.newLibuvResources).to.be.empty; + expect(messages.newResources).to.be.empty; } diff --git a/test/tools/fixtures/process_resource_script.in.js b/test/tools/fixtures/process_resource_script.in.js index 845afc5d9b..2b5a3b40ac 100644 --- a/test/tools/fixtures/process_resource_script.in.js +++ b/test/tools/fixtures/process_resource_script.in.js @@ -93,6 +93,7 @@ async function main() { log({ beforeExitHappened: true }); }); await run({ MongoClient, uri, log, expect, ClientEncryption, BSON }); + log({ newResources: getNewResources() }); } main() From aefa1ccdb153d5a885f76dcf6f44a083c0c06917 Mon Sep 17 00:00:00 2001 From: Aditi Khare <106987683+aditi-khare-mongoDB@users.noreply.github.com> Date: Mon, 6 Jan 2025 10:17:16 -0500 Subject: [PATCH 36/43] Update test/integration/node-specific/resource_tracking_script_builder.ts Co-authored-by: Anna Henningsen --- .../node-specific/resource_tracking_script_builder.ts | 2 +- 1 file changed, 1 insertion(+), 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 7094ad862b..bdb733a8eb 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -168,7 +168,7 @@ export async function runScriptAndGetProcessInfo( await writeFile(scriptName, scriptContent, { encoding: 'utf8' }); const logFile = name + '.logs.txt'; - const script = spawn(process.argv[0], [scriptName], { stdio: ['ignore', 'ignore', 'ignore'] }); + const script = spawn(process.execPath, [scriptName], { stdio: ['ignore', 'ignore', 'ignore'] }); const willClose = once(script, 'close'); From 2a43e4d4aa0b1b930443fdd9ec4e7d4b5c0bee3e Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Mon, 6 Jan 2025 14:15:11 -0500 Subject: [PATCH 37/43] temp --- socket-connection-rtt-monitoring.logs.txt | 3 + .../node-specific/client_close.test.ts | 62 ++++++++----------- .../resource_tracking_script_builder.ts | 10 ++- .../fixtures/process_resource_script.in.js | 2 +- 4 files changed, 35 insertions(+), 42 deletions(-) create mode 100644 socket-connection-rtt-monitoring.logs.txt diff --git a/socket-connection-rtt-monitoring.logs.txt b/socket-connection-rtt-monitoring.logs.txt new file mode 100644 index 0000000000..9231c6dee6 --- /dev/null +++ b/socket-connection-rtt-monitoring.logs.txt @@ -0,0 +1,3 @@ +{} +{"newResources":{"libuvResources":[],"activeResources":["TCPSocketWrap","TCPSocketWrap","TCPSocketWrap","TCPSocketWrap","TCPSocketWrap","TCPSocketWrap","TCPSocketWrap","TCPSocketWrap","TCPSocketWrap"]}} +{"beforeExitHappened":true} diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index d4784e6e38..330460b339 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -113,15 +113,15 @@ describe.only('MongoClient.close() Integration', () => { const connectionMonitoringReport = () => process.report .getReport() - .libuv.filter(r => r.type === 'tcp' && r.is_active) + .libuv.filter(r => r.type === 'tcp') .map(r => r.remoteEndpoint); - // assert socket creation + // assert socket creation const servers = client.topology?.s.servers; for (const server of servers) { const { host, port } = server[1].s.description.hostAddress; expect(connectionMonitoringReport()).to.deep.include({host, port}); - } + } await client.close(); @@ -153,7 +153,7 @@ describe.only('MongoClient.close() Integration', () => { describe('Node.js resource: Socket', () => { describe('when rtt monitoring is turned on', () => { it('no sockets remain after client.close()', async () => { - const run = async ({ MongoClient, uri, expect, sleep }) => { + const run = async ({ MongoClient, uri, log, expect, sleep }) => { const heartbeatFrequencyMS = 100; const client = new MongoClient(uri, { serverMonitoringMode: 'stream', @@ -161,48 +161,40 @@ describe.only('MongoClient.close() Integration', () => { }); await client.connect(); - const servers = Array.from(client.topology.s.servers.keys()); - - // a hashmap of - const serversHeartbeatOccurred = servers.reduce( - (acc, hostname) => ({ ...acc, [hostname]: false }), - {} - ); - const activeSocketsReport = () => - process.report.getReport().libuv.filter(r => r.type === 'tcp' && r.is_active); + process.report.getReport().libuv.filter(r => r.type === 'tcp'); const socketsAddressesBeforeHeartbeat = activeSocketsReport().map( r => r.address ); - const rttSocketReport = () => + const activeSocketsAfterHeartbeat = () => activeSocketsReport() .filter(r => !socketsAddressesBeforeHeartbeat.includes(r.address)) .map(r => r.remoteEndpoint.host + ':' + r.remoteEndpoint.port); - client.on('serverHeartbeatSucceeded', async ev => { - // assert creation of rttPinger socket - const newSocketsAfterHeartbeat = rttSocketReport(); - expect(newSocketsAfterHeartbeat).to.deep.contain(ev.connectionId); + // set of servers whose hearbeats have occurred + const heartbeatOccurredSet = new Set(); - // assert rttPinger socket is connected to a server - expect(serversHeartbeatOccurred.keys()).to.deep.contain(ev.connectionId); - serversHeartbeatOccurred[ev.connectionId] = true; - }); + client.on('serverHeartbeatSucceeded', async ev => heartbeatOccurredSet.add(ev.connectionId)); - // ensure there is enough time for the heartbeatFrequencyMS for the event to occur + // ensure there is enough time for the events to occur await sleep(heartbeatFrequencyMS * 10); - // all servers should have had a heartbeat event - expect(serversHeartbeatOccurred.values().filter(r => r !== true)).to.be.empty; + // all servers should have had a heartbeat event and had a new socket created for rtt pinger + log(heartbeatOccurredSet); + const servers = client.topology.s.servers + for (const server of servers) { + expect(heartbeatOccurredSet).to.deep.contain(server[0]); + expect(activeSocketsAfterHeartbeat()).to.deep.contain(server[0]); + } // close the client await client.close(); - // upon close, assert rttPinger socket is cleaned up - const newSocketsAfterClose = rttSocketReport(); - expect(newSocketsAfterClose).to.have.length(0); + // upon close, assert rttPinger sockets are cleaned up + const activeSocketsAfterClose = activeSocketsAfterHeartbeat(); + expect(activeSocketsAfterClose).to.have.length(0); }; await runScriptAndGetProcessInfo('socket-connection-rtt-monitoring', config, run); @@ -235,30 +227,30 @@ describe.only('MongoClient.close() Integration', () => { }); describe('after a minPoolSize has been set on the ConnectionPool', () => { - it('no sockets remain after client.close()', async function () { + it.only('no sockets remain after client.close()', async function () { const run = async function ({ MongoClient, uri, log, expect }) { log({hi: 1}); const options = { minPoolSize: 2 }; const client = new MongoClient(uri, options); await client.connect(); const connectionMonitoringReport = () => - process.report.getReport().libuv.filter(r => r.type === 'tcp' && r.is_active).map(r => r.remoteEndpoint); + process.report.getReport().libuv.filter(r => r.type === 'tcp').map(r => r.remoteEndpoint); - // assert socket creation - // there should be three sockets for each server: client connection socket, monitor socket, pool size monitoring socket + // assert socket creation + // there should be a client connection socket for each server, one monitor socket total, and one pool size monitoring socket total const servers = client.topology?.s.servers; + for (const server of servers) { - log({ monreport: connectionMonitoringReport() }); const { host, port } = server[1].s.description.hostAddress; const relevantHostAddresses = connectionMonitoringReport().filter(r => r.host === host && r.port === port); - log({relevantHostAddresses}); expect(relevantHostAddresses).length.to.be.gte(3); } + await client.close(); }; await runScriptAndGetProcessInfo( - 'socket-minPoolSize', + 'socket-minPoolSize', 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 5d0a4ebc43..041c1de71d 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -8,7 +8,6 @@ import { parseSnapshot } from 'v8-heapsnapshot'; import { type BSON, type ClientEncryption, type MongoClient } from '../../mongodb'; import { type TestConfiguration } from '../../tools/runner/config'; -import { sleep } from '../../tools/utils'; export type ResourceTestFunction = HeapResourceTestFunction | ProcessResourceTestFunction; @@ -25,7 +24,6 @@ export type ProcessResourceTestFunction = (options: { expect: typeof expect; ClientEncryption?: typeof ClientEncryption; BSON?: typeof BSON; - sleep?: typeof sleep; }) => Promise; const HEAP_RESOURCE_SCRIPT_PATH = path.resolve( @@ -170,7 +168,7 @@ export async function runScriptAndGetProcessInfo( await writeFile(scriptName, scriptContent, { encoding: 'utf8' }); const logFile = name + '.logs.txt'; - const script = spawn(process.argv[0], [scriptName], { stdio: ['ignore', 'ignore', 'ignore'] }); + const script = spawn(process.execPath, [scriptName], { stdio: ['ignore', 'ignore', 'ignore'] }); const willClose = once(script, 'close'); @@ -186,14 +184,14 @@ export async function runScriptAndGetProcessInfo( // delete temporary files await unlink(scriptName); - await unlink(logFile); + // await unlink(logFile); // assertions about exit status if (exitCode) { const assertionError = new AssertionError( - messages.error.message + '\n\t' + JSON.stringify(messages.error.resources, undefined, 2) + messages.error?.message + '\n\t' + JSON.stringify(messages.error?.resources, undefined, 2) ); - assertionError.stack = messages.error.stack + new Error().stack.slice('Error'.length); + assertionError.stack = messages.error?.stack + new Error().stack.slice('Error'.length); throw assertionError; } diff --git a/test/tools/fixtures/process_resource_script.in.js b/test/tools/fixtures/process_resource_script.in.js index ebf3dd6f86..0849d99fa0 100644 --- a/test/tools/fixtures/process_resource_script.in.js +++ b/test/tools/fixtures/process_resource_script.in.js @@ -17,7 +17,7 @@ const { setTimeout } = require('timers'); let originalReport; const logFile = scriptName + '.logs.txt'; -const sleep = promisify(setTimeout); +const sleep = util.promisify(setTimeout); const run = func; From e9a31081618a769087092964e425af2fe79d5cbf Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Mon, 6 Jan 2025 14:17:02 -0500 Subject: [PATCH 38/43] make stderr inherit --- .../node-specific/resource_tracking_script_builder.ts | 2 +- 1 file changed, 1 insertion(+), 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 bdb733a8eb..69273c90c5 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -168,7 +168,7 @@ export async function runScriptAndGetProcessInfo( await writeFile(scriptName, scriptContent, { encoding: 'utf8' }); const logFile = name + '.logs.txt'; - const script = spawn(process.execPath, [scriptName], { stdio: ['ignore', 'ignore', 'ignore'] }); + const script = spawn(process.execPath, [scriptName], { stdio: ['ignore', 'ignore', 'inherit'] }); const willClose = once(script, 'close'); From e96a94c2701cc115a57fd9eab28c556fcd6fd376 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Mon, 6 Jan 2025 16:11:21 -0500 Subject: [PATCH 39/43] sockets working --- socket-connection-rtt-monitoring.logs.txt | 3 - socket-minPoolSize.logs.txt | 2 + .../node-specific/client_close.test.ts | 102 ++++++++++-------- .../resource_tracking_script_builder.ts | 13 ++- .../fixtures/process_resource_script.in.js | 11 +- 5 files changed, 73 insertions(+), 58 deletions(-) delete mode 100644 socket-connection-rtt-monitoring.logs.txt create mode 100644 socket-minPoolSize.logs.txt diff --git a/socket-connection-rtt-monitoring.logs.txt b/socket-connection-rtt-monitoring.logs.txt deleted file mode 100644 index 9231c6dee6..0000000000 --- a/socket-connection-rtt-monitoring.logs.txt +++ /dev/null @@ -1,3 +0,0 @@ -{} -{"newResources":{"libuvResources":[],"activeResources":["TCPSocketWrap","TCPSocketWrap","TCPSocketWrap","TCPSocketWrap","TCPSocketWrap","TCPSocketWrap","TCPSocketWrap","TCPSocketWrap","TCPSocketWrap"]}} -{"beforeExitHappened":true} diff --git a/socket-minPoolSize.logs.txt b/socket-minPoolSize.logs.txt new file mode 100644 index 0000000000..51bf5d6419 --- /dev/null +++ b/socket-minPoolSize.logs.txt @@ -0,0 +1,2 @@ +{} +{"error":{"message":"Cannot read properties of undefined (reading 'prototype')","stack":"TypeError: Cannot read properties of undefined (reading 'prototype')\n at func (/Users/aditi.khare/Desktop/node-mongodb-native/socket-minPoolSize.cjs:13:90)\n at main (/Users/aditi.khare/Desktop/node-mongodb-native/socket-minPoolSize.cjs:109:9)\n at Object. (/Users/aditi.khare/Desktop/node-mongodb-native/socket-minPoolSize.cjs:113:1)\n at Module._compile (node:internal/modules/cjs/loader:1376:14)\n at Module._extensions..js (node:internal/modules/cjs/loader:1435:10)\n at Module.load (node:internal/modules/cjs/loader:1207:32)\n at Module._load (node:internal/modules/cjs/loader:1023:12)\n at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:135:12)\n at node:internal/main/run_main_module:28:49","resources":{"libuvResources":[],"activeResources":["FSReqPromise"]}}} diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 330460b339..522df93c74 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-empty-function */ -import { expect } from 'chai'; -import { MongoClient } from '../../mongodb'; +import sinon = require('sinon'); import { type TestConfiguration } from '../../tools/runner/config'; import { runScriptAndGetProcessInfo } from './resource_tracking_script_builder'; +import { ConnectionPool, MongoClient } from '../../mongodb'; describe.only('MongoClient.close() Integration', () => { // note: these tests are set-up in accordance of the resource ownership tree @@ -93,7 +93,9 @@ describe.only('MongoClient.close() Integration', () => { describe('MonitorInterval', () => { describe('Node.js resource: Timer', () => { describe('after a new monitor is made', () => { - it.skip('monitor interval timer is cleaned up by client.close()', async () => {}); + it.skip('monitor interval timer is cleaned up by client.close()', async () => { + + }); }); describe('after a heartbeat fails', () => { @@ -105,7 +107,7 @@ describe.only('MongoClient.close() Integration', () => { describe('Connection Monitoring', () => { describe('Node.js resource: Socket', () => { it('no sockets remain after client.close()', metadata, async function () { - const run = async function({ MongoClient, uri, expect }) { + const run = async function ({ MongoClient, uri, expect }) { const client = new MongoClient(uri); await client.connect(); @@ -116,11 +118,11 @@ describe.only('MongoClient.close() Integration', () => { .libuv.filter(r => r.type === 'tcp') .map(r => r.remoteEndpoint); - // assert socket creation const servers = client.topology?.s.servers; + // assert socket creation for (const server of servers) { const { host, port } = server[1].s.description.hostAddress; - expect(connectionMonitoringReport()).to.deep.include({host, port}); + expect(connectionMonitoringReport()).to.deep.include({ host, port }); } await client.close(); @@ -131,11 +133,7 @@ describe.only('MongoClient.close() Integration', () => { expect(connectionMonitoringReport()).to.not.deep.include({ host, port }); } }; - await runScriptAndGetProcessInfo( - 'socket-connection-monitoring', - config, - run - ); + await runScriptAndGetProcessInfo('socket-connection-monitoring', config, run); }); }); }); @@ -143,8 +141,29 @@ describe.only('MongoClient.close() Integration', () => { describe('RTT Pinger', () => { describe('Node.js resource: Timer', () => { describe('after entering monitor streaming mode ', () => { - it.skip('the rtt pinger timer is cleaned up by client.close()', async () => { - // helloReply has a topologyVersion defined + it.only('the rtt pinger timer is cleaned up by client.close()', async () => { + const run = async ({ MongoClient, uri, log, expect, sleep }) => { + const heartbeatFrequencyMS = 10000; + const client = new MongoClient(uri, { + serverMonitoringMode: 'stream', + heartbeatFrequencyMS + }); + await client.connect(); + + + client.on('serverHeartbeatSucceeded', async ev => log({ hearbeatHappened: ev.connectionId })); + + // ensure there is enough time for the events to occur + await sleep(heartbeatFrequencyMS * 10); + + // close the client + await client.close(); + + // upon close, assert timers are cleaned up + + }; + + await runScriptAndGetProcessInfo('socket-connection-rtt-monitoring', config, run); }); }); }); @@ -153,7 +172,7 @@ describe.only('MongoClient.close() Integration', () => { describe('Node.js resource: Socket', () => { describe('when rtt monitoring is turned on', () => { it('no sockets remain after client.close()', async () => { - const run = async ({ MongoClient, uri, log, expect, sleep }) => { + const run = async ({ MongoClient, uri, expect, sleep }) => { const heartbeatFrequencyMS = 100; const client = new MongoClient(uri, { serverMonitoringMode: 'stream', @@ -171,19 +190,20 @@ describe.only('MongoClient.close() Integration', () => { const activeSocketsAfterHeartbeat = () => activeSocketsReport() .filter(r => !socketsAddressesBeforeHeartbeat.includes(r.address)) - .map(r => r.remoteEndpoint.host + ':' + r.remoteEndpoint.port); + .map(r => r.remoteEndpoint?.host + ':' + r.remoteEndpoint?.port); // set of servers whose hearbeats have occurred const heartbeatOccurredSet = new Set(); - client.on('serverHeartbeatSucceeded', async ev => heartbeatOccurredSet.add(ev.connectionId)); + client.on('serverHeartbeatSucceeded', async ev => + heartbeatOccurredSet.add(ev.connectionId) + ); // ensure there is enough time for the events to occur await sleep(heartbeatFrequencyMS * 10); // all servers should have had a heartbeat event and had a new socket created for rtt pinger - log(heartbeatOccurredSet); - const servers = client.topology.s.servers + const servers = client.topology.s.servers; for (const server of servers) { expect(heartbeatOccurredSet).to.deep.contain(server[0]); expect(activeSocketsAfterHeartbeat()).to.deep.contain(server[0]); @@ -221,39 +241,31 @@ describe.only('MongoClient.close() Integration', () => { describe('Connection', () => { describe('Node.js resource: Socket', () => { - describe('after a connection is checked out', () => { + describe('after a minPoolSize has been set on the ConnectionPool', () => { it('no sockets remain after client.close()', async function () { - }); - }); + const run = async function ({ MongoClient, uri, expect }) { + const connectionMonitoringReport = () => + process.report.getReport().libuv.filter(r => r.type === 'tcp'); - describe('after a minPoolSize has been set on the ConnectionPool', () => { - it.only('no sockets remain after client.close()', async function () { - const run = async function ({ MongoClient, uri, log, expect }) { - log({hi: 1}); - const options = { minPoolSize: 2 }; + // assert no sockets to start with + expect(connectionMonitoringReport()).to.have.length(0); + const options = { minPoolSize: 1 }; const client = new MongoClient(uri, options); await client.connect(); - const connectionMonitoringReport = () => - process.report.getReport().libuv.filter(r => r.type === 'tcp').map(r => r.remoteEndpoint); - // assert socket creation - // there should be a client connection socket for each server, one monitor socket total, and one pool size monitoring socket total - const servers = client.topology?.s.servers; - - for (const server of servers) { - const { host, port } = server[1].s.description.hostAddress; - const relevantHostAddresses = connectionMonitoringReport().filter(r => r.host === host && r.port === port); - expect(relevantHostAddresses).length.to.be.gte(3); - } + // regardless of pool size: there should be a client connection socket for each server, and one monitor socket total + // with minPoolSize = 1, there should be one or more extra active sockets + expect(connectionMonitoringReport()).to.have.length.gte( + client.topology?.s.servers.size + 2 + ); await client.close(); + + // assert socket clean-up + expect(connectionMonitoringReport()).to.have.length(0); }; - await runScriptAndGetProcessInfo( - 'socket-minPoolSize', - config, - run - ); + await runScriptAndGetProcessInfo('socket-minPoolSize', config, run); }); }); }); @@ -313,10 +325,10 @@ describe.only('MongoClient.close() Integration', () => { await runScriptAndGetProcessInfo( 'tls-file-read-auto-encryption', config, - async function run({ MongoClient, uri, expect, ClientEncryption, BSON }) { + async function run({ MongoClient, uri, expect, mongodb }) { const infiniteFile = '/dev/zero'; - const kmsProviders = BSON.EJSON.parse(process.env.CSFLE_KMS_PROVIDERS); + const kmsProviders = mongodb.BSON.EJSON.parse(process.env.CSFLE_KMS_PROVIDERS); const masterKey = { region: 'us-east-1', key: 'arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0' @@ -327,7 +339,7 @@ describe.only('MongoClient.close() Integration', () => { await keyVaultClient.connect(); await keyVaultClient.db('keyvault').collection('datakeys'); - const clientEncryption = new ClientEncryption(keyVaultClient, { + const clientEncryption = new mongodb.ClientEncryption(keyVaultClient, { keyVaultNamespace: 'keyvault.datakeys', kmsProviders }); diff --git a/test/integration/node-specific/resource_tracking_script_builder.ts b/test/integration/node-specific/resource_tracking_script_builder.ts index 041c1de71d..39be5818eb 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -6,8 +6,11 @@ import * as path from 'node:path'; import { AssertionError, expect } from 'chai'; import { parseSnapshot } from 'v8-heapsnapshot'; -import { type BSON, type ClientEncryption, type MongoClient } from '../../mongodb'; +import type * as mongodb from '../../mongodb'; +import { type MongoClient } from '../../mongodb'; import { type TestConfiguration } from '../../tools/runner/config'; +import { type sleep } from '../../tools/utils'; +import * as sinon from 'sinon'; export type ResourceTestFunction = HeapResourceTestFunction | ProcessResourceTestFunction; @@ -22,8 +25,9 @@ export type ProcessResourceTestFunction = (options: { uri: string; log?: (out: any) => void; expect: typeof expect; - ClientEncryption?: typeof ClientEncryption; - BSON?: typeof BSON; + mongodb: typeof mongodb; + sleep?: typeof sleep; + sinon: typeof sinon; }) => Promise; const HEAP_RESOURCE_SCRIPT_PATH = path.resolve( @@ -197,5 +201,6 @@ export async function runScriptAndGetProcessInfo( // assertions about resource status expect(messages.beforeExitHappened).to.be.true; - expect(messages.newResources).to.be.empty; + expect(messages.newResources.libuvResources).to.be.empty; + expect(messages.newResources.activeResources).to.be.empty; } diff --git a/test/tools/fixtures/process_resource_script.in.js b/test/tools/fixtures/process_resource_script.in.js index 0849d99fa0..cffd123649 100644 --- a/test/tools/fixtures/process_resource_script.in.js +++ b/test/tools/fixtures/process_resource_script.in.js @@ -7,11 +7,13 @@ const func = FUNCTION_STRING; const scriptName = SCRIPT_NAME_STRING; const uri = URI_STRING; -const { MongoClient, ClientEncryption, BSON } = require(driverPath); +const mongodb = require(driverPath); +const { MongoClient } = mongodb; const process = require('node:process'); const util = require('node:util'); const timers = require('node:timers'); const fs = require('node:fs'); +const sinon = require('sinon'); const { expect } = require('chai'); const { setTimeout } = require('timers'); @@ -29,7 +31,6 @@ const run = func; * In order to be counted as a new resource, a resource MUST: * - Must NOT share an address with a libuv resource that existed at the start of script * - Must be referenced. See [here](https://nodejs.org/api/timers.html#timeoutref) for more context. - * - Must NOT be an inactive server * * We're using the following tool to track resources: `process.report.getReport().libuv` * For more context, see documentation for [process.report.getReport()](https://nodejs.org/api/report.html), and [libuv](https://docs.libuv.org/en/v1.x/handle.html). @@ -41,7 +42,6 @@ function getNewLibuvResourceArray() { /** * @typedef {Object} LibuvResource - * @property {boolean} is_active Is the resource active? For a socket, this means it is allowing I/O. For a timer, this means a timer is has not expired. * @property {string} type What is the resource type? For example, 'tcp' | 'timer' | 'udp' | 'tty'... (See more in [docs](https://docs.libuv.org/en/v1.x/handle.html)). * @property {boolean} is_referenced Is the resource keeping the JS event loop active? * @@ -51,8 +51,7 @@ function getNewLibuvResourceArray() { 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 - (!serverType.includes(resource.type) || resource.is_active) + resource.is_referenced // if a resource is unreferenced, it's not keeping the event loop open ); } @@ -93,7 +92,7 @@ async function main() { process.on('beforeExit', () => { log({ beforeExitHappened: true }); }); - await run({ MongoClient, uri, log, expect, ClientEncryption, BSON, sleep }); + await run({ MongoClient, uri, log, expect, mongodb, sleep, sinon }); log({ newResources: getNewResources() }); } From d8285298e364e1995ad331cd72c2503377fa7978 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Tue, 7 Jan 2025 18:29:47 -0500 Subject: [PATCH 40/43] timers --- socket-minPoolSize.logs.txt | 2 - .../node-specific/client_close.test.ts | 103 +++++++++++++++--- .../resource_tracking_script_builder.ts | 7 +- .../fixtures/process_resource_script.in.js | 10 +- 4 files changed, 99 insertions(+), 23 deletions(-) delete mode 100644 socket-minPoolSize.logs.txt diff --git a/socket-minPoolSize.logs.txt b/socket-minPoolSize.logs.txt deleted file mode 100644 index 51bf5d6419..0000000000 --- a/socket-minPoolSize.logs.txt +++ /dev/null @@ -1,2 +0,0 @@ -{} -{"error":{"message":"Cannot read properties of undefined (reading 'prototype')","stack":"TypeError: Cannot read properties of undefined (reading 'prototype')\n at func (/Users/aditi.khare/Desktop/node-mongodb-native/socket-minPoolSize.cjs:13:90)\n at main (/Users/aditi.khare/Desktop/node-mongodb-native/socket-minPoolSize.cjs:109:9)\n at Object. (/Users/aditi.khare/Desktop/node-mongodb-native/socket-minPoolSize.cjs:113:1)\n at Module._compile (node:internal/modules/cjs/loader:1376:14)\n at Module._extensions..js (node:internal/modules/cjs/loader:1435:10)\n at Module.load (node:internal/modules/cjs/loader:1207:32)\n at Module._load (node:internal/modules/cjs/loader:1023:12)\n at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:135:12)\n at node:internal/main/run_main_module:28:49","resources":{"libuvResources":[],"activeResources":["FSReqPromise"]}}} diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 522df93c74..7915d44053 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -1,8 +1,6 @@ /* eslint-disable @typescript-eslint/no-empty-function */ -import sinon = require('sinon'); import { type TestConfiguration } from '../../tools/runner/config'; import { runScriptAndGetProcessInfo } from './resource_tracking_script_builder'; -import { ConnectionPool, MongoClient } from '../../mongodb'; describe.only('MongoClient.close() Integration', () => { // note: these tests are set-up in accordance of the resource ownership tree @@ -93,8 +91,30 @@ describe.only('MongoClient.close() Integration', () => { describe('MonitorInterval', () => { describe('Node.js resource: Timer', () => { describe('after a new monitor is made', () => { - it.skip('monitor interval timer is cleaned up by client.close()', async () => { + it('monitor interval timer is cleaned up by client.close()', async function () { + const run = async function ({ MongoClient, uri, expect, sleep, getTimerCount }) { + const heartbeatFrequencyMS = 2000; + const client = new MongoClient(uri, { heartbeatFrequencyMS }); + let heartbeatHappened = false; + client.on('serverHeartbeatSucceeded', () => heartbeatHappened = true); + await client.connect(); + await sleep(heartbeatFrequencyMS * 2.5); + expect(heartbeatHappened).to.be.true; + + function getMonitorTimer(servers) { + for (const server of servers) { + return server[1]?.monitor.monitorId.timerId; + } + }; + const servers = client.topology.s.servers; + expect(getMonitorTimer(servers)).to.exist; + await client.close(); + expect(getMonitorTimer(servers)).to.not.exist; + + expect(getTimerCount()).to.equal(0); + }; + await runScriptAndGetProcessInfo('timer-monitor-interval', config, run); }); }); @@ -141,29 +161,36 @@ describe.only('MongoClient.close() Integration', () => { describe('RTT Pinger', () => { describe('Node.js resource: Timer', () => { describe('after entering monitor streaming mode ', () => { - it.only('the rtt pinger timer is cleaned up by client.close()', async () => { - const run = async ({ MongoClient, uri, log, expect, sleep }) => { - const heartbeatFrequencyMS = 10000; + it('the rtt pinger timer is cleaned up by client.close()', async function () { + const run = async function ({ MongoClient, uri, expect, sleep, getTimerCount }) { + const heartbeatFrequencyMS = 2000; const client = new MongoClient(uri, { serverMonitoringMode: 'stream', heartbeatFrequencyMS }); await client.connect(); + let heartbeatHappened = false; + client.on('serverHeartbeatSucceeded', () => heartbeatHappened = true); + await sleep(heartbeatFrequencyMS * 2.5); + expect(heartbeatHappened).to.be.true; - client.on('serverHeartbeatSucceeded', async ev => log({ hearbeatHappened: ev.connectionId })); + function getRttTimer(servers) { + for (const server of servers) { + return server[1]?.monitor.rttPinger.monitorId; + } + }; - // ensure there is enough time for the events to occur - await sleep(heartbeatFrequencyMS * 10); + const servers = client.topology.s.servers; + expect(getRttTimer(servers)).to.exist; - // close the client await client.close(); + expect(getRttTimer(servers)).to.not.exist; - // upon close, assert timers are cleaned up - + expect(getTimerCount()).to.equal(0); }; - await runScriptAndGetProcessInfo('socket-connection-rtt-monitoring', config, run); + await runScriptAndGetProcessInfo('timer-rtt-monitor', config, run); }); }); }); @@ -228,14 +255,60 @@ describe.only('MongoClient.close() Integration', () => { describe('ConnectionPool', () => { describe('Node.js resource: minPoolSize timer', () => { describe('after new connection pool is created', () => { - it.skip('the minPoolSize timer is cleaned up by client.close()', async () => {}); + it('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; + client.on('connectionPoolReady', () => (minPoolSizeTimerCreated = true)); + await client.connect(); + + expect(minPoolSizeTimerCreated).to.be.true; + + const servers = client.topology?.s.servers; + + // note: minPoolSizeCheckFrequencyMS = 100 ms by client, so this test has a chance of being flaky + for (const server of servers) { + const minPoolSizeTimer = server[1].pool.minPoolSizeTimer; + expect(minPoolSizeTimer).to.exist; + break; + } + + await client.close(); + expect(getTimerCount()).to.equal(0); + }; + await runScriptAndGetProcessInfo('timer-min-pool-size', config, run); + }); }); }); describe('Node.js resource: checkOut Timer', () => { // waitQueueTimeoutMS describe('after new connection pool is created', () => { - it.skip('the wait queue timer is cleaned up by client.close()', async () => {}); + it('the wait queue timer is cleaned up by client.close()', async function () { + const run = async function ({ MongoClient, uri, expect, sinon, mongodb, getTimerCount }) { + const waitQueueTimeoutMS = 999999; + const client = new MongoClient(uri, { minPoolSize: 1, waitQueueTimeoutMS }); + + sinon.spy(mongodb.Timeout, 'expires'); + const timeoutContextSpy = sinon.spy(mongodb.TimeoutContext, 'create'); + // TODO delay promise.race non-timeout promise + + await client + .db('db') + .collection('collection') + .insertOne({ x: 1 }) + .catch(e => e); + + expect(timeoutContextSpy.getCalls()).to.have.length.greaterThanOrEqual(1); + expect(mongodb.Timeout.expires).to.have.been.calledWith(999999); + + await client.close(); + expect(getTimerCount()).to.equal(0); + sinon.restore(); + }; + + await runScriptAndGetProcessInfo('timer-check-out', 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 39be5818eb..f83349f4bd 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -4,13 +4,13 @@ import { readFile, unlink, writeFile } from 'node:fs/promises'; import * as path from 'node:path'; import { AssertionError, expect } from 'chai'; +import type * as sinon from 'sinon'; import { parseSnapshot } from 'v8-heapsnapshot'; import type * as mongodb from '../../mongodb'; import { type MongoClient } from '../../mongodb'; import { type TestConfiguration } from '../../tools/runner/config'; import { type sleep } from '../../tools/utils'; -import * as sinon from 'sinon'; export type ResourceTestFunction = HeapResourceTestFunction | ProcessResourceTestFunction; @@ -25,9 +25,10 @@ export type ProcessResourceTestFunction = (options: { uri: string; log?: (out: any) => void; expect: typeof expect; - mongodb: typeof mongodb; + mongodb?: typeof mongodb; sleep?: typeof sleep; - sinon: typeof sinon; + sinon?: typeof sinon; + getTimerCount?: () => Number; }) => Promise; const HEAP_RESOURCE_SCRIPT_PATH = path.resolve( diff --git a/test/tools/fixtures/process_resource_script.in.js b/test/tools/fixtures/process_resource_script.in.js index cffd123649..b71fca3528 100644 --- a/test/tools/fixtures/process_resource_script.in.js +++ b/test/tools/fixtures/process_resource_script.in.js @@ -50,8 +50,7 @@ function getNewLibuvResourceArray() { 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 + !originalReportAddresses.includes(resource.address) && resource.is_referenced // if a resource is unreferenced, it's not keeping the event loop open ); } @@ -80,6 +79,11 @@ function getNewResources() { }; } +/** + * @returns Number of active timers in event loop + */ +const getTimerCount = () => process.getActiveResourcesInfo().filter(r => r === 'Timeout').length; + // A log function for debugging function log(message) { // remove outer parentheses for easier parsing @@ -92,7 +96,7 @@ async function main() { process.on('beforeExit', () => { log({ beforeExitHappened: true }); }); - await run({ MongoClient, uri, log, expect, mongodb, sleep, sinon }); + await run({ MongoClient, uri, log, expect, mongodb, sleep, sinon, getTimerCount }); log({ newResources: getNewResources() }); } From 24ebcdee68e6f10d79bd7a82c70b7bf5a56f37fe Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Wed, 8 Jan 2025 00:04:57 -0500 Subject: [PATCH 41/43] eod commit --- .../node-specific/client_close.test.ts | 81 +++++++++++++------ .../resource_tracking_script_builder.ts | 2 +- 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 7915d44053..b3a6424571 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -1,6 +1,11 @@ /* eslint-disable @typescript-eslint/no-empty-function */ +const { expect } = require('chai'); +import * as sinon from 'sinon'; +const mongodb = require('../../mongodb'); +const { MongoClient } = mongodb; import { type TestConfiguration } from '../../tools/runner/config'; import { runScriptAndGetProcessInfo } from './resource_tracking_script_builder'; +import { sleep } from '../../tools/utils'; describe.only('MongoClient.close() Integration', () => { // note: these tests are set-up in accordance of the resource ownership tree @@ -91,7 +96,7 @@ describe.only('MongoClient.close() Integration', () => { describe('MonitorInterval', () => { describe('Node.js resource: Timer', () => { describe('after a new monitor is made', () => { - it('monitor interval timer is cleaned up by client.close()', async function () { + it('monitor interval timer is cleaned up by client.close()', metadata, async function () { const run = async function ({ MongoClient, uri, expect, sleep, getTimerCount }) { const heartbeatFrequencyMS = 2000; const client = new MongoClient(uri, { heartbeatFrequencyMS }); @@ -119,7 +124,7 @@ describe.only('MongoClient.close() Integration', () => { }); describe('after a heartbeat fails', () => { - it.skip('the new monitor interval timer is cleaned up by client.close()', async () => {}); + it.skip('the new monitor interval timer is cleaned up by client.close()', metadata, async () => {}); }); }); }); @@ -161,7 +166,7 @@ describe.only('MongoClient.close() Integration', () => { describe('RTT Pinger', () => { describe('Node.js resource: Timer', () => { describe('after entering monitor streaming mode ', () => { - it('the rtt pinger timer is cleaned up by client.close()', async function () { + it('the rtt pinger timer is cleaned up by client.close()', metadata, async function () { const run = async function ({ MongoClient, uri, expect, sleep, getTimerCount }) { const heartbeatFrequencyMS = 2000; const client = new MongoClient(uri, { @@ -198,7 +203,7 @@ describe.only('MongoClient.close() Integration', () => { describe('Connection', () => { describe('Node.js resource: Socket', () => { describe('when rtt monitoring is turned on', () => { - it('no sockets remain after client.close()', async () => { + it('no sockets remain after client.close()', metadata, async () => { const run = async ({ MongoClient, uri, expect, sleep }) => { const heartbeatFrequencyMS = 100; const client = new MongoClient(uri, { @@ -266,14 +271,17 @@ describe.only('MongoClient.close() Integration', () => { const servers = client.topology?.s.servers; - // note: minPoolSizeCheckFrequencyMS = 100 ms by client, so this test has a chance of being flaky - for (const server of servers) { - const minPoolSizeTimer = server[1].pool.minPoolSizeTimer; - expect(minPoolSizeTimer).to.exist; - break; + + function getMinPoolSizeTimer(servers) { + for (const server of servers) { + return server[1].pool.minPoolSizeTimer; + } } + // note: minPoolSizeCheckFrequencyMS = 100 ms by client, so this test has a chance of being flaky + expect(getMinPoolSizeTimer(servers)).to.exist; await client.close(); + expect(getMinPoolSizeTimer(servers)).to.not.exist; expect(getTimerCount()).to.equal(0); }; await runScriptAndGetProcessInfo('timer-min-pool-size', config, run); @@ -285,29 +293,30 @@ describe.only('MongoClient.close() Integration', () => { // waitQueueTimeoutMS describe('after new connection pool is created', () => { it('the wait queue timer is cleaned up by client.close()', async function () { - const run = async function ({ MongoClient, uri, expect, sinon, mongodb, getTimerCount }) { - const waitQueueTimeoutMS = 999999; + // note: this test is not called in a separate process since it requires stubbing internal function + const run = async function ({ MongoClient, uri, expect, sinon, sleep, mongodb, getTimerCount }) { + const waitQueueTimeoutMS = 999; const client = new MongoClient(uri, { minPoolSize: 1, waitQueueTimeoutMS }); + const timeoutStartedSpy = sinon.spy(mongodb.Timeout, 'expires'); + let checkoutTimeoutStarted = false; - sinon.spy(mongodb.Timeout, 'expires'); - const timeoutContextSpy = sinon.spy(mongodb.TimeoutContext, 'create'); - // TODO delay promise.race non-timeout promise + // make waitQueue hang so check out timer isn't cleared and check that the timeout has started + sinon.stub(mongodb.ConnectionPool.prototype, 'processWaitQueue').callsFake(async () => { + checkoutTimeoutStarted = timeoutStartedSpy.getCalls().map(r => r.args).filter(r => r.includes(999)) ? true : false; + }); - await client - .db('db') - .collection('collection') - .insertOne({ x: 1 }) - .catch(e => e); + client.db('db').collection('collection').insertOne({ x: 1 }).catch(e => e); - expect(timeoutContextSpy.getCalls()).to.have.length.greaterThanOrEqual(1); - expect(mongodb.Timeout.expires).to.have.been.calledWith(999999); + // don't allow entire checkout timer to elapse to ensure close is called mid-timeout + await sleep(waitQueueTimeoutMS / 2); + expect(checkoutTimeoutStarted).to.be.true; await client.close(); expect(getTimerCount()).to.equal(0); - sinon.restore(); }; - await runScriptAndGetProcessInfo('timer-check-out', config, run); + const getTimerCount = () => process.getActiveResourcesInfo().filter(r => r === 'Timeout').length; + await run({ MongoClient, uri: config.uri, sleep, sinon, expect, mongodb, getTimerCount}); }); }); }); @@ -348,8 +357,32 @@ describe.only('MongoClient.close() Integration', () => { describe('SrvPoller', () => { describe('Node.js resource: Timer', () => { + // srv polling is not available for load-balanced mode + const metadata: MongoDBMetadataUI = { + requires: { + topology: ['single', 'replicaset', 'sharded'] + } + }; describe('after SRVPoller is created', () => { - it.skip('timers are cleaned up by client.close()', async () => {}); + it.only('timers are cleaned up by client.close()', metadata, async () => { + const run = async function ({ MongoClient, uri, expect, sinon, getTimerCount }) { + const dns = require('dns'); + + sinon.stub(dns.promises, 'resolveTxt').callsFake(async () => uri); + sinon.stub(dns.promises, 'resolveSrv').callsFake(async () => uri); + + const srvUri = uri.replace('mongodb://', 'mongodb+srv://'); + const client = new MongoClient(srvUri); + await client.connect(); + + + await client.close(); + expect(getTimerCount()).to.equal(0); + }; + + const getTimerCount = () => process.getActiveResourcesInfo().filter(r => r === 'Timeout').length; + await run({ MongoClient, uri: config.uri, sleep, sinon, expect, mongodb, getTimerCount}); + }); }); }); }); diff --git a/test/integration/node-specific/resource_tracking_script_builder.ts b/test/integration/node-specific/resource_tracking_script_builder.ts index acd41d32d4..0fe91c27a7 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -188,7 +188,7 @@ export async function runScriptAndGetProcessInfo( .reduce((acc, curr) => ({ ...acc, ...curr }), {}); // delete temporary files - await unlink(scriptName); + // await unlink(scriptName); // await unlink(logFile); // assertions about exit status From f0034549d25a07cb350ca59ba131f8dd5da18561 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Thu, 9 Jan 2025 02:57:58 -0500 Subject: [PATCH 42/43] rebase rebase --- src/utils.ts | 1 + .../node-specific/client_close.test.ts | 74 ++++++++++++++----- 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index c23161612a..6feda06632 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1166,6 +1166,7 @@ export function parseUnsignedInteger(value: unknown): number | null { * @returns void */ export function checkParentDomainMatch(address: string, srvHost: string): void { + return; // Remove trailing dot if exists on either the resolved address or the srv hostname const normalizedAddress = address.endsWith('.') ? address.slice(0, address.length - 1) : address; const normalizedSrvHost = srvHost.endsWith('.') ? srvHost.slice(0, srvHost.length - 1) : srvHost; diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index b3a6424571..6e01de170a 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/no-empty-function */ -const { expect } = require('chai'); +import { expect } from 'chai'; import * as sinon from 'sinon'; -const mongodb = require('../../mongodb'); -const { MongoClient } = mongodb; +import { MongoClient } from '../../mongodb'; import { type TestConfiguration } from '../../tools/runner/config'; import { runScriptAndGetProcessInfo } from './resource_tracking_script_builder'; import { sleep } from '../../tools/utils'; +import { ConnectionPool, Timeout } from '../../mongodb'; describe.only('MongoClient.close() Integration', () => { // note: these tests are set-up in accordance of the resource ownership tree @@ -80,7 +80,32 @@ describe.only('MongoClient.close() Integration', () => { describe('Topology', () => { describe('Node.js resource: Server Selection Timer', () => { describe('after a Topology is created through client.connect()', () => { - it.skip('server selection timers are cleaned up by client.close()', async () => {}); + it.only('server selection timers are cleaned up by client.close()', async () => { + // note: this test is not called in a separate process since it requires stubbing internal class: Timeout + const run = async function ({ MongoClient, uri, expect, sinon, sleep, getTimerCount }) { + const serverSelectionTimeoutMS = 777; + const client = new MongoClient(uri, { minPoolSize: 1, serverSelectionTimeoutMS }); + const timeoutStartedSpy = sinon.spy(Timeout, 'expires'); + let serverSelectionTimeoutStarted = false; + + // make server selection hang so check out timer isn't cleared and check that the timeout has started + sinon.stub(Promise, 'race').callsFake(() => { + serverSelectionTimeoutStarted = timeoutStartedSpy.getCalls().filter(r => r.args.includes(777)).flat().length > 0; + }); + + client.db('db').collection('collection').insertOne({ x: 1 }).catch(e => e); + + // don't allow entire checkout timer to elapse to ensure close is called mid-timeout + await sleep(serverSelectionTimeoutMS / 2); + expect(serverSelectionTimeoutStarted).to.be.true; + + await client.close(); + expect(getTimerCount()).to.equal(0); + }; + + const getTimerCount = () => process.getActiveResourcesInfo().filter(r => r === 'Timeout').length; + await run({ MongoClient, uri: config.uri, sleep, sinon, expect, getTimerCount}); + }); }); }); @@ -294,14 +319,14 @@ describe.only('MongoClient.close() Integration', () => { describe('after new connection pool is created', () => { it('the wait queue timer is cleaned up by client.close()', async function () { // note: this test is not called in a separate process since it requires stubbing internal function - const run = async function ({ MongoClient, uri, expect, sinon, sleep, mongodb, getTimerCount }) { + const run = async function ({ MongoClient, uri, expect, sinon, sleep, getTimerCount }) { const waitQueueTimeoutMS = 999; const client = new MongoClient(uri, { minPoolSize: 1, waitQueueTimeoutMS }); - const timeoutStartedSpy = sinon.spy(mongodb.Timeout, 'expires'); + const timeoutStartedSpy = sinon.spy(Timeout, 'expires'); let checkoutTimeoutStarted = false; // make waitQueue hang so check out timer isn't cleared and check that the timeout has started - sinon.stub(mongodb.ConnectionPool.prototype, 'processWaitQueue').callsFake(async () => { + sinon.stub(ConnectionPool.prototype, 'processWaitQueue').callsFake(async () => { checkoutTimeoutStarted = timeoutStartedSpy.getCalls().map(r => r.args).filter(r => r.includes(999)) ? true : false; }); @@ -316,7 +341,7 @@ describe.only('MongoClient.close() Integration', () => { }; const getTimerCount = () => process.getActiveResourcesInfo().filter(r => r === 'Timeout').length; - await run({ MongoClient, uri: config.uri, sleep, sinon, expect, mongodb, getTimerCount}); + await run({ MongoClient, uri: config.uri, sleep, sinon, expect, getTimerCount}); }); }); }); @@ -364,24 +389,39 @@ describe.only('MongoClient.close() Integration', () => { } }; describe('after SRVPoller is created', () => { - it.only('timers are cleaned up by client.close()', metadata, async () => { - const run = async function ({ MongoClient, uri, expect, sinon, getTimerCount }) { + it.skip('timers are cleaned up by client.close()', metadata, async () => { + const run = async function ({ MongoClient, uri, expect, log, sinon, mongodb, getTimerCount }) { const dns = require('dns'); - sinon.stub(dns.promises, 'resolveTxt').callsFake(async () => uri); - sinon.stub(dns.promises, 'resolveSrv').callsFake(async () => uri); + sinon.stub(dns.promises, 'resolveTxt').callsFake(async () => { + throw { code: 'ENODATA' }; + }); + sinon.stub(dns.promises, 'resolveSrv').callsFake(async () => { + const formattedUri = mongodb.HostAddress.fromString(uri.split('//')[1]); + return [ + { + name: formattedUri.host, + port: formattedUri.port, + weight: 0, + priority: 0, + protocol: formattedUri.host.isIPv6 ? 'IPv6' : 'IPv4' + } + ]; + }); + /* sinon.stub(mongodb, 'checkParentDomainMatch').callsFake(async () => { + console.log('in here!!!'); + }); */ - const srvUri = uri.replace('mongodb://', 'mongodb+srv://'); - const client = new MongoClient(srvUri); + const client = new MongoClient('mongodb+srv://localhost'); await client.connect(); - - await client.close(); expect(getTimerCount()).to.equal(0); + sinon.restore(); }; const getTimerCount = () => process.getActiveResourcesInfo().filter(r => r === 'Timeout').length; - await run({ MongoClient, uri: config.uri, sleep, sinon, expect, mongodb, getTimerCount}); + // await run({ MongoClient, uri: config.uri, sleep, sinon, expect, mongodb, getTimerCount}); + await runScriptAndGetProcessInfo('srv-poller-timer', config, run); }); }); }); From 1f321ecd77e68fcd0540236dfdafed61849d8bc4 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Thu, 9 Jan 2025 11:07:28 -0500 Subject: [PATCH 43/43] push for virtual office --- heartbeat-failed-monitor-timer.cjs | 141 +++++++++++++++++ heartbeat-failed-monitor-timer.logs.txt | 2 + src/utils.ts | 1 - .../node-specific/client_close.test.ts | 108 ++++++++----- .../resource_tracking_script_builder.ts | 4 +- .../fixtures/process_resource_script.in.js | 6 +- timer-srv-poller.cjs | 147 ++++++++++++++++++ timer-srv-poller.logs.txt | 5 + 8 files changed, 369 insertions(+), 45 deletions(-) create mode 100644 heartbeat-failed-monitor-timer.cjs create mode 100644 heartbeat-failed-monitor-timer.logs.txt create mode 100644 timer-srv-poller.cjs create mode 100644 timer-srv-poller.logs.txt diff --git a/heartbeat-failed-monitor-timer.cjs b/heartbeat-failed-monitor-timer.cjs new file mode 100644 index 0000000000..934a72cc7b --- /dev/null +++ b/heartbeat-failed-monitor-timer.cjs @@ -0,0 +1,141 @@ +'use strict'; + +/* eslint-disable no-undef */ +/* eslint-disable no-unused-vars */ +const driverPath = "/Users/aditi.khare/Desktop/node-mongodb-native/lib"; +const func = (async function ({ MongoClient, uri, expect, sleep, getTimerCount }) { + const heartbeatFrequencyMS = 2000; + const client = new MongoClient('mongodb://fakeUri', { heartbeatFrequencyMS }); + let heartbeatHappened = false; + client.on('serverHeartbeatFailed', () => heartbeatHappened = true); + client.connect(); + await sleep(heartbeatFrequencyMS * 2.5); + expect(heartbeatHappened).to.be.true; + function getMonitorTimer(servers) { + for (const server of servers) { + return server[1]?.monitor.monitorId.timerId; + } + } + ; + const servers = client.topology.s.servers; + expect(getMonitorTimer(servers)).to.exist; + await client.close(); + expect(getMonitorTimer(servers)).to.not.exist; + expect(getTimerCount()).to.equal(0); + }); +const scriptName = "heartbeat-failed-monitor-timer"; +const uri = "mongodb://localhost:27017/integration_tests?authSource=admin"; + +const mongodb = require(driverPath); +const { MongoClient } = mongodb; +const process = require('node:process'); +const util = require('node:util'); +const timers = require('node:timers'); +const fs = require('node:fs'); +const sinon = require('sinon'); +const { expect } = require('chai'); +const { setTimeout } = require('timers'); + +let originalReport; +const logFile = scriptName + '.logs.txt'; +const sleep = util.promisify(setTimeout); + +const run = func; + +/** + * + * Returns an array containing the new libuv resources created after script started. + * A new resource is something that will keep the event loop running. + * + * In order to be counted as a new resource, a resource MUST: + * - Must NOT share an address with a libuv resource that existed at the start of script + * - Must be referenced. See [here](https://nodejs.org/api/timers.html#timeoutref) for more context. + * + * We're using the following tool to track resources: `process.report.getReport().libuv` + * For more context, see documentation for [process.report.getReport()](https://nodejs.org/api/report.html), and [libuv](https://docs.libuv.org/en/v1.x/handle.html). + * + */ +function getNewLibuvResourceArray() { + let currReport = process.report.getReport().libuv; + const originalReportAddresses = originalReport.map(resource => resource.address); + + /** + * @typedef {Object} LibuvResource + * @property {string} type What is the resource type? For example, 'tcp' | 'timer' | 'udp' | 'tty'... (See more in [docs](https://docs.libuv.org/en/v1.x/handle.html)). + * @property {boolean} is_referenced Is the resource keeping the JS event loop active? + * + * @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 + ); + } + + currReport = currReport.filter(resource => isNewLibuvResource(resource)); + return currReport; +} + +/** + * Returns an object of the new resources created after script started. + * + * + * In order to be counted as a new resource, a resource MUST either: + * - Meet the criteria to be returned by our helper utility `getNewLibuvResourceArray()` + * OR + * - Be returned by `process.getActiveResourcesInfo() and is not 'TTYWrap' + * + * The reason we are using both methods to detect active resources is: + * - `process.report.getReport().libuv` does not detect active requests (such as timers or file reads) accurately + * - `process.getActiveResourcesInfo()` does not contain enough server information we need for our assertions + * + */ +function getNewResources() { + + return { + libuvResources: getNewLibuvResourceArray(), + activeResources: process.getActiveResourcesInfo() + }; +} + +/** + * @returns Number of active timers in event loop + */ +const getTimerCount = () => process.getActiveResourcesInfo().filter(r => r === 'Timeout').length; + +// A log function for debugging +function log(message) { + // remove outer parentheses for easier parsing + const messageToLog = JSON.stringify(message) + ' \n'; + fs.writeFileSync(logFile, messageToLog, { flag: 'a' }); +} + +async function main() { + originalReport = process.report.getReport().libuv; + process.on('beforeExit', () => { + log({ beforeExitHappened: true }); + }); + await run({ MongoClient, uri, log, expect, mongodb, sleep, sinon, getTimerCount }); + log({ newResources: getNewResources() }); +} + +main() + .then(() => {}) + .catch(e => { + log({ error: { message: e.message, stack: e.stack, resources: getNewResources() } }); + process.exit(1); + }); + +setTimeout(() => { + // this means something was in the event loop such that it hung for more than 10 seconds + // so we kill the process + log({ + error: { + message: 'Process timed out: resources remain in the event loop', + resources: getNewResources() + } + }); + process.exit(99); + // using `unref` will ensure this setTimeout call is not a resource / does not keep the event loop running +}, 10000).unref(); diff --git a/heartbeat-failed-monitor-timer.logs.txt b/heartbeat-failed-monitor-timer.logs.txt new file mode 100644 index 0000000000..b3e7779798 --- /dev/null +++ b/heartbeat-failed-monitor-timer.logs.txt @@ -0,0 +1,2 @@ +{"error":{"message":"expected 1 to equal +0","stack":"AssertionError: expected 1 to equal +0\n at func (/Users/aditi.khare/Desktop/node-mongodb-native/heartbeat-failed-monitor-timer.cjs:24:64)\n at async main (/Users/aditi.khare/Desktop/node-mongodb-native/heartbeat-failed-monitor-timer.cjs:119:3)","resources":{"libuvResources":[],"activeResources":[]}}} +{"error":{"message":"expected 1 to equal +0","stack":"AssertionError: expected 1 to equal +0\n at func (/Users/aditi.khare/Desktop/node-mongodb-native/heartbeat-failed-monitor-timer.cjs:24:64)\n at async main (/Users/aditi.khare/Desktop/node-mongodb-native/heartbeat-failed-monitor-timer.cjs:119:3)","resources":{"libuvResources":[],"activeResources":["TTYWrap"]}}} diff --git a/src/utils.ts b/src/utils.ts index 6feda06632..c23161612a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1166,7 +1166,6 @@ export function parseUnsignedInteger(value: unknown): number | null { * @returns void */ export function checkParentDomainMatch(address: string, srvHost: string): void { - return; // Remove trailing dot if exists on either the resolved address or the srv hostname const normalizedAddress = address.endsWith('.') ? address.slice(0, address.length - 1) : address; const normalizedSrvHost = srvHost.endsWith('.') ? srvHost.slice(0, srvHost.length - 1) : srvHost; diff --git a/test/integration/node-specific/client_close.test.ts b/test/integration/node-specific/client_close.test.ts index 6e01de170a..51e1bb1c49 100644 --- a/test/integration/node-specific/client_close.test.ts +++ b/test/integration/node-specific/client_close.test.ts @@ -1,11 +1,12 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import { expect } from 'chai'; import * as sinon from 'sinon'; -import { MongoClient } from '../../mongodb'; +import { Connection, HostAddress, MongoClient } from '../../mongodb'; import { type TestConfiguration } from '../../tools/runner/config'; import { runScriptAndGetProcessInfo } from './resource_tracking_script_builder'; import { sleep } from '../../tools/utils'; -import { ConnectionPool, Timeout } from '../../mongodb'; +import { ConnectionPool } from '../../mongodb'; +import { getActiveResourcesInfo } from 'process'; describe.only('MongoClient.close() Integration', () => { // note: these tests are set-up in accordance of the resource ownership tree @@ -80,31 +81,34 @@ describe.only('MongoClient.close() Integration', () => { describe('Topology', () => { describe('Node.js resource: Server Selection Timer', () => { describe('after a Topology is created through client.connect()', () => { - it.only('server selection timers are cleaned up by client.close()', async () => { - // note: this test is not called in a separate process since it requires stubbing internal class: Timeout - const run = async function ({ MongoClient, uri, expect, sinon, sleep, getTimerCount }) { - const serverSelectionTimeoutMS = 777; + it('server selection timers are cleaned up by client.close()', async () => { + const run = async function ({ MongoClient, uri, expect, sinon, sleep, mongodb, getTimerCount }) { + const serverSelectionTimeoutMS = 2222; const client = new MongoClient(uri, { minPoolSize: 1, serverSelectionTimeoutMS }); - const timeoutStartedSpy = sinon.spy(Timeout, 'expires'); + const timers = require('timers'); + const timeoutStartedSpy = sinon.spy(timers, 'setTimeout'); let serverSelectionTimeoutStarted = false; // make server selection hang so check out timer isn't cleared and check that the timeout has started - sinon.stub(Promise, 'race').callsFake(() => { - serverSelectionTimeoutStarted = timeoutStartedSpy.getCalls().filter(r => r.args.includes(777)).flat().length > 0; + sinon.stub(Promise, 'race').callsFake(async ([serverPromise, timeout]) => { + serverSelectionTimeoutStarted = timeoutStartedSpy.getCalls().filter(r => r.args.includes(serverSelectionTimeoutMS)).flat().length > 0; + await timeout; }); - client.db('db').collection('collection').insertOne({ x: 1 }).catch(e => e); + const insertPromise = client.db('db').collection('collection').insertOne({ x: 1 }); - // don't allow entire checkout timer to elapse to ensure close is called mid-timeout + // don't allow entire server selection timer to elapse to ensure close is called mid-timeout await sleep(serverSelectionTimeoutMS / 2); expect(serverSelectionTimeoutStarted).to.be.true; + expect(getTimerCount()).to.not.equal(0); await client.close(); expect(getTimerCount()).to.equal(0); - }; - const getTimerCount = () => process.getActiveResourcesInfo().filter(r => r === 'Timeout').length; - await run({ MongoClient, uri: config.uri, sleep, sinon, expect, getTimerCount}); + const err = await insertPromise.catch(e => e); + expect(err).to.be.instanceOf(mongodb.MongoServerSelectionError); + }; + await runScriptAndGetProcessInfo('timer-server-selection', config, run); }); }); }); @@ -143,13 +147,35 @@ describe.only('MongoClient.close() Integration', () => { expect(getTimerCount()).to.equal(0); }; - await runScriptAndGetProcessInfo('timer-monitor-interval', config, run); }); }); describe('after a heartbeat fails', () => { - it.skip('the new monitor interval timer is cleaned up by client.close()', metadata, async () => {}); + it('the new monitor interval timer is cleaned up by client.close()', metadata, async () => { + const run = async function ({ MongoClient, uri, expect, sleep, getTimerCount }) { + const heartbeatFrequencyMS = 2000; + const client = new MongoClient('mongodb://fakeUri', { heartbeatFrequencyMS }); + let heartbeatHappened = false; + client.on('serverHeartbeatFailed', () => heartbeatHappened = true); + client.connect(); + await sleep(heartbeatFrequencyMS * 2.5); + expect(heartbeatHappened).to.be.true; + + function getMonitorTimer(servers) { + for (const server of servers) { + return server[1]?.monitor.monitorId.timerId; + } + }; + const servers = client.topology.s.servers; + expect(getMonitorTimer(servers)).to.exist; + await client.close(); + expect(getMonitorTimer(servers)).to.not.exist; + + expect(getTimerCount()).to.equal(0); + }; + await runScriptAndGetProcessInfo('timer-heartbeat-failed-monitor', config, run); + }); }); }); }); @@ -219,7 +245,6 @@ describe.only('MongoClient.close() Integration', () => { expect(getTimerCount()).to.equal(0); }; - await runScriptAndGetProcessInfo('timer-rtt-monitor', config, run); }); }); @@ -315,22 +340,22 @@ describe.only('MongoClient.close() Integration', () => { }); describe('Node.js resource: checkOut Timer', () => { - // waitQueueTimeoutMS describe('after new connection pool is created', () => { it('the wait queue timer is cleaned up by client.close()', async function () { - // note: this test is not called in a separate process since it requires stubbing internal function + // note: this test is not called in a separate process since it stubs internal class: ConnectionPool const run = async function ({ MongoClient, uri, expect, sinon, sleep, getTimerCount }) { const waitQueueTimeoutMS = 999; const client = new MongoClient(uri, { minPoolSize: 1, waitQueueTimeoutMS }); - const timeoutStartedSpy = sinon.spy(Timeout, 'expires'); + const timers = require('timers'); + const timeoutStartedSpy = sinon.spy(timers, 'setTimeout'); let checkoutTimeoutStarted = false; // make waitQueue hang so check out timer isn't cleared and check that the timeout has started sinon.stub(ConnectionPool.prototype, 'processWaitQueue').callsFake(async () => { - checkoutTimeoutStarted = timeoutStartedSpy.getCalls().map(r => r.args).filter(r => r.includes(999)) ? true : false; + checkoutTimeoutStarted = timeoutStartedSpy.getCalls().filter(r => r.args.includes(waitQueueTimeoutMS)).flat().length > 0; }); - client.db('db').collection('collection').insertOne({ x: 1 }).catch(e => e); + const insertPromise = client.db('db').collection('collection').insertOne({ x: 1 }).catch(e => e); // don't allow entire checkout timer to elapse to ensure close is called mid-timeout await sleep(waitQueueTimeoutMS / 2); @@ -338,8 +363,10 @@ describe.only('MongoClient.close() Integration', () => { await client.close(); expect(getTimerCount()).to.equal(0); - }; + const err = await insertPromise; + expect(err).to.not.be.instanceOf(Error); + }; const getTimerCount = () => process.getActiveResourcesInfo().filter(r => r === 'Timeout').length; await run({ MongoClient, uri: config.uri, sleep, sinon, expect, getTimerCount}); }); @@ -389,39 +416,40 @@ describe.only('MongoClient.close() Integration', () => { } }; describe('after SRVPoller is created', () => { - it.skip('timers are cleaned up by client.close()', metadata, async () => { - const run = async function ({ MongoClient, uri, expect, log, sinon, mongodb, getTimerCount }) { + it.only('timers are cleaned up by client.close()', metadata, async () => { + const run = async function ({ MongoClient, uri, expect, mongodb, sinon, getTimerCount }) { const dns = require('dns'); - sinon.stub(dns.promises, 'resolveTxt').callsFake(async () => { throw { code: 'ENODATA' }; }); sinon.stub(dns.promises, 'resolveSrv').callsFake(async () => { - const formattedUri = mongodb.HostAddress.fromString(uri.split('//')[1]); return [ { - name: formattedUri.host, - port: formattedUri.port, + name: 'domain.localhost', + port: 27017, weight: 0, priority: 0, - protocol: formattedUri.host.isIPv6 ? 'IPv6' : 'IPv4' + protocol: 'IPv6' } ]; }); - /* sinon.stub(mongodb, 'checkParentDomainMatch').callsFake(async () => { - console.log('in here!!!'); - }); */ const client = new MongoClient('mongodb+srv://localhost'); - await client.connect(); + client.connect(); + + const topology = client.topology; + const prevDesc = topology; + log({ topology }); + const currDesc = prevDesc; + client.topology.emit( + 'topologyDescriptionChanged', + mongodb.TopologyDescriptionChangedEvent(client.topology.s.id, prevDesc, currDesc) + ); + await client.close(); expect(getTimerCount()).to.equal(0); - sinon.restore(); }; - - const getTimerCount = () => process.getActiveResourcesInfo().filter(r => r === 'Timeout').length; - // await run({ MongoClient, uri: config.uri, sleep, sinon, expect, mongodb, getTimerCount}); - await runScriptAndGetProcessInfo('srv-poller-timer', config, run); + await runScriptAndGetProcessInfo('timer-srv-poller', config, run); }); }); }); @@ -564,4 +592,4 @@ describe.only('MongoClient.close() Integration', () => { it.skip('all active server-side cursors are closed by client.close()', async function () {}); }); }); -}); +}); \ No newline at end of file diff --git a/test/integration/node-specific/resource_tracking_script_builder.ts b/test/integration/node-specific/resource_tracking_script_builder.ts index ebab8d889e..0fe91c27a7 100644 --- a/test/integration/node-specific/resource_tracking_script_builder.ts +++ b/test/integration/node-specific/resource_tracking_script_builder.ts @@ -188,8 +188,8 @@ export async function runScriptAndGetProcessInfo( .reduce((acc, curr) => ({ ...acc, ...curr }), {}); // delete temporary files - await unlink(scriptName); - await unlink(logFile); + // await unlink(scriptName); + // await unlink(logFile); // assertions about exit status if (exitCode) { diff --git a/test/tools/fixtures/process_resource_script.in.js b/test/tools/fixtures/process_resource_script.in.js index 08db75853e..18093cd71c 100644 --- a/test/tools/fixtures/process_resource_script.in.js +++ b/test/tools/fixtures/process_resource_script.in.js @@ -20,6 +20,7 @@ const { setTimeout } = require('timers'); let originalReport; const logFile = scriptName + '.logs.txt'; const sleep = util.promisify(setTimeout); + const run = func; /** @@ -64,7 +65,7 @@ function getNewLibuvResourceArray() { * In order to be counted as a new resource, a resource MUST either: * - Meet the criteria to be returned by our helper utility `getNewLibuvResourceArray()` * OR - * - Be returned by `process.getActiveResourcesInfo() + * - Be returned by `process.getActiveResourcesInfo() and is not 'TTYWrap' * * The reason we are using both methods to detect active resources is: * - `process.report.getReport().libuv` does not detect active requests (such as timers or file reads) accurately @@ -72,9 +73,10 @@ function getNewLibuvResourceArray() { * */ function getNewResources() { + return { libuvResources: getNewLibuvResourceArray(), - activeResources: process.getActiveResourcesInfo() + activeResources: process.getActiveResourcesInfo().filter(r => r !== 'TTYWrap') }; } diff --git a/timer-srv-poller.cjs b/timer-srv-poller.cjs new file mode 100644 index 0000000000..3eed87c21f --- /dev/null +++ b/timer-srv-poller.cjs @@ -0,0 +1,147 @@ +'use strict'; + +/* eslint-disable no-undef */ +/* eslint-disable no-unused-vars */ +const driverPath = "/Users/aditi.khare/Desktop/node-mongodb-native/lib"; +const func = (async function ({ MongoClient, uri, expect, mongodb, sinon, getTimerCount }) { + const dns = require('dns'); + sinon.stub(dns.promises, 'resolveTxt').callsFake(async () => { + throw { code: 'ENODATA' }; + }); + sinon.stub(dns.promises, 'resolveSrv').callsFake(async () => { + return [ + { + name: 'domain.localhost', + port: 27017, + weight: 0, + priority: 0, + protocol: 'IPv6' + } + ]; + }); + const client = new MongoClient('mongodb+srv://localhost'); + client.connect(); + const topology = client.topology; + const prevDesc = topology; + log({ prevDesc }); + const currDesc = prevDesc; + client.topology.emit('topologyDescriptionChanged', mongodb.TopologyDescriptionChangedEvent(client.topology.s.id, prevDesc, currDesc)); + await client.close(); + expect(getTimerCount()).to.equal(0); + }); +const scriptName = "timer-srv-poller"; +const uri = "mongodb://localhost:27017/integration_tests?authSource=admin"; + +const mongodb = require(driverPath); +const { MongoClient } = mongodb; +const process = require('node:process'); +const util = require('node:util'); +const timers = require('node:timers'); +const fs = require('node:fs'); +const sinon = require('sinon'); +const { expect } = require('chai'); +const { setTimeout } = require('timers'); + +let originalReport; +const logFile = scriptName + '.logs.txt'; +const sleep = util.promisify(setTimeout); + +const run = func; + +/** + * + * Returns an array containing the new libuv resources created after script started. + * A new resource is something that will keep the event loop running. + * + * In order to be counted as a new resource, a resource MUST: + * - Must NOT share an address with a libuv resource that existed at the start of script + * - Must be referenced. See [here](https://nodejs.org/api/timers.html#timeoutref) for more context. + * + * We're using the following tool to track resources: `process.report.getReport().libuv` + * For more context, see documentation for [process.report.getReport()](https://nodejs.org/api/report.html), and [libuv](https://docs.libuv.org/en/v1.x/handle.html). + * + */ +function getNewLibuvResourceArray() { + let currReport = process.report.getReport().libuv; + const originalReportAddresses = originalReport.map(resource => resource.address); + + /** + * @typedef {Object} LibuvResource + * @property {string} type What is the resource type? For example, 'tcp' | 'timer' | 'udp' | 'tty'... (See more in [docs](https://docs.libuv.org/en/v1.x/handle.html)). + * @property {boolean} is_referenced Is the resource keeping the JS event loop active? + * + * @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 + ); + } + + currReport = currReport.filter(resource => isNewLibuvResource(resource)); + return currReport; +} + +/** + * Returns an object of the new resources created after script started. + * + * + * In order to be counted as a new resource, a resource MUST either: + * - Meet the criteria to be returned by our helper utility `getNewLibuvResourceArray()` + * OR + * - Be returned by `process.getActiveResourcesInfo() and is not 'TTYWrap' + * + * The reason we are using both methods to detect active resources is: + * - `process.report.getReport().libuv` does not detect active requests (such as timers or file reads) accurately + * - `process.getActiveResourcesInfo()` does not contain enough server information we need for our assertions + * + */ +function getNewResources() { + + return { + libuvResources: getNewLibuvResourceArray(), + activeResources: process.getActiveResourcesInfo().filter(r => r !== 'TTYWrap') + }; +} + +/** + * @returns Number of active timers in event loop + */ +const getTimerCount = () => process.getActiveResourcesInfo().filter(r => r === 'Timeout').length; + +// A log function for debugging +function log(message) { + // remove outer parentheses for easier parsing + const messageToLog = JSON.stringify(message) + ' \n'; + fs.writeFileSync(logFile, messageToLog, { flag: 'a' }); +} + +async function main() { + originalReport = process.report.getReport().libuv; + process.on('beforeExit', () => { + log({ beforeExitHappened: true }); + }); + await run({ MongoClient, uri, log, expect, mongodb, sleep, sinon, getTimerCount }); + log({ newResources: getNewResources() }); +} + +main() + .then(() => {}) + .catch(e => { + log({ error: { message: e.message, stack: e.stack, resources: getNewResources() } }); + process.exit(1); + }); + +setTimeout(() => { + // this means something was in the event loop such that it hung for more than 10 seconds + // so we kill the process + log({ + error: { + message: 'Process timed out: resources remain in the event loop', + resources: getNewResources() + } + }); + process.exit(99); + // using `unref` will ensure this setTimeout call is not a resource / does not keep the event loop running +}, 10000).unref(); diff --git a/timer-srv-poller.logs.txt b/timer-srv-poller.logs.txt new file mode 100644 index 0000000000..a8ba4c89e6 --- /dev/null +++ b/timer-srv-poller.logs.txt @@ -0,0 +1,5 @@ +{"error":{"message":"mongodb.TopologyDescription is not a constructor","stack":"TypeError: mongodb.TopologyDescription is not a constructor\n at func (/Users/aditi.khare/Desktop/node-mongodb-native/timer-srv-poller.cjs:24:50)\n at main (/Users/aditi.khare/Desktop/node-mongodb-native/timer-srv-poller.cjs:123:9)\n at Object. (/Users/aditi.khare/Desktop/node-mongodb-native/timer-srv-poller.cjs:127:1)\n at Module._compile (node:internal/modules/cjs/loader:1376:14)\n at Module._extensions..js (node:internal/modules/cjs/loader:1435:10)\n at Module.load (node:internal/modules/cjs/loader:1207:32)\n at Module._load (node:internal/modules/cjs/loader:1023:12)\n at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:135:12)\n at node:internal/main/run_main_module:28:49","resources":{"libuvResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x000000013c9686d0","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}],"activeResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"]}}} +{"error":{"message":"Cannot read properties of undefined (reading 'description')","stack":"TypeError: Cannot read properties of undefined (reading 'description')\n at func (/Users/aditi.khare/Desktop/node-mongodb-native/timer-srv-poller.cjs:25:55)\n at main (/Users/aditi.khare/Desktop/node-mongodb-native/timer-srv-poller.cjs:125:9)\n at Object. (/Users/aditi.khare/Desktop/node-mongodb-native/timer-srv-poller.cjs:129:1)\n at Module._compile (node:internal/modules/cjs/loader:1376:14)\n at Module._extensions..js (node:internal/modules/cjs/loader:1435:10)\n at Module.load (node:internal/modules/cjs/loader:1207:32)\n at Module._load (node:internal/modules/cjs/loader:1023:12)\n at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:135:12)\n at node:internal/main/run_main_module:28:49","resources":{"libuvResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000123a2ef20","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}],"activeResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"]}}} +{"error":{"message":"Cannot read properties of undefined (reading 'topologyPrivate')","stack":"TypeError: Cannot read properties of undefined (reading 'topologyPrivate')\n at func (/Users/aditi.khare/Desktop/node-mongodb-native/timer-srv-poller.cjs:25:55)\n at main (/Users/aditi.khare/Desktop/node-mongodb-native/timer-srv-poller.cjs:125:9)\n at Object. (/Users/aditi.khare/Desktop/node-mongodb-native/timer-srv-poller.cjs:129:1)\n at Module._compile (node:internal/modules/cjs/loader:1376:14)\n at Module._extensions..js (node:internal/modules/cjs/loader:1435:10)\n at Module.load (node:internal/modules/cjs/loader:1207:32)\n at Module._load (node:internal/modules/cjs/loader:1023:12)\n at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:135:12)\n at node:internal/main/run_main_module:28:49","resources":{"libuvResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000155796170","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}],"activeResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"]}}} +{} +{"error":{"message":"Cannot read properties of undefined (reading 'emit')","stack":"TypeError: Cannot read properties of undefined (reading 'emit')\n at func (/Users/aditi.khare/Desktop/node-mongodb-native/timer-srv-poller.cjs:28:45)\n at main (/Users/aditi.khare/Desktop/node-mongodb-native/timer-srv-poller.cjs:125:9)\n at Object. (/Users/aditi.khare/Desktop/node-mongodb-native/timer-srv-poller.cjs:129:1)\n at Module._compile (node:internal/modules/cjs/loader:1376:14)\n at Module._extensions..js (node:internal/modules/cjs/loader:1435:10)\n at Module.load (node:internal/modules/cjs/loader:1207:32)\n at Module._load (node:internal/modules/cjs/loader:1023:12)\n at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:135:12)\n at node:internal/main/run_main_module:28:49","resources":{"libuvResources":[{"type":"tcp","is_active":false,"is_referenced":true,"address":"0x0000000106a07460","localEndpoint":null,"remoteEndpoint":null,"sendBufferSize":0,"recvBufferSize":0,"writeQueueSize":0,"readable":false,"writable":false}],"activeResources":["FSReqPromise","GetAddrInfoReqWrap","TCPSocketWrap","Timeout"]}}}