Skip to content

Commit 617e9af

Browse files
Merge branch 'NODE-6615/integration-client-close' into NODE-6620/sockets
2 parents 2f676ab + e60a42b commit 617e9af

File tree

4 files changed

+88
-65
lines changed

4 files changed

+88
-65
lines changed

logs.txt

Whitespace-only changes.

test/integration/node-specific/client_close.test.ts

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,13 @@ describe.skip('MongoClient.close() Integration', () => {
1818
await runScriptAndGetProcessInfo(
1919
'tls-file-read',
2020
config,
21-
async function run({ MongoClient, uri, log, chai }) {
21+
async function run({ MongoClient, uri, expect }) {
2222
const infiniteFile = '/dev/zero';
2323
const client = new MongoClient(uri, { tlsCertificateKeyFile: infiniteFile });
2424
client.connect();
25-
log({ ActiveResources: process.getActiveResourcesInfo() });
26-
chai.expect(process.getActiveResourcesInfo()).to.include('FSReqPromise');
25+
expect(process.getActiveResourcesInfo()).to.include('FSReqPromise');
2726
await client.close();
28-
chai.expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise');
27+
expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise');
2928
}
3029
);
3130
});
@@ -41,27 +40,37 @@ describe.skip('MongoClient.close() Integration', () => {
4140

4241
describe('MongoClientAuthProviders', () => {
4342
describe('Node.js resource: Token file read', () => {
43+
let tokenFileEnvCache;
44+
45+
beforeEach(function () {
46+
if (process.env.AUTH === 'auth') {
47+
this.currentTest.skipReason = 'OIDC test environment requires auth disabled';
48+
return this.skip();
49+
}
50+
tokenFileEnvCache = process.env.OIDC_TOKEN_FILE;
51+
});
52+
53+
afterEach(function () {
54+
process.env.OIDC_TOKEN_FILE = tokenFileEnvCache;
55+
});
56+
4457
describe('when MongoClientAuthProviders is instantiated and token file read hangs', () => {
4558
it('the file read is interrupted by client.close()', async () => {
4659
await runScriptAndGetProcessInfo(
4760
'token-file-read',
4861
config,
49-
async function run({ MongoClient, uri, log, chai }) {
62+
async function run({ MongoClient, uri, expect }) {
5063
const infiniteFile = '/dev/zero';
51-
log({ ActiveResources: process.getActiveResourcesInfo() });
52-
53-
// speculative authentication call to getToken() is during initial handshake
54-
const client = new MongoClient(uri, {
55-
authMechanismProperties: { TOKEN_RESOURCE: infiniteFile }
56-
});
64+
process.env.OIDC_TOKEN_FILE = infiniteFile;
65+
const options = {
66+
authMechanismProperties: { ENVIRONMENT: 'test' },
67+
authMechanism: 'MONGODB-OIDC'
68+
};
69+
const client = new MongoClient(uri, options);
5770
client.connect();
58-
59-
log({ ActiveResources: process.getActiveResourcesInfo() });
60-
61-
chai.expect(process.getActiveResourcesInfo()).to.include('FSReqPromise');
71+
expect(process.getActiveResourcesInfo()).to.include('FSReqPromise');
6272
await client.close();
63-
64-
chai.expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise');
73+
expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise');
6574
}
6675
);
6776
});
@@ -220,9 +229,9 @@ describe.skip('MongoClient.close() Integration', () => {
220229
describe('when KMSRequest reads an infinite TLS file', () => {
221230
it('the file read is interrupted by client.close()', async () => {
222231
await runScriptAndGetProcessInfo(
223-
'tls-file-read',
232+
'tls-file-read-auto-encryption',
224233
config,
225-
async function run({ MongoClient, uri, log, chai, ClientEncryption, BSON }) {
234+
async function run({ MongoClient, uri, expect, ClientEncryption, BSON }) {
226235
const infiniteFile = '/dev/zero';
227236

228237
const kmsProviders = BSON.EJSON.parse(process.env.CSFLE_KMS_PROVIDERS);
@@ -281,23 +290,23 @@ describe.skip('MongoClient.close() Integration', () => {
281290
const encryptedClient = new MongoClient(uri, encryptionOptions);
282291
await encryptedClient.connect();
283292

293+
expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise');
294+
284295
const insertPromise = encryptedClient
285296
.db('db')
286297
.collection('coll')
287298
.insertOne({ a: 1 });
288299

289-
chai.expect(process.getActiveResourcesInfo()).to.include('FSReqPromise');
290-
log({ activeResourcesBeforeClose: process.getActiveResourcesInfo() });
300+
expect(process.getActiveResourcesInfo()).to.include('FSReqPromise');
291301

292302
await keyVaultClient.close();
293303
await encryptedClient.close();
294304

295-
chai.expect(process.getActiveResourcesInfo()).to.include('FSReqPromise');
296-
log({ activeResourcesAfterClose: process.getActiveResourcesInfo() });
305+
expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise');
297306

298307
const err = await insertPromise.catch(e => e);
299-
chai.expect(err).to.exist;
300-
chai.expect(err.errmsg).to.contain('Error in KMS response');
308+
expect(err).to.exist;
309+
expect(err.errmsg).to.contain('Error in KMS response');
301310
}
302311
);
303312
});
@@ -316,9 +325,9 @@ describe.skip('MongoClient.close() Integration', () => {
316325
describe('when KMSRequest reads an infinite TLS file read', () => {
317326
it('the file read is interrupted by client.close()', async () => {
318327
await runScriptAndGetProcessInfo(
319-
'tls-file-read',
328+
'tls-file-read-client-encryption',
320329
config,
321-
async function run({ MongoClient, uri, log, chai, ClientEncryption, BSON }) {
330+
async function run({ MongoClient, uri, expect, ClientEncryption, BSON }) {
322331
const infiniteFile = '/dev/zero';
323332
const kmsProviders = BSON.EJSON.parse(process.env.CSFLE_KMS_PROVIDERS);
324333
const masterKey = {
@@ -339,19 +348,15 @@ describe.skip('MongoClient.close() Integration', () => {
339348

340349
const dataKeyPromise = clientEncryption.createDataKey(provider, { masterKey });
341350

342-
chai.expect(process.getActiveResourcesInfo()).to.include('FSReqPromise');
343-
344-
log({ activeResourcesBeforeClose: process.getActiveResourcesInfo() });
351+
expect(process.getActiveResourcesInfo()).to.include('FSReqPromise');
345352

346353
await keyVaultClient.close();
347354

348-
chai.expect(process.getActiveResourcesInfo()).to.include('FSReqPromise');
349-
350-
log({ activeResourcesAfterClose: process.getActiveResourcesInfo() });
355+
expect(process.getActiveResourcesInfo()).to.not.include('FSReqPromise');
351356

352357
const err = await dataKeyPromise.catch(e => e);
353-
chai.expect(err).to.exist;
354-
chai.expect(err.errmsg).to.contain('Error in KMS response');
358+
expect(err).to.exist;
359+
expect(err.errmsg).to.contain('Error in KMS response');
355360
}
356361
);
357362
});

test/integration/node-specific/resource_tracking_script_builder.ts

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { fork, spawn } from 'node:child_process';
22
import { on, once } from 'node:events';
3-
import * as fs from 'node:fs';
43
import { readFile, unlink, writeFile } from 'node:fs/promises';
54
import * as path from 'node:path';
65

@@ -21,8 +20,8 @@ export type HeapResourceTestFunction = (options: {
2120
export type ProcessResourceTestFunction = (options: {
2221
MongoClient: typeof MongoClient;
2322
uri: string;
24-
log: (out: any) => void;
25-
chai: { expect: typeof expect };
23+
log?: (out: any) => void;
24+
expect: typeof expect;
2625
ClientEncryption?: typeof ClientEncryption;
2726
BSON?: typeof BSON;
2827
}) => Promise<void>;
@@ -48,7 +47,7 @@ export async function testScriptFactory(
4847

4948
resourceScript = resourceScript.replace('DRIVER_SOURCE_PATH', DRIVER_SRC_PATH);
5049
resourceScript = resourceScript.replace('FUNCTION_STRING', `(${func.toString()})`);
51-
resourceScript = resourceScript.replace('NAME_STRING', JSON.stringify(name));
50+
resourceScript = resourceScript.replace('SCRIPT_NAME_STRING', JSON.stringify(name));
5251
resourceScript = resourceScript.replace('URI_STRING', JSON.stringify(uri));
5352
resourceScript = resourceScript.replace('ITERATIONS_STRING', `${iterations}`);
5453

@@ -141,11 +140,11 @@ export async function runScriptAndReturnHeapInfo(
141140
* **The provided function is run in an isolated Node.js process**
142141
*
143142
* A user of this function will likely need to familiarize themselves with the surrounding scripting, but briefly:
144-
* - Every MongoClient you construct should have an asyncResource attached to it like so:
145-
* ```js
146-
* mongoClient.asyncResource = new this.async_hooks.AsyncResource('MongoClient');
147-
* ```
148-
* - You can perform any number of operations and connects/closes of MongoClients
143+
* - Many MongoClient operations (construction, connection, commands) can result in resources that keep the JS event loop running.
144+
* - Timers
145+
* - Active Sockets
146+
* - File Read Hangs
147+
*
149148
* - This function performs assertions that at the end of the provided function, the js event loop has been exhausted
150149
*
151150
* @param name - the name of the script, this defines the name of the file, it will be cleaned up if the function returns successfully
@@ -169,23 +168,19 @@ export async function runScriptAndGetProcessInfo(
169168
await writeFile(scriptName, scriptContent, { encoding: 'utf8' });
170169
const logFile = 'logs.txt';
171170

172-
const processDiedController = new AbortController();
173171
const script = spawn(process.argv[0], [scriptName], { stdio: ['ignore', 'ignore', 'ignore'] });
174172

175-
// Interrupt our awaiting of messages if the process crashed
176-
script.once('close', exitCode => {
177-
if (exitCode !== 0) {
178-
processDiedController.abort(new Error(`process exited with: ${exitCode}`));
179-
}
180-
});
181-
182173
const willClose = once(script, 'close');
183174

184175
// make sure the process ended
185176
const [exitCode] = await willClose;
186177

187178
// format messages from child process as an object
188-
const messages = (await readFile(logFile, 'utf-8')).trim().split('\n').map(line => JSON.parse(line)).reduce((acc, curr) => ({ ...acc, ...curr }), {});
179+
const messages = (await readFile(logFile, 'utf-8'))
180+
.trim()
181+
.split('\n')
182+
.map(line => JSON.parse(line))
183+
.reduce((acc, curr) => ({ ...acc, ...curr }), {});
189184

190185
// delete temporary files
191186
await unlink(scriptName);

test/tools/fixtures/process_resource_script.in.js

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,62 @@
44
/* eslint-disable no-unused-vars */
55
const driverPath = DRIVER_SOURCE_PATH;
66
const func = FUNCTION_STRING;
7-
const name = NAME_STRING;
7+
const scriptName = SCRIPT_NAME_STRING;
88
const uri = URI_STRING;
99

1010
const { MongoClient, ClientEncryption, BSON } = require(driverPath);
1111
const process = require('node:process');
1212
const util = require('node:util');
1313
const timers = require('node:timers');
1414
const fs = require('node:fs');
15-
const chai = require('chai');
15+
const { expect } = require('chai');
1616
const { setTimeout } = require('timers');
1717

1818
let originalReport;
1919
const logFile = 'logs.txt';
2020

2121
const run = func;
22-
const serverType = ['tcp', 'udp'];
2322

24-
// Returns an array containing new the resources created after script start
23+
/**
24+
*
25+
* Returns an array containing the new resources created after script started.
26+
* A new resource is something that will keep the event loop running.
27+
*
28+
* In order to be counted as a new resource, a resource MUST:
29+
* - Must NOT share an address with a libuv resource that existed at the start of script
30+
* - Must be referenced. See [here](https://nodejs.org/api/timers.html#timeoutref) for more context.
31+
* - Must NOT be an inactive server
32+
*
33+
* We're using the following tool to track resources: `process.report.getReport().libuv`
34+
* 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).
35+
*
36+
*/
2537
function getNewLibuvResourceArray() {
2638
let currReport = process.report.getReport().libuv;
2739
const originalReportAddresses = originalReport.map(resource => resource.address);
28-
currReport = currReport.filter(
29-
resource =>
40+
41+
/**
42+
* @typedef {Object} LibuvResource
43+
* @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.
44+
* @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)).
45+
* @property {boolean} is_referenced Is the resource keeping the JS event loop active?
46+
*
47+
* @param {LibuvResource} resource
48+
*/
49+
function isNewLibuvResource(resource) {
50+
const serverType = ['tcp', 'udp'];
51+
return (
3052
!originalReportAddresses.includes(resource.address) &&
3153
resource.is_referenced && // if a resource is unreferenced, it's not keeping the event loop open
3254
(!serverType.includes(resource.type) || resource.is_active)
33-
);
55+
);
56+
}
57+
58+
currReport = currReport.filter(resource => isNewLibuvResource(resource));
3459
return currReport;
3560
}
3661

62+
// A log function for debugging
3763
function log(message) {
3864
// remove outer parentheses for easier parsing
3965
const messageToLog = JSON.stringify(message) + ' \n';
@@ -45,22 +71,19 @@ async function main() {
4571
process.on('beforeExit', () => {
4672
log({ beforeExitHappened: true });
4773
});
48-
await run({ MongoClient, uri, log, chai, ClientEncryption, BSON });
74+
await run({ MongoClient, uri, log, expect, ClientEncryption, BSON });
4975
log({ newLibuvResources: getNewLibuvResourceArray() });
5076
}
5177

5278
main()
53-
.then(() => {
54-
log({ exitCode: 0 });
55-
})
79+
.then(() => {})
5680
.catch(e => {
5781
log({ exitCode: 1, error: util.inspect(e) });
5882
});
5983

6084
setTimeout(() => {
6185
// this means something was in the event loop such that it hung for more than 10 seconds
6286
// so we kill the process
63-
log({ exitCode: 99 });
6487
process.exit(99);
6588
// using `unref` will ensure this setTimeout call is not a resource / does not keep the event loop running
6689
}, 10000).unref();

0 commit comments

Comments
 (0)