Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(NODE-6421): add support for timeoutMS to explain helpers #4268

Merged
merged 4 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
"mocha": "^10.4.0",
"mocha-sinon": "^2.1.2",
"mongodb-client-encryption": "^6.1.0",
"mongodb-legacy": "^6.1.2",
"mongodb-legacy": "^6.1.3",
"nyc": "^15.1.0",
"prettier": "^3.3.3",
"semver": "^7.6.3",
Expand Down
55 changes: 50 additions & 5 deletions src/cursor/aggregation_cursor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { Document } from '../bson';
import { MongoAPIError } from '../error';
import type { ExplainCommandOptions, ExplainVerbosityLike } from '../explain';
import {
Explain,
type ExplainCommandOptions,
type ExplainVerbosityLike,
validateExplainTimeoutOptions
} from '../explain';
import type { MongoClient } from '../mongo_client';
import { AggregateOperation, type AggregateOptions } from '../operations/aggregate';
import { executeOperation } from '../operations/execute_operation';
Expand Down Expand Up @@ -65,26 +70,66 @@ export class AggregationCursor<TSchema = any> extends AbstractCursor<TSchema> {

/** @internal */
async _initialize(session: ClientSession): Promise<InitialCursorResponse> {
const aggregateOperation = new AggregateOperation(this.namespace, this.pipeline, {
const options = {
...this.aggregateOptions,
...this.cursorOptions,
session
});
};
try {
validateExplainTimeoutOptions(options, Explain.fromOptions(options));
} catch {
throw new MongoAPIError(
'timeoutMS cannot be used with explain when explain is specified in aggregateOptions'
);
}

const aggregateOperation = new AggregateOperation(this.namespace, this.pipeline, options);

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

return { server: aggregateOperation.server, session, response };
}

/** Execute the explain for the cursor */
async explain(verbosity?: ExplainVerbosityLike | ExplainCommandOptions): Promise<Document> {
async explain(): Promise<Document>;
async explain(verbosity: ExplainVerbosityLike | ExplainCommandOptions): Promise<Document>;
async explain(options: { timeoutMS?: number }): Promise<Document>;
async explain(
verbosity: ExplainVerbosityLike | ExplainCommandOptions,
options: { timeoutMS?: number }
): Promise<Document>;
async explain(
verbosity?: ExplainVerbosityLike | ExplainCommandOptions | { timeoutMS?: number },
options?: { timeoutMS?: number }
): Promise<Document> {
let explain: ExplainVerbosityLike | ExplainCommandOptions | undefined;
let timeout: { timeoutMS?: number } | undefined;
if (verbosity == null && options == null) {
explain = true;
timeout = undefined;
} else if (verbosity != null && options == null) {
explain =
typeof verbosity !== 'object'
? verbosity
: 'verbosity' in verbosity
? verbosity
: undefined;

timeout = typeof verbosity === 'object' && 'timeoutMS' in verbosity ? verbosity : undefined;
} else {
// @ts-expect-error TS isn't smart enough to determine that if both options are provided, the first is explain options
explain = verbosity;
timeout = options;
}
durran marked this conversation as resolved.
Show resolved Hide resolved

return (
await executeOperation(
this.client,
new AggregateOperation(this.namespace, this.pipeline, {
...this.aggregateOptions, // NOTE: order matters here, we may need to refine this
...this.cursorOptions,
explain: verbosity ?? true
...timeout,
explain: explain ?? true
})
)
).shift(this.deserializationOptions);
Expand Down
57 changes: 51 additions & 6 deletions src/cursor/find_cursor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { type Document } from '../bson';
import { CursorResponse } from '../cmap/wire_protocol/responses';
import { MongoInvalidArgumentError, MongoTailableCursorError } from '../error';
import { type ExplainCommandOptions, type ExplainVerbosityLike } from '../explain';
import { MongoAPIError, MongoInvalidArgumentError, MongoTailableCursorError } from '../error';
import {
Explain,
type ExplainCommandOptions,
type ExplainVerbosityLike,
validateExplainTimeoutOptions
} from '../explain';
import type { MongoClient } from '../mongo_client';
import type { CollationOptions } from '../operations/command';
import { CountOperation, type CountOptions } from '../operations/count';
Expand Down Expand Up @@ -63,11 +68,21 @@ export class FindCursor<TSchema = any> extends AbstractCursor<TSchema> {

/** @internal */
async _initialize(session: ClientSession): Promise<InitialCursorResponse> {
const findOperation = new FindOperation(this.namespace, this.cursorFilter, {
const options = {
...this.findOptions, // NOTE: order matters here, we may need to refine this
...this.cursorOptions,
session
});
};

try {
validateExplainTimeoutOptions(options, Explain.fromOptions(options));
} catch {
throw new MongoAPIError(
'timeoutMS cannot be used with explain when explain is specified in findOptions'
);
}

const findOperation = new FindOperation(this.namespace, this.cursorFilter, options);

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

Expand Down Expand Up @@ -133,14 +148,44 @@ export class FindCursor<TSchema = any> extends AbstractCursor<TSchema> {
}

/** Execute the explain for the cursor */
async explain(verbosity?: ExplainVerbosityLike | ExplainCommandOptions): Promise<Document> {
async explain(): Promise<Document>;
async explain(verbosity: ExplainVerbosityLike | ExplainCommandOptions): Promise<Document>;
async explain(options: { timeoutMS?: number }): Promise<Document>;
async explain(
verbosity: ExplainVerbosityLike | ExplainCommandOptions,
options: { timeoutMS?: number }
): Promise<Document>;
async explain(
verbosity?: ExplainVerbosityLike | ExplainCommandOptions | { timeoutMS?: number },
options?: { timeoutMS?: number }
): Promise<Document> {
let explain: ExplainVerbosityLike | ExplainCommandOptions | undefined;
let timeout: { timeoutMS?: number } | undefined;
if (verbosity == null && options == null) {
explain = true;
timeout = undefined;
} else if (verbosity != null && options == null) {
explain =
typeof verbosity !== 'object'
? verbosity
: 'verbosity' in verbosity
? verbosity
: undefined;
timeout = typeof verbosity === 'object' && 'timeoutMS' in verbosity ? verbosity : undefined;
} else {
// @ts-expect-error TS isn't smart enough to determine that if both options are provided, the first is explain options
explain = verbosity;
timeout = options;
}
durran marked this conversation as resolved.
Show resolved Hide resolved

return (
await executeOperation(
this.client,
new FindOperation(this.namespace, this.cursorFilter, {
...this.findOptions, // NOTE: order matters here, we may need to refine this
...this.cursorOptions,
explain: verbosity ?? true
...timeout,
explain: explain ?? true
})
)
).shift(this.deserializationOptions);
Expand Down
37 changes: 37 additions & 0 deletions src/explain.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { type Document } from 'bson';
nbbeeken marked this conversation as resolved.
Show resolved Hide resolved

import { MongoAPIError } from './error';

/** @public */
export const ExplainVerbosity = Object.freeze({
queryPlanner: 'queryPlanner',
Expand Down Expand Up @@ -86,3 +90,36 @@ export class Explain {
return new Explain(verbosity, maxTimeMS);
}
}

export function validateExplainTimeoutOptions(options: Document, explain?: Explain) {
const { maxTimeMS, timeoutMS } = options;
if (timeoutMS != null && (maxTimeMS != null || explain?.maxTimeMS != null)) {
throw new MongoAPIError('Cannot use maxTimeMS with timeoutMS for explain commands.');
}
}

/**
* Applies an explain to a given command.
* @internal
*
* @param command - the command on which to apply the explain
* @param options - the options containing the explain verbosity
*/
export function decorateWithExplain(
command: Document,
explain: Explain
): {
explain: Document;
verbosity: ExplainVerbosity;
maxTimeMS?: number;
} {
type ExplainCommand = ReturnType<typeof decorateWithExplain>;
const { verbosity, maxTimeMS } = explain;
const baseCommand: ExplainCommand = { explain: command, verbosity };

if (typeof maxTimeMS === 'number') {
baseCommand.maxTimeMS = maxTimeMS;
}

return baseCommand;
}
15 changes: 8 additions & 7 deletions src/operations/command.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import type { BSONSerializeOptions, Document } from '../bson';
import { type MongoDBResponseConstructor } from '../cmap/wire_protocol/responses';
import { MongoInvalidArgumentError } from '../error';
import { Explain, type ExplainOptions } from '../explain';
import {
decorateWithExplain,
Explain,
type ExplainOptions,
validateExplainTimeoutOptions
} from '../explain';
import { ReadConcern } from '../read_concern';
import type { ReadPreference } from '../read_preference';
import type { Server } from '../sdam/server';
import { MIN_SECONDARY_WRITE_WIRE_VERSION } from '../sdam/server_selection';
import type { ClientSession } from '../sessions';
import { type TimeoutContext } from '../timeout';
import {
commandSupportsReadConcern,
decorateWithExplain,
maxWireVersion,
MongoDBNamespace
} from '../utils';
import { commandSupportsReadConcern, maxWireVersion, MongoDBNamespace } from '../utils';
import { WriteConcern, type WriteConcernOptions } from '../write_concern';
import type { ReadConcernLike } from './../read_concern';
import { AbstractOperation, Aspect, type OperationOptions } from './operation';
Expand Down Expand Up @@ -97,6 +97,7 @@ export abstract class CommandOperation<T> extends AbstractOperation<T> {

if (this.hasAspect(Aspect.EXPLAINABLE)) {
this.explain = Explain.fromOptions(options);
validateExplainTimeoutOptions(this.options, this.explain);
} else if (options?.explain != null) {
throw new MongoInvalidArgumentError(`Option "explain" is not supported on this command`);
}
Expand Down
9 changes: 7 additions & 2 deletions src/operations/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import type { Document } from '../bson';
import { CursorResponse, ExplainedCursorResponse } from '../cmap/wire_protocol/responses';
import { type AbstractCursorOptions, type CursorTimeoutMode } from '../cursor/abstract_cursor';
import { MongoInvalidArgumentError } from '../error';
import { type ExplainOptions } from '../explain';
import {
decorateWithExplain,
type ExplainOptions,
validateExplainTimeoutOptions
} from '../explain';
import { ReadConcern } from '../read_concern';
import type { Server } from '../sdam/server';
import type { ClientSession } from '../sessions';
import { formatSort, type Sort } from '../sort';
import { type TimeoutContext } from '../timeout';
import { decorateWithExplain, type MongoDBNamespace, normalizeHintField } from '../utils';
import { type MongoDBNamespace, normalizeHintField } from '../utils';
import { type CollationOptions, CommandOperation, type CommandOperationOptions } from './command';
import { Aspect, defineAspects, type Hint } from './operation';

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

let findCommand = makeFindCommand(this.ns, this.filter, options);
if (this.explain) {
validateExplainTimeoutOptions(this.options, this.explain);
findCommand = decorateWithExplain(findCommand, this.explain);
}

Expand Down
27 changes: 0 additions & 27 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
MongoParseError,
MongoRuntimeError
} from './error';
import type { Explain, ExplainVerbosity } from './explain';
import type { MongoClient } from './mongo_client';
import type { CommandOperationOptions, OperationParent } from './operations/command';
import type { Hint, OperationOptions } from './operations/operation';
Expand Down Expand Up @@ -245,32 +244,6 @@ export function decorateWithReadConcern(
}
}

/**
* Applies an explain to a given command.
* @internal
*
* @param command - the command on which to apply the explain
* @param options - the options containing the explain verbosity
*/
export function decorateWithExplain(
command: Document,
explain: Explain
): {
explain: Document;
verbosity: ExplainVerbosity;
maxTimeMS?: number;
} {
type ExplainCommand = ReturnType<typeof decorateWithExplain>;
const { verbosity, maxTimeMS } = explain;
const baseCommand: ExplainCommand = { explain: command, verbosity };

if (typeof maxTimeMS === 'number') {
baseCommand.maxTimeMS = maxTimeMS;
}

return baseCommand;
}

/**
* @internal
*/
Expand Down
Loading