Skip to content

Commit 9777eff

Browse files
committed
feat(NODE-6392): add timeoutMS support to ClientEncryption helpers
1 parent 450b163 commit 9777eff

File tree

2 files changed

+186
-12
lines changed

2 files changed

+186
-12
lines changed

src/client-side-encryption/client_encryption.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ export class ClientEncryption {
7474
_tlsOptions: CSFLEKMSTlsOptions;
7575
/** @internal */
7676
_kmsProviders: KMSProviders;
77+
/** @internal */
78+
_timeoutMS?: number;
7779

7880
/** @internal */
7981
_mongoCrypt: MongoCrypt;
@@ -120,6 +122,7 @@ export class ClientEncryption {
120122
this._proxyOptions = options.proxyOptions ?? {};
121123
this._tlsOptions = options.tlsOptions ?? {};
122124
this._kmsProviders = options.kmsProviders || {};
125+
this._timeoutMS = options.timeoutMS ?? client.options.timeoutMS;
123126

124127
if (options.keyVaultNamespace == null) {
125128
throw new MongoCryptInvalidArgumentError('Missing required option `keyVaultNamespace`');
@@ -303,7 +306,8 @@ export class ClientEncryption {
303306
.db(dbName)
304307
.collection<DataKey>(collectionName)
305308
.bulkWrite(replacements, {
306-
writeConcern: { w: 'majority' }
309+
writeConcern: { w: 'majority' },
310+
timeoutMS: this._timeoutMS
307311
});
308312

309313
return { bulkWriteResult: result };
@@ -332,7 +336,7 @@ export class ClientEncryption {
332336
return await this._keyVaultClient
333337
.db(dbName)
334338
.collection<DataKey>(collectionName)
335-
.deleteOne({ _id }, { writeConcern: { w: 'majority' } });
339+
.deleteOne({ _id }, { writeConcern: { w: 'majority' }, timeoutMS: this._timeoutMS });
336340
}
337341

338342
/**
@@ -355,7 +359,7 @@ export class ClientEncryption {
355359
return this._keyVaultClient
356360
.db(dbName)
357361
.collection<DataKey>(collectionName)
358-
.find({}, { readConcern: { level: 'majority' } });
362+
.find({}, { readConcern: { level: 'majority' }, timeoutMS: this._timeoutMS });
359363
}
360364

361365
/**
@@ -381,7 +385,7 @@ export class ClientEncryption {
381385
return await this._keyVaultClient
382386
.db(dbName)
383387
.collection<DataKey>(collectionName)
384-
.findOne({ _id }, { readConcern: { level: 'majority' } });
388+
.findOne({ _id }, { readConcern: { level: 'majority' }, timeoutMS: this._timeoutMS });
385389
}
386390

387391
/**
@@ -408,7 +412,10 @@ export class ClientEncryption {
408412
return await this._keyVaultClient
409413
.db(dbName)
410414
.collection<DataKey>(collectionName)
411-
.findOne({ keyAltNames: keyAltName }, { readConcern: { level: 'majority' } });
415+
.findOne(
416+
{ keyAltNames: keyAltName },
417+
{ readConcern: { level: 'majority' }, timeoutMS: this._timeoutMS }
418+
);
412419
}
413420

414421
/**
@@ -442,7 +449,7 @@ export class ClientEncryption {
442449
.findOneAndUpdate(
443450
{ _id },
444451
{ $addToSet: { keyAltNames: keyAltName } },
445-
{ writeConcern: { w: 'majority' }, returnDocument: 'before' }
452+
{ writeConcern: { w: 'majority' }, returnDocument: 'before', timeoutMS: this._timeoutMS }
446453
);
447454

448455
return value;
@@ -503,7 +510,8 @@ export class ClientEncryption {
503510
.collection<DataKey>(collectionName)
504511
.findOneAndUpdate({ _id }, pipeline, {
505512
writeConcern: { w: 'majority' },
506-
returnDocument: 'before'
513+
returnDocument: 'before',
514+
timeoutMS: this._timeoutMS
507515
});
508516

509517
return value;
@@ -818,6 +826,11 @@ export interface ClientEncryptionOptions {
818826
* TLS options for kms providers to use.
819827
*/
820828
tlsOptions?: CSFLEKMSTlsOptions;
829+
830+
/**
831+
* The timeout setting to be used for all the operations on ClientEncryption.
832+
*/
833+
timeoutMS?: number;
821834
}
822835

823836
/**

test/integration/client-side-encryption/driver.test.ts

Lines changed: 166 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,33 @@
11
import { EJSON, UUID } from 'bson';
22
import { expect } from 'chai';
33
import * as crypto from 'crypto';
4+
import * as sinon from 'sinon';
45

6+
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
7+
import { StateMachine } from '../../../lib/client-side-encryption/state_machine';
58
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
69
import { ClientEncryption } from '../../../src/client-side-encryption/client_encryption';
7-
import { type Collection, type CommandStartedEvent, type MongoClient } from '../../mongodb';
8-
import * as BSON from '../../mongodb';
9-
import { getEncryptExtraOptions } from '../../tools/utils';
10-
11-
const metadata = {
10+
import {
11+
BSON,
12+
type Collection,
13+
type CommandStartedEvent,
14+
type MongoClient,
15+
MongoOperationTimeoutError
16+
} from '../../mongodb';
17+
import { type FailPoint, getEncryptExtraOptions } from '../../tools/utils';
18+
19+
const metadata: MongoDBMetadataUI = {
1220
requires: {
1321
mongodb: '>=4.2.0',
1422
clientSideEncryption: true
1523
}
1624
};
1725

26+
const LOCAL_KEY = Buffer.from(
27+
'Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk',
28+
'base64'
29+
);
30+
1831
describe('Client Side Encryption Functional', function () {
1932
const dataDbName = 'db';
2033
const dataCollName = 'coll';
@@ -401,6 +414,154 @@ describe('Client Side Encryption Functional', function () {
401414
});
402415
}
403416
);
417+
418+
describe.only('CSOT on ClientEncryption', function () {
419+
function makeBlockingFailFor(command: string, blockTimeMS: number) {
420+
beforeEach(async function () {
421+
const utilClient = this.configuration.newClient();
422+
await utilClient.db('admin').command({
423+
configureFailPoint: 'failCommand',
424+
mode: { times: 2 },
425+
data: {
426+
failCommands: [command],
427+
blockConnection: true,
428+
blockTimeMS,
429+
appName: 'clientEncryption'
430+
}
431+
} as FailPoint);
432+
await utilClient.close();
433+
});
434+
435+
afterEach(async function () {
436+
sinon.restore();
437+
const utilClient = this.configuration.newClient();
438+
utilClient
439+
.db('admin')
440+
.command({ configureFailPoint: 'failCommand', mode: 'off' } as FailPoint);
441+
await utilClient.close();
442+
});
443+
}
444+
445+
async function expectCSOTTimeout(fn: () => Promise<void>) {
446+
const start = performance.now();
447+
const error = await fn().then(
448+
() => null,
449+
error => error
450+
);
451+
const end = performance.now();
452+
if (error?.name === 'MongoBulkWriteError') {
453+
expect(error)
454+
.to.have.property('errorResponse')
455+
.that.is.instanceOf(MongoOperationTimeoutError);
456+
} else {
457+
expect(error).to.be.instanceOf(MongoOperationTimeoutError);
458+
}
459+
expect(end - start).to.be.within(500, 1000);
460+
}
461+
462+
let client;
463+
let clientEncryption: ClientEncryption;
464+
465+
beforeEach(async function () {
466+
if (!this.configuration.clientSideEncryption.enabled) {
467+
this.skip();
468+
}
469+
470+
client = this.configuration.newClient({}, { appName: 'clientEncryption' });
471+
await client.connect();
472+
clientEncryption = new ClientEncryption(client, {
473+
kmsProviders: { local: { key: LOCAL_KEY } },
474+
keyVaultNamespace,
475+
keyVaultClient: null,
476+
timeoutMS: 500,
477+
...getEncryptExtraOptions()
478+
});
479+
});
480+
481+
afterEach(async function () {
482+
await client.close();
483+
});
484+
485+
describe('rewrapManyDataKey', function () {
486+
makeBlockingFailFor('update', 2000);
487+
488+
beforeEach(async function () {
489+
sinon.stub(StateMachine.prototype, 'execute').callsFake(async function () {
490+
return BSON.serialize({ v: [{ _id: new UUID() }] });
491+
});
492+
});
493+
494+
afterEach(async function () {
495+
sinon.restore();
496+
});
497+
498+
it('throws a timeout error if the bulk operation takes too long', async function () {
499+
await expectCSOTTimeout(async () => {
500+
await clientEncryption.rewrapManyDataKey({ _id: new UUID() }, { provider: 'local' });
501+
});
502+
});
503+
});
504+
505+
describe('deleteKey', function () {
506+
makeBlockingFailFor('delete', 2000);
507+
508+
it('throws a timeout error if the delete operation takes too long', async function () {
509+
await expectCSOTTimeout(async () => {
510+
await clientEncryption.deleteKey(new UUID());
511+
});
512+
});
513+
});
514+
515+
describe('getKey', function () {
516+
makeBlockingFailFor('find', 2000);
517+
518+
it('throws a timeout error if the bulk operation takes too long', async function () {
519+
await expectCSOTTimeout(async () => {
520+
await clientEncryption.getKey(new UUID());
521+
});
522+
});
523+
});
524+
525+
describe('getKeys', function () {
526+
makeBlockingFailFor('find', 2000);
527+
528+
it('throws a timeout error if the find operation takes too long', async function () {
529+
await expectCSOTTimeout(async () => {
530+
await clientEncryption.getKeys().toArray();
531+
});
532+
});
533+
});
534+
535+
describe('removeKeyAltName', function () {
536+
makeBlockingFailFor('findAndModify', 2000);
537+
538+
it('throws a timeout error if the findAndModify operation takes too long', async function () {
539+
await expectCSOTTimeout(async () => {
540+
await clientEncryption.removeKeyAltName(new UUID(), 'blah');
541+
});
542+
});
543+
});
544+
545+
describe('addKeyAltName', function () {
546+
makeBlockingFailFor('findAndModify', 2000);
547+
548+
it('throws a timeout error if the findAndModify operation takes too long', async function () {
549+
await expectCSOTTimeout(async () => {
550+
await clientEncryption.addKeyAltName(new UUID(), 'blah');
551+
});
552+
});
553+
});
554+
555+
describe('getKeyByAltName', function () {
556+
makeBlockingFailFor('find', 2000);
557+
558+
it('throws a timeout error if the find operation takes too long', async function () {
559+
await expectCSOTTimeout(async () => {
560+
await clientEncryption.getKeyByAltName('blah');
561+
});
562+
});
563+
});
564+
});
404565
});
405566

406567
describe('Range Explicit Encryption with JS native types', function () {

0 commit comments

Comments
 (0)