Skip to content

Commit

Permalink
feat(NODE-6342): support maxTimeMS for explain commands (#4207)
Browse files Browse the repository at this point in the history
  • Loading branch information
baileympearson authored Sep 16, 2024
1 parent 7b71e1f commit 20396e1
Show file tree
Hide file tree
Showing 10 changed files with 410 additions and 20 deletions.
4 changes: 2 additions & 2 deletions src/cursor/aggregation_cursor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Document } from '../bson';
import type { ExplainVerbosityLike } from '../explain';
import type { ExplainCommandOptions, ExplainVerbosityLike } 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 @@ -66,7 +66,7 @@ export class AggregationCursor<TSchema = any> extends AbstractCursor<TSchema> {
}

/** Execute the explain for the cursor */
async explain(verbosity?: ExplainVerbosityLike): Promise<Document> {
async explain(verbosity?: ExplainVerbosityLike | ExplainCommandOptions): Promise<Document> {
return (
await executeOperation(
this.client,
Expand Down
4 changes: 2 additions & 2 deletions src/cursor/find_cursor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type Document } from '../bson';
import { CursorResponse } from '../cmap/wire_protocol/responses';
import { MongoInvalidArgumentError, MongoTailableCursorError } from '../error';
import { type ExplainVerbosityLike } from '../explain';
import { type ExplainCommandOptions, type ExplainVerbosityLike } 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 @@ -133,7 +133,7 @@ export class FindCursor<TSchema = any> extends AbstractCursor<TSchema> {
}

/** Execute the explain for the cursor */
async explain(verbosity?: ExplainVerbosityLike): Promise<Document> {
async explain(verbosity?: ExplainVerbosityLike | ExplainCommandOptions): Promise<Document> {
return (
await executeOperation(
this.client,
Expand Down
55 changes: 46 additions & 9 deletions src/explain.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { MongoInvalidArgumentError } from './error';

/** @public */
export const ExplainVerbosity = Object.freeze({
queryPlanner: 'queryPlanner',
Expand All @@ -19,33 +17,72 @@ export type ExplainVerbosity = string;
export type ExplainVerbosityLike = ExplainVerbosity | boolean;

/** @public */
export interface ExplainCommandOptions {
/** The explain verbosity for the command. */
verbosity: ExplainVerbosity;
/** The maxTimeMS setting for the command. */
maxTimeMS?: number;
}

/**
* @public
*
* When set, this configures an explain command. Valid values are boolean (for legacy compatibility,
* see {@link ExplainVerbosityLike}), a string containing the explain verbosity, or an object containing the verbosity and
* an optional maxTimeMS.
*
* Examples of valid usage:
*
* ```typescript
* collection.find({ name: 'john doe' }, { explain: true });
* collection.find({ name: 'john doe' }, { explain: false });
* collection.find({ name: 'john doe' }, { explain: 'queryPlanner' });
* collection.find({ name: 'john doe' }, { explain: { verbosity: 'queryPlanner' } });
* ```
*
* maxTimeMS can be configured to limit the amount of time the server
* spends executing an explain by providing an object:
*
* ```typescript
* // limits the `explain` command to no more than 2 seconds
* collection.find({ name: 'john doe' }, {
* explain: {
* verbosity: 'queryPlanner',
* maxTimeMS: 2000
* }
* });
* ```
*/
export interface ExplainOptions {
/** Specifies the verbosity mode for the explain output. */
explain?: ExplainVerbosityLike;
explain?: ExplainVerbosityLike | ExplainCommandOptions;
}

/** @internal */
export class Explain {
verbosity: ExplainVerbosity;
readonly verbosity: ExplainVerbosity;
readonly maxTimeMS?: number;

constructor(verbosity: ExplainVerbosityLike) {
private constructor(verbosity: ExplainVerbosityLike, maxTimeMS?: number) {
if (typeof verbosity === 'boolean') {
this.verbosity = verbosity
? ExplainVerbosity.allPlansExecution
: ExplainVerbosity.queryPlanner;
} else {
this.verbosity = verbosity;
}

this.maxTimeMS = maxTimeMS;
}

static fromOptions(options?: ExplainOptions): Explain | undefined {
if (options?.explain == null) return;
static fromOptions({ explain }: ExplainOptions = {}): Explain | undefined {
if (explain == null) return;

const explain = options.explain;
if (typeof explain === 'boolean' || typeof explain === 'string') {
return new Explain(explain);
}

throw new MongoInvalidArgumentError('Field "explain" must be a string or a boolean');
const { verbosity, maxTimeMS } = explain;
return new Explain(verbosity, maxTimeMS);
}
}
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,12 @@ export type { RunCursorCommandOptions } from './cursor/run_command_cursor';
export type { DbOptions, DbPrivate } from './db';
export type { Encrypter, EncrypterOptions } from './encrypter';
export type { AnyError, ErrorDescription, MongoNetworkErrorOptions } from './error';
export type { Explain, ExplainOptions, ExplainVerbosityLike } from './explain';
export type {
Explain,
ExplainCommandOptions,
ExplainOptions,
ExplainVerbosityLike
} from './explain';
export type {
GridFSBucketReadStreamOptions,
GridFSBucketReadStreamOptionsWithRevision,
Expand Down
21 changes: 16 additions & 5 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
MongoParseError,
MongoRuntimeError
} from './error';
import type { Explain } from './explain';
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 @@ -251,12 +251,23 @@ export function decorateWithReadConcern(
* @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): Document {
if (command.explain) {
return command;
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 { explain: command, verbosity: explain.verbosity };
return baseCommand;
}

/**
Expand Down
47 changes: 46 additions & 1 deletion test/integration/crud/crud.prose.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { expect } from 'chai';
import { once } from 'events';

import { MongoBulkWriteError, type MongoClient, MongoServerError } from '../../mongodb';
import { type CommandStartedEvent } from '../../../mongodb';
import {
type Collection,
MongoBulkWriteError,
type MongoClient,
MongoServerError
} from '../../mongodb';
import { filterForCommands } from '../shared';

describe('CRUD Prose Spec Tests', () => {
let client: MongoClient;
Expand Down Expand Up @@ -143,4 +150,42 @@ describe('CRUD Prose Spec Tests', () => {
}
});
});

describe('14. `explain` helpers allow users to specify `maxTimeMS`', function () {
let client: MongoClient;
const commands: CommandStartedEvent[] = [];
let collection: Collection;

beforeEach(async function () {
client = this.configuration.newClient({}, { monitorCommands: true });
await client.connect();

await client.db('explain-test').dropDatabase();
collection = await client.db('explain-test').createCollection('collection');

client.on('commandStarted', filterForCommands('explain', commands));
commands.length = 0;
});

afterEach(async function () {
await client.close();
});

it('sets maxTimeMS on explain commands, when specified', async function () {
await collection
.find(
{ name: 'john doe' },
{
explain: {
maxTimeMS: 2000,
verbosity: 'queryPlanner'
}
}
)
.toArray();

const [{ command }] = commands;
expect(command).to.have.property('maxTimeMS', 2000);
});
});
});
Loading

0 comments on commit 20396e1

Please sign in to comment.