Skip to content

Commit f745b99

Browse files
aditi-khare-mongoDBdariakp
authored andcommitted
feat(NODE-6391): Add timeoutMS support to explicit encryption (#4269)
1 parent 4588ff2 commit f745b99

File tree

5 files changed

+429
-16
lines changed

5 files changed

+429
-16
lines changed

src/client-side-encryption/client_encryption.ts

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { type MongoClient, type MongoClientOptions } from '../mongo_client';
2424
import { type Filter, type WithId } from '../mongo_types';
2525
import { type CreateCollectionOptions } from '../operations/create_collection';
2626
import { type DeleteResult } from '../operations/delete';
27-
import { TimeoutContext } from '../timeout';
27+
import { type CSOTTimeoutContext, TimeoutContext } from '../timeout';
2828
import { MongoDBCollectionNamespace, resolveTimeoutOptions } from '../utils';
2929
import * as cryptoCallbacks from './crypto_callbacks';
3030
import {
@@ -220,7 +220,13 @@ export class ClientEncryption {
220220
socketOptions: autoSelectSocketOptions(this._client.s.options)
221221
});
222222

223-
const dataKey = deserialize(await stateMachine.execute(this, context)) as DataKey;
223+
const timeoutContext =
224+
options?.timeoutContext ??
225+
TimeoutContext.create(resolveTimeoutOptions(this._client, { timeoutMS: this._timeoutMS }));
226+
227+
const dataKey = deserialize(
228+
await stateMachine.execute(this, context, timeoutContext)
229+
) as DataKey;
224230

225231
const { db: dbName, collection: collectionName } = MongoDBCollectionNamespace.fromString(
226232
this._keyVaultNamespace
@@ -229,7 +235,12 @@ export class ClientEncryption {
229235
const { insertedId } = await this._keyVaultClient
230236
.db(dbName)
231237
.collection<DataKey>(collectionName)
232-
.insertOne(dataKey, { writeConcern: { w: 'majority' } });
238+
.insertOne(dataKey, {
239+
writeConcern: { w: 'majority' },
240+
timeoutMS: timeoutContext?.csotEnabled()
241+
? timeoutContext?.getRemainingTimeMSOrThrow()
242+
: undefined
243+
});
233244

234245
return insertedId;
235246
}
@@ -511,6 +522,7 @@ export class ClientEncryption {
511522
}
512523
}
513524
];
525+
514526
const value = await this._keyVaultClient
515527
.db(dbName)
516528
.collection<DataKey>(collectionName)
@@ -555,16 +567,25 @@ export class ClientEncryption {
555567
}
556568
} = options;
557569

