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/srv-poller-timer.cjs b/srv-poller-timer.cjs new file mode 100644 index 0000000000..ee45321f1e --- /dev/null +++ b/srv-poller-timer.cjs @@ -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(); diff --git a/srv-poller-timer.logs.txt b/srv-poller-timer.logs.txt new file mode 100644 index 0000000000..3e2667b147 --- /dev/null +++ b/srv-poller-timer.logs.txt @@ -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. (/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. (/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"]}}} 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); }); }); }); diff --git a/timer-server-selection.cjs b/timer-server-selection.cjs new file mode 100644 index 0000000000..bdb07b4d24 --- /dev/null +++ b/timer-server-selection.cjs @@ -0,0 +1,136 @@ +'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, sinon, sleep, mongodb, getTimerCount }) { + const serverSelectionTimeoutMS = 777; + const client = new MongoClient(uri, { minPoolSize: 1, serverSelectionTimeoutMS }); + const timeoutStartedSpy = sinon.spy(mongodb.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().map(r => r.args).filter(r => r.includes(777)) ? true : false; + }); + 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 scriptName = "timer-server-selection"; +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(); diff --git a/timer-server-selection.logs.txt b/timer-server-selection.logs.txt new file mode 100644 index 0000000000..4dcdc45ba0 --- /dev/null +++ b/timer-server-selection.logs.txt @@ -0,0 +1,2 @@ +{"error":{"message":"Cannot read properties of undefined (reading 'expires')","stack":"TypeError: Cannot read properties of undefined (reading 'expires')\n at Function.spy (/Users/aditi.khare/Desktop/node-mongodb-native/node_modules/sinon/lib/sinon/spy.js:178:61)\n at Sandbox.spy (/Users/aditi.khare/Desktop/node-mongodb-native/node_modules/sinon/lib/sinon/sandbox.js:449:37)\n at func (/Users/aditi.khare/Desktop/node-mongodb-native/timer-server-selection.cjs:9:57)\n at main (/Users/aditi.khare/Desktop/node-mongodb-native/timer-server-selection.cjs:114:9)\n at Object. (/Users/aditi.khare/Desktop/node-mongodb-native/timer-server-selection.cjs:118: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)","resources":{"libuvResources":[],"activeResources":["FSReqPromise","TTYWrap"]}}} +{"error":{"message":"Cannot read properties of undefined (reading 'expires')","stack":"TypeError: Cannot read properties of undefined (reading 'expires')\n at Function.spy (/Users/aditi.khare/Desktop/node-mongodb-native/node_modules/sinon/lib/sinon/spy.js:178:61)\n at Sandbox.spy (/Users/aditi.khare/Desktop/node-mongodb-native/node_modules/sinon/lib/sinon/sandbox.js:449:37)\n at func (/Users/aditi.khare/Desktop/node-mongodb-native/timer-server-selection.cjs:9:57)\n at main (/Users/aditi.khare/Desktop/node-mongodb-native/timer-server-selection.cjs:114:9)\n at Object. (/Users/aditi.khare/Desktop/node-mongodb-native/timer-server-selection.cjs:118: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)","resources":{"libuvResources":[],"activeResources":["FSReqPromise","TTYWrap"]}}}