Skip to content

Commit 94ca4d9

Browse files
authored
feat(shell-api): add maxTimeMS config options MONGOSH-537 (#1068)
1 parent 24bf52b commit 94ca4d9

File tree

10 files changed

+132
-76
lines changed

10 files changed

+132
-76
lines changed

packages/cli-repl/src/cli-repl.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ describe('CliRepl', () => {
200200
it('returns the list of available config options when asked to', () => {
201201
expect(cliRepl.listConfigOptions()).to.deep.equal([
202202
'displayBatchSize',
203+
'maxTimeMS',
203204
'enableTelemetry',
204205
'snippetIndexSourceURLs',
205206
'snippetRegistryURL',

packages/shell-api/src/collection.ts

Lines changed: 49 additions & 50 deletions
Large diffs are not rendered by default.

packages/shell-api/src/database.ts

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ export default class Database extends ShellApiWithMongoClass {
5858
_mongo: Mongo;
5959
_name: string;
6060
_collections: Record<string, Collection>;
61-
_baseOptions: CommandOperationOptions;
6261
_session: Session | undefined;
6362
_cachedCollectionNames: string[] = [];
6463

@@ -68,11 +67,7 @@ export default class Database extends ShellApiWithMongoClass {
6867
this._name = name;
6968
const collections: Record<string, Collection> = {};
7069
this._collections = collections;
71-
this._baseOptions = {};
72-
if (session !== undefined) {
73-
this._session = session;
74-
this._baseOptions.session = session._session;
75-
}
70+
this._session = session;
7671
const proxy = new Proxy(this, {
7772
get: (target, prop): any => {
7873
if (prop in target) {
@@ -97,6 +92,18 @@ export default class Database extends ShellApiWithMongoClass {
9792
return proxy;
9893
}
9994

95+
async _baseOptions(): Promise<CommandOperationOptions> {
96+
const options: CommandOperationOptions = {};
97+
if (this._session) {
98+
options.session = this._session._session;
99+
}
100+
const maxTimeMS = await this._internalState.shellApi.config.get('maxTimeMS');
101+
if (typeof maxTimeMS === 'number') {
102+
options.maxTimeMS = maxTimeMS;
103+
}
104+
return options;
105+
}
106+
100107
/**
101108
* Internal method to determine what is printed for this class.
102109
*/
@@ -126,23 +133,23 @@ export default class Database extends ShellApiWithMongoClass {
126133
return this._mongo._serviceProvider.runCommandWithCheck(
127134
this._name,
128135
cmd,
129-
{ ...this._mongo._getExplicitlyRequestedReadPref(), ...this._baseOptions, ...options }
136+
{ ...this._mongo._getExplicitlyRequestedReadPref(), ...await this._baseOptions(), ...options }
130137
);
131138
}
132139

133140
public async _runAdminCommand(cmd: Document, options: CommandOperationOptions = {}): Promise<Document> {
134141
return this._mongo._serviceProvider.runCommandWithCheck(
135142
ADMIN_DB,
136143
cmd,
137-
{ ...this._mongo._getExplicitlyRequestedReadPref(), ...this._baseOptions, ...options }
144+
{ ...this._mongo._getExplicitlyRequestedReadPref(), ...await this._baseOptions(), ...options }
138145
);
139146
}
140147

141148
private async _listCollections(filter: Document, options: ListCollectionsOptions): Promise<Document[]> {
142149
return await this._mongo._serviceProvider.listCollections(
143150
this._name,
144151
filter,
145-
{ ...this._mongo._getExplicitlyRequestedReadPref(), ...this._baseOptions, ...options }
152+
{ ...this._mongo._getExplicitlyRequestedReadPref(), ...await this._baseOptions(), ...options }
146153
) || [];
147154
}
148155

@@ -202,7 +209,7 @@ export default class Database extends ShellApiWithMongoClass {
202209
return await this._mongo._serviceProvider.runCommand(
203210
this._name,
204211
cmd,
205-
this._baseOptions
212+
await this._baseOptions()
206213
);
207214
} catch (e) {
208215
return e;
@@ -317,7 +324,7 @@ export default class Database extends ShellApiWithMongoClass {
317324
const providerCursor = this._mongo._serviceProvider.aggregateDb(
318325
this._name,
319326
pipeline,
320-
{ ...this._baseOptions, ...aggOptions },
327+
{ ...await this._baseOptions(), ...aggOptions },
321328
dbOptions
322329
);
323330
const cursor = new AggregationCursor(this._mongo, providerCursor);
@@ -362,7 +369,7 @@ export default class Database extends ShellApiWithMongoClass {
362369
async dropDatabase(writeConcern?: WriteConcern): Promise<Document> {
363370
return await this._mongo._serviceProvider.dropDatabase(
364371
this._name,
365-
{ ...this._baseOptions, writeConcern }
372+
{ ...await this._baseOptions(), writeConcern }
366373
);
367374
}
368375

@@ -591,7 +598,7 @@ export default class Database extends ShellApiWithMongoClass {
591598
return await this._mongo._serviceProvider.createCollection(
592599
this._name,
593600
name,
594-
{ ...this._baseOptions, ...options }
601+
{ ...await this._baseOptions(), ...options }
595602
);
596603
}
597604

@@ -601,7 +608,7 @@ export default class Database extends ShellApiWithMongoClass {
601608
assertArgsDefinedType([name, source, pipeline], ['string', 'string', true], 'Database.createView');
602609
this._emitDatabaseApiCall('createView', { name, source, pipeline, options });
603610
const ccOpts = {
604-
...this._baseOptions,
611+
...await this._baseOptions(),
605612
viewOn: source,
606613
pipeline: pipeline
607614
} as Document;
@@ -1032,15 +1039,15 @@ export default class Database extends ShellApiWithMongoClass {
10321039
setFreeMonitoring: 1,
10331040
action: 'enable'
10341041
},
1035-
this._baseOptions
1042+
await this._baseOptions()
10361043
);
10371044
let result: any;
10381045
let error: any;
10391046
try {
10401047
result = await this._mongo._serviceProvider.runCommand(
10411048
ADMIN_DB,
10421049
{ getFreeMonitoringStatus: 1 },
1043-
this._baseOptions
1050+
await this._baseOptions()
10441051
);
10451052
} catch (err) {
10461053
error = err;
@@ -1060,7 +1067,7 @@ export default class Database extends ShellApiWithMongoClass {
10601067
getParameter: 1,
10611068
cloudFreeMonitoringEndpointURL: 1
10621069
},
1063-
this._baseOptions
1070+
await this._baseOptions()
10641071
);
10651072
return `Unable to get immediate response from the Cloud Monitoring service. Please check your firewall settings to ensure that mongod can communicate with '${urlResult.cloudFreeMonitoringEndpointURL || '<unknown>'}'`;
10661073
}
@@ -1419,7 +1426,7 @@ export default class Database extends ShellApiWithMongoClass {
14191426
this._emitDatabaseApiCall('watch', { pipeline, options });
14201427
const cursor = new ChangeStreamCursor(
14211428
this._mongo._serviceProvider.watch(pipeline, {
1422-
...this._baseOptions,
1429+
...await this._baseOptions(),
14231430
...options
14241431
}, {}, this._name),
14251432
this._name,

packages/shell-api/src/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -636,7 +636,7 @@ export async function setHideIndex(coll: Collection, index: string | Document, h
636636
collMod: coll._name,
637637
index: cmd
638638
},
639-
coll._database._baseOptions
639+
await coll._database._baseOptions()
640640
);
641641
}
642642

packages/shell-api/src/integration.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2204,6 +2204,42 @@ describe('Shell API (integration)', function() {
22042204
});
22052205
});
22062206

2207+
describe('maxTimeMS support', () => {
2208+
skipIfServerVersion(testServer, '< 4.2');
2209+
2210+
beforeEach(async() => {
2211+
await collection.insertMany([...Array(10).keys()].map(i => ({ i })));
2212+
const cfg = new ShellUserConfig();
2213+
internalState.setEvaluationListener({
2214+
getConfig(key: string) { return cfg[key]; },
2215+
setConfig(key: string, value: any) { cfg[key] = value; return 'success'; },
2216+
listConfigOptions() { return Object.keys(cfg); }
2217+
});
2218+
});
2219+
2220+
it('config changes affect maxTimeMS', async() => {
2221+
await shellApi.config.set('maxTimeMS', 100);
2222+
try {
2223+
// eslint-disable-next-line no-constant-condition
2224+
await (await collection.find({ $where: function() { while (true); } })).next();
2225+
expect.fail('missed exception');
2226+
} catch (err) {
2227+
expect(err.codeName).to.equal('MaxTimeMSExpired');
2228+
}
2229+
});
2230+
2231+
it('maxTimeMS can be passed explicitly', async() => {
2232+
await shellApi.config.set('maxTimeMS', null);
2233+
try {
2234+
// eslint-disable-next-line no-constant-condition
2235+
await (await collection.find({ $where: function() { while (true); } }, {}, { maxTimeMS: 100 })).next();
2236+
expect.fail('missed exception');
2237+
} catch (err) {
2238+
expect(err.codeName).to.equal('MaxTimeMSExpired');
2239+
}
2240+
});
2241+
});
2242+
22072243
describe('cursor map/forEach', () => {
22082244
beforeEach(async() => {
22092245
await collection.insertMany([...Array(10).keys()].map(i => ({ i })));

packages/shell-api/src/interruptor.spec.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,13 @@ describe('interruptor', () => {
8080

8181
it('causes an interrupt error to be thrown on exit', async() => {
8282
let resolveCall: (result: any) => void;
83-
serviceProvider.runCommandWithCheck.callsFake(() => {
84-
return new Promise(resolve => {
85-
resolveCall = resolve;
86-
});
87-
});
83+
serviceProvider.runCommandWithCheck.resolves(new Promise(resolve => {
84+
resolveCall = resolve;
85+
}));
8886

8987
const runCommand = database.runCommand({ some: 1 });
88+
await new Promise(setImmediate);
89+
await new Promise(setImmediate); // ticks due to db._baseOptions() being async
9090
internalState.interrupted.set();
9191
resolveCall({ ok: 1 });
9292

packages/shell-api/src/shell-api.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,7 @@ describe('ShellApi', () => {
731731
it('will work with defaults', async() => {
732732
expect(await config.get('displayBatchSize')).to.equal(20);
733733
expect((await toShellResult(config)).printable).to.deep.equal(
734-
new Map([['displayBatchSize', 20], ['enableTelemetry', false]] as any));
734+
new Map([['displayBatchSize', 20], ['maxTimeMS', null], ['enableTelemetry', false]] as any));
735735
});
736736

737737
it('rejects setting all config keys', async() => {

packages/shell-api/src/shell-internal-state.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,12 @@ describe('ShellInternalState', () => {
4747
expect(() => run('db = 42')).to.throw("[COMMON-10002] Cannot reassign 'db' to non-Database type");
4848
});
4949

50-
it('allows setting db to a db and causes prefetching', () => {
50+
it('allows setting db to a db and causes prefetching', async() => {
5151
serviceProvider.listCollections
5252
.resolves([ { name: 'coll1' }, { name: 'coll2' } ]);
5353
expect(run('db = db.getSiblingDB("moo"); db.getName()')).to.equal('moo');
54+
await new Promise(setImmediate);
55+
await new Promise(setImmediate); // ticks due to db._baseOptions() being async
5456
expect(serviceProvider.listCollections.calledWith('moo', {}, {
5557
readPreference: 'primaryPreferred',
5658
nameOnly: true

packages/types/src/index.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ describe('config validation', () => {
3131
expect(await validate('displayBatchSize', 0)).to.equal('displayBatchSize must be a positive integer');
3232
expect(await validate('displayBatchSize', 1)).to.equal(null);
3333
expect(await validate('displayBatchSize', Infinity)).to.equal(null);
34+
expect(await validate('maxTimeMS', 'foo')).to.equal('maxTimeMS must be null or a positive integer');
35+
expect(await validate('maxTimeMS', -1)).to.equal('maxTimeMS must be null or a positive integer');
36+
expect(await validate('maxTimeMS', 0)).to.equal('maxTimeMS must be null or a positive integer');
37+
expect(await validate('maxTimeMS', 1)).to.equal(null);
38+
expect(await validate('maxTimeMS', null)).to.equal(null);
3439
expect(await validate('enableTelemetry', 'foo')).to.equal('enableTelemetry must be a boolean');
3540
expect(await validate('enableTelemetry', -1)).to.equal('enableTelemetry must be a boolean');
3641
expect(await validate('enableTelemetry', false)).to.equal(null);

packages/types/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ export interface MongoshBus {
343343

344344
export class ShellUserConfig {
345345
displayBatchSize = 20;
346+
maxTimeMS: number | null = null;
346347
enableTelemetry = false;
347348
}
348349

@@ -355,6 +356,11 @@ export class ShellUserConfigValidator {
355356
return `${key} must be a positive integer`;
356357
}
357358
return null;
359+
case 'maxTimeMS':
360+
if (value !== null && (typeof value !== 'number' || value <= 0)) {
361+
return `${key} must be null or a positive integer`;
362+
}
363+
return null;
358364
case 'enableTelemetry':
359365
if (typeof value !== 'boolean') {
360366
return `${key} must be a boolean`;

0 commit comments

Comments
 (0)