570+
const timeoutContext =
571+
this._timeoutMS != null
572+
? TimeoutContext.create(resolveTimeoutOptions(this._client, { timeoutMS: this._timeoutMS }))
573+
: undefined;
574+
558575
if (Array.isArray(encryptedFields.fields)) {
559576
const createDataKeyPromises = encryptedFields.fields.map(async field =>
560577
field == null || typeof field !== 'object' || field.keyId != null
561578
? field
562579
: {
563580
...field,
564-
keyId: await this.createDataKey(provider, { masterKey })
581+
keyId: await this.createDataKey(provider, {
582+
masterKey,
583+
// clone the timeoutContext
584+
// in order to avoid sharing the same timeout for server selection and connection checkout across different concurrent operations
585+
timeoutContext: timeoutContext?.csotEnabled() ? timeoutContext?.clone() : undefined
586+
})
565587
}
566588
);
567-
568589
const createDataKeyResolutions = await Promise.allSettled(createDataKeyPromises);
569590

570591
encryptedFields.fields = createDataKeyResolutions.map((resolution, index) =>
@@ -582,7 +603,10 @@ export class ClientEncryption {
582603
try {
583604
const collection = await db.createCollection<TSchema>(name, {
584605
...createCollectionOptions,
585-
encryptedFields
606+
encryptedFields,
607+
timeoutMS: timeoutContext?.csotEnabled()
608+
? timeoutContext?.getRemainingTimeMSOrThrow()
609+
: undefined
586610
});
587611
return { collection, encryptedFields };
588612
} catch (cause) {
@@ -667,7 +691,12 @@ export class ClientEncryption {
667691
socketOptions: autoSelectSocketOptions(this._client.s.options)
668692
});
669693

670-
const { v } = deserialize(await stateMachine.execute(this, context));
694+
const timeoutContext =
695+
this._timeoutMS != null
696+
? TimeoutContext.create(resolveTimeoutOptions(this._client, { timeoutMS: this._timeoutMS }))
697+
: undefined;
698+
699+
const { v } = deserialize(await stateMachine.execute(this, context, timeoutContext));
671700

672701
return v;
673702
}
@@ -747,7 +776,11 @@ export class ClientEncryption {
747776
});
748777
const context = this._mongoCrypt.makeExplicitEncryptionContext(valueBuffer, contextOptions);
749778

750-
const { v } = deserialize(await stateMachine.execute(this, context));
779+
const timeoutContext =
780+
this._timeoutMS != null
781+
? TimeoutContext.create(resolveTimeoutOptions(this._client, { timeoutMS: this._timeoutMS }))
782+
: undefined;
783+
const { v } = deserialize(await stateMachine.execute(this, context, timeoutContext));
751784
return v;
752785
}
753786
}
@@ -833,7 +866,8 @@ export interface ClientEncryptionOptions {
833866
*/
834867
tlsOptions?: CSFLEKMSTlsOptions;
835868

836-
/**
869+
/** @internal TODO(NODE-5688): make this public
870+
*
837871
* The timeout setting to be used for all the operations on ClientEncryption.
838872
*/
839873
timeoutMS?: number;
@@ -965,6 +999,9 @@ export interface ClientEncryptionCreateDataKeyProviderOptions {
965999

9661000
/** @experimental */
9671001
keyMaterial?: Buffer | Binary;
1002+
1003+
/** @internal */
1004+
timeoutContext?: CSOTTimeoutContext;
9681005
}
9691006

9701007
/**

src/timeout.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,20 @@ export class CSOTTimeoutContext extends TimeoutContext {
323323
return remainingTimeMS;
324324
}
325325

326+
/**
327+
* @internal
328+
* This method is intended to be used in situations where concurrent operation are on the same deadline, but cannot share a single `TimeoutContext` instance.
329+
* Returns a new instance of `CSOTTimeoutContext` constructed with identical options, but setting the `start` property to `this.start`.
330+
*/
331+
clone(): CSOTTimeoutContext {
332+
const timeoutContext = new CSOTTimeoutContext({
333+
timeoutMS: this.timeoutMS,
334+
serverSelectionTimeoutMS: this.serverSelectionTimeoutMS
335+
});
336+
timeoutContext.start = this.start;
337+
return timeoutContext;
338+
}
339+
326340
override refreshed(): CSOTTimeoutContext {
327341
return new CSOTTimeoutContext(this);
328342
}

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

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
Connection,
1313
CSOTTimeoutContext,
1414
type MongoClient,
15+
MongoCryptCreateDataKeyError,
16+
MongoCryptCreateEncryptedCollectionError,
1517
MongoOperationTimeoutError,
1618
StateMachine
1719
} from '../../mongodb';
@@ -1050,4 +1052,165 @@ describe('CSOT', function () {
10501052
);
10511053
});
10521054
});
1055+
1056+
describe('Explicit Encryption', function () {
1057+
describe('#createEncryptedCollection', function () {
1058+
let client: MongoClient;
1059+
let clientEncryption: ClientEncryption;
1060+
let local_key;
1061+
const timeoutMS = 1000;
1062+
1063+
const encryptedCollectionMetadata: MongoDBMetadataUI = {
1064+
requires: {
1065+
clientSideEncryption: true,
1066+
mongodb: '>=7.0.0',
1067+
topology: '!single'
1068+
}
1069+
};
1070+
1071+
beforeEach(async function () {
1072+
local_key = { local: EJSON.parse(process.env.CSFLE_KMS_PROVIDERS).local };
1073+
client = this.configuration.newClient({ timeoutMS });
1074+
await client.connect();
1075+
await client.db('keyvault').createCollection('datakeys');
1076+
clientEncryption = new ClientEncryption(client, {
1077+
keyVaultNamespace: 'keyvault.datakeys',
1078+
keyVaultClient: client,
1079+
kmsProviders: local_key
1080+
});
1081+
});
1082+
1083+
afterEach(async function () {
1084+
await client
1085+
.db()
1086+
.admin()
1087+
.command({
1088+
configureFailPoint: 'failCommand',
1089+
mode: 'off'
1090+
} as FailPoint);
1091+
await client
1092+
.db('db')
1093+
.collection('newnew')
1094+
.drop()
1095+
.catch(() => null);
1096+
await client
1097+
.db('keyvault')
1098+
.collection('datakeys')
1099+
.drop()
1100+
.catch(() => null);
1101+
await client.close();
1102+
});
1103+
1104+
async function runCreateEncryptedCollection() {
1105+
const createCollectionOptions = {
1106+
encryptedFields: { fields: [{ path: 'ssn', bsonType: 'string', keyId: null }] }
1107+
};
1108+
1109+
const db = client.db('db');
1110+
1111+
return await measureDuration(() =>
1112+
clientEncryption
1113+
.createEncryptedCollection(db, 'newnew', {
1114+
provider: 'local',
1115+
createCollectionOptions,
1116+
masterKey: null
1117+
})
1118+
.catch(err => err)
1119+
);
1120+
}
1121+
1122+
context(
1123+
'when `createDataKey` hangs longer than timeoutMS and `createCollection` does not hang',
1124+
() => {
1125+
it(
1126+
'`createEncryptedCollection throws `MongoCryptCreateDataKeyError` due to a timeout error',
1127+
encryptedCollectionMetadata,
1128+
async function () {
1129+
await client
1130+
.db()
1131+
.admin()
1132+
.command({
1133+
configureFailPoint: 'failCommand',
1134+
mode: {
1135+
times: 1
1136+
},
1137+
data: {
1138+
failCommands: ['insert'],
1139+
blockConnection: true,
1140+
blockTimeMS: timeoutMS * 1.2
1141+
}
1142+
} as FailPoint);
1143+
1144+
const { duration, result: err } = await runCreateEncryptedCollection();
1145+
expect(err).to.be.instanceOf(MongoCryptCreateDataKeyError);
1146+
expect(err.cause).to.be.instanceOf(MongoOperationTimeoutError);
1147+
expect(duration).to.be.within(timeoutMS - 100, timeoutMS + 100);
1148+
}
1149+
);
1150+
}
1151+
);
1152+
1153+
context(
1154+
'when `createDataKey` does not hang and `createCollection` hangs longer than timeoutMS',
1155+
() => {
1156+
it(
1157+
'`createEncryptedCollection throws `MongoCryptCreateEncryptedCollectionError` due to a timeout error',
1158+
encryptedCollectionMetadata,
1159+
async function () {
1160+
await client
1161+
.db()
1162+
.admin()
1163+
.command({
1164+
configureFailPoint: 'failCommand',
1165+
mode: {
1166+
times: 1
1167+
},
1168+
data: {
1169+
failCommands: ['create'],
1170+
blockConnection: true,
1171+
blockTimeMS: timeoutMS * 1.2
1172+
}
1173+
} as FailPoint);
1174+
1175+
const { duration, result: err } = await runCreateEncryptedCollection();
1176+
expect(err).to.be.instanceOf(MongoCryptCreateEncryptedCollectionError);
1177+
expect(err.cause).to.be.instanceOf(MongoOperationTimeoutError);
1178+
expect(duration).to.be.within(timeoutMS - 100, timeoutMS + 100);
1179+
}
1180+
);
1181+
}
1182+
);
1183+
1184+
context(
1185+
'when `createDataKey` and `createCollection` cumulatively hang longer than timeoutMS',
1186+
() => {
1187+
it(
1188+
'`createEncryptedCollection throws `MongoCryptCreateEncryptedCollectionError` due to a timeout error',
1189+
encryptedCollectionMetadata,
1190+
async function () {
1191+
await client
1192+
.db()
1193+
.admin()
1194+
.command({
1195+
configureFailPoint: 'failCommand',
1196+
mode: {
1197+
times: 2
1198+
},
1199+
data: {
1200+
failCommands: ['insert', 'create'],
1201+
blockConnection: true,
1202+
blockTimeMS: timeoutMS * 0.6
1203+
}
1204+
} as FailPoint);
1205+
1206+
const { duration, result: err } = await runCreateEncryptedCollection();
1207+
expect(err).to.be.instanceOf(MongoCryptCreateEncryptedCollectionError);
1208+
expect(err.cause).to.be.instanceOf(MongoOperationTimeoutError);
1209+
expect(duration).to.be.within(timeoutMS - 100, timeoutMS + 100);
1210+
}
1211+
);
1212+
}
1213+
);
1214+
});
1215+
});
10531216
});

0 commit comments

Comments
 (0)