Skip to content

Commit 8bfa945

Browse files
test(NODE-5237): fix flaky deadlock tests and modernize deadlock test suite (#3679)
1 parent 63ae351 commit 8bfa945

File tree

3 files changed

+340
-354
lines changed

3 files changed

+340
-354
lines changed
Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
import * as BSON from 'bson';
2+
import { expect } from 'chai';
3+
import { readFileSync } from 'fs';
4+
import * as path from 'path';
5+
import * as util from 'util';
6+
7+
import { CommandStartedEvent, MongoClient, MongoClientOptions } from '../../mongodb';
8+
import { installNodeDNSWorkaroundHooks } from '../../tools/runner/hooks/configuration';
9+
import { getEncryptExtraOptions } from '../../tools/utils';
10+
import { dropCollection } from '../shared';
11+
12+
/* REFERENCE: (note commit hash) */
13+
/* https://github.com/mongodb/specifications/blob/b3beada 72ae1c992294ae6a8eea572003a274c35/source/client-side-encryption/tests/README.rst#deadlock-tests */
14+
15+
const LOCAL_KEY = Buffer.from(
16+
'Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk',
17+
'base64'
18+
);
19+
20+
const externalKey = BSON.EJSON.parse(
21+
readFileSync(
22+
path.resolve(__dirname, '../../spec/client-side-encryption/external/external-key.json'),
23+
{ encoding: 'utf-8' }
24+
)
25+
);
26+
const $jsonSchema = BSON.EJSON.parse(
27+
readFileSync(
28+
path.resolve(__dirname, '../../spec/client-side-encryption/external/external-schema.json'),
29+
{ encoding: 'utf-8' }
30+
)
31+
);
32+
33+
class CapturingMongoClient extends MongoClient {
34+
commandStartedEvents: Array<CommandStartedEvent> = [];
35+
clientsCreated = 0;
36+
constructor(url: string, options: MongoClientOptions = {}) {
37+
options = { ...options, monitorCommands: true, [Symbol.for('@@mdb.skipPingOnConnect')]: true };
38+
if (process.env.MONGODB_API_VERSION) {
39+
options.serverApi = process.env.MONGODB_API_VERSION as MongoClientOptions['serverApi'];
40+
}
41+
42+
super(url, options);
43+
44+
this.on('commandStarted', ev => this.commandStartedEvents.push(ev));
45+
this.on('topologyOpening', () => this.clientsCreated++);
46+
}
47+
}
48+
49+
function deadlockTest(
50+
{
51+
maxPoolSize,
52+
bypassAutoEncryption,
53+
useKeyVaultClient
54+
}: { maxPoolSize: number; useKeyVaultClient: boolean; bypassAutoEncryption: boolean },
55+
assertions
56+
) {
57+
return async function () {
58+
const url = this.configuration.url();
59+
const clientTest = this.clientTest;
60+
const ciphertext = this.ciphertext;
61+
62+
const clientEncryptedOpts = {
63+
autoEncryption: {
64+
keyVaultNamespace: 'keyvault.datakeys',
65+
kmsProviders: { local: { key: LOCAL_KEY } },
66+
bypassAutoEncryption,
67+
keyVaultClient: useKeyVaultClient ? this.clientKeyVault : undefined,
68+
extraOptions: getEncryptExtraOptions()
69+
},
70+
maxPoolSize
71+
};
72+
73+
const clientEncrypted = new CapturingMongoClient(url, clientEncryptedOpts);
74+
75+
await clientEncrypted.connect();
76+
77+
try {
78+
if (bypassAutoEncryption) {
79+
await clientTest.db('db').collection('coll').insertOne({ _id: 0, encrypted: ciphertext });
80+
} else {
81+
await clientEncrypted
82+
.db('db')
83+
.collection('coll')
84+
.insertOne({ _id: 0, encrypted: 'string0' });
85+
}
86+
87+
const res = await clientEncrypted.db('db').collection('coll').findOne({ _id: 0 });
88+
89+
expect(res).to.have.property('_id', 0);
90+
expect(res).to.have.property('encrypted', 'string0');
91+
assertions(clientEncrypted, this.clientKeyVault);
92+
} finally {
93+
await clientEncrypted.close();
94+
}
95+
};
96+
}
97+
98+
const metadata = {
99+
requires: {
100+
clientSideEncryption: true,
101+
mongodb: '>=4.2.0',
102+
topology: '!load-balanced'
103+
}
104+
};
105+
describe('Connection Pool Deadlock Prevention', function () {
106+
installNodeDNSWorkaroundHooks();
107+
beforeEach(async function () {
108+
const mongodbClientEncryption = this.configuration.mongodbClientEncryption;
109+
const url: string = this.configuration.url();
110+
111+
this.clientTest = new CapturingMongoClient(url);
112+
this.clientKeyVault = new CapturingMongoClient(url, {
113+
monitorCommands: true,
114+
maxPoolSize: 1
115+
});
116+
117+
this.clientEncryption = undefined;
118+
this.ciphertext = undefined;
119+
120+
await this.clientTest.connect();
121+
await this.clientKeyVault.connect();
122+
await dropCollection(this.clientTest.db('keyvault'), 'datakeys');
123+
await dropCollection(this.clientTest.db('db'), 'coll');
124+
125+
await this.clientTest
126+
.db('keyvault')
127+
.collection('datakeys')
128+
.insertOne(externalKey, {
129+
writeConcern: { w: 'majority' }
130+
});
131+
132+
await this.clientTest.db('db').createCollection('coll', { validator: { $jsonSchema } });
133+
134+
this.clientEncryption = new mongodbClientEncryption.ClientEncryption(this.clientTest, {
135+
kmsProviders: { local: { key: LOCAL_KEY } },
136+
keyVaultNamespace: 'keyvault.datakeys',
137+
keyVaultClient: this.keyVaultClient,
138+
extraOptions: getEncryptExtraOptions()
139+
});
140+
this.clientEncryption.encryptPromisified = util.promisify(
141+
this.clientEncryption.encrypt.bind(this.clientEncryption)
142+
);
143+
144+
this.ciphertext = await this.clientEncryption.encryptPromisified('string0', {
145+
algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic',
146+
keyAltName: 'local'
147+
});
148+
});
149+
150+
afterEach(function () {
151+
return Promise.all([this.clientKeyVault.close(), this.clientTest.close()]).then(() => {
152+
this.clientKeyVault = undefined;
153+
this.clientTest = undefined;
154+
this.clientEncryption = undefined;
155+
});
156+
});
157+
158+
const CASE1 = { maxPoolSize: 1, bypassAutoEncryption: false, useKeyVaultClient: false };
159+
it(
160+
'Case 1',
161+
metadata,
162+
deadlockTest(CASE1, clientEncrypted => {
163+
expect(clientEncrypted.clientsCreated, 'Incorrect number of clients created').to.equal(2);
164+
165+
const events = clientEncrypted.commandStartedEvents;
166+
expect(events).to.have.lengthOf(4);
167+
168+
expect(events[0].command).to.have.property('listCollections');
169+
expect(events[0].command.$db).to.equal('db');
170+
171+
expect(events[1].command).to.have.property('find');
172+
expect(events[1].command.$db).to.equal('keyvault');
173+
174+
expect(events[2].command).to.have.property('insert');
175+
expect(events[2].command.$db).to.equal('db');
176+
177+
expect(events[3].command).to.have.property('find');
178+
expect(events[3].command.$db).to.equal('db');
179+
})
180+
);
181+
182+
const CASE2 = { maxPoolSize: 1, bypassAutoEncryption: false, useKeyVaultClient: true };
183+
it(
184+
'Case 2',
185+
metadata,
186+
deadlockTest(CASE2, (clientEncrypted, clientKeyVault) => {
187+
expect(clientEncrypted.clientsCreated, 'Incorrect number of clients created').to.equal(2);
188+
189+
const events = clientEncrypted.commandStartedEvents;
190+
expect(events).to.have.lengthOf(3);
191+
192+
expect(events[0].command).to.have.property('listCollections');
193+
expect(events[0].command.$db).to.equal('db');
194+
195+
expect(events[1].command).to.have.property('insert');
196+
expect(events[1].command.$db).to.equal('db');
197+
198+
expect(events[2].command).to.have.property('find');
199+
expect(events[2].command.$db).to.equal('db');
200+
201+
const keyVaultEvents = clientKeyVault.commandStartedEvents;
202+
expect(keyVaultEvents).to.have.lengthOf(1);
203+
204+
expect(keyVaultEvents[0].command).to.have.property('find');
205+
expect(keyVaultEvents[0].command.$db).to.equal('keyvault');
206+
})
207+
);
208+
209+
const CASE3 = { maxPoolSize: 1, bypassAutoEncryption: true, useKeyVaultClient: false };
210+
it(
211+
'Case 3',
212+
metadata,
213+
deadlockTest(CASE3, clientEncrypted => {
214+
expect(clientEncrypted.clientsCreated, 'Incorrect number of clients created').to.equal(2);
215+
216+
const events = clientEncrypted.commandStartedEvents;
217+
expect(events).to.have.lengthOf(2);
218+
219+
expect(events[0].command).to.have.property('find');
220+
expect(events[0].command.$db).to.equal('db');
221+
222+
expect(events[1].command).to.have.property('find');
223+
expect(events[1].command.$db).to.equal('keyvault');
224+
})
225+
);
226+
227+
const CASE4 = { maxPoolSize: 1, bypassAutoEncryption: true, useKeyVaultClient: true };
228+
it(
229+
'Case 4',
230+
metadata,
231+
deadlockTest(CASE4, (clientEncrypted, clientKeyVault) => {
232+
expect(clientEncrypted.clientsCreated, 'Incorrect number of clients created').to.equal(1);
233+
234+
const events = clientEncrypted.commandStartedEvents;
235+
expect(events).to.have.lengthOf(1);
236+
237+
expect(events[0].command).to.have.property('find');
238+
expect(events[0].command.$db).to.equal('db');
239+
240+
const keyVaultEvents = clientKeyVault.commandStartedEvents;
241+
expect(keyVaultEvents).to.have.lengthOf(1);
242+
243+
expect(keyVaultEvents[0].command).to.have.property('find');
244+
expect(keyVaultEvents[0].command.$db).to.equal('keyvault');
245+
})
246+
);
247+
248+
const CASE5 = { maxPoolSize: 0, bypassAutoEncryption: false, useKeyVaultClient: false };
249+
it(
250+
'Case 5',
251+
metadata,
252+
deadlockTest(CASE5, clientEncrypted => {
253+
expect(clientEncrypted.clientsCreated, 'Incorrect number of clients created').to.equal(1);
254+
255+
const events = clientEncrypted.commandStartedEvents;
256+
expect(events).to.have.lengthOf(5);
257+
258+
expect(events[0].command).to.have.property('listCollections');
259+
expect(events[0].command.$db).to.equal('db');
260+
261+
expect(events[1].command).to.have.property('listCollections');
262+
expect(events[1].command.$db).to.equal('keyvault');
263+
264+
expect(events[2].command).to.have.property('find');
265+
expect(events[2].command.$db).to.equal('keyvault');
266+
267+
expect(events[3].command).to.have.property('insert');
268+
expect(events[3].command.$db).to.equal('db');
269+
270+
expect(events[4].command).to.have.property('find');
271+
expect(events[4].command.$db).to.equal('db');
272+
})
273+
);
274+
275+
const CASE6 = { maxPoolSize: 0, bypassAutoEncryption: false, useKeyVaultClient: true };
276+
it(
277+
'Case 6',
278+
metadata,
279+
deadlockTest(CASE6, (clientEncrypted, clientKeyVault) => {
280+
expect(clientEncrypted.clientsCreated, 'Incorrect number of clients created').to.equal(1);
281+
282+
const events = clientEncrypted.commandStartedEvents;
283+
expect(events).to.have.lengthOf(3);
284+
285+
expect(events[0].command).to.have.property('listCollections');
286+
expect(events[0].command.$db).to.equal('db');
287+
288+
expect(events[1].command).to.have.property('insert');
289+
expect(events[1].command.$db).to.equal('db');
290+
291+
expect(events[2].command).to.have.property('find');
292+
expect(events[2].command.$db).to.equal('db');
293+
294+
const keyVaultEvents = clientKeyVault.commandStartedEvents;
295+
expect(keyVaultEvents).to.have.lengthOf(1);
296+
297+
expect(keyVaultEvents[0].command).to.have.property('find');
298+
expect(keyVaultEvents[0].command.$db).to.equal('keyvault');
299+
})
300+
);
301+
302+
const CASE7 = { maxPoolSize: 0, bypassAutoEncryption: true, useKeyVaultClient: false };
303+
it(
304+
'Case 7',
305+
metadata,
306+
deadlockTest(CASE7, clientEncrypted => {
307+
expect(clientEncrypted.clientsCreated, 'Incorrect number of clients created').to.equal(1);
308+
309+
const events = clientEncrypted.commandStartedEvents;
310+
expect(events).to.have.lengthOf(2);
311+
312+
expect(events[0].command).to.have.property('find');
313+
expect(events[0].command.$db).to.equal('db');
314+
315+
expect(events[1].command).to.have.property('find');
316+
expect(events[1].command.$db).to.equal('keyvault');
317+
})
318+
);
319+
320+
const CASE8 = { maxPoolSize: 0, bypassAutoEncryption: true, useKeyVaultClient: true };
321+
it(
322+
'Case 8',
323+
metadata,
324+
deadlockTest(CASE8, (clientEncrypted, clientKeyVault) => {
325+
expect(clientEncrypted.clientsCreated, 'Incorrect number of clients created').to.equal(1);
326+
327+
const events = clientEncrypted.commandStartedEvents;
328+
expect(events).to.have.lengthOf(1);
329+
330+
expect(events[0].command).to.have.property('find');
331+
expect(events[0].command.$db).to.equal('db');
332+
333+
const keyVaultEvents = clientKeyVault.commandStartedEvents;
334+
expect(keyVaultEvents).to.have.lengthOf(1);
335+
336+
expect(keyVaultEvents[0].command).to.have.property('find');
337+
expect(keyVaultEvents[0].command.$db).to.equal('keyvault');
338+
})
339+
);
340+
});

0 commit comments

Comments
 (0)