Skip to content

Commit 1f321ec

Browse files
push for virtual office
1 parent 4901502 commit 1f321ec

8 files changed

+369
-45
lines changed

heartbeat-failed-monitor-timer.cjs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
'use strict';
2+
3+
/* eslint-disable no-undef */
4+
/* eslint-disable no-unused-vars */
5+
const driverPath = "/Users/aditi.khare/Desktop/node-mongodb-native/lib";
6+
const func = (async function ({ MongoClient, uri, expect, sleep, getTimerCount }) {
7+
const heartbeatFrequencyMS = 2000;
8+
const client = new MongoClient('mongodb://fakeUri', { heartbeatFrequencyMS });
9+
let heartbeatHappened = false;
10+
client.on('serverHeartbeatFailed', () => heartbeatHappened = true);
11+
client.connect();
12+
await sleep(heartbeatFrequencyMS * 2.5);
13+
expect(heartbeatHappened).to.be.true;
14+
function getMonitorTimer(servers) {
15+
for (const server of servers) {
16+
return server[1]?.monitor.monitorId.timerId;
17+
}
18+
}
19+
;
20+
const servers = client.topology.s.servers;
21+
expect(getMonitorTimer(servers)).to.exist;
22+
await client.close();
23+
expect(getMonitorTimer(servers)).to.not.exist;
24+
expect(getTimerCount()).to.equal(0);
25+
});
26+
const scriptName = "heartbeat-failed-monitor-timer";
27+
const uri = "mongodb://localhost:27017/integration_tests?authSource=admin";
28+
29+
const mongodb = require(driverPath);
30+
const { MongoClient } = mongodb;
31+
const process = require('node:process');
32+
const util = require('node:util');
33+
const timers = require('node:timers');
34+
const fs = require('node:fs');
35+
const sinon = require('sinon');
36+
const { expect } = require('chai');
37+
const { setTimeout } = require('timers');
38+
39+
let originalReport;
40+
const logFile = scriptName + '.logs.txt';
41+
const sleep = util.promisify(setTimeout);
42+
43+
const run = func;
44+
45+
/**
46+
*
47+
* Returns an array containing the new libuv resources created after script started.
48+
* A new resource is something that will keep the event loop running.
49+
*
50+
* In order to be counted as a new resource, a resource MUST:
51+
* - Must NOT share an address with a libuv resource that existed at the start of script
52+
* - Must be referenced. See [here](https://nodejs.org/api/timers.html#timeoutref) for more context.
53+
*
54+
* We're using the following tool to track resources: `process.report.getReport().libuv`
55+
* 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).
56+
*
57+
*/
58+
function getNewLibuvResourceArray() {
59+
let currReport = process.report.getReport().libuv;
60+
const originalReportAddresses = originalReport.map(resource => resource.address);
61+
62+
/**
63+
* @typedef {Object} LibuvResource
64+
* @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)).
65+
* @property {boolean} is_referenced Is the resource keeping the JS event loop active?
66+
*
67+
* @param {LibuvResource} resource
68+
*/
69+
function isNewLibuvResource(resource) {
70+
const serverType = ['tcp', 'udp'];
71+
return (
72+
!originalReportAddresses.includes(resource.address) && resource.is_referenced // if a resource is unreferenced, it's not keeping the event loop open
73+
);
74+
}
75+
76+
currReport = currReport.filter(resource => isNewLibuvResource(resource));
77+
return currReport;
78+
}
79+
80+
/**
81+
* Returns an object of the new resources created after script started.
82+
*
83+
*
84+
* In order to be counted as a new resource, a resource MUST either:
85+
* - Meet the criteria to be returned by our helper utility `getNewLibuvResourceArray()`
86+
* OR
87+
* - Be returned by `process.getActiveResourcesInfo() and is not 'TTYWrap'
88+
*
89+
* The reason we are using both methods to detect active resources is:
90+
* - `process.report.getReport().libuv` does not detect active requests (such as timers or file reads) accurately
91+
* - `process.getActiveResourcesInfo()` does not contain enough server information we need for our assertions
92+
*
93+
*/
94+
function getNewResources() {
95+
96+
return {
97+
libuvResources: getNewLibuvResourceArray(),
98+
activeResources: process.getActiveResourcesInfo()
99+
};
100+
}
101+
102+
/**
103+
* @returns Number of active timers in event loop
104+
*/
105+
const getTimerCount = () => process.getActiveResourcesInfo().filter(r => r === 'Timeout').length;
106+
107+
// A log function for debugging
108+
function log(message) {
109+
// remove outer parentheses for easier parsing
110+
const messageToLog = JSON.stringify(message) + ' \n';
111+
fs.writeFileSync(logFile, messageToLog, { flag: 'a' });
112+
}
113+
114+
async function main() {
115+
originalReport = process.report.getReport().libuv;
116+
process.on('beforeExit', () => {
117+
log({ beforeExitHappened: true });
118+
});
119+
await run({ MongoClient, uri, log, expect, mongodb, sleep, sinon, getTimerCount });
120+
log({ newResources: getNewResources() });
121+
}
122+
123+
main()
124+
.then(() => {})
125+
.catch(e => {
126+
log({ error: { message: e.message, stack: e.stack, resources: getNewResources() } });
127+
process.exit(1);
128+
});
129+
130+
setTimeout(() => {
131+
// this means something was in the event loop such that it hung for more than 10 seconds
132+
// so we kill the process
133+
log({
134+
error: {
135+
message: 'Process timed out: resources remain in the event loop',
136+
resources: getNewResources()
137+
}
138+
});
139+
process.exit(99);
140+
// using `unref` will ensure this setTimeout call is not a resource / does not keep the event loop running
141+
}, 10000).unref();
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{"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":[]}}}
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":["TTYWrap"]}}}

