Skip to content

Commit

Permalink
rebase
Browse files Browse the repository at this point in the history
  • Loading branch information
aditi-khare-mongoDB committed Jan 9, 2025
1 parent 24ebcde commit cf46f71
Show file tree
Hide file tree
Showing 6 changed files with 345 additions and 17 deletions.
1 change: 1 addition & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
146 changes: 146 additions & 0 deletions srv-poller-timer.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
'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, log, sinon, mongodb, 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,
weight: 0,
priority: 0,
protocol: formattedUri.host.isIPv6 ? 'IPv6' : 'IPv4'
}
];
});
/* sinon.stub(mongodb, 'checkParentDomainMatch').callsFake(async () => {
console.log('in here!!!');
}); */
const client = new MongoClient('mongodb+srv://localhost');
await client.connect();
await client.close();
expect(getTimerCount()).to.equal(0);
sinon.restore();
});
const scriptName = "srv-poller-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()
*
* 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();
3 changes: 3 additions & 0 deletions srv-poller-timer.logs.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{"domainMatch":{"BSON":{"BSONType":{"double":1,"string":2,"object":3,"array":4,"binData":5,"undefined":6,"objectId":7,"bool":8,"date":9,"null":10,"regex":11,"dbPointer":12,"javascript":13,"symbol":14,"javascriptWithScope":15,"int":16,"timestamp":17,"long":18,"decimal":19,"minKey":-1,"maxKey":127},"EJSON":{},"onDemand":{"ByteUtils":{},"NumberUtils":{}}},"BSONType":{"double":1,"string":2,"object":3,"array":4,"binData":5,"undefined":6,"objectId":7,"bool":8,"date":9,"null":10,"regex":11,"dbPointer":12,"javascript":13,"symbol":14,"javascriptWithScope":15,"int":16,"timestamp":17,"long":18,"decimal":19,"minKey":-1,"maxKey":127},"BatchType":{"INSERT":1,"UPDATE":2,"DELETE":3},"AutoEncryptionLoggerLevel":{"FatalError":0,"Error":1,"Warning":2,"Info":3,"Trace":4},"GSSAPICanonicalizationValue":{"on":true,"off":false,"none":"none","forward":"forward","forwardAndReverse":"forwardAndReverse"},"AuthMechanism":{"MONGODB_AWS":"MONGODB-AWS","MONGODB_CR":"MONGODB-CR","MONGODB_DEFAULT":"DEFAULT","MONGODB_GSSAPI":"GSSAPI","MONGODB_PLAIN":"PLAIN","MONGODB_SCRAM_SHA1":"SCRAM-SHA-1","MONGODB_SCRAM_SHA256":"SCRAM-SHA-256","MONGODB_X509":"MONGODB-X509","MONGODB_OIDC":"MONGODB-OIDC"},"Compressor":{"none":0,"snappy":1,"zlib":2,"zstd":3},"CURSOR_FLAGS":["tailable","oplogReplay","noCursorTimeout","awaitData","exhaust","partial"],"CursorTimeoutMode":{"ITERATION":"iteration","LIFETIME":"cursorLifetime"},"MongoErrorLabel":{"RetryableWriteError":"RetryableWriteError","TransientTransactionError":"TransientTransactionError","UnknownTransactionCommitResult":"UnknownTransactionCommitResult","ResumableChangeStreamError":"ResumableChangeStreamError","HandshakeError":"HandshakeError","ResetPool":"ResetPool","PoolRequstedRetry":"PoolRequstedRetry","InterruptInUseConnections":"InterruptInUseConnections","NoWritesPerformed":"NoWritesPerformed"},"ExplainVerbosity":{"queryPlanner":"queryPlanner","queryPlannerExtended":"queryPlannerExtended","executionStats":"executionStats","allPlansExecution":"allPlansExecution"},"ServerApiVersion":{"v1":"1"},"ReturnDocument":{"BEFORE":"before","AFTER":"after"},"ProfilingLevel":{"off":"off","slowOnly":"slow_only","all":"all"},"ReadConcernLevel":{"local":"local","majority":"majority","linearizable":"linearizable","available":"available","snapshot":"snapshot"},"ReadPreferenceMode":{"primary":"primary","primaryPreferred":"primaryPreferred","secondary":"secondary","secondaryPreferred":"secondaryPreferred","nearest":"nearest"},"ServerType":{"Standalone":"Standalone","Mongos":"Mongos","PossiblePrimary":"PossiblePrimary","RSPrimary":"RSPrimary","RSSecondary":"RSSecondary","RSArbiter":"RSArbiter","RSOther":"RSOther","RSGhost":"RSGhost","Unknown":"Unknown","LoadBalancer":"LoadBalancer"},"TopologyType":{"Single":"Single","ReplicaSetNoPrimary":"ReplicaSetNoPrimary","ReplicaSetWithPrimary":"ReplicaSetWithPrimary","Sharded":"Sharded","Unknown":"Unknown","LoadBalanced":"LoadBalanced"}}}
{"error":{"message":"Cannot read properties of undefined (reading 'fromString')","stack":"TypeError: Cannot read properties of undefined (reading 'fromString')\n at Object.<anonymous> (/Users/aditi.khare/Desktop/node-mongodb-native/srv-poller-timer.cjs:12:74)\n at Object.invoke (/Users/aditi.khare/Desktop/node-mongodb-native/node_modules/sinon/lib/sinon/behavior.js:177:32)\n at Object.functionStub (/Users/aditi.khare/Desktop/node-mongodb-native/node_modules/sinon/lib/sinon/stub.js:44:43)\n at Function.invoke (/Users/aditi.khare/Desktop/node-mongodb-native/node_modules/sinon/lib/sinon/proxy-invoke.js:53:47)\n at bound querySrv [as resolveSrv] (/Users/aditi.khare/Desktop/node-mongodb-native/node_modules/sinon/lib/sinon/proxy.js:275:26)\n at resolveSRVRecord (/Users/aditi.khare/Desktop/node-mongodb-native/lib/connection_string.js:43:42)\n at MongoClient._connect (/Users/aditi.khare/Desktop/node-mongodb-native/lib/mongo_client.js:200:74)\n at MongoClient.connect (/Users/aditi.khare/Desktop/node-mongodb-native/lib/mongo_client.js:164:40)\n at func (/Users/aditi.khare/Desktop/node-mongodb-native/srv-poller-timer.cjs:27:42)\n at main (/Users/aditi.khare/Desktop/node-mongodb-native/srv-poller-timer.cjs:124:9)","resources":{"libuvResources":[],"activeResources":["FSReqPromise","TTYWrap"]}}}
{"error":{"message":"Cannot read properties of undefined (reading 'fromString')","stack":"TypeError: Cannot read properties of undefined (reading 'fromString')\n at Object.<anonymous> (/Users/aditi.khare/Desktop/node-mongodb-native/srv-poller-timer.cjs:12:74)\n at Object.invoke (/Users/aditi.khare/Desktop/node-mongodb-native/node_modules/sinon/lib/sinon/behavior.js:177:32)\n at Object.functionStub (/Users/aditi.khare/Desktop/node-mongodb-native/node_modules/sinon/lib/sinon/stub.js:44:43)\n at Function.invoke (/Users/aditi.khare/Desktop/node-mongodb-native/node_modules/sinon/lib/sinon/proxy-invoke.js:53:47)\n at bound querySrv [as resolveSrv] (/Users/aditi.khare/Desktop/node-mongodb-native/node_modules/sinon/lib/sinon/proxy.js:275:26)\n at resolveSRVRecord (/Users/aditi.khare/Desktop/node-mongodb-native/lib/connection_string.js:43:42)\n at MongoClient._connect (/Users/aditi.khare/Desktop/node-mongodb-native/lib/mongo_client.js:200:74)\n at MongoClient.connect (/Users/aditi.khare/Desktop/node-mongodb-native/lib/mongo_client.js:164:40)\n at func (/Users/aditi.khare/Desktop/node-mongodb-native/srv-poller-timer.cjs:27:42)\n at main (/Users/aditi.khare/Desktop/node-mongodb-native/srv-poller-timer.cjs:124:9)","resources":{"libuvResources":[],"activeResources":["FSReqPromise","TTYWrap"]}}}
74 changes: 57 additions & 17 deletions test/integration/node-specific/client_close.test.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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});
});
});
});

Expand Down Expand Up @@ -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;
});

Expand All @@ -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});
});
});
});
Expand Down Expand Up @@ -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);
});
});
});
Expand Down
Loading

0 comments on commit cf46f71

Please sign in to comment.