Skip to content

Commit 5b2629b

Browse files
baileympearsondariakp
authored andcommitted
feat(NODE-6421): add support for timeoutMS to explain helpers (#4268)
1 parent 8b84eb8 commit 5b2629b

File tree

14 files changed

+625
-62
lines changed

14 files changed

+625
-62
lines changed

package-lock.json

+4-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
"mocha": "^10.4.0",
9898
"mocha-sinon": "^2.1.2",
9999
"mongodb-client-encryption": "^6.1.0",
100-
"mongodb-legacy": "^6.1.2",
100+
"mongodb-legacy": "^6.1.3",
101101
"nyc": "^15.1.0",
102102
"prettier": "^3.3.3",
103103
"semver": "^7.6.3",

src/cursor/aggregation_cursor.ts

+33-7
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import type { Document } from '../bson';
22
import { MongoAPIError } from '../error';
3-
import type { ExplainCommandOptions, ExplainVerbosityLike } from '../explain';
3+
import {
4+
Explain,
5+
ExplainableCursor,
6+
type ExplainCommandOptions,
7+
type ExplainVerbosityLike,
8+
validateExplainTimeoutOptions
9+
} from '../explain';
410
import type { MongoClient } from '../mongo_client';
511
import { AggregateOperation, type AggregateOptions } from '../operations/aggregate';
612
import { executeOperation } from '../operations/execute_operation';
713
import type { ClientSession } from '../sessions';
814
import type { Sort } from '../sort';
915
import { mergeOptions, type MongoDBNamespace } from '../utils';
1016
import {
11-
AbstractCursor,
1217
type AbstractCursorOptions,
1318
CursorTimeoutMode,
1419
type InitialCursorResponse
@@ -24,7 +29,7 @@ export interface AggregationCursorOptions extends AbstractCursorOptions, Aggrega
2429
* or higher stream
2530
* @public
2631
*/
27-
export class AggregationCursor<TSchema = any> extends AbstractCursor<TSchema> {
32+
export class AggregationCursor<TSchema = any> extends ExplainableCursor<TSchema> {
2833
public readonly pipeline: Document[];
2934
/** @internal */
3035
private aggregateOptions: AggregateOptions;
@@ -65,26 +70,47 @@ export class AggregationCursor<TSchema = any> extends AbstractCursor<TSchema> {
6570

6671
/** @internal */
6772
async _initialize(session: ClientSession): Promise<InitialCursorResponse> {
68-
const aggregateOperation = new AggregateOperation(this.namespace, this.pipeline, {
73+
const options = {
6974
...this.aggregateOptions,
7075
...this.cursorOptions,
7176
session
72-
});
77+
};
78+
try {
79+
validateExplainTimeoutOptions(options, Explain.fromOptions(options));
80+
} catch {
81+
throw new MongoAPIError(
82+
'timeoutMS cannot be used with explain when explain is specified in aggregateOptions'
83+
);
84+
}
85+
86+
const aggregateOperation = new AggregateOperation(this.namespace, this.pipeline, options);
7387

7488
const response = await executeOperation(this.client, aggregateOperation, this.timeoutContext);
7589

7690
return { server: aggregateOperation.server, session, response };
7791
}
7892

7993
/** Execute the explain for the cursor */
80-
async explain(verbosity?: ExplainVerbosityLike | ExplainCommandOptions): Promise<Document> {
94+
async explain(): Promise<Document>;
95+
async explain(verbosity: ExplainVerbosityLike | ExplainCommandOptions): Promise<Document>;
96+
async explain(options: { timeoutMS?: number }): Promise<Document>;
97+
async explain(
98+
verbosity: ExplainVerbosityLike | ExplainCommandOptions,
99+
options: { timeoutMS?: number }
100+
): Promise<Document>;
101+
async explain(
102+
verbosity?: ExplainVerbosityLike | ExplainCommandOptions | { timeoutMS?: number },
103+
options?: { timeoutMS?: number }
104+
): Promise<Document> {
105+
const { explain, timeout } = this.resolveExplainTimeoutOptions(verbosity, options);
81106
return (
82107
await executeOperation(
83108
this.client,
84109
new AggregateOperation(this.namespace, this.pipeline, {
85110
...this.aggregateOptions, // NOTE: order matters here, we may need to refine this
86111
...this.cursorOptions,
87-
explain: verbosity ?? true
112+
...timeout,
113+
explain: explain ?? true
88114
})
89115
)
90116
).shift(this.deserializationOptions);

src/cursor/find_cursor.ts

+37-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { type Document } from '../bson';
22
import { CursorResponse } from '../cmap/wire_protocol/responses';
3-
import { MongoInvalidArgumentError, MongoTailableCursorError } from '../error';
4-
import { type ExplainCommandOptions, type ExplainVerbosityLike } from '../explain';
3+
import { MongoAPIError, MongoInvalidArgumentError, MongoTailableCursorError } from '../error';
4+
import {
5+
Explain,
6+
ExplainableCursor,
7+
type ExplainCommandOptions,
8+
type ExplainVerbosityLike,
9+
validateExplainTimeoutOptions
10+
} from '../explain';
511
import type { MongoClient } from '../mongo_client';
612
import type { CollationOptions } from '../operations/command';
713
import { CountOperation, type CountOptions } from '../operations/count';
@@ -11,7 +17,7 @@ import type { Hint } from '../operations/operation';
1117
import type { ClientSession } from '../sessions';
1218
import { formatSort, type Sort, type SortDirection } from '../sort';
1319
import { emitWarningOnce, mergeOptions, type MongoDBNamespace, squashError } from '../utils';
14-
import { AbstractCursor, type InitialCursorResponse } from './abstract_cursor';
20+
import { type InitialCursorResponse } from './abstract_cursor';
1521

1622
/** @public Flags allowed for cursor */
1723
export const FLAGS = [
@@ -24,7 +30,7 @@ export const FLAGS = [
2430
] as const;
2531

2632
/** @public */
27-
export class FindCursor<TSchema = any> extends AbstractCursor<TSchema> {
33+
export class FindCursor<TSchema = any> extends ExplainableCursor<TSchema> {
2834
/** @internal */
2935
private cursorFilter: Document;
3036
/** @internal */
@@ -63,11 +69,21 @@ export class FindCursor<TSchema = any> extends AbstractCursor<TSchema> {
6369

6470
/** @internal */
6571
async _initialize(session: ClientSession): Promise<InitialCursorResponse> {
66-
const findOperation = new FindOperation(this.namespace, this.cursorFilter, {
72+
const options = {
6773
...this.findOptions, // NOTE: order matters here, we may need to refine this
6874
...this.cursorOptions,
6975
session
70-
});
76+
};
77+
78+
try {
79+
validateExplainTimeoutOptions(options, Explain.fromOptions(options));
80+
} catch {
81+
throw new MongoAPIError(
82+
'timeoutMS cannot be used with explain when explain is specified in findOptions'
83+
);
84+
}
85+
86+
const findOperation = new FindOperation(this.namespace, this.cursorFilter, options);
7187

7288
const response = await executeOperation(this.client, findOperation, this.timeoutContext);
7389

@@ -133,14 +149,27 @@ export class FindCursor<TSchema = any> extends AbstractCursor<TSchema> {
133149
}
134150

135151
/** Execute the explain for the cursor */
136-
async explain(verbosity?: ExplainVerbosityLike | ExplainCommandOptions): Promise<Document> {
152+
async explain(): Promise<Document>;
153+
async explain(verbosity: ExplainVerbosityLike | ExplainCommandOptions): Promise<Document>;
154+
async explain(options: { timeoutMS?: number }): Promise<Document>;
155+
async explain(
156+
verbosity: ExplainVerbosityLike | ExplainCommandOptions,
157+
options: { timeoutMS?: number }
158+
): Promise<Document>;
159+
async explain(
160+
verbosity?: ExplainVerbosityLike | ExplainCommandOptions | { timeoutMS?: number },
161+
options?: { timeoutMS?: number }
162+
): Promise<Document> {
163+
const { explain, timeout } = this.resolveExplainTimeoutOptions(verbosity, options);
164+
137165
return (
138166
await executeOperation(
139167
this.client,
140168
new FindOperation(this.namespace, this.cursorFilter, {
141169
...this.findOptions, // NOTE: order matters here, we may need to refine this
142170
...this.cursorOptions,
143-
explain: verbosity ?? true
171+
...timeout,
172+
explain: explain ?? true
144173
})
145174
)
146175
).shift(this.deserializationOptions);

src/explain.ts

+85
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import { type Document } from './bson';
2+
import { AbstractCursor } from './cursor/abstract_cursor';
3+
import { MongoAPIError } from './error';
4+
15
/** @public */
26
export const ExplainVerbosity = Object.freeze({
37
queryPlanner: 'queryPlanner',
@@ -86,3 +90,84 @@ export class Explain {
8690
return new Explain(verbosity, maxTimeMS);
8791
}
8892
}
93+
94+
export function validateExplainTimeoutOptions(options: Document, explain?: Explain) {
95+
const { maxTimeMS, timeoutMS } = options;
96+
if (timeoutMS != null && (maxTimeMS != null || explain?.maxTimeMS != null)) {
97+
throw new MongoAPIError('Cannot use maxTimeMS with timeoutMS for explain commands.');
98+
}
99+
}
100+
101+
/**
102+
* Applies an explain to a given command.
103+
* @internal
104+
*
105+
* @param command - the command on which to apply the explain
106+
* @param options - the options containing the explain verbosity
107+
*/
108+
export function decorateWithExplain(
109+
command: Document,
110+
explain: Explain
111+
): {
112+
explain: Document;
113+
verbosity: ExplainVerbosity;
114+
maxTimeMS?: number;
115+
} {
116+
type ExplainCommand = ReturnType<typeof decorateWithExplain>;
117+
const { verbosity, maxTimeMS } = explain;
118+
const baseCommand: ExplainCommand = { explain: command, verbosity };
119+
120+
if (typeof maxTimeMS === 'number') {
121+
baseCommand.maxTimeMS = maxTimeMS;
122+
}
123+
124+
return baseCommand;
125+
}
126+
127+
/**
128+
* @public
129+
*
130+
* A base class for any cursors that have `explain()` methods.
131+
*/
132+
export abstract class ExplainableCursor<TSchema> extends AbstractCursor<TSchema> {
133+
/** Execute the explain for the cursor */
134+
abstract explain(): Promise<Document>;
135+
abstract explain(verbosity: ExplainVerbosityLike | ExplainCommandOptions): Promise<Document>;
136+
abstract explain(options: { timeoutMS?: number }): Promise<Document>;
137+
abstract explain(
138+
verbosity: ExplainVerbosityLike | ExplainCommandOptions,
139+
options: { timeoutMS?: number }
140+
): Promise<Document>;
141+
abstract explain(
142+
verbosity?: ExplainVerbosityLike | ExplainCommandOptions | { timeoutMS?: number },
143+
options?: { timeoutMS?: number }
144+
): Promise<Document>;
145+
146+
protected resolveExplainTimeoutOptions(
147+
verbosity?: ExplainVerbosityLike | ExplainCommandOptions | { timeoutMS?: number },
148+
options?: { timeoutMS?: number }
149+
): { timeout?: { timeoutMS?: number }; explain?: ExplainVerbosityLike | ExplainCommandOptions } {
150+
let explain: ExplainVerbosityLike | ExplainCommandOptions | undefined;
151+
let timeout: { timeoutMS?: number } | undefined;
152+
153+
if (verbosity == null && options == null) {
154+
explain = undefined;
155+
timeout = undefined;
156+
} else if (verbosity != null && options == null) {
157+
explain =
158+
typeof verbosity !== 'object'
159+
? verbosity
160+
: 'verbosity' in verbosity
161+
? verbosity
162+
: undefined;
163+
164+
timeout = typeof verbosity === 'object' && 'timeoutMS' in verbosity ? verbosity : undefined;
165+
} else {
166+
// @ts-expect-error TS isn't smart enough to determine that if both options are provided, the first is explain options
167+
explain = verbosity;
168+
timeout = options;
169+
}
170+
171+
return { timeout, explain };
172+
}
173+
}

src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { ListCollectionsCursor } from './cursor/list_collections_cursor';
1010
import { ListIndexesCursor } from './cursor/list_indexes_cursor';
1111
import type { RunCommandCursor } from './cursor/run_command_cursor';
1212
import { Db } from './db';
13+
import { ExplainableCursor } from './explain';
1314
import { GridFSBucket } from './gridfs';
1415
import { GridFSBucketReadStream } from './gridfs/download';
1516
import { GridFSBucketWriteStream } from './gridfs/upload';
@@ -91,6 +92,7 @@ export {
9192
ClientSession,
9293
Collection,
9394
Db,
95+
ExplainableCursor,
9496
FindCursor,
9597
GridFSBucket,
9698
GridFSBucketReadStream,

src/operations/command.ts

+8-7
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import type { BSONSerializeOptions, Document } from '../bson';
22
import { type MongoDBResponseConstructor } from '../cmap/wire_protocol/responses';
33
import { MongoInvalidArgumentError } from '../error';
4-
import { Explain, type ExplainOptions } from '../explain';
4+
import {
5+
decorateWithExplain,
6+
Explain,
7+
type ExplainOptions,
8+
validateExplainTimeoutOptions
9+
} from '../explain';
510
import { ReadConcern } from '../read_concern';
611
import type { ReadPreference } from '../read_preference';
712
import type { Server } from '../sdam/server';
813
import { MIN_SECONDARY_WRITE_WIRE_VERSION } from '../sdam/server_selection';
914
import type { ClientSession } from '../sessions';
1015
import { type TimeoutContext } from '../timeout';
11-
import {
12-
commandSupportsReadConcern,
13-
decorateWithExplain,
14-
maxWireVersion,
15-
MongoDBNamespace
16-
} from '../utils';
16+
import { commandSupportsReadConcern, maxWireVersion, MongoDBNamespace } from '../utils';
1717
import { WriteConcern, type WriteConcernOptions } from '../write_concern';
1818
import type { ReadConcernLike } from './../read_concern';
1919
import { AbstractOperation, Aspect, type OperationOptions } from './operation';
@@ -97,6 +97,7 @@ export abstract class CommandOperation<T> extends AbstractOperation<T> {
9797

9898
if (this.hasAspect(Aspect.EXPLAINABLE)) {
9999
this.explain = Explain.fromOptions(options);
100+
validateExplainTimeoutOptions(this.options, this.explain);
100101
} else if (options?.explain != null) {
101102
throw new MongoInvalidArgumentError(`Option "explain" is not supported on this command`);
102103
}

src/operations/find.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ import type { Document } from '../bson';
22
import { CursorResponse, ExplainedCursorResponse } from '../cmap/wire_protocol/responses';
33
import { type AbstractCursorOptions, type CursorTimeoutMode } from '../cursor/abstract_cursor';
44
import { MongoInvalidArgumentError } from '../error';
5-
import { type ExplainOptions } from '../explain';
5+
import {
6+
decorateWithExplain,
7+
type ExplainOptions,
8+
validateExplainTimeoutOptions
9+
} from '../explain';
610
import { ReadConcern } from '../read_concern';
711
import type { Server } from '../sdam/server';
812
import type { ClientSession } from '../sessions';
913
import { formatSort, type Sort } from '../sort';
1014
import { type TimeoutContext } from '../timeout';
11-
import { decorateWithExplain, type MongoDBNamespace, normalizeHintField } from '../utils';
15+
import { type MongoDBNamespace, normalizeHintField } from '../utils';
1216
import { type CollationOptions, CommandOperation, type CommandOperationOptions } from './command';
1317
import { Aspect, defineAspects, type Hint } from './operation';
1418

@@ -119,6 +123,7 @@ export class FindOperation extends CommandOperation<CursorResponse> {
119123

120124
let findCommand = makeFindCommand(this.ns, this.filter, options);
121125
if (this.explain) {
126+
validateExplainTimeoutOptions(this.options, this.explain);
122127
findCommand = decorateWithExplain(findCommand, this.explain);
123128
}
124129

0 commit comments

Comments
 (0)