src/utils.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1166,7 +1166,6 @@ export function parseUnsignedInteger(value: unknown): number | null {
11661166
* @returns void
11671167
*/
11681168
export function checkParentDomainMatch(address: string, srvHost: string): void {
1169-
return;
11701169
// Remove trailing dot if exists on either the resolved address or the srv hostname
11711170
const normalizedAddress = address.endsWith('.') ? address.slice(0, address.length - 1) : address;
11721171
const normalizedSrvHost = srvHost.endsWith('.') ? srvHost.slice(0, srvHost.length - 1) : srvHost;

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

Lines changed: 68 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
/* eslint-disable @typescript-eslint/no-empty-function */
22
import { expect } from 'chai';
33
import * as sinon from 'sinon';
4-
import { MongoClient } from '../../mongodb';
4+
import { Connection, HostAddress, MongoClient } from '../../mongodb';
55
import { type TestConfiguration } from '../../tools/runner/config';
66
import { runScriptAndGetProcessInfo } from './resource_tracking_script_builder';
77
import { sleep } from '../../tools/utils';
8-
import { ConnectionPool, Timeout } from '../../mongodb';
8+
import { ConnectionPool } from '../../mongodb';
9+
import { getActiveResourcesInfo } from 'process';
910

1011
describe.only('MongoClient.close() Integration', () => {
1112
// note: these tests are set-up in accordance of the resource ownership tree
@@ -80,31 +81,34 @@ describe.only('MongoClient.close() Integration', () => {
8081
describe('Topology', () => {
8182
describe('Node.js resource: Server Selection Timer', () => {
8283
describe('after a Topology is created through client.connect()', () => {
83-
it.only('server selection timers are cleaned up by client.close()', async () => {
84-
// note: this test is not called in a separate process since it requires stubbing internal class: Timeout
85-
const run = async function ({ MongoClient, uri, expect, sinon, sleep, getTimerCount }) {
86-
const serverSelectionTimeoutMS = 777;
84+
it('server selection timers are cleaned up by client.close()', async () => {
85+
const run = async function ({ MongoClient, uri, expect, sinon, sleep, mongodb, getTimerCount }) {
86+
const serverSelectionTimeoutMS = 2222;
8787
const client = new MongoClient(uri, { minPoolSize: 1, serverSelectionTimeoutMS });
88-
const timeoutStartedSpy = sinon.spy(Timeout, 'expires');
88+
const timers = require('timers');
89+
const timeoutStartedSpy = sinon.spy(timers, 'setTimeout');
8990
let serverSelectionTimeoutStarted = false;
9091

9192
// make server selection hang so check out timer isn't cleared and check that the timeout has started
92-
sinon.stub(Promise, 'race').callsFake(() => {
93-
serverSelectionTimeoutStarted = timeoutStartedSpy.getCalls().filter(r => r.args.includes(777)).flat().length > 0;
93+
sinon.stub(Promise, 'race').callsFake(async ([serverPromise, timeout]) => {
94+
serverSelectionTimeoutStarted = timeoutStartedSpy.getCalls().filter(r => r.args.includes(serverSelectionTimeoutMS)).flat().length > 0;
95+
await timeout;
9496
});
9597

96-
client.db('db').collection('collection').insertOne({ x: 1 }).catch(e => e);
98+
const insertPromise = client.db('db').collection('collection').insertOne({ x: 1 });
9799

98-
// don't allow entire checkout timer to elapse to ensure close is called mid-timeout
100+
// don't allow entire server selection timer to elapse to ensure close is called mid-timeout
99101
await sleep(serverSelectionTimeoutMS / 2);
100102
expect(serverSelectionTimeoutStarted).to.be.true;
101103

104+
expect(getTimerCount()).to.not.equal(0);
102105
await client.close();
103106
expect(getTimerCount()).to.equal(0);
104-
};
105107

106-
const getTimerCount = () => process.getActiveResourcesInfo().filter(r => r === 'Timeout').length;
107-
await run({ MongoClient, uri: config.uri, sleep, sinon, expect, getTimerCount});
108+
const err = await insertPromise.catch(e => e);
109+
expect(err).to.be.instanceOf(mongodb.MongoServerSelectionError);
110+
};
111+
await runScriptAndGetProcessInfo('timer-server-selection', config, run);
108112
});
109113
});
110114
});
@@ -143,13 +147,35 @@ describe.only('MongoClient.close() Integration', () => {
143147

144148
expect(getTimerCount()).to.equal(0);
145149
};
146-
147150
await runScriptAndGetProcessInfo('timer-monitor-interval', config, run);
148151
});
149152
});
150153

151154
describe('after a heartbeat fails', () => {
152-
it.skip('the new monitor interval timer is cleaned up by client.close()', metadata, async () => {});
155+
it('the new monitor interval timer is cleaned up by client.close()', metadata, async () => {
156+
const run = async function ({ MongoClient, uri, expect, sleep, getTimerCount }) {
157+
const heartbeatFrequencyMS = 2000;
158+
const client = new MongoClient('mongodb://fakeUri', { heartbeatFrequencyMS });
159+
let heartbeatHappened = false;
160+
client.on('serverHeartbeatFailed', () => heartbeatHappened = true);
161+
client.connect();
162+
await sleep(heartbeatFrequencyMS * 2.5);
163+
expect(heartbeatHappened).to.be.true;
164+
165+
function getMonitorTimer(servers) {
166+
for (const server of servers) {
167+
return server[1]?.monitor.monitorId.timerId;
168+
}
169+
};
170+
const servers = client.topology.s.servers;
171+
expect(getMonitorTimer(servers)).to.exist;
172+
await client.close();
173+
expect(getMonitorTimer(servers)).to.not.exist;
174+
175+
expect(getTimerCount()).to.equal(0);
176+
};
177+
await runScriptAndGetProcessInfo('timer-heartbeat-failed-monitor', config, run);
178+
});
153179
});
154180
});
155181
});
@@ -219,7 +245,6 @@ describe.only('MongoClient.close() Integration', () => {
219245

220246
expect(getTimerCount()).to.equal(0);
221247
};
222-
223248
await runScriptAndGetProcessInfo('timer-rtt-monitor', config, run);
224249
});
225250
});
@@ -315,31 +340,33 @@ describe.only('MongoClient.close() Integration', () => {
315340
});
316341

317342
describe('Node.js resource: checkOut Timer', () => {
318-
// waitQueueTimeoutMS
319343
describe('after new connection pool is created', () => {
320344
it('the wait queue timer is cleaned up by client.close()', async function () {
321-
// note: this test is not called in a separate process since it requires stubbing internal function
345+
// note: this test is not called in a separate process since it stubs internal class: ConnectionPool
322346
const run = async function ({ MongoClient, uri, expect, sinon, sleep, getTimerCount }) {
323347
const waitQueueTimeoutMS = 999;
324348
const client = new MongoClient(uri, { minPoolSize: 1, waitQueueTimeoutMS });
325-
const timeoutStartedSpy = sinon.spy(Timeout, 'expires');
349+
const timers = require('timers');
350+
const timeoutStartedSpy = sinon.spy(timers, 'setTimeout');
326351
let checkoutTimeoutStarted = false;
327352

328353
// make waitQueue hang so check out timer isn't cleared and check that the timeout has started
329354
sinon.stub(ConnectionPool.prototype, 'processWaitQueue').callsFake(async () => {
330-
checkoutTimeoutStarted = timeoutStartedSpy.getCalls().map(r => r.args).filter(r => r.includes(999)) ? true : false;
355+
checkoutTimeoutStarted = timeoutStartedSpy.getCalls().filter(r => r.args.includes(waitQueueTimeoutMS)).flat().length > 0;
331356
});
332357

333-
client.db('db').collection('collection').insertOne({ x: 1 }).catch(e => e);
358+
const insertPromise = client.db('db').collection('collection').insertOne({ x: 1 }).catch(e => e);
334359

335360
// don't allow entire checkout timer to elapse to ensure close is called mid-timeout
336361
await sleep(waitQueueTimeoutMS / 2);
337362
expect(checkoutTimeoutStarted).to.be.true;
338363

339364
await client.close();
340365
expect(getTimerCount()).to.equal(0);
341-
};
342366

367+
const err = await insertPromise;
368+
expect(err).to.not.be.instanceOf(Error);
369+
};
343370
const getTimerCount = () => process.getActiveResourcesInfo().filter(r => r === 'Timeout').length;
344371
await run({ MongoClient, uri: config.uri, sleep, sinon, expect, getTimerCount});
345372
});
@@ -389,39 +416,40 @@ describe.only('MongoClient.close() Integration', () => {
389416
}
390417
};
391418
describe('after SRVPoller is created', () => {
392-
it.skip('timers are cleaned up by client.close()', metadata, async () => {
393-
const run = async function ({ MongoClient, uri, expect, log, sinon, mongodb, getTimerCount }) {
419+
it.only('timers are cleaned up by client.close()', metadata, async () => {
420+
const run = async function ({ MongoClient, uri, expect, mongodb, sinon, getTimerCount }) {
394421
const dns = require('dns');
395-
396422
sinon.stub(dns.promises, 'resolveTxt').callsFake(async () => {
397423
throw { code: 'ENODATA' };
398424
});
399425
sinon.stub(dns.promises, 'resolveSrv').callsFake(async () => {
400-
const formattedUri = mongodb.HostAddress.fromString(uri.split('//')[1]);
401426
return [
402427
{
403-
name: formattedUri.host,
404-
port: formattedUri.port,
428+
name: 'domain.localhost',
429+
port: 27017,
405430
weight: 0,
406431
priority: 0,
407-
protocol: formattedUri.host.isIPv6 ? 'IPv6' : 'IPv4'
432+
protocol: 'IPv6'
408433
}
409434
];
410435
});
411-
/* sinon.stub(mongodb, 'checkParentDomainMatch').callsFake(async () => {
412-
console.log('in here!!!');
413-
}); */
414436

415437
const client = new MongoClient('mongodb+srv://localhost');
416-
await client.connect();
438+
client.connect();
439+
440+
const topology = client.topology;
441+
const prevDesc = topology;
442+
log({ topology });
443+
const currDesc = prevDesc;
444+
client.topology.emit(
445+
'topologyDescriptionChanged',
446+
mongodb.TopologyDescriptionChangedEvent(client.topology.s.id, prevDesc, currDesc)
447+
);
448+
417449
await client.close();
418450
expect(getTimerCount()).to.equal(0);
419-
sinon.restore();
420451
};
421-
422-
const getTimerCount = () => process.getActiveResourcesInfo().filter(r => r === 'Timeout').length;
423-
// await run({ MongoClient, uri: config.uri, sleep, sinon, expect, mongodb, getTimerCount});
424-
await runScriptAndGetProcessInfo('srv-poller-timer', config, run);
452+
await runScriptAndGetProcessInfo('timer-srv-poller', config, run);
425453
});
426454
});
427455
});
@@ -564,4 +592,4 @@ describe.only('MongoClient.close() Integration', () => {
564592
it.skip('all active server-side cursors are closed by client.close()', async function () {});
565593
});
566594
});
567-
});
595+
});

test/integration/node-specific/resource_tracking_script_builder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,8 @@ export async function runScriptAndGetProcessInfo(
188188
.reduce((acc, curr) => ({ ...acc, ...curr }), {});
189189

190190
// delete temporary files
191-
await unlink(scriptName);
192-
await unlink(logFile);
191+
// await unlink(scriptName);
192+
// await unlink(logFile);
193193

194194
// assertions about exit status
195195
if (exitCode) {

0 commit comments

Comments
 (0)