Skip to content

Commit ed50ef5

Browse files
authored
test(NODE-4262): simplify leak checker for startSession fixes (#3281)
1 parent 0936b58 commit ed50ef5

File tree

6 files changed

+199
-222
lines changed

6 files changed

+199
-222
lines changed

test/integration/client-side-encryption/client_side_encryption.prose.test.js

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
'use strict';
22
const BSON = require('bson');
3-
const chai = require('chai');
3+
const { expect } = require('chai');
44
const fs = require('fs');
55
const path = require('path');
6+
67
const { deadlockTests } = require('./client_side_encryption.prose.deadlock');
8+
const { dropCollection, APMEventCollector } = require('../shared');
79

8-
const expect = chai.expect;
9-
chai.use(require('chai-subset'));
10+
const { EJSON } = BSON;
1011
const { LEGACY_HELLO_COMMAND } = require('../../../src/constants');
1112

1213
const getKmsProviders = (localKey, kmipEndpoint, azureEndpoint, gcpEndpoint) => {
@@ -53,10 +54,6 @@ describe('Client Side Encryption Prose Tests', metadata, function () {
5354
const keyVaultCollName = 'datakeys';
5455
const keyVaultNamespace = `${keyVaultDbName}.${keyVaultCollName}`;
5556

56-
const shared = require('../shared');
57-
const dropCollection = shared.dropCollection;
58-
const APMEventCollector = shared.APMEventCollector;
59-
6057
const LOCAL_KEY = Buffer.from(
6158
'Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk',
6259
'base64'
@@ -337,9 +334,6 @@ describe('Client Side Encryption Prose Tests', metadata, function () {
337334
// and confirming that the externalClient is firing off keyVault requests during
338335
// encrypted operations
339336
describe('External Key Vault Test', function () {
340-
const fs = require('fs');
341-
const path = require('path');
342-
const { EJSON } = BSON;
343337
function loadExternal(file) {
344338
return EJSON.parse(
345339
fs.readFileSync(path.resolve(__dirname, '../../spec/client-side-encryption/external', file))
@@ -541,9 +535,6 @@ describe('Client Side Encryption Prose Tests', metadata, function () {
541535
});
542536

543537
describe('BSON size limits and batch splitting', function () {
544-
const fs = require('fs');
545-
const path = require('path');
546-
const { EJSON } = BSON;
547538
function loadLimits(file) {
548539
return EJSON.parse(
549540
fs.readFileSync(path.resolve(__dirname, '../../spec/client-side-encryption/limits', file))
@@ -621,7 +612,7 @@ describe('Client Side Encryption Prose Tests', metadata, function () {
621612
}
622613
});
623614

624-
after(function () {
615+
afterEach(function () {
625616
return this.client && this.client.close();
626617
});
627618

test/integration/client-side-encryption/client_side_encryption.spec.test.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,17 @@ const skippedAuthTests = [
5757
// TODO(NODE-4006): Investigate csfle test "operation fails with maxWireVersion < 8"
5858
const skippedMaxWireVersionTest = 'operation fails with maxWireVersion < 8';
5959

60-
const SKIPPED_TESTS = new Set(
61-
isAuthEnabled ? skippedAuthTests.concat(skippedMaxWireVersionTest) : [skippedMaxWireVersionTest]
62-
);
60+
const SKIPPED_TESTS = new Set([
61+
...(isAuthEnabled
62+
? skippedAuthTests.concat(skippedMaxWireVersionTest)
63+
: [skippedMaxWireVersionTest]),
64+
// TODO(NODE-4288): Fix FLE 2 tests
65+
'default state collection names are applied',
66+
'drop removes all state collections',
67+
'CreateCollection from encryptedFields.',
68+
'DropCollection from encryptedFields',
69+
'DropCollection from remote encryptedFields'
70+
]);
6371

6472
describe('Client Side Encryption', function () {
6573
const testContext = new TestRunnerContext();

test/mocha_mongodb.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
"ts-node/register",
66
"test/tools/runner/chai-addons.js",
77
"test/tools/runner/hooks/configuration.js",
8-
"test/tools/runner/hooks/client_leak_checker.js",
9-
"test/tools/runner/hooks/session_leak_checker.js"
8+
"test/tools/runner/hooks/leak_checker.ts"
109
],
1110
"extension": ["js", "ts"],
1211
"ui": "test/tools/runner/metadata_ui.js",

test/tools/runner/hooks/client_leak_checker.js

Lines changed: 0 additions & 55 deletions
This file was deleted.
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/* eslint-disable @typescript-eslint/no-this-alias */
2+
import { expect } from 'chai';
3+
import * as chalk from 'chalk';
4+
import * as net from 'net';
5+
6+
import { MongoClient } from '../../../../src/mongo_client';
7+
import { ServerSessionPool } from '../../../../src/sessions';
8+
9+
class LeakChecker {
10+
static originalAcquire: typeof ServerSessionPool.prototype.acquire;
11+
static originalRelease: typeof ServerSessionPool.prototype.release;
12+
static kAcquiredCount: symbol;
13+
14+
static originalConnect: typeof MongoClient.prototype.connect;
15+
static originalClose: typeof MongoClient.prototype.close;
16+
static kConnectCount: symbol;
17+
18+
static {
19+
this.originalAcquire = ServerSessionPool.prototype.acquire;
20+
this.originalRelease = ServerSessionPool.prototype.release;
21+
this.kAcquiredCount = Symbol('acquiredCount');
22+
this.originalConnect = MongoClient.prototype.connect;
23+
this.originalClose = MongoClient.prototype.close;
24+
this.kConnectCount = Symbol('connectedCount');
25+
}
26+
27+
clients: Set<MongoClient>;
28+
sessionPools: Set<ServerSessionPool>;
29+
30+
constructor(public titlePath: string) {
31+
this.clients = new Set<MongoClient>();
32+
this.sessionPools = new Set<ServerSessionPool>();
33+
}
34+
35+
setupSessionLeakChecker() {
36+
const leakChecker = this;
37+
ServerSessionPool.prototype.acquire = function (...args) {
38+
leakChecker.sessionPools.add(this);
39+
40+
this[LeakChecker.kAcquiredCount] ??= 0;
41+
this[LeakChecker.kAcquiredCount] += 1;
42+
43+
return LeakChecker.originalAcquire.call(this, ...args);
44+
};
45+
46+
ServerSessionPool.prototype.release = function (...args) {
47+
if (!(LeakChecker.kAcquiredCount in this)) {
48+
throw new Error('releasing before acquiring even once??');
49+
} else {
50+
this[LeakChecker.kAcquiredCount] -= 1;
51+
}
52+
53+
return LeakChecker.originalRelease.call(this, ...args);
54+
};
55+
}
56+
57+
setupClientLeakChecker() {
58+
const leakChecker = this;
59+
MongoClient.prototype.connect = function (...args) {
60+
leakChecker.clients.add(this);
61+
this[LeakChecker.kConnectCount] ??= 0;
62+
63+
const lastArg = args[args.length - 1];
64+
const lastArgIsCallback = typeof lastArg === 'function';
65+
if (lastArgIsCallback) {
66+
const argsWithoutCallback = args.slice(0, args.length - 1);
67+
return LeakChecker.originalConnect.call(this, ...argsWithoutCallback, (error, client) => {
68+
if (error == null) {
69+
this[LeakChecker.kConnectCount] += 1; // only increment on successful connects
70+
}
71+
return lastArg(error, client);
72+
});
73+
} else {
74+
return LeakChecker.originalConnect.call(this, ...args).then(client => {
75+
this[LeakChecker.kConnectCount] += 1; // only increment on successful connects
76+
return client;
77+
});
78+
}
79+
};
80+
81+
MongoClient.prototype.close = function (...args) {
82+
this[LeakChecker.kConnectCount] ??= 0; // prevents NaN, its fine to call close on a client that never called connect
83+
this[LeakChecker.kConnectCount] -= 1;
84+
return LeakChecker.originalClose.call(this, ...args);
85+
};
86+
}
87+
88+
setup() {
89+
this.setupSessionLeakChecker();
90+
this.setupClientLeakChecker();
91+
}
92+
93+
reset() {
94+
for (const sessionPool of this.sessionPools) {
95+
delete sessionPool[LeakChecker.kAcquiredCount];
96+
}
97+
ServerSessionPool.prototype.acquire = LeakChecker.originalAcquire;
98+
ServerSessionPool.prototype.release = LeakChecker.originalRelease;
99+
this.sessionPools.clear();
100+
101+
for (const client of this.clients) {
102+
delete client[LeakChecker.kConnectCount];
103+
}
104+
MongoClient.prototype.connect = LeakChecker.originalConnect;
105+
MongoClient.prototype.close = LeakChecker.originalClose;
106+
this.clients.clear();
107+
}
108+
109+
assert() {
110+
for (const pool of this.sessionPools) {
111+
expect(pool[LeakChecker.kAcquiredCount], 'ServerSessionPool acquired count').to.equal(0);
112+
}
113+
for (const client of this.clients) {
114+
expect(client[LeakChecker.kConnectCount], 'MongoClient connect count').to.be.lessThanOrEqual(
115+
0
116+
);
117+
}
118+
}
119+
}
120+
121+
let currentLeakChecker: LeakChecker | null;
122+
123+
const leakCheckerBeforeEach = async function () {
124+
currentLeakChecker = new LeakChecker(this.currentTest.fullTitle());
125+
currentLeakChecker.setup();
126+
};
127+
128+
const leakCheckerAfterEach = async function () {
129+
let thrownError: Error | undefined;
130+
try {
131+
currentLeakChecker.assert();
132+
} catch (error) {
133+
thrownError = error;
134+
}
135+
136+
currentLeakChecker?.reset();
137+
currentLeakChecker = null;
138+
139+
if (thrownError instanceof Error) {
140+
this.test.error(thrownError);
141+
}
142+
};
143+
144+
const TRACE_SOCKETS = process.env.TRACE_SOCKETS === 'true' ? true : false;
145+
const kSocketId = Symbol('socketId');
146+
const originalCreateConnection = net.createConnection;
147+
let socketCounter = 0n;
148+
149+
const socketLeakCheckBeforeAll = function socketLeakCheckBeforeAll() {
150+
// @ts-expect-error: Typescript says this is readonly, but it is not at runtime
151+
net.createConnection = options => {
152+
const socket = originalCreateConnection(options);
153+
socket[kSocketId] = socketCounter.toString().padStart(5, '0');
154+
socketCounter++;
155+
return socket;
156+
};
157+
};
158+
159+
const filterHandlesForSockets = function (handle: any): handle is net.Socket {
160+
// Stdio are instanceof Socket so look for fd to be null
161+
return handle?.fd == null && handle instanceof net.Socket && handle?.destroyed !== true;
162+
};
163+
164+
const socketLeakCheckAfterEach: Mocha.AsyncFunc = async function socketLeakCheckAfterEach() {
165+
const indent = ' '.repeat(this.currentTest.titlePath().length + 1);
166+
167+
const handles = (process as any)._getActiveHandles();
168+
const sockets: net.Socket[] = handles.filter(handle => filterHandlesForSockets(handle));
169+
170+
for (const socket of sockets) {
171+
console.log(
172+
chalk.yellow(
173+
`${indent}⚡︎ socket ${socket[kSocketId]} not destroyed [${socket.localAddress}:${socket.localPort}${socket.remoteAddress}:${socket.remotePort}]`
174+
)
175+
);
176+
}
177+
};
178+
179+
const beforeAll = TRACE_SOCKETS ? [socketLeakCheckBeforeAll] : [];
180+
const beforeEach = [leakCheckerBeforeEach];
181+
const afterEach = [leakCheckerAfterEach, ...(TRACE_SOCKETS ? [socketLeakCheckAfterEach] : [])];
182+
module.exports = { mochaHooks: { beforeAll, beforeEach, afterEach } };

0 commit comments

Comments
 (0)