Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0af55e9
skeleton
aditi-khare-mongoDB Dec 16, 2024
4798bc2
skeleton updates
aditi-khare-mongoDB Dec 16, 2024
8adca00
refactor for table
aditi-khare-mongoDB Dec 17, 2024
3412bb1
boilerplate execution
aditi-khare-mongoDB Dec 17, 2024
1531bec
fix
aditi-khare-mongoDB Dec 17, 2024
4200f3f
preliminary tests finished
aditi-khare-mongoDB Dec 18, 2024
321ef71
remove misc file
aditi-khare-mongoDB Dec 18, 2024
1503084
temp for screen-share
aditi-khare-mongoDB Dec 19, 2024
a128974
lint
aditi-khare-mongoDB Dec 19, 2024
4e55dee
boilerplate fully working
aditi-khare-mongoDB Dec 19, 2024
f560088
TLS test case running
aditi-khare-mongoDB Dec 20, 2024
ef9cc90
clea up lint
aditi-khare-mongoDB Dec 20, 2024
9ed1528
clean up
aditi-khare-mongoDB Dec 20, 2024
1c8e20f
most of tree reformatting done
aditi-khare-mongoDB Dec 20, 2024
1de9809
reorganized tests and added in most of neal's suggestions
aditi-khare-mongoDB Dec 20, 2024
e77405f
TLS test cases and socket test cases
aditi-khare-mongoDB Dec 30, 2024
11aa73c
TLS test cases
aditi-khare-mongoDB Dec 30, 2024
c99579a
fix message formatting
aditi-khare-mongoDB Dec 30, 2024
1aeb046
Merge branch 'main' into NODE-6615/integration-client-close
aditi-khare-mongoDB Dec 30, 2024
47c20db
fix message formatting + test cases naming
aditi-khare-mongoDB Dec 30, 2024
133b20d
Delete logs.txt
aditi-khare-mongoDB Dec 30, 2024
8fd53a4
requested changes: remove log calls, change chai to expect, clarify s…
aditi-khare-mongoDB Dec 30, 2024
e60a42b
requested changes: additional expectation
aditi-khare-mongoDB Dec 30, 2024
d9ed22a
removed resources that we are no longer integration testing - connect…
aditi-khare-mongoDB Jan 2, 2025
83b5685
requested changes: add in exitCode message, skip unimplemented tests,…
aditi-khare-mongoDB Jan 2, 2025
454859e
requested changes: fix exitCode
aditi-khare-mongoDB Jan 2, 2025
5eb92ee
neal's requested changes
aditi-khare-mongoDB Jan 3, 2025
77fadbb
re-add assertion
aditi-khare-mongoDB Jan 5, 2025
aefa1cc
Update test/integration/node-specific/resource_tracking_script_builde…
aditi-khare-mongoDB Jan 6, 2025
e9a3108
make stderr inherit
aditi-khare-mongoDB Jan 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 191 additions & 0 deletions test/integration/node-specific/client_close.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { expect } from 'chai';

import { type TestConfiguration } from '../../tools/runner/config';
import { runScriptAndGetProcessInfo } from './resource_tracking_script_builder';

describe.skip('client.close() Integration', () => {
let config: TestConfiguration;

beforeEach(function () {
config = this.configuration;
});

describe('MongoClient', () => {
describe('when client is being instantiated and reads a long docker file', () => {
// our docker env detection uses fs.access which will not be aborted until after it runs
// fs.access does not support abort signals
it('the file read is not interrupted by client.close()', 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 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('when MongoClientAuthProviders is instantiated and token file read hangs', () => {
it('the file read is interrupted by client.close()', async () => {});
});
});

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 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 () => {});

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 () => {});
});
});

describe('Cursor', () => {
describe('after cursors are created', () => {
it('all active server-side cursors are closed by client.close()', async () => {});
});
});
});
4 changes: 2 additions & 2 deletions test/integration/node-specific/resource_clean_up.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 }) {
Expand Down
112 changes: 101 additions & 11 deletions test/integration/node-specific/resource_tracking_script_builder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { fork } from 'node:child_process';
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';

Expand All @@ -9,34 +10,52 @@ import { parseSnapshot } from 'v8-heapsnapshot';
import { type MongoClient } from '../../mongodb';
import { type TestConfiguration } from '../../tools/runner/config';

export type ResourceTestFunction = (options: {
export type ResourceTestFunction = HeapResourceTestFunction | ProcessResourceTestFunction;

export type HeapResourceTestFunction = (options: {
MongoClient: typeof MongoClient;
uri: string;
iteration: number;
}) => Promise<void>;

const RESOURCE_SCRIPT_PATH = path.resolve(__dirname, '../../tools/fixtures/resource_script.in.js');
export type ProcessResourceTestFunction = (options: {
MongoClient: typeof MongoClient;
uri: string;
}) => Promise<void>;

const HEAP_RESOURCE_SCRIPT_PATH = path.resolve(
__dirname,
'../../tools/fixtures/resource_script.in.js'
);
const REPORT_RESOURCE_SCRIPT_PATH = path.resolve(
__dirname,
'../../tools/fixtures/process_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;
}

/**
* 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**
*
Expand All @@ -57,16 +76,22 @@ 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,
func: HeapResourceTestFunction,
{ iterations = 100 } = {}
) {
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();
Expand Down Expand Up @@ -106,3 +131,68 @@ export async function runScript(
heap
};
}

/**
* 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: ProcessResourceTestFunction
) {
const scriptName = `${name}.cjs`;
const scriptContent = await testScriptFactory(
name,
config.url(),
REPORT_RESOURCE_SCRIPT_PATH,
func
);
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
const [exitCode] = await willClose;

const formattedLogRead = '{' + fs.readFileSync(logFile, 'utf-8').slice(0, -3) + '}';
const messages = JSON.parse(formattedLogRead);

// delete temporary files
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;
}
Loading
Loading