diff --git a/src/cmap/connection.ts b/src/cmap/connection.ts index 7ad367e673..507615e9f0 100644 --- a/src/cmap/connection.ts +++ b/src/cmap/connection.ts @@ -86,6 +86,7 @@ export interface CommandOptions extends BSONSerializeOptions { documentsReturnedIn?: string; noResponse?: boolean; omitReadPreference?: boolean; + omitMaxTimeMS?: boolean; // TODO(NODE-2802): Currently the CommandOptions take a property willRetryWrite which is a hint // from executeOperation that the txnNum should be applied to this command. @@ -421,7 +422,7 @@ export class Connection extends TypedEventEmitter { ...options }; - if (options.timeoutContext?.csotEnabled()) { + if (!options.omitMaxTimeMS && options.timeoutContext?.csotEnabled()) { const { maxTimeMS } = options.timeoutContext; if (maxTimeMS > 0 && Number.isFinite(maxTimeMS)) cmd.maxTimeMS = maxTimeMS; } @@ -621,7 +622,6 @@ export class Connection extends TypedEventEmitter { for await (const document of this.sendCommand(ns, command, options, responseType)) { if (options.timeoutContext?.csotEnabled()) { if (MongoDBResponse.is(document)) { - // TODO(NODE-5684): test coverage to be added once cursors are enabling CSOT if (document.isMaxTimeExpiredError) { throw new MongoOperationTimeoutError('Server reported a timeout error', { cause: new MongoServerError(document.toObject()) diff --git a/src/cmap/wire_protocol/on_data.ts b/src/cmap/wire_protocol/on_data.ts index 23fd88e282..64c636f41f 100644 --- a/src/cmap/wire_protocol/on_data.ts +++ b/src/cmap/wire_protocol/on_data.ts @@ -93,7 +93,6 @@ export function onData( const timeoutForSocketRead = timeoutContext?.timeoutForSocketRead; timeoutForSocketRead?.throwIfExpired(); - // eslint-disable-next-line github/no-then timeoutForSocketRead?.then(undefined, errorHandler); return iterator; diff --git a/src/collection.ts b/src/collection.ts index f3a206b0c7..a73a5276f5 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -11,7 +11,7 @@ import { type ListSearchIndexesOptions } from './cursor/list_search_indexes_cursor'; import type { Db } from './db'; -import { MongoInvalidArgumentError } from './error'; +import { MongoInvalidArgumentError, MongoOperationTimeoutError } from './error'; import type { MongoClient, PkFactory } from './mongo_client'; import type { Filter, @@ -678,7 +678,9 @@ export class Collection { new DropIndexOperation(this as TODO_NODE_3286, '*', resolveOptions(this, options)) ); return true; - } catch { + } catch (error) { + if (error instanceof MongoOperationTimeoutError) throw error; // TODO: Check the spec for index management behaviour/file a drivers ticket for this + // Seems like we should throw all errors return false; } } diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index 51206b51a2..d0f386923a 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -21,6 +21,7 @@ import { ReadPreference, type ReadPreferenceLike } from '../read_preference'; import { type AsyncDisposable, configureResourceManagement } from '../resource_management'; import type { Server } from '../sdam/server'; import { ClientSession, maybeClearPinnedConnection } from '../sessions'; +import { TimeoutContext } from '../timeout'; import { type MongoDBNamespace, squashError } from '../utils'; /** @@ -60,6 +61,17 @@ export interface CursorStreamOptions { /** @public */ export type CursorFlag = (typeof CURSOR_FLAGS)[number]; +/** @public*/ +export const CursorTimeoutMode = Object.freeze({ + ITERATION: 'iteration', + LIFETIME: 'cursorLifetime' +} as const); + +/** @public + * TODO(NODE-5688): Document and release + * */ +export type CursorTimeoutMode = (typeof CursorTimeoutMode)[keyof typeof CursorTimeoutMode]; + /** @public */ export interface AbstractCursorOptions extends BSONSerializeOptions { session?: ClientSession; @@ -105,6 +117,8 @@ export interface AbstractCursorOptions extends BSONSerializeOptions { noCursorTimeout?: boolean; /** @internal TODO(NODE-5688): make this public */ timeoutMS?: number; + /** @internal TODO(NODE-5688): make this public */ + timeoutMode?: CursorTimeoutMode; } /** @internal */ @@ -117,6 +131,8 @@ export type InternalAbstractCursorOptions = Omit { - await this.cleanup(); + async close(options?: { timeoutMS?: number }): Promise { + await this.cleanup(options?.timeoutMS); } /** @@ -658,6 +727,8 @@ export abstract class AbstractCursor< this.cursorId = null; this.documents?.clear(); + this.timeoutContext?.clear(); + this.timeoutContext = undefined; this.isClosed = false; this.isKilled = false; this.initialized = false; @@ -707,7 +778,7 @@ export abstract class AbstractCursor< } ); - return await executeOperation(this.cursorClient, getMoreOperation); + return await executeOperation(this.cursorClient, getMoreOperation, this.timeoutContext); } /** @@ -718,6 +789,12 @@ export abstract class AbstractCursor< * a significant refactor. */ private async cursorInit(): Promise { + if (this.cursorOptions.timeoutMS != null) { + this.timeoutContext = TimeoutContext.create({ + serverSelectionTimeoutMS: this.client.options.serverSelectionTimeoutMS, + timeoutMS: this.cursorOptions.timeoutMS + }); + } try { const state = await this._initialize(this.cursorSession); const response = state.response; @@ -729,7 +806,7 @@ export abstract class AbstractCursor< } catch (error) { // the cursor is now initialized, even if an error occurred this.initialized = true; - await this.cleanup(error); + await this.cleanup(undefined, error); throw error; } @@ -763,6 +840,7 @@ export abstract class AbstractCursor< // otherwise need to call getMore const batchSize = this.cursorOptions.batchSize || 1000; + this.cursorOptions.omitMaxTimeMS = this.cursorOptions.timeoutMS != null; try { const response = await this.getMore(batchSize); @@ -770,7 +848,7 @@ export abstract class AbstractCursor< this.documents = response; } catch (error) { try { - await this.cleanup(error); + await this.cleanup(undefined, error); } catch (error) { // `cleanupCursor` should never throw, squash and throw the original error squashError(error); @@ -791,7 +869,7 @@ export abstract class AbstractCursor< } /** @internal */ - private async cleanup(error?: Error) { + private async cleanup(timeoutMS?: number, error?: Error) { this.isClosed = true; const session = this.cursorSession; try { @@ -806,11 +884,23 @@ export abstract class AbstractCursor< this.isKilled = true; const cursorId = this.cursorId; this.cursorId = Long.ZERO; + let timeoutContext: TimeoutContext | undefined; + if (timeoutMS != null) { + this.timeoutContext?.clear(); + timeoutContext = TimeoutContext.create({ + serverSelectionTimeoutMS: this.client.options.serverSelectionTimeoutMS, + timeoutMS + }); + } else { + this.timeoutContext?.refresh(); + timeoutContext = this.timeoutContext; + } await executeOperation( this.cursorClient, new KillCursorsOperation(cursorId, this.cursorNamespace, this.selectedServer, { session - }) + }), + timeoutContext ); } } catch (error) { diff --git a/src/cursor/aggregation_cursor.ts b/src/cursor/aggregation_cursor.ts index 9762c8a03b..056f28454c 100644 --- a/src/cursor/aggregation_cursor.ts +++ b/src/cursor/aggregation_cursor.ts @@ -1,4 +1,5 @@ import type { Document } from '../bson'; +import { MongoAPIError } from '../error'; import type { ExplainCommandOptions, ExplainVerbosityLike } from '../explain'; import type { MongoClient } from '../mongo_client'; import { AggregateOperation, type AggregateOptions } from '../operations/aggregate'; @@ -9,6 +10,7 @@ import { mergeOptions, type MongoDBNamespace } from '../utils'; import { AbstractCursor, type AbstractCursorOptions, + CursorTimeoutMode, type InitialCursorResponse } from './abstract_cursor'; @@ -38,6 +40,15 @@ export class AggregationCursor extends AbstractCursor { this.pipeline = pipeline; this.aggregateOptions = options; + + const lastStage: Document | undefined = this.pipeline[this.pipeline.length - 1]; + + if ( + this.cursorOptions.timeoutMS != null && + this.cursorOptions.timeoutMode === CursorTimeoutMode.ITERATION && + (lastStage?.$merge != null || lastStage?.$out != null) + ) + throw new MongoAPIError('Cannot use $out or $merge stage with ITERATION timeoutMode'); } clone(): AggregationCursor { @@ -60,7 +71,7 @@ export class AggregationCursor extends AbstractCursor { session }); - const response = await executeOperation(this.client, aggregateOperation); + const response = await executeOperation(this.client, aggregateOperation, this.timeoutContext); return { server: aggregateOperation.server, session, response }; } @@ -95,6 +106,13 @@ export class AggregationCursor extends AbstractCursor { addStage(stage: Document): AggregationCursor; addStage(stage: Document): AggregationCursor { this.throwIfInitialized(); + if ( + this.cursorOptions.timeoutMS != null && + this.cursorOptions.timeoutMode === CursorTimeoutMode.ITERATION && + (stage.$out != null || stage.$merge != null) + ) { + throw new MongoAPIError('Cannot use $out or $merge stage with ITERATION timeoutMode'); + } this.pipeline.push(stage); return this as unknown as AggregationCursor; } diff --git a/src/cursor/change_stream_cursor.ts b/src/cursor/change_stream_cursor.ts index b42ce3e130..13f5867555 100644 --- a/src/cursor/change_stream_cursor.ts +++ b/src/cursor/change_stream_cursor.ts @@ -133,7 +133,11 @@ export class ChangeStreamCursor< session }); - const response = await executeOperation(session.client, aggregateOperation); + const response = await executeOperation( + session.client, + aggregateOperation, + this.timeoutContext + ); const server = aggregateOperation.server; this.maxWireVersion = maxWireVersion(server); diff --git a/src/cursor/find_cursor.ts b/src/cursor/find_cursor.ts index 83a12818bd..96b764dc7f 100644 --- a/src/cursor/find_cursor.ts +++ b/src/cursor/find_cursor.ts @@ -69,7 +69,7 @@ export class FindCursor extends AbstractCursor { session }); - const response = await executeOperation(this.client, findOperation); + const response = await executeOperation(this.client, findOperation, this.timeoutContext); // the response is not a cursor when `explain` is enabled this.numReturned = response.batchSize; diff --git a/src/cursor/list_collections_cursor.ts b/src/cursor/list_collections_cursor.ts index a529709556..9b69de1b93 100644 --- a/src/cursor/list_collections_cursor.ts +++ b/src/cursor/list_collections_cursor.ts @@ -41,7 +41,7 @@ export class ListCollectionsCursor< session }); - const response = await executeOperation(this.parent.client, operation); + const response = await executeOperation(this.parent.client, operation, this.timeoutContext); return { server: operation.server, session, response }; } diff --git a/src/cursor/list_indexes_cursor.ts b/src/cursor/list_indexes_cursor.ts index 799ddf5bdb..0f768f3b69 100644 --- a/src/cursor/list_indexes_cursor.ts +++ b/src/cursor/list_indexes_cursor.ts @@ -30,7 +30,7 @@ export class ListIndexesCursor extends AbstractCursor { session }); - const response = await executeOperation(this.parent.client, operation); + const response = await executeOperation(this.parent.client, operation, this.timeoutContext); return { server: operation.server, session, response }; } diff --git a/src/cursor/run_command_cursor.ts b/src/cursor/run_command_cursor.ts index 78b9826b9b..6b31ce2263 100644 --- a/src/cursor/run_command_cursor.ts +++ b/src/cursor/run_command_cursor.ts @@ -9,12 +9,20 @@ import type { ReadConcernLike } from '../read_concern'; import type { ReadPreferenceLike } from '../read_preference'; import type { ClientSession } from '../sessions'; import { ns } from '../utils'; -import { AbstractCursor, type InitialCursorResponse } from './abstract_cursor'; +import { + AbstractCursor, + type CursorTimeoutMode, + type InitialCursorResponse +} from './abstract_cursor'; /** @public */ export type RunCursorCommandOptions = { readPreference?: ReadPreferenceLike; session?: ClientSession; + /** @internal */ + timeoutMS?: number; + /** @internal */ + timeoutMode?: CursorTimeoutMode; } & BSONSerializeOptions; /** @public */ @@ -105,7 +113,7 @@ export class RunCommandCursor extends AbstractCursor { responseType: CursorResponse }); - const response = await executeOperation(this.client, operation); + const response = await executeOperation(this.client, operation, this.timeoutContext); return { server: operation.server, @@ -123,6 +131,6 @@ export class RunCommandCursor extends AbstractCursor { ...this.getMoreOptions }); - return await executeOperation(this.client, getMoreOperation); + return await executeOperation(this.client, getMoreOperation, this.timeoutContext); } } diff --git a/src/index.ts b/src/index.ts index 693fcf0349..e555d97e9e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -109,7 +109,7 @@ export { AutoEncryptionLoggerLevel } from './client-side-encryption/auto_encrypt export { GSSAPICanonicalizationValue } from './cmap/auth/gssapi'; export { AuthMechanism } from './cmap/auth/providers'; export { Compressor } from './cmap/wire_protocol/compression'; -export { CURSOR_FLAGS } from './cursor/abstract_cursor'; +export { CURSOR_FLAGS, type CursorTimeoutMode } from './cursor/abstract_cursor'; export { MongoErrorLabel } from './error'; export { ExplainVerbosity } from './explain'; export { ServerApiVersion } from './mongo_client'; diff --git a/src/operations/aggregate.ts b/src/operations/aggregate.ts index 50494cbba7..096fe37271 100644 --- a/src/operations/aggregate.ts +++ b/src/operations/aggregate.ts @@ -1,5 +1,6 @@ import type { Document } from '../bson'; import { CursorResponse, ExplainedCursorResponse } from '../cmap/wire_protocol/responses'; +import { type CursorTimeoutMode } from '../cursor/abstract_cursor'; import { MongoInvalidArgumentError } from '../error'; import type { Server } from '../sdam/server'; import type { ClientSession } from '../sessions'; @@ -36,6 +37,9 @@ export interface AggregateOptions extends CommandOperationOptions { let?: Document; out?: string; + + /** @internal */ + timeoutMode?: CursorTimeoutMode; } /** @internal */ diff --git a/src/operations/execute_operation.ts b/src/operations/execute_operation.ts index 2523058ecf..f59df27569 100644 --- a/src/operations/execute_operation.ts +++ b/src/operations/execute_operation.ts @@ -288,8 +288,7 @@ async function tryOperation< previousOperationError = operationError; // Reset timeouts - timeoutContext.serverSelectionTimeout?.clear(); - timeoutContext.connectionCheckoutTimeout?.clear(); + timeoutContext.clear(); } } diff --git a/src/operations/find.ts b/src/operations/find.ts index 5f359324d5..c39695cc0b 100644 --- a/src/operations/find.ts +++ b/src/operations/find.ts @@ -1,5 +1,6 @@ import type { Document } from '../bson'; import { CursorResponse, ExplainedCursorResponse } from '../cmap/wire_protocol/responses'; +import { type CursorTimeoutMode } from '../cursor/abstract_cursor'; import { MongoInvalidArgumentError } from '../error'; import { ReadConcern } from '../read_concern'; import type { Server } from '../sdam/server'; @@ -64,6 +65,9 @@ export interface FindOptions * @deprecated Starting from MongoDB 4.4 this flag is not needed and will be ignored. */ oplogReplay?: boolean; + + /** @internal*/ + timeoutMode?: CursorTimeoutMode; } /** @internal */ diff --git a/src/operations/indexes.ts b/src/operations/indexes.ts index c96a5d7345..220d438d83 100644 --- a/src/operations/indexes.ts +++ b/src/operations/indexes.ts @@ -1,7 +1,7 @@ import type { Document } from '../bson'; import { CursorResponse } from '../cmap/wire_protocol/responses'; import type { Collection } from '../collection'; -import { type AbstractCursorOptions } from '../cursor/abstract_cursor'; +import { type AbstractCursorOptions, type CursorTimeoutMode } from '../cursor/abstract_cursor'; import { MongoCompatibilityError } from '../error'; import { type OneOrMore } from '../mongo_types'; import type { Server } from '../sdam/server'; @@ -360,7 +360,12 @@ export class DropIndexOperation extends CommandOperation { } /** @public */ -export type ListIndexesOptions = AbstractCursorOptions; +export type ListIndexesOptions = AbstractCursorOptions & { + /** @internal TODO(NODE-5688): make this public */ + timeoutMode?: CursorTimeoutMode; + /** @internal */ + omitMaxTimeMS?: boolean; +}; /** @internal */ export class ListIndexesOperation extends CommandOperation { diff --git a/src/operations/list_collections.ts b/src/operations/list_collections.ts index 702db0fe3f..50df243a3f 100644 --- a/src/operations/list_collections.ts +++ b/src/operations/list_collections.ts @@ -1,5 +1,6 @@ import type { Binary, Document } from '../bson'; import { CursorResponse } from '../cmap/wire_protocol/responses'; +import { type CursorTimeoutMode } from '../cursor/abstract_cursor'; import type { Db } from '../db'; import type { Server } from '../sdam/server'; import type { ClientSession } from '../sessions'; @@ -16,6 +17,8 @@ export interface ListCollectionsOptions extends Omit { public readonly start: number; public ended: number | null = null; public duration: number; - public timedOut = false; + private timedOut = false; public cleared = false; get remainingTime(): number { @@ -100,6 +100,7 @@ export class Timeout extends Promise { clear(): void { clearTimeout(this.id); this.id = undefined; + this.timedOut = false; this.cleared = true; } @@ -190,6 +191,10 @@ export abstract class TimeoutContext { abstract get timeoutForSocketRead(): Timeout | null; abstract csotEnabled(): this is CSOTTimeoutContext; + + abstract refresh(): void; + + abstract clear(): void; } /** @internal */ @@ -288,6 +293,18 @@ export class CSOTTimeoutContext extends TimeoutContext { if (remainingTimeMS > 0) return Timeout.expires(remainingTimeMS); return Timeout.reject(new MongoOperationTimeoutError('Timed out before socket read')); } + + refresh(): void { + this.start = Math.trunc(performance.now()); + this.minRoundTripTime = 0; + this._serverSelectionTimeout?.clear(); + this._connectionCheckoutTimeout?.clear(); + } + + clear(): void { + this._serverSelectionTimeout?.clear(); + this._connectionCheckoutTimeout?.clear(); + } } /** @internal */ @@ -326,4 +343,12 @@ export class LegacyTimeoutContext extends TimeoutContext { get timeoutForSocketRead(): Timeout | null { return null; } + + refresh(): void { + return; + } + + clear(): void { + return; + } } diff --git a/test/integration/client-side-operations-timeout/client_side_operations_timeout.prose.test.ts b/test/integration/client-side-operations-timeout/client_side_operations_timeout.prose.test.ts index 406aa53ed6..0d36998fd9 100644 --- a/test/integration/client-side-operations-timeout/client_side_operations_timeout.prose.test.ts +++ b/test/integration/client-side-operations-timeout/client_side_operations_timeout.prose.test.ts @@ -4,7 +4,9 @@ import { expect } from 'chai'; import * as semver from 'semver'; import * as sinon from 'sinon'; +import { type CommandStartedEvent } from '../../../mongodb'; import { + type CommandSucceededEvent, MongoClient, MongoOperationTimeoutError, MongoServerSelectionError, @@ -216,12 +218,52 @@ describe('CSOT spec prose tests', function () { }); }); - context.skip('5. Blocking Iteration Methods', () => { + context('5. Blocking Iteration Methods', () => { /** * Tests in this section MUST only be run against server versions 4.4 and higher and only apply to drivers that have a * blocking method for cursor iteration that executes `getMore` commands in a loop until a document is available or an * error occurs. */ + const failpoint: FailPoint = { + configureFailPoint: 'failCommand', + mode: 'alwaysOn', + data: { + failCommands: ['getMore'], + blockConnection: true, + blockTimeMS: 20 + } + }; + let internalClient: MongoClient; + let client: MongoClient; + let commandStarted: CommandStartedEvent[]; + let commandSucceeded: CommandSucceededEvent[]; + + beforeEach(async function () { + internalClient = this.configuration.newClient(); + await internalClient.db('db').dropCollection('coll'); + // Creating capped collection to be able to create tailable find cursor + const coll = await internalClient + .db('db') + .createCollection('coll', { capped: true, size: 1_000_000 }); + await coll.insertOne({ x: 1 }); + await internalClient.db().admin().command(failpoint); + + client = this.configuration.newClient(undefined, { timeoutMS: 20, monitorCommands: true }); + commandStarted = []; + commandSucceeded = []; + + client.on('commandStarted', ev => commandStarted.push(ev)); + client.on('commandSucceeded', ev => commandSucceeded.push(ev)); + }); + + afterEach(async function () { + await internalClient + .db() + .admin() + .command({ ...failpoint, mode: 'off' }); + await internalClient.close(); + await client.close(); + }); context('Tailable cursors', () => { /** @@ -248,6 +290,29 @@ describe('CSOT spec prose tests', function () { * - Expect this to fail with a timeout error. * 1. Verify that a `find` command and two `getMore` commands were executed against the `db.coll` collection during the test. */ + + it.skip('send correct number of finds and getMores', async function () { + const cursor = client + .db('db') + .collection('coll') + .find({}, { tailable: true, awaitData: true }) + .project({ _id: 0 }); + const doc = await cursor.next(); + expect(doc).to.deep.equal({ x: 1 }); + // Check that there are no getMores sent + expect(commandStarted.filter(e => e.command.getMore != null)).to.have.lengthOf(0); + + const maybeError = await cursor.next().then( + () => null, + e => e + ); + + expect(maybeError).to.be.instanceof(MongoOperationTimeoutError); + // Expect 1 find + expect(commandStarted.filter(e => e.command.find != null)).to.have.lengthOf(1); + // Expect 2 getMore + expect(commandStarted.filter(e => e.command.getMore != null)).to.have.lengthOf(2); + }).skipReason = 'TODO(NODE-6305)'; }); context('Change Streams', () => { @@ -272,6 +337,23 @@ describe('CSOT spec prose tests', function () { * - Expect this to fail with a timeout error. * 1. Verify that an `aggregate` command and two `getMore` commands were executed against the `db.coll` collection during the test. */ + it.skip('sends correct number of aggregate and getMores', async function () { + const changeStream = client.db('db').collection('coll').watch(); + const maybeError = await changeStream.next().then( + () => null, + e => e + ); + + expect(maybeError).to.be.instanceof(MongoOperationTimeoutError); + const aggregates = commandStarted + .filter(e => e.command.aggregate != null) + .map(e => e.command); + const getMores = commandStarted.filter(e => e.command.getMore != null).map(e => e.command); + // Expect 1 aggregate + expect(aggregates).to.have.lengthOf(1); + // Expect 1 getMore + expect(getMores).to.have.lengthOf(1); + }).skipReason = 'TODO(NODE-6305)'; }); }); diff --git a/test/integration/client-side-operations-timeout/client_side_operations_timeout.spec.test.ts b/test/integration/client-side-operations-timeout/client_side_operations_timeout.spec.test.ts index a178cecc5d..99914fa08e 100644 --- a/test/integration/client-side-operations-timeout/client_side_operations_timeout.spec.test.ts +++ b/test/integration/client-side-operations-timeout/client_side_operations_timeout.spec.test.ts @@ -4,49 +4,55 @@ import * as semver from 'semver'; import { loadSpecTests } from '../../spec'; import { runUnifiedSuite } from '../../tools/unified-spec-runner/runner'; -const enabled = [ - 'override-collection-timeoutMS', - 'override-database-timeoutMS', - 'override-operation-timeoutMS', - 'retryability-legacy-timeouts', - 'retryability-timeoutMS', - 'sessions-override-operation-timeoutMS', - 'sessions-override-timeoutMS', - 'sessions-inherit-timeoutMS' -]; +const skippedSpecs = { + bulkWrite: 'TODO(NODE-6274)', + 'change-streams': 'TODO(NODE-6035)', + 'convenient-transactions': 'TODO(NODE-5687)', + 'deprecated-options': 'TODO(NODE-5689)', + 'gridfs-advanced': 'TODO(NODE-6275)', + 'gridfs-delete': 'TODO(NODE-6275)', + 'gridfs-download': 'TODO(NODE-6275)', + 'gridfs-find': 'TODO(NODE-6275)', + 'gridfs-upload': 'TODO(NODE-6275)', + 'tailable-awaitData': 'TODO(NODE-6035)', + 'tailable-non-awaitData': 'TODO(NODE-6035)' +}; -const cursorOperations = [ - 'aggregate', - 'countDocuments', - 'listIndexes', - 'createChangeStream', - 'listCollections', - 'listCollectionNames' -]; - -const bulkWriteOperations = [ - 'timeoutMS applies to whole operation, not individual attempts - bulkWrite on collection', - 'timeoutMS applies to whole operation, not individual attempts - insertMany on collection' -]; +const skippedTests = { + 'timeoutMS can be configured on a MongoClient - insertMany on collection': 'TODO(NODE-6274)', + 'timeoutMS can be configured on a MongoClient - bulkWrite on collection': 'TODO(NODE-6274)', + 'timeoutMS can be configured on a MongoClient - createChangeStream on client': 'TODO(NODE-6305)', + 'timeoutMS applies to whole operation, not individual attempts - createChangeStream on client': + 'TODO(NODE-6305)', + 'Tailable cursor iteration timeoutMS is refreshed for getMore - failure': 'TODO(NODE-6305)', + 'Tailable cursor awaitData iteration timeoutMS is refreshed for getMore - failure': + 'TODO(NODE-6305)', + 'timeoutMS applies to whole operation, not individual attempts - insertMany on collection': + 'TODO(NODE-6274)', + 'timeoutMS applies to whole operation, not individual attempts - bulkWrite on collection': + 'TODO(NODE-6274)', + 'command is not sent if RTT is greater than timeoutMS': 'TODO(DRIVERS-2965)', + 'Non=tailable cursor iteration timeoutMS is refreshed for getMore if timeoutMode is iteration - failure': + 'TODO(DRIVERS-2965)', + 'Non-tailable cursor lifetime remaining timeoutMS applied to getMore if timeoutMode is unset': + 'TODO(DRIVERS-2965)', + 'maxTimeMS value in the command is less than timeoutMS': + 'TODO(DRIVERS-2970): see modified test in unified-csot-node-specs' +}; describe('CSOT spec tests', function () { - const specs = loadSpecTests(join('client-side-operations-timeout')); + const specs = loadSpecTests('client-side-operations-timeout'); for (const spec of specs) { for (const test of spec.tests) { - // not one of the test suites listed in kickoff - if (!enabled.includes(spec.name)) { - test.skipReason = 'TODO(NODE-5684): Not working yet'; + if (skippedSpecs[spec.name] != null) { + test.skipReason = skippedSpecs[spec.name]; + } + if (skippedTests[test.description] != null) { + test.skipReason = skippedTests[test.description]; } - - // Cursor operation - if (test.operations.find(operation => cursorOperations.includes(operation.name))) - test.skipReason = 'TODO(NODE-5684): Not working yet'; - - if (bulkWriteOperations.includes(test.description)) - test.skipReason = - 'TODO(NODE-6274): update test runner to check errorResponse field of MongoBulkWriteError in isTimeoutError assertion'; } } + runUnifiedSuite(specs, (test, configuration) => { const sessionCSOTTests = ['timeoutMS applied to withTransaction']; if ( @@ -59,3 +65,10 @@ describe('CSOT spec tests', function () { return false; }); }); + +describe('CSOT modified spec tests', function () { + const specs = loadSpecTests( + join('..', 'integration', 'client-side-operations-timeout', 'unified-csot-node-specs') + ); + runUnifiedSuite(specs); +}); diff --git a/test/integration/client-side-operations-timeout/node_csot.test.ts b/test/integration/client-side-operations-timeout/node_csot.test.ts index cc767c1d80..f5ada7eef9 100644 --- a/test/integration/client-side-operations-timeout/node_csot.test.ts +++ b/test/integration/client-side-operations-timeout/node_csot.test.ts @@ -1,4 +1,6 @@ /* Anything javascript specific relating to timeouts */ +import { setTimeout } from 'node:timers/promises'; + import { expect } from 'chai'; import * as semver from 'semver'; import * as sinon from 'sinon'; @@ -7,6 +9,9 @@ import { BSON, type ClientSession, type Collection, + type CommandFailedEvent, + type CommandStartedEvent, + type CommandSucceededEvent, Connection, type Db, type FindCursor, @@ -18,7 +23,9 @@ import { } from '../../mongodb'; import { type FailPoint } from '../../tools/utils'; -describe('CSOT driver tests', { requires: { mongodb: '>=4.4' } }, () => { +const metadata = { requires: { mongodb: '>=4.4' } }; + +describe('CSOT driver tests', metadata, () => { describe('timeoutMS inheritance', () => { let client: MongoClient; let db: Db; @@ -171,8 +178,8 @@ describe('CSOT driver tests', { requires: { mongodb: '>=4.4' } }, () => { describe('server-side maxTimeMS errors are transformed', () => { let client: MongoClient; - let commandsSucceeded; - let commandsFailed; + let commandsSucceeded: CommandSucceededEvent[]; + let commandsFailed: CommandFailedEvent[]; beforeEach(async function () { client = this.configuration.newClient({ timeoutMS: 500_000, monitorCommands: true }); @@ -221,18 +228,22 @@ describe('CSOT driver tests', { requires: { mongodb: '>=4.4' } }, () => { await client.db('admin').command({ ...failpoint, mode: 'off' }); }); - it('throws a MongoOperationTimeoutError error and emits command failed', async () => { - const error = await client - .db() - .command({ ping: 1 }) - .catch(error => error); - expect(error).to.be.instanceOf(MongoOperationTimeoutError); - expect(error.cause).to.be.instanceOf(MongoServerError); - expect(error.cause).to.have.property('code', 50); - - expect(commandsFailed).to.have.lengthOf(1); - expect(commandsFailed).to.have.nested.property('[0].failure.cause.code', 50); - }); + it( + 'throws a MongoOperationTimeoutError error and emits command failed', + metadata, + async () => { + const error = await client + .db() + .command({ ping: 1 }) + .catch(error => error); + expect(error).to.be.instanceOf(MongoOperationTimeoutError); + expect(error.cause).to.be.instanceOf(MongoServerError); + expect(error.cause).to.have.property('code', 50); + + expect(commandsFailed).to.have.lengthOf(1); + expect(commandsFailed).to.have.nested.property('[0].failure.cause.code', 50); + } + ); }); describe('when a maxTimeExpired error is returned inside a writeErrors array', () => { @@ -267,18 +278,22 @@ describe('CSOT driver tests', { requires: { mongodb: '>=4.4' } }, () => { afterEach(() => sinon.restore()); - it('throws a MongoOperationTimeoutError error and emits command succeeded', async () => { - const error = await client - .db('admin') - .command({ giveMeWriteErrors: 1 }) - .catch(error => error); - expect(error).to.be.instanceOf(MongoOperationTimeoutError); - expect(error.cause).to.be.instanceOf(MongoServerError); - expect(error.cause).to.have.nested.property('writeErrors[3].code', 50); - - expect(commandsSucceeded).to.have.lengthOf(1); - expect(commandsSucceeded).to.have.nested.property('[0].reply.writeErrors[3].code', 50); - }); + it( + 'throws a MongoOperationTimeoutError error and emits command succeeded', + metadata, + async () => { + const error = await client + .db('admin') + .command({ giveMeWriteErrors: 1 }) + .catch(error => error); + expect(error).to.be.instanceOf(MongoOperationTimeoutError); + expect(error.cause).to.be.instanceOf(MongoServerError); + expect(error.cause).to.have.nested.property('writeErrors[3].code', 50); + + expect(commandsSucceeded).to.have.lengthOf(1); + expect(commandsSucceeded).to.have.nested.property('[0].reply.writeErrors[3].code', 50); + } + ); }); describe('when a maxTimeExpired error is returned inside a writeConcernError embedded document', () => { @@ -306,22 +321,266 @@ describe('CSOT driver tests', { requires: { mongodb: '>=4.4' } }, () => { await client.db('admin').command({ ...failpoint, mode: 'off' }); }); - it('throws a MongoOperationTimeoutError error and emits command succeeded', async () => { - const error = await client + it( + 'throws a MongoOperationTimeoutError error and emits command succeeded', + metadata, + async () => { + const error = await client + .db() + .collection('a') + .insertOne({}) + .catch(error => error); + expect(error).to.be.instanceOf(MongoOperationTimeoutError); + expect(error.cause).to.be.instanceOf(MongoServerError); + expect(error.cause).to.have.nested.property('writeConcernError.code', 50); + + expect(commandsSucceeded).to.have.lengthOf(1); + expect(commandsSucceeded).to.have.nested.property('[0].reply.writeConcernError.code', 50); + } + ); + }); + }); + + describe('Non-Tailable cursors', () => { + let client: MongoClient; + let internalClient: MongoClient; + let commandStarted: CommandStartedEvent[]; + let commandSucceeded: CommandSucceededEvent[]; + const failpoint: FailPoint = { + configureFailPoint: 'failCommand', + mode: 'alwaysOn', + data: { + failCommands: ['find', 'getMore'], + blockConnection: true, + blockTimeMS: 50 + } + }; + + beforeEach(async function () { + internalClient = this.configuration.newClient(); + await internalClient + .db('db') + .dropCollection('coll') + .catch(() => null); + await internalClient + .db('db') + .collection('coll') + .insertMany( + Array.from({ length: 3 }, () => { + return { x: 1 }; + }) + ); + + await internalClient.db().admin().command(failpoint); + + client = this.configuration.newClient(undefined, { monitorCommands: true }); + commandStarted = []; + commandSucceeded = []; + client.on('commandStarted', ev => commandStarted.push(ev)); + client.on('commandSucceeded', ev => commandSucceeded.push(ev)); + }); + + afterEach(async function () { + await internalClient + .db() + .admin() + .command({ ...failpoint, mode: 'off' }); + await internalClient.close(); + await client.close(); + }); + + context('ITERATION mode', () => { + context('when executing an operation', () => { + it( + 'must apply the configured timeoutMS to the initial operation execution', + metadata, + async function () { + const cursor = client + .db('db') + .collection('coll') + .find({}, { batchSize: 3, timeoutMode: 'iteration', timeoutMS: 10 }) + .limit(3); + + const maybeError = await cursor.next().then( + () => null, + e => e + ); + + expect(maybeError).to.be.instanceOf(MongoOperationTimeoutError); + } + ); + + it('refreshes the timeout for any getMores', metadata, async function () { + const cursor = client + .db('db') + .collection('coll') + .find({}, { batchSize: 1, timeoutMode: 'iteration', timeoutMS: 100 }) + .project({ _id: 0 }); + + // Iterating over 3 documents in the collection, each artificially taking ~50 ms due to failpoint. If timeoutMS is not refreshed, then we'd expect to error + for await (const doc of cursor) { + expect(doc).to.deep.equal({ x: 1 }); + } + + const finds = commandSucceeded.filter(ev => ev.commandName === 'find'); + const getMores = commandSucceeded.filter(ev => ev.commandName === 'getMore'); + + expect(finds).to.have.length(1); // Expecting 1 find + expect(getMores).to.have.length(3); // Expecting 3 getMores (including final empty getMore) + }); + + it( + 'does not append a maxTimeMS to the original command or getMores', + metadata, + async function () { + const cursor = client + .db('db') + .collection('coll') + .find({}, { batchSize: 1, timeoutMode: 'iteration', timeoutMS: 100 }) + .project({ _id: 0 }); + await cursor.toArray(); + + expect(commandStarted).to.have.length.gte(3); // Find and 2 getMores + expect( + commandStarted.filter(ev => { + return ( + ev.command.find != null && + ev.command.getMore != null && + ev.command.maxTimeMS != null + ); + }) + ).to.have.lengthOf(0); + } + ); + }); + }); + + context('LIFETIME mode', () => { + let client: MongoClient; + let internalClient: MongoClient; + let commandStarted: CommandStartedEvent[]; + let commandSucceeded: CommandSucceededEvent[]; + const failpoint: FailPoint = { + configureFailPoint: 'failCommand', + mode: 'alwaysOn', + data: { + failCommands: ['find', 'getMore'], + blockConnection: true, + blockTimeMS: 50 + } + }; + + beforeEach(async function () { + internalClient = this.configuration.newClient(); + await internalClient + .db('db') + .dropCollection('coll') + .catch(() => null); + await internalClient + .db('db') + .collection('coll') + .insertMany( + Array.from({ length: 3 }, () => { + return { x: 1 }; + }) + ); + + await internalClient.db().admin().command(failpoint); + + client = this.configuration.newClient(undefined, { monitorCommands: true }); + commandStarted = []; + commandSucceeded = []; + client.on('commandStarted', ev => commandStarted.push(ev)); + client.on('commandSucceeded', ev => commandSucceeded.push(ev)); + }); + + afterEach(async function () { + await internalClient .db() - .collection('a') - .insertOne({}) - .catch(error => error); - expect(error).to.be.instanceOf(MongoOperationTimeoutError); - expect(error.cause).to.be.instanceOf(MongoServerError); - expect(error.cause).to.have.nested.property('writeConcernError.code', 50); - - expect(commandsSucceeded).to.have.lengthOf(1); - expect(commandsSucceeded).to.have.nested.property('[0].reply.writeConcernError.code', 50); + .admin() + .command({ ...failpoint, mode: 'off' }); + await internalClient.close(); + await client.close(); + }); + context('when executing a next call', () => { + context( + 'when there are documents available from previously retrieved batch and timeout has expired', + () => { + it('returns documents without error', metadata, async function () { + const cursor = client + .db('db') + .collection('coll') + .find({}, { timeoutMode: 'cursorLifetime', timeoutMS: 100 }) + .project({ _id: 0 }); + const doc = await cursor.next(); + expect(doc).to.deep.equal({ x: 1 }); + expect(cursor.documents.length).to.be.gt(0); + + await setTimeout(100); + + const docOrErr = await cursor.next().then( + d => d, + e => e + ); + + expect(docOrErr).to.not.be.instanceOf(MongoOperationTimeoutError); + expect(docOrErr).to.be.deep.equal({ x: 1 }); + }); + } + ); + context('when a getMore is required and the timeout has expired', () => { + it('throws a MongoOperationTimeoutError', metadata, async function () { + const cursor = client + .db('db') + .collection('coll') + .find({}, { batchSize: 1, timeoutMode: 'cursorLifetime', timeoutMS: 100 }) + + .project({ _id: 0 }); + + const doc = await cursor.next(); + expect(doc).to.deep.equal({ x: 1 }); + expect(cursor.documents.length).to.equal(0); + + await setTimeout(100); + + const docOrErr = await cursor.next().then( + d => d, + e => e + ); + + expect(docOrErr).to.be.instanceOf(MongoOperationTimeoutError); + }); + }); + + it('does not apply maxTimeMS to a getMore', metadata, async function () { + const cursor = client + .db('db') + .collection('coll') + .find({}, { batchSize: 1, timeoutMode: 'cursorLifetime', timeoutMS: 1000 }) + .project({ _id: 0 }); + + for await (const _doc of cursor) { + // Ignore _doc + } + + const getMores = commandStarted + .filter(ev => ev.command.getMore != null) + .map(ev => ev.command); + expect(getMores.length).to.be.gt(0); + + for (const getMore of getMores) { + expect(getMore.maxTimeMS).to.not.exist; + } + }); }); }); }); + describe.skip('Tailable non-awaitData cursors').skipReason = + 'TODO(NODE-6305): implement CSOT for Tailable cursors'; + describe.skip('Tailable awaitData cursors').skipReason = + 'TODO(NODE-6305): implement CSOT for Tailable cursors'; + describe('when using an explicit session', () => { const metadata: MongoDBMetadataUI = { requires: { topology: ['replicaset'], mongodb: '>=4.4' } diff --git a/test/integration/client-side-operations-timeout/unified-csot-node-specs/command-execution.json b/test/integration/client-side-operations-timeout/unified-csot-node-specs/command-execution.json new file mode 100644 index 0000000000..dd6fcb2cf8 --- /dev/null +++ b/test/integration/client-side-operations-timeout/unified-csot-node-specs/command-execution.json @@ -0,0 +1,153 @@ +{ + "description": "timeoutMS behaves correctly during command execution", + "schemaVersion": "1.9", + "runOnRequirements": [ + { + "minServerVersion": "4.4.7", + "topologies": [ + "single", + "replicaset", + "sharded" + ], + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "failPointClient", + "useMultipleMongoses": false + } + } + ], + "initialData": [ + { + "collectionName": "coll", + "databaseName": "test", + "documents": [] + }, + { + "collectionName": "timeoutColl", + "databaseName": "test", + "documents": [] + } + ], + "tests": [ + { + "description": "maxTimeMS value in the command is less than timeoutMS", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "failPointClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": "alwaysOn", + "data": { + "failCommands": [ + "hello", + "isMaster" + ], + "appName": "reduceMaxTimeMSTest", + "blockConnection": true, + "blockTimeMS": 50 + } + } + } + }, + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "client": { + "id": "client", + "useMultipleMongoses": false, + "uriOptions": { + "appName": "reduceMaxTimeMSTest", + "w": 1, + "timeoutMS": 500, + "heartbeatFrequencyMS": 500 + }, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database", + "client": "client", + "databaseName": "test" + } + }, + { + "collection": { + "id": "timeoutCollection", + "database": "database", + "collectionName": "timeoutColl" + } + } + ] + } + }, + { + "name": "insertOne", + "object": "timeoutCollection", + "arguments": { + "document": { + "_id": 1 + }, + "timeoutMS": 100000 + } + }, + { + "name": "wait", + "object": "testRunner", + "arguments": { + "ms": 1500 + } + }, + { + "name": "insertOne", + "object": "timeoutCollection", + "arguments": { + "document": { + "_id": 2 + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "commandName": "insert", + "databaseName": "test", + "command": { + "insert": "timeoutColl" + } + } + }, + { + "commandStartedEvent": { + "commandName": "insert", + "databaseName": "test", + "command": { + "insert": "timeoutColl", + "maxTimeMS": { + "$$lte": 500 + } + } + } + } + ] + } + ] + } + ] +} diff --git a/test/spec/client-side-operations-timeout/README.md b/test/spec/client-side-operations-timeout/README.md new file mode 100644 index 0000000000..a960c2de21 --- /dev/null +++ b/test/spec/client-side-operations-timeout/README.md @@ -0,0 +1,661 @@ +# Client Side Operations Timeouts Tests + +______________________________________________________________________ + +## Introduction + +This document describes the tests that drivers MUST run to validate the behavior of the timeoutMS option. These tests +are broken up into automated YAML/JSON tests and additional prose tests. + +## Spec Tests + +This directory contains a set of YAML and JSON spec tests. Drivers MUST run these as described in the "Unified Test +Runner" specification. Because the tests introduced in this specification are timing-based, there is a risk that some of +them may intermittently fail without any bugs being present in the driver. As a mitigation, drivers MAY execute these +tests in two new Evergreen tasks that use single-node replica sets: one with only authentication enabled and another +with both authentication and TLS enabled. Drivers that choose to do so SHOULD use the `single-node-auth.json` and +`single-node-auth-ssl.json` files in the `drivers-evergreen-tools` repository to create these clusters. + +## Prose Tests + +There are some tests that cannot be expressed in the unified YAML/JSON format. For each of these tests, drivers MUST +create a MongoClient without the `timeoutMS` option set (referred to as `internalClient`). Any fail points set during a +test MUST be unset using `internalClient` after the test has been executed. All MongoClient instances created for tests +MUST be configured with read/write concern `majority`, read preference `primary`, and command monitoring enabled to +listen for `command_started` events. + +### 1. Multi-batch inserts + +This test MUST only run against standalones on server versions 4.4 and higher. The `insertMany` call takes an +exceedingly long time on replicasets and sharded clusters. Drivers MAY adjust the timeouts used in this test to allow +for differing bulk encoding performance. + +1. Using `internalClient`, drop the `db.coll` collection. + +2. Using `internalClient`, set the following fail point: + + ```javascript + { + configureFailPoint: "failCommand", + mode: { + times: 2 + }, + data: { + failCommands: ["insert"], + blockConnection: true, + blockTimeMS: 1010 + } + } + ``` + +3. Create a new MongoClient (referred to as `client`) with `timeoutMS=2000`. + +4. Using `client`, insert 50 1-megabyte documents in a single `insertMany` call. + + - Expect this to fail with a timeout error. + +5. Verify that two `insert` commands were executed against `db.coll` as part of the `insertMany` call. + +### 2. maxTimeMS is not set for commands sent to mongocryptd + +This test MUST only be run against enterprise server versions 4.2 and higher. + +1. Launch a mongocryptd process on 23000. +2. Create a MongoClient (referred to as `client`) using the URI `mongodb://localhost:23000/?timeoutMS=1000`. +3. Using `client`, execute the `{ ping: 1 }` command against the `admin` database. +4. Verify via command monitoring that the `ping` command sent did not contain a `maxTimeMS` field. + +### 3. ClientEncryption + +Each test under this category MUST only be run against server versions 4.4 and higher. In these tests, `LOCAL_MASTERKEY` +refers to the following base64: + +```javascript +Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk +``` + +For each test, perform the following setup: + +1. Using `internalClient`, drop and create the `keyvault.datakeys` collection. + +2. Create a MongoClient (referred to as `keyVaultClient`) with `timeoutMS=10`. + +3. Create a `ClientEncryption` object that wraps `keyVaultClient` (referred to as `clientEncryption`). Configure this + object with `keyVaultNamespace` set to `keyvault.datakeys` and the following KMS providers map: + + ```javascript + { + "local": { "key": } + } + ``` + +#### createDataKey + +1. Using `internalClient`, set the following fail point: + + ```javascript + { + configureFailPoint: "failCommand", + mode: { + times: 1 + }, + data: { + failCommands: ["insert"], + blockConnection: true, + blockTimeMS: 15 + } + } + ``` + +2. Call `clientEncryption.createDataKey()` with the `local` KMS provider. + + - Expect this to fail with a timeout error. + +3. Verify that an `insert` command was executed against to `keyvault.datakeys` as part of the `createDataKey` call. + +#### encrypt + +1. Call `client_encryption.createDataKey()` with the `local` KMS provider. + + - Expect a BSON binary with subtype 4 to be returned, referred to as `datakeyId`. + +2. Using `internalClient`, set the following fail point: + + ```javascript + { + configureFailPoint: "failCommand", + mode: { + times: 1 + }, + data: { + failCommands: ["find"], + blockConnection: true, + blockTimeMS: 15 + } + } + ``` + +3. Call `clientEncryption.encrypt()` with the value `hello`, the algorithm + `AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic`, and the keyId `datakeyId`. + + - Expect this to fail with a timeout error. + +4. Verify that a `find` command was executed against the `keyvault.datakeys` collection as part of the `encrypt` call. + +#### decrypt + +1. Call `clientEncryption.createDataKey()` with the `local` KMS provider. + + - Expect this to return a BSON binary with subtype 4, referred to as `dataKeyId`. + +2. Call `clientEncryption.encrypt()` with the value `hello`, the algorithm + `AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic`, and the keyId `dataKeyId`. + + - Expect this to return a BSON binary with subtype 6, referred to as `encrypted`. + +3. Close and re-create the `keyVaultClient` and `clientEncryption` objects. + +4. Using `internalClient`, set the following fail point: + + ```javascript + { + configureFailPoint: "failCommand", + mode: { + times: 1 + }, + data: { + failCommands: ["find"], + blockConnection: true, + blockTimeMS: 15 + } + } + ``` + +5. Call `clientEncryption.decrypt()` with the value `encrypted`. + + - Expect this to fail with a timeout error. + +6. Verify that a `find` command was executed against the `keyvault.datakeys` collection as part of the `decrypt` call. + +### 4. Background Connection Pooling + +The tests in this section MUST only be run if the server version is 4.4 or higher and the URI has authentication fields +(i.e. a username and password). Each test in this section requires drivers to create a MongoClient and then wait for +some CMAP events to be published. Drivers MUST wait for up to 10 seconds and fail the test if the specified events are +not published within that time. + +#### timeoutMS used for handshake commands + +1. Using `internalClient`, set the following fail point: + + ```javascript + { + configureFailPoint: "failCommand", + mode: { + times: 1 + }, + data: { + failCommands: ["saslContinue"], + blockConnection: true, + blockTimeMS: 15, + appName: "timeoutBackgroundPoolTest" + } + } + ``` + +2. Create a MongoClient (referred to as `client`) configured with the following: + + - `minPoolSize` of 1 + - `timeoutMS` of 10 + - `appName` of `timeoutBackgroundPoolTest` + - CMAP monitor configured to listen for `ConnectionCreatedEvent` and `ConnectionClosedEvent` events. + +3. Wait for a `ConnectionCreatedEvent` and a `ConnectionClosedEvent` to be published. + +#### timeoutMS is refreshed for each handshake command + +1. Using `internalClient`, set the following fail point: + + ```javascript + { + configureFailPoint: "failCommand", + mode: "alwaysOn", + data: { + failCommands: ["hello", "isMaster", "saslContinue"], + blockConnection: true, + blockTimeMS: 15, + appName: "refreshTimeoutBackgroundPoolTest" + } + } + ``` + +2. Create a MongoClient (referred to as `client`) configured with the following: + + - `minPoolSize` of 1 + - `timeoutMS` of 20 + - `appName` of `refreshTimeoutBackgroundPoolTest` + - CMAP monitor configured to listen for `ConnectionCreatedEvent` and `ConnectionReady` events. + +3. Wait for a `ConnectionCreatedEvent` and a `ConnectionReady` to be published. + +### 5. Blocking Iteration Methods + +Tests in this section MUST only be run against server versions 4.4 and higher and only apply to drivers that have a +blocking method for cursor iteration that executes `getMore` commands in a loop until a document is available or an +error occurs. + +#### Tailable cursors + +1. Using `internalClient`, drop the `db.coll` collection. + +2. Using `internalClient`, insert the document `{ x: 1 }` into `db.coll`. + +3. Using `internalClient`, set the following fail point: + + ```javascript + { + configureFailPoint: "failCommand", + mode: "alwaysOn", + data: { + failCommands: ["getMore"], + blockConnection: true, + blockTimeMS: 15 + } + } + ``` + +4. Create a new MongoClient (referred to as `client`) with `timeoutMS=20`. + +5. Using `client`, create a tailable cursor on `db.coll` with `cursorType=tailable`. + + - Expect this to succeed and return a cursor with a non-zero ID. + +6. Call either a blocking or non-blocking iteration method on the cursor. + + - Expect this to succeed and return the document `{ x: 1 }` without sending a `getMore` command. + +7. Call the blocking iteration method on the resulting cursor. + + - Expect this to fail with a timeout error. + +8. Verify that a `find` command and two `getMore` commands were executed against the `db.coll` collection during the + test. + +#### Change Streams + +1. Using `internalClient`, drop the `db.coll` collection. + +2. Using `internalClient`, set the following fail point: + + ```javascript + { + configureFailPoint: "failCommand", + mode: "alwaysOn", + data: { + failCommands: ["getMore"], + blockConnection: true, + blockTimeMS: 15 + } + } + ``` + +3. Create a new MongoClient (referred to as `client`) with `timeoutMS=20`. + +4. Using `client`, use the `watch` helper to create a change stream against `db.coll`. + + - Expect this to succeed and return a change stream with a non-zero ID. + +5. Call the blocking iteration method on the resulting change stream. + + - Expect this to fail with a timeout error. + +6. Verify that an `aggregate` command and two `getMore` commands were executed against the `db.coll` collection during + the test. + +### 6. GridFS - Upload + +Tests in this section MUST only be run against server versions 4.4 and higher. + +#### uploads via openUploadStream can be timed out + +1. Using `internalClient`, drop and re-create the `db.fs.files` and `db.fs.chunks` collections. + +2. Using `internalClient`, set the following fail point: + + ```javascript + { + configureFailPoint: "failCommand", + mode: { times: 1 }, + data: { + failCommands: ["insert"], + blockConnection: true, + blockTimeMS: 15 + } + } + ``` + +3. Create a new MongoClient (referred to as `client`) with `timeoutMS=10`. + +4. Using `client`, create a GridFS bucket (referred to as `bucket`) that wraps the `db` database. + +5. Call `bucket.open_upload_stream()` with the filename `filename` to create an upload stream (referred to as + `uploadStream`). + + - Expect this to succeed and return a non-null stream. + +6. Using `uploadStream`, upload a single `0x12` byte. + +7. Call `uploadStream.close()` to flush the stream and insert chunks. + + - Expect this to fail with a timeout error. + +#### Aborting an upload stream can be timed out + +This test only applies to drivers that provide an API to abort a GridFS upload stream. + +1. Using `internalClient`, drop and re-create the `db.fs.files` and `db.fs.chunks` collections. + +2. Using `internalClient`, set the following fail point: + + ```javascript + { + configureFailPoint: "failCommand", + mode: { times: 1 }, + data: { + failCommands: ["delete"], + blockConnection: true, + blockTimeMS: 15 + } + } + ``` + +3. Create a new MongoClient (referred to as `client`) with `timeoutMS=10`. + +4. Using `client`, create a GridFS bucket (referred to as `bucket`) that wraps the `db` database with + `chunkSizeBytes=2`. + +5. Call `bucket.open_upload_stream()` with the filename `filename` to create an upload stream (referred to as + `uploadStream`). + + - Expect this to succeed and return a non-null stream. + +6. Using `uploadStream`, upload the bytes `[0x01, 0x02, 0x03, 0x04]`. + +7. Call `uploadStream.abort()`. + + - Expect this to fail with a timeout error. + +### 7. GridFS - Download + +This test MUST only be run against server versions 4.4 and higher. + +1. Using `internalClient`, drop and re-create the `db.fs.files` and `db.fs.chunks` collections. + +2. Using `internalClient`, insert the following document into the `db.fs.files` collection: + + ```javascript + { + "_id": { + "$oid": "000000000000000000000005" + }, + "length": 10, + "chunkSize": 4, + "uploadDate": { + "$date": "1970-01-01T00:00:00.000Z" + }, + "md5": "57d83cd477bfb1ccd975ab33d827a92b", + "filename": "length-10", + "contentType": "application/octet-stream", + "aliases": [], + "metadata": {} + } + ``` + +3. Create a new MongoClient (referred to as `client`) with `timeoutMS=10`. + +4. Using `client`, create a GridFS bucket (referred to as `bucket`) that wraps the `db` database. + +5. Call `bucket.open_download_stream` with the id `{ "$oid": "000000000000000000000005" }` to create a download stream + (referred to as `downloadStream`). + + - Expect this to succeed and return a non-null stream. + +6. Using `internalClient`, set the following fail point: + + ```javascript + { + configureFailPoint: "failCommand", + mode: { times: 1 }, + data: { + failCommands: ["find"], + blockConnection: true, + blockTimeMS: 15 + } + } + ``` + +7. Read from the `downloadStream`. + + - Expect this to fail with a timeout error. + +8. Verify that two `find` commands were executed during the read: one against `db.fs.files` and another against + `db.fs.chunks`. + +### 8. Server Selection + +#### serverSelectionTimeoutMS honored if timeoutMS is not set + +1. Create a MongoClient (referred to as `client`) with URI `mongodb://invalid/?serverSelectionTimeoutMS=10`. +2. Using `client`, execute the command `{ ping: 1 }` against the `admin` database. + - Expect this to fail with a server selection timeout error after no more than 15ms. + +#### timeoutMS honored for server selection if it's lower than serverSelectionTimeoutMS + +1. Create a MongoClient (referred to as `client`) with URI + `mongodb://invalid/?timeoutMS=10&serverSelectionTimeoutMS=20`. +2. Using `client`, run the command `{ ping: 1 }` against the `admin` database. + - Expect this to fail with a server selection timeout error after no more than 15ms. + +#### serverSelectionTimeoutMS honored for server selection if it's lower than timeoutMS + +1. Create a MongoClient (referred to as `client`) with URI + `mongodb://invalid/?timeoutMS=20&serverSelectionTimeoutMS=10`. +2. Using `client`, run the command `{ ping: 1 }` against the `admin` database. + - Expect this to fail with a server selection timeout error after no more than 15ms. + +#### serverSelectionTimeoutMS honored for server selection if timeoutMS=0 + +1. Create a MongoClient (referred to as `client`) with URI `mongodb://invalid/?timeoutMS=0&serverSelectionTimeoutMS=10`. +2. Using `client`, run the command `{ ping: 1 }` against the `admin` database. + - Expect this to fail with a server selection timeout error after no more than 15ms. + +#### timeoutMS honored for connection handshake commands if it's lower than serverSelectionTimeoutMS + +This test MUST only be run if the server version is 4.4 or higher and the URI has authentication fields (i.e. a username +and password). + +1. Using `internalClient`, set the following fail point: + + ```javascript + { + configureFailPoint: failCommand, + mode: { times: 1 }, + data: { + failCommands: ["saslContinue"], + blockConnection: true, + blockTimeMS: 15 + } + } + ``` + +2. Create a new MongoClient (referred to as `client`) with `timeoutMS=10` and `serverSelectionTimeoutMS=20`. + +3. Using `client`, insert the document `{ x: 1 }` into collection `db.coll`. + + - Expect this to fail with a timeout error after no more than 15ms. + +#### serverSelectionTimeoutMS honored for connection handshake commands if it's lower than timeoutMS + +This test MUST only be run if the server version is 4.4 or higher and the URI has authentication fields (i.e. a username +and password). + +1. Using `internalClient`, set the following fail point: + + ```javascript + { + configureFailPoint: failCommand, + mode: { times: 1 }, + data: { + failCommands: ["saslContinue"], + blockConnection: true, + blockTimeMS: 15 + } + } + ``` + +2. Create a new MongoClient (referred to as `client`) with `timeoutMS=20` and `serverSelectionTimeoutMS=10`. + +3. Using `client`, insert the document `{ x: 1 }` into collection `db.coll`. + + - Expect this to fail with a timeout error after no more than 15ms. + +### 9. endSession + +This test MUST only be run against replica sets and sharded clusters with server version 4.4 or higher. It MUST be run +three times: once with the timeout specified via the MongoClient `timeoutMS` option, once with the timeout specified via +the ClientSession `defaultTimeoutMS` option, and once more with the timeout specified via the `timeoutMS` option for the +`endSession` operation. In all cases, the timeout MUST be set to 10 milliseconds. + +1. Using `internalClient`, drop the `db.coll` collection. + +2. Using `internalClient`, set the following fail point: + + ```javascript + { + configureFailPoint: failCommand, + mode: { times: 1 }, + data: { + failCommands: ["abortTransaction"], + blockConnection: true, + blockTimeMS: 15 + } + } + ``` + +3. Create a new MongoClient (referred to as `client`) and an explicit ClientSession derived from that MongoClient + (referred to as `session`). + +4. Execute the following code: + + ```typescript + coll = client.database("db").collection("coll") + session.start_transaction() + coll.insert_one({x: 1}, session=session) + ``` + +5. Using `session`, execute `session.end_session` + + - Expect this to fail with a timeout error after no more than 15ms. + +### 10. Convenient Transactions + +Tests in this section MUST only run against replica sets and sharded clusters with server versions 4.4 or higher. + +#### timeoutMS is refreshed for abortTransaction if the callback fails + +1. Using `internalClient`, drop the `db.coll` collection. + +2. Using `internalClient`, set the following fail point: + + ```javascript + { + configureFailPoint: failCommand, + mode: { times: 2 }, + data: { + failCommands: ["insert", "abortTransaction"], + blockConnection: true, + blockTimeMS: 15 + } + } + ``` + +3. Create a new MongoClient (referred to as `client`) configured with `timeoutMS=10` and an explicit ClientSession + derived from that MongoClient (referred to as `session`). + +4. Using `session`, execute a `withTransaction` operation with the following callback: + + ```typescript + def callback() { + coll = client.database("db").collection("coll") + coll.insert_one({ _id: 1 }, session=session) + } + ``` + +5. Expect the previous `withTransaction` call to fail with a timeout error. + +6. Verify that the following events were published during the `withTransaction` call: + + 1. `command_started` and `command_failed` events for an `insert` command. + 2. `command_started` and `command_failed` events for an `abortTransaction` command. + +### 11. Multi-batch bulkWrites + +This test MUST only run against server versions 8.0+. + +1. Using `internalClient`, drop the `db.coll` collection. + +2. Using `internalClient`, set the following fail point: + + ```javascript + { + configureFailPoint: "failCommand", + mode: { + times: 2 + }, + data: { + failCommands: ["bulkWrite"], + blockConnection: true, + blockTimeMS: 1010 + } + } + ``` + +3. Using `internalClient`, perform a `hello` command and record the `maxBsonObjectSize` and `maxMessageSizeBytes` values + in the response. + +4. Create a new MongoClient (referred to as `client`) with `timeoutMS=2000`. + +5. Create a list of write models (referred to as `models`) with the following write model repeated + (`maxMessageSizeBytes / maxBsonObjectSize + 1`) times: + + ```json + InsertOne { + "namespace": "db.coll", + "document": { "a": "b".repeat(maxBsonObjectSize - 500) } + } + ``` + +6. Call `bulkWrite` on `client` with `models`. + + - Expect this to fail with a timeout error. + +7. Verify that two `bulkWrite` commands were executed as part of the `MongoClient.bulkWrite` call. + +## Unit Tests + +The tests enumerated in this section could not be expressed in either spec or prose format. Drivers SHOULD implement +these if it is possible to do so using the driver's existing test infrastructure. + +- Operations should ignore `waitQueueTimeoutMS` if `timeoutMS` is also set. +- If `timeoutMS` is set for an operation, the remaining `timeoutMS` value should apply to connection checkout after a + server has been selected. +- If `timeoutMS` is not set for an operation, `waitQueueTimeoutMS` should apply to connection checkout after a server + has been selected. +- If a new connection is required to execute an operation, + `min(remaining computedServerSelectionTimeout, connectTimeoutMS)` should apply to socket establishment. +- For drivers that have control over OCSP behavior, `min(remaining computedServerSelectionTimeout, 5 seconds)` should + apply to HTTP requests against OCSP responders. +- If `timeoutMS` is unset, operations fail after two non-consecutive socket timeouts. +- The remaining `timeoutMS` value should apply to HTTP requests against KMS servers for CSFLE. +- The remaining `timeoutMS` value should apply to commands sent to mongocryptd as part of automatic encryption. +- When doing `minPoolSize` maintenance, `connectTimeoutMS` is used as the timeout for socket establishment. diff --git a/test/spec/client-side-operations-timeout/change-streams.json b/test/spec/client-side-operations-timeout/change-streams.json index aef77bb452..8cffb08e26 100644 --- a/test/spec/client-side-operations-timeout/change-streams.json +++ b/test/spec/client-side-operations-timeout/change-streams.json @@ -104,7 +104,7 @@ "aggregate" ], "blockConnection": true, - "blockTimeMS": 55 + "blockTimeMS": 250 } } } @@ -114,7 +114,7 @@ "object": "collection", "arguments": { "pipeline": [], - "timeoutMS": 50 + "timeoutMS": 200 }, "expectError": { "isTimeoutError": true @@ -242,7 +242,7 @@ "getMore" ], "blockConnection": true, - "blockTimeMS": 15 + "blockTimeMS": 150 } } } @@ -252,7 +252,7 @@ "object": "collection", "arguments": { "pipeline": [], - "timeoutMS": 20, + "timeoutMS": 200, "batchSize": 2, "maxAwaitTimeMS": 1 }, @@ -310,7 +310,7 @@ "object": "collection", "arguments": { "pipeline": [], - "timeoutMS": 20 + "timeoutMS": 200 }, "saveResultAsEntity": "changeStream" }, @@ -330,7 +330,7 @@ "aggregate" ], "blockConnection": true, - "blockTimeMS": 12, + "blockTimeMS": 120, "errorCode": 7, "errorLabels": [ "ResumableChangeStreamError" @@ -412,7 +412,7 @@ "arguments": { "pipeline": [], "maxAwaitTimeMS": 1, - "timeoutMS": 100 + "timeoutMS": 200 }, "saveResultAsEntity": "changeStream" }, @@ -431,7 +431,7 @@ "getMore" ], "blockConnection": true, - "blockTimeMS": 150 + "blockTimeMS": 250 } } } @@ -534,7 +534,7 @@ "getMore" ], "blockConnection": true, - "blockTimeMS": 15 + "blockTimeMS": 250 } } } @@ -544,7 +544,7 @@ "object": "collection", "arguments": { "pipeline": [], - "timeoutMS": 10 + "timeoutMS": 200 }, "saveResultAsEntity": "changeStream" }, diff --git a/test/spec/client-side-operations-timeout/change-streams.yml b/test/spec/client-side-operations-timeout/change-streams.yml index b2a052d01b..c813be035a 100644 --- a/test/spec/client-side-operations-timeout/change-streams.yml +++ b/test/spec/client-side-operations-timeout/change-streams.yml @@ -67,12 +67,12 @@ tests: data: failCommands: ["aggregate"] blockConnection: true - blockTimeMS: 55 + blockTimeMS: 250 - name: createChangeStream object: *collection arguments: pipeline: [] - timeoutMS: 50 + timeoutMS: 200 expectError: isTimeoutError: true expectEvents: @@ -142,12 +142,12 @@ tests: data: failCommands: ["aggregate", "getMore"] blockConnection: true - blockTimeMS: 15 + blockTimeMS: 150 - name: createChangeStream object: *collection arguments: pipeline: [] - timeoutMS: 20 + timeoutMS: 200 batchSize: 2 maxAwaitTimeMS: 1 saveResultAsEntity: &changeStream changeStream @@ -171,16 +171,16 @@ tests: maxTimeMS: 1 # The timeout should be applied to the entire resume attempt, not individually to each command. The test creates a - # change stream with timeoutMS=20 which returns an empty initial batch and then sets a fail point to block both - # getMore and aggregate for 12ms each and fail with a resumable error. When the resume attempt happens, the getMore - # and aggregate block for longer than 20ms total, so it times out. + # change stream with timeoutMS=200 which returns an empty initial batch and then sets a fail point to block both + # getMore and aggregate for 120ms each and fail with a resumable error. When the resume attempt happens, the getMore + # and aggregate block for longer than 200ms total, so it times out. - description: "timeoutMS applies to full resume attempt in a next call" operations: - name: createChangeStream object: *collection arguments: pipeline: [] - timeoutMS: 20 + timeoutMS: 200 saveResultAsEntity: &changeStream changeStream - name: failPoint object: testRunner @@ -192,7 +192,7 @@ tests: data: failCommands: ["getMore", "aggregate"] blockConnection: true - blockTimeMS: 12 + blockTimeMS: 120 errorCode: 7 # HostNotFound - resumable but does not require an SDAM state change. # failCommand doesn't correctly add the ResumableChangeStreamError by default. It needs to be specified # manually here so the error is considered resumable. The failGetMoreAfterCursorCheckout fail point @@ -234,9 +234,9 @@ tests: # Specify a short maxAwaitTimeMS because otherwise the getMore on the new cursor will wait for 1000ms and # time out. maxAwaitTimeMS: 1 - timeoutMS: 100 + timeoutMS: 200 saveResultAsEntity: &changeStream changeStream - # Block getMore for 150ms to force the next() call to time out. + # Block getMore for 250ms to force the next() call to time out. - name: failPoint object: testRunner arguments: @@ -247,7 +247,7 @@ tests: data: failCommands: ["getMore"] blockConnection: true - blockTimeMS: 150 + blockTimeMS: 250 # The original aggregate didn't return any events so this should do a getMore and return a timeout error. - name: iterateUntilDocumentOrError object: *changeStream @@ -290,7 +290,7 @@ tests: collection: *collectionName # The timeoutMS value should be refreshed for getMore's. This is a failure test. The createChangeStream operation - # sets timeoutMS=10 and the getMore blocks for 15ms, causing iteration to fail with a timeout error. + # sets timeoutMS=200 and the getMore blocks for 250ms, causing iteration to fail with a timeout error. - description: "timeoutMS is refreshed for getMore - failure" operations: - name: failPoint @@ -303,12 +303,12 @@ tests: data: failCommands: ["getMore"] blockConnection: true - blockTimeMS: 15 + blockTimeMS: 250 - name: createChangeStream object: *collection arguments: pipeline: [] - timeoutMS: 10 + timeoutMS: 200 saveResultAsEntity: &changeStream changeStream # The first iteration should do a getMore - name: iterateUntilDocumentOrError diff --git a/test/spec/client-side-operations-timeout/close-cursors.json b/test/spec/client-side-operations-timeout/close-cursors.json index 1361971c4c..79b0de7b6a 100644 --- a/test/spec/client-side-operations-timeout/close-cursors.json +++ b/test/spec/client-side-operations-timeout/close-cursors.json @@ -75,7 +75,7 @@ "getMore" ], "blockConnection": true, - "blockTimeMS": 50 + "blockTimeMS": 250 } } } @@ -86,7 +86,7 @@ "arguments": { "filter": {}, "batchSize": 2, - "timeoutMS": 20 + "timeoutMS": 200 }, "saveResultAsEntity": "cursor" }, @@ -175,7 +175,7 @@ "killCursors" ], "blockConnection": true, - "blockTimeMS": 30 + "blockTimeMS": 250 } } } @@ -186,7 +186,7 @@ "arguments": { "filter": {}, "batchSize": 2, - "timeoutMS": 20 + "timeoutMS": 200 }, "saveResultAsEntity": "cursor" }, @@ -194,7 +194,7 @@ "name": "close", "object": "cursor", "arguments": { - "timeoutMS": 40 + "timeoutMS": 400 } } ], @@ -215,7 +215,7 @@ { "commandStartedEvent": { "command": { - "killCursors": "collection", + "killCursors": "coll", "maxTimeMS": { "$$type": [ "int", diff --git a/test/spec/client-side-operations-timeout/close-cursors.yml b/test/spec/client-side-operations-timeout/close-cursors.yml index db26e79ca3..c4c4ea0acd 100644 --- a/test/spec/client-side-operations-timeout/close-cursors.yml +++ b/test/spec/client-side-operations-timeout/close-cursors.yml @@ -46,13 +46,13 @@ tests: data: failCommands: ["getMore"] blockConnection: true - blockTimeMS: 50 + blockTimeMS: 250 - name: createFindCursor object: *collection arguments: filter: {} batchSize: 2 - timeoutMS: 20 + timeoutMS: 200 saveResultAsEntity: &cursor cursor # Iterate the cursor three times. The third should do a getMore, which should fail with a timeout error. - name: iterateUntilDocumentOrError @@ -99,18 +99,18 @@ tests: data: failCommands: ["killCursors"] blockConnection: true - blockTimeMS: 30 + blockTimeMS: 250 - name: createFindCursor object: *collection arguments: filter: {} batchSize: 2 - timeoutMS: 20 + timeoutMS: 200 saveResultAsEntity: &cursor cursor - name: close object: *cursor arguments: - timeoutMS: 40 + timeoutMS: 400 expectEvents: - client: *client events: @@ -120,7 +120,7 @@ tests: commandName: find - commandStartedEvent: command: - killCursors: *collection + killCursors: *collectionName maxTimeMS: { $$type: ["int", "long"] } commandName: killCursors - commandSucceededEvent: diff --git a/test/spec/client-side-operations-timeout/command-execution.json b/test/spec/client-side-operations-timeout/command-execution.json index b9b306c7fb..aa9c3eb23f 100644 --- a/test/spec/client-side-operations-timeout/command-execution.json +++ b/test/spec/client-side-operations-timeout/command-execution.json @@ -3,7 +3,7 @@ "schemaVersion": "1.9", "runOnRequirements": [ { - "minServerVersion": "4.9", + "minServerVersion": "4.4.7", "topologies": [ "single", "replicaset", diff --git a/test/spec/client-side-operations-timeout/command-execution.yml b/test/spec/client-side-operations-timeout/command-execution.yml index 400a90867a..6ba0585b3c 100644 --- a/test/spec/client-side-operations-timeout/command-execution.yml +++ b/test/spec/client-side-operations-timeout/command-execution.yml @@ -3,9 +3,8 @@ description: "timeoutMS behaves correctly during command execution" schemaVersion: "1.9" runOnRequirements: - # The appName filter cannot be used to set a fail point on connection handshakes until server version 4.9 due to - # SERVER-49220/SERVER-49336. - - minServerVersion: "4.9" + # Require SERVER-49336 for failCommand + appName on the initial handshake. + - minServerVersion: "4.4.7" # Skip load-balanced and serverless which do not support RTT measurements. topologies: [ single, replicaset, sharded ] serverless: forbid diff --git a/test/spec/client-side-operations-timeout/convenient-transactions.json b/test/spec/client-side-operations-timeout/convenient-transactions.json index 07e676d5f5..3868b3026c 100644 --- a/test/spec/client-side-operations-timeout/convenient-transactions.json +++ b/test/spec/client-side-operations-timeout/convenient-transactions.json @@ -21,7 +21,7 @@ "client": { "id": "client", "uriOptions": { - "timeoutMS": 50 + "timeoutMS": 500 }, "useMultipleMongoses": false, "observeEvents": [ @@ -81,6 +81,9 @@ } } ] + }, + "expectError": { + "isClientError": true } } ], @@ -109,7 +112,7 @@ "insert" ], "blockConnection": true, - "blockTimeMS": 30 + "blockTimeMS": 300 } } } @@ -182,6 +185,21 @@ } } } + }, + { + "commandStartedEvent": { + "commandName": "abortTransaction", + "databaseName": "admin", + "command": { + "abortTransaction": 1, + "maxTimeMS": { + "$$type": [ + "int", + "long" + ] + } + } + } } ] } diff --git a/test/spec/client-side-operations-timeout/convenient-transactions.yml b/test/spec/client-side-operations-timeout/convenient-transactions.yml index d79aa4bd05..02d48b8324 100644 --- a/test/spec/client-side-operations-timeout/convenient-transactions.yml +++ b/test/spec/client-side-operations-timeout/convenient-transactions.yml @@ -13,7 +13,7 @@ createEntities: - client: id: &client client uriOptions: - timeoutMS: 50 + timeoutMS: 500 useMultipleMongoses: false observeEvents: - commandStartedEvent @@ -49,6 +49,8 @@ tests: timeoutMS: 100 expectError: isClientError: true + expectError: + isClientError: true expectEvents: # The only operation run fails with a client-side error, so there should be no events for the client. - client: *client @@ -66,7 +68,7 @@ tests: data: failCommands: ["insert"] blockConnection: true - blockTimeMS: 30 + blockTimeMS: 300 - name: withTransaction object: *session arguments: @@ -88,9 +90,6 @@ tests: expectEvents: - client: *client events: - # Because the second insert expects an error and gets an error, it technically succeeds, so withTransaction - # will try to run commitTransaction. This will fail client-side, though, because the timeout has already - # expired, so no command is sent. - commandStartedEvent: commandName: insert databaseName: *databaseName @@ -103,3 +102,9 @@ tests: command: insert: *collectionName maxTimeMS: { $$type: ["int", "long"] } + - commandStartedEvent: + commandName: abortTransaction + databaseName: admin + command: + abortTransaction: 1 + maxTimeMS: { $$type: [ "int", "long" ] } diff --git a/test/spec/client-side-operations-timeout/deprecated-options.json b/test/spec/client-side-operations-timeout/deprecated-options.json index 322e944910..d3e4631ff4 100644 --- a/test/spec/client-side-operations-timeout/deprecated-options.json +++ b/test/spec/client-side-operations-timeout/deprecated-options.json @@ -1,5 +1,5 @@ { - "description": "operations ignore deprected timeout options if timeoutMS is set", + "description": "operations ignore deprecated timeout options if timeoutMS is set", "schemaVersion": "1.9", "runOnRequirements": [ { diff --git a/test/spec/client-side-operations-timeout/deprecated-options.yml b/test/spec/client-side-operations-timeout/deprecated-options.yml index 461ba6ab13..582a8983ae 100644 --- a/test/spec/client-side-operations-timeout/deprecated-options.yml +++ b/test/spec/client-side-operations-timeout/deprecated-options.yml @@ -1,4 +1,4 @@ -description: "operations ignore deprected timeout options if timeoutMS is set" +description: "operations ignore deprecated timeout options if timeoutMS is set" schemaVersion: "1.9" diff --git a/test/spec/client-side-operations-timeout/gridfs-advanced.yml b/test/spec/client-side-operations-timeout/gridfs-advanced.yml index bc788bacc3..f6c37e165b 100644 --- a/test/spec/client-side-operations-timeout/gridfs-advanced.yml +++ b/test/spec/client-side-operations-timeout/gridfs-advanced.yml @@ -119,7 +119,7 @@ tests: update: *filesCollectionName maxTimeMS: { $$type: ["int", "long"] } - # Tests for the "drop" opration. Any tests that might result in multiple commands being sent do not have expectEvents + # Tests for the "drop" operation. Any tests that might result in multiple commands being sent do not have expectEvents # assertions as these assertions reduce test robustness and can cause flaky failures. - description: "timeoutMS can be overridden for drop" diff --git a/test/spec/client-side-operations-timeout/non-tailable-cursors.json b/test/spec/client-side-operations-timeout/non-tailable-cursors.json index 0a5448a6bb..291c6e72aa 100644 --- a/test/spec/client-side-operations-timeout/non-tailable-cursors.json +++ b/test/spec/client-side-operations-timeout/non-tailable-cursors.json @@ -17,7 +17,7 @@ "client": { "id": "client", "uriOptions": { - "timeoutMS": 10 + "timeoutMS": 200 }, "useMultipleMongoses": false, "observeEvents": [ @@ -84,7 +84,7 @@ "find" ], "blockConnection": true, - "blockTimeMS": 15 + "blockTimeMS": 250 } } } @@ -143,7 +143,7 @@ "getMore" ], "blockConnection": true, - "blockTimeMS": 15 + "blockTimeMS": 125 } } } @@ -153,7 +153,7 @@ "object": "collection", "arguments": { "filter": {}, - "timeoutMS": 20, + "timeoutMS": 200, "batchSize": 2 }, "expectError": { @@ -221,7 +221,7 @@ "getMore" ], "blockConnection": true, - "blockTimeMS": 15 + "blockTimeMS": 150 } } } @@ -232,7 +232,7 @@ "arguments": { "filter": {}, "timeoutMode": "cursorLifetime", - "timeoutMS": 20, + "timeoutMS": 200, "batchSize": 2 }, "expectError": { @@ -299,7 +299,7 @@ "find" ], "blockConnection": true, - "blockTimeMS": 15 + "blockTimeMS": 250 } } } @@ -355,7 +355,7 @@ "getMore" ], "blockConnection": true, - "blockTimeMS": 15 + "blockTimeMS": 125 } } } @@ -366,7 +366,7 @@ "arguments": { "filter": {}, "timeoutMode": "iteration", - "timeoutMS": 20, + "timeoutMS": 200, "batchSize": 2 } } @@ -427,7 +427,7 @@ "getMore" ], "blockConnection": true, - "blockTimeMS": 15 + "blockTimeMS": 250 } } } diff --git a/test/spec/client-side-operations-timeout/non-tailable-cursors.yml b/test/spec/client-side-operations-timeout/non-tailable-cursors.yml index 8cd953dec4..29037b4c0a 100644 --- a/test/spec/client-side-operations-timeout/non-tailable-cursors.yml +++ b/test/spec/client-side-operations-timeout/non-tailable-cursors.yml @@ -12,7 +12,7 @@ createEntities: - client: id: &client client uriOptions: - timeoutMS: 10 + timeoutMS: 200 useMultipleMongoses: false observeEvents: - commandStartedEvent @@ -53,7 +53,7 @@ tests: data: failCommands: ["find"] blockConnection: true - blockTimeMS: 15 + blockTimeMS: 250 - name: find object: *collection arguments: @@ -86,14 +86,14 @@ tests: data: failCommands: ["find", "getMore"] blockConnection: true - blockTimeMS: 15 - # Run a find with timeoutMS=20 and batchSize=1 to force two batches, which will cause a find and a getMore to be - # sent. Both will block for 15ms so together they will go over the timeout. + blockTimeMS: 125 + # Run a find with timeoutMS=200 and batchSize=1 to force two batches, which will cause a find and a getMore to be + # sent. Both will block for 125ms, so together they will go over the timeout. - name: find object: *collection arguments: filter: {} - timeoutMS: 20 + timeoutMS: 200 batchSize: 2 expectError: isTimeoutError: true @@ -127,13 +127,13 @@ tests: data: failCommands: ["find", "getMore"] blockConnection: true - blockTimeMS: 15 + blockTimeMS: 150 - name: find object: *collection arguments: filter: {} timeoutMode: cursorLifetime - timeoutMS: 20 + timeoutMS: 200 batchSize: 2 expectError: isTimeoutError: true @@ -168,7 +168,7 @@ tests: data: failCommands: ["find"] blockConnection: true - blockTimeMS: 15 + blockTimeMS: 250 - name: find object: *collection arguments: @@ -187,8 +187,8 @@ tests: maxTimeMS: { $$exists: false } # If timeoutMode=ITERATION, timeoutMS applies separately to the initial find and the getMore on the cursor. Neither - # command should have a maxTimeMS field. This is a success test. The "find" is executed with timeoutMS=20 and both - # "find" and "getMore" commands are blocked for 15ms each. Neither exceeds the timeout, so iteration succeeds. + # command should have a maxTimeMS field. This is a success test. The "find" is executed with timeoutMS=200 and both + # "find" and "getMore" commands are blocked for 125ms each. Neither exceeds the timeout, so iteration succeeds. - description: "timeoutMS is refreshed for getMore if timeoutMode is iteration - success" operations: - name: failPoint @@ -201,13 +201,13 @@ tests: data: failCommands: ["find", "getMore"] blockConnection: true - blockTimeMS: 15 + blockTimeMS: 125 - name: find object: *collection arguments: filter: {} timeoutMode: iteration - timeoutMS: 20 + timeoutMS: 200 batchSize: 2 expectEvents: - client: *client @@ -227,8 +227,8 @@ tests: maxTimeMS: { $$exists: false } # If timeoutMode=ITERATION, timeoutMS applies separately to the initial find and the getMore on the cursor. Neither - # command should have a maxTimeMS field. This is a failure test. The "find" inherits timeoutMS=10 and "getMore" - # commands are blocked for 15ms, causing iteration to fail with a timeout error. + # command should have a maxTimeMS field. This is a failure test. The "find" inherits timeoutMS=200 and "getMore" + # commands are blocked for 250ms, causing iteration to fail with a timeout error. - description: "timeoutMS is refreshed for getMore if timeoutMode is iteration - failure" operations: - name: failPoint @@ -241,7 +241,7 @@ tests: data: failCommands: ["getMore"] blockConnection: true - blockTimeMS: 15 + blockTimeMS: 250 - name: find object: *collection arguments: diff --git a/test/spec/client-side-operations-timeout/retryability-timeoutMS.json b/test/spec/client-side-operations-timeout/retryability-timeoutMS.json index a28dbd2685..9daad260ef 100644 --- a/test/spec/client-side-operations-timeout/retryability-timeoutMS.json +++ b/test/spec/client-side-operations-timeout/retryability-timeoutMS.json @@ -108,6 +108,11 @@ }, { "description": "operation is retried multiple times for non-zero timeoutMS - insertOne on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -198,6 +203,11 @@ }, { "description": "operation is retried multiple times if timeoutMS is zero - insertOne on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -327,6 +337,11 @@ }, { "description": "operation is retried multiple times for non-zero timeoutMS - insertMany on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -419,6 +434,11 @@ }, { "description": "operation is retried multiple times if timeoutMS is zero - insertMany on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -546,6 +566,11 @@ }, { "description": "operation is retried multiple times for non-zero timeoutMS - deleteOne on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -634,6 +659,11 @@ }, { "description": "operation is retried multiple times if timeoutMS is zero - deleteOne on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -760,6 +790,11 @@ }, { "description": "operation is retried multiple times for non-zero timeoutMS - replaceOne on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -851,6 +886,11 @@ }, { "description": "operation is retried multiple times if timeoutMS is zero - replaceOne on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -982,6 +1022,11 @@ }, { "description": "operation is retried multiple times for non-zero timeoutMS - updateOne on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -1075,6 +1120,11 @@ }, { "description": "operation is retried multiple times if timeoutMS is zero - updateOne on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -1203,6 +1253,11 @@ }, { "description": "operation is retried multiple times for non-zero timeoutMS - findOneAndDelete on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -1291,6 +1346,11 @@ }, { "description": "operation is retried multiple times if timeoutMS is zero - findOneAndDelete on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -1417,6 +1477,11 @@ }, { "description": "operation is retried multiple times for non-zero timeoutMS - findOneAndReplace on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -1508,6 +1573,11 @@ }, { "description": "operation is retried multiple times if timeoutMS is zero - findOneAndReplace on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -1639,6 +1709,11 @@ }, { "description": "operation is retried multiple times for non-zero timeoutMS - findOneAndUpdate on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -1732,6 +1807,11 @@ }, { "description": "operation is retried multiple times if timeoutMS is zero - findOneAndUpdate on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -1868,6 +1948,11 @@ }, { "description": "operation is retried multiple times for non-zero timeoutMS - bulkWrite on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -1964,6 +2049,11 @@ }, { "description": "operation is retried multiple times if timeoutMS is zero - bulkWrite on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -2095,6 +2185,11 @@ }, { "description": "operation is retried multiple times for non-zero timeoutMS - listDatabases on client", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -2183,6 +2278,11 @@ }, { "description": "operation is retried multiple times if timeoutMS is zero - listDatabases on client", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -2303,6 +2403,11 @@ }, { "description": "operation is retried multiple times for non-zero timeoutMS - listDatabaseNames on client", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -2390,6 +2495,11 @@ }, { "description": "operation is retried multiple times if timeoutMS is zero - listDatabaseNames on client", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -2512,6 +2622,11 @@ }, { "description": "operation is retried multiple times for non-zero timeoutMS - createChangeStream on client", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -2600,6 +2715,11 @@ }, { "description": "operation is retried multiple times if timeoutMS is zero - createChangeStream on client", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -2730,6 +2850,11 @@ }, { "description": "operation is retried multiple times for non-zero timeoutMS - aggregate on database", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -2825,6 +2950,11 @@ }, { "description": "operation is retried multiple times if timeoutMS is zero - aggregate on database", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -2955,6 +3085,11 @@ }, { "description": "operation is retried multiple times for non-zero timeoutMS - listCollections on database", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -3043,6 +3178,11 @@ }, { "description": "operation is retried multiple times if timeoutMS is zero - listCollections on database", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -3166,6 +3306,11 @@ }, { "description": "operation is retried multiple times for non-zero timeoutMS - listCollectionNames on database", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -3254,6 +3399,11 @@ }, { "description": "operation is retried multiple times if timeoutMS is zero - listCollectionNames on database", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -3377,6 +3527,11 @@ }, { "description": "operation is retried multiple times for non-zero timeoutMS - createChangeStream on database", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -3465,6 +3620,11 @@ }, { "description": "operation is retried multiple times if timeoutMS is zero - createChangeStream on database", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -3588,6 +3748,11 @@ }, { "description": "operation is retried multiple times for non-zero timeoutMS - aggregate on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -3676,6 +3841,11 @@ }, { "description": "operation is retried multiple times if timeoutMS is zero - aggregate on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -3799,6 +3969,11 @@ }, { "description": "operation is retried multiple times for non-zero timeoutMS - count on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -3887,6 +4062,11 @@ }, { "description": "operation is retried multiple times if timeoutMS is zero - count on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -4010,6 +4190,11 @@ }, { "description": "operation is retried multiple times for non-zero timeoutMS - countDocuments on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -4098,6 +4283,11 @@ }, { "description": "operation is retried multiple times if timeoutMS is zero - countDocuments on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -4218,6 +4408,11 @@ }, { "description": "operation is retried multiple times for non-zero timeoutMS - estimatedDocumentCount on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -4305,6 +4500,11 @@ }, { "description": "operation is retried multiple times if timeoutMS is zero - estimatedDocumentCount on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -4428,6 +4628,11 @@ }, { "description": "operation is retried multiple times for non-zero timeoutMS - distinct on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -4517,6 +4722,11 @@ }, { "description": "operation is retried multiple times if timeoutMS is zero - distinct on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -4641,6 +4851,11 @@ }, { "description": "operation is retried multiple times for non-zero timeoutMS - find on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -4729,6 +4944,11 @@ }, { "description": "operation is retried multiple times if timeoutMS is zero - find on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -4852,6 +5072,11 @@ }, { "description": "operation is retried multiple times for non-zero timeoutMS - findOne on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -4940,6 +5165,11 @@ }, { "description": "operation is retried multiple times if timeoutMS is zero - findOne on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -5060,6 +5290,11 @@ }, { "description": "operation is retried multiple times for non-zero timeoutMS - listIndexes on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -5147,6 +5382,11 @@ }, { "description": "operation is retried multiple times if timeoutMS is zero - listIndexes on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -5269,6 +5509,11 @@ }, { "description": "operation is retried multiple times for non-zero timeoutMS - createChangeStream on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", @@ -5357,6 +5602,11 @@ }, { "description": "operation is retried multiple times if timeoutMS is zero - createChangeStream on collection", + "runOnRequirements": [ + { + "minServerVersion": "4.3.1" + } + ], "operations": [ { "name": "failPoint", diff --git a/test/spec/client-side-operations-timeout/retryability-timeoutMS.yml b/test/spec/client-side-operations-timeout/retryability-timeoutMS.yml index 039f7ca42e..6f47d6c2e4 100644 --- a/test/spec/client-side-operations-timeout/retryability-timeoutMS.yml +++ b/test/spec/client-side-operations-timeout/retryability-timeoutMS.yml @@ -84,6 +84,8 @@ tests: expectError: isTimeoutError: true - description: "operation is retried multiple times for non-zero timeoutMS - insertOne on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -125,6 +127,8 @@ tests: insert: *collectionName maxTimeMS: { $$type: ["int", "long"] } - description: "operation is retried multiple times if timeoutMS is zero - insertOne on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -191,6 +195,8 @@ tests: expectError: isTimeoutError: true - description: "operation is retried multiple times for non-zero timeoutMS - insertMany on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -233,6 +239,8 @@ tests: insert: *collectionName maxTimeMS: { $$type: ["int", "long"] } - description: "operation is retried multiple times if timeoutMS is zero - insertMany on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -299,6 +307,8 @@ tests: expectError: isTimeoutError: true - description: "operation is retried multiple times for non-zero timeoutMS - deleteOne on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -340,6 +350,8 @@ tests: delete: *collectionName maxTimeMS: { $$type: ["int", "long"] } - description: "operation is retried multiple times if timeoutMS is zero - deleteOne on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -406,6 +418,8 @@ tests: expectError: isTimeoutError: true - description: "operation is retried multiple times for non-zero timeoutMS - replaceOne on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -448,6 +462,8 @@ tests: update: *collectionName maxTimeMS: { $$type: ["int", "long"] } - description: "operation is retried multiple times if timeoutMS is zero - replaceOne on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -515,6 +531,8 @@ tests: expectError: isTimeoutError: true - description: "operation is retried multiple times for non-zero timeoutMS - updateOne on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -557,6 +575,8 @@ tests: update: *collectionName maxTimeMS: { $$type: ["int", "long"] } - description: "operation is retried multiple times if timeoutMS is zero - updateOne on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -623,6 +643,8 @@ tests: expectError: isTimeoutError: true - description: "operation is retried multiple times for non-zero timeoutMS - findOneAndDelete on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -664,6 +686,8 @@ tests: findAndModify: *collectionName maxTimeMS: { $$type: ["int", "long"] } - description: "operation is retried multiple times if timeoutMS is zero - findOneAndDelete on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -730,6 +754,8 @@ tests: expectError: isTimeoutError: true - description: "operation is retried multiple times for non-zero timeoutMS - findOneAndReplace on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -772,6 +798,8 @@ tests: findAndModify: *collectionName maxTimeMS: { $$type: ["int", "long"] } - description: "operation is retried multiple times if timeoutMS is zero - findOneAndReplace on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -839,6 +867,8 @@ tests: expectError: isTimeoutError: true - description: "operation is retried multiple times for non-zero timeoutMS - findOneAndUpdate on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -881,6 +911,8 @@ tests: findAndModify: *collectionName maxTimeMS: { $$type: ["int", "long"] } - description: "operation is retried multiple times if timeoutMS is zero - findOneAndUpdate on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -949,6 +981,8 @@ tests: expectError: isTimeoutError: true - description: "operation is retried multiple times for non-zero timeoutMS - bulkWrite on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -992,6 +1026,8 @@ tests: insert: *collectionName maxTimeMS: { $$type: ["int", "long"] } - description: "operation is retried multiple times if timeoutMS is zero - bulkWrite on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -1059,6 +1095,8 @@ tests: expectError: isTimeoutError: true - description: "operation is retried multiple times for non-zero timeoutMS - listDatabases on client" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -1100,6 +1138,8 @@ tests: listDatabases: 1 maxTimeMS: { $$type: ["int", "long"] } - description: "operation is retried multiple times if timeoutMS is zero - listDatabases on client" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -1163,6 +1203,8 @@ tests: expectError: isTimeoutError: true - description: "operation is retried multiple times for non-zero timeoutMS - listDatabaseNames on client" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -1203,6 +1245,8 @@ tests: listDatabases: 1 maxTimeMS: { $$type: ["int", "long"] } - description: "operation is retried multiple times if timeoutMS is zero - listDatabaseNames on client" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -1267,6 +1311,8 @@ tests: expectError: isTimeoutError: true - description: "operation is retried multiple times for non-zero timeoutMS - createChangeStream on client" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -1308,6 +1354,8 @@ tests: aggregate: 1 maxTimeMS: { $$type: ["int", "long"] } - description: "operation is retried multiple times if timeoutMS is zero - createChangeStream on client" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -1373,6 +1421,8 @@ tests: expectError: isTimeoutError: true - description: "operation is retried multiple times for non-zero timeoutMS - aggregate on database" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -1414,6 +1464,8 @@ tests: aggregate: 1 maxTimeMS: { $$type: ["int", "long"] } - description: "operation is retried multiple times if timeoutMS is zero - aggregate on database" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -1479,6 +1531,8 @@ tests: expectError: isTimeoutError: true - description: "operation is retried multiple times for non-zero timeoutMS - listCollections on database" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -1520,6 +1574,8 @@ tests: listCollections: 1 maxTimeMS: { $$type: ["int", "long"] } - description: "operation is retried multiple times if timeoutMS is zero - listCollections on database" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -1585,6 +1641,8 @@ tests: expectError: isTimeoutError: true - description: "operation is retried multiple times for non-zero timeoutMS - listCollectionNames on database" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -1626,6 +1684,8 @@ tests: listCollections: 1 maxTimeMS: { $$type: ["int", "long"] } - description: "operation is retried multiple times if timeoutMS is zero - listCollectionNames on database" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -1691,6 +1751,8 @@ tests: expectError: isTimeoutError: true - description: "operation is retried multiple times for non-zero timeoutMS - createChangeStream on database" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -1732,6 +1794,8 @@ tests: aggregate: 1 maxTimeMS: { $$type: ["int", "long"] } - description: "operation is retried multiple times if timeoutMS is zero - createChangeStream on database" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -1797,6 +1861,8 @@ tests: expectError: isTimeoutError: true - description: "operation is retried multiple times for non-zero timeoutMS - aggregate on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -1838,6 +1904,8 @@ tests: aggregate: *collectionName maxTimeMS: { $$type: ["int", "long"] } - description: "operation is retried multiple times if timeoutMS is zero - aggregate on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -1903,6 +1971,8 @@ tests: expectError: isTimeoutError: true - description: "operation is retried multiple times for non-zero timeoutMS - count on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -1944,6 +2014,8 @@ tests: count: *collectionName maxTimeMS: { $$type: ["int", "long"] } - description: "operation is retried multiple times if timeoutMS is zero - count on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -2009,6 +2081,8 @@ tests: expectError: isTimeoutError: true - description: "operation is retried multiple times for non-zero timeoutMS - countDocuments on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -2050,6 +2124,8 @@ tests: aggregate: *collectionName maxTimeMS: { $$type: ["int", "long"] } - description: "operation is retried multiple times if timeoutMS is zero - countDocuments on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -2113,6 +2189,8 @@ tests: expectError: isTimeoutError: true - description: "operation is retried multiple times for non-zero timeoutMS - estimatedDocumentCount on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -2153,6 +2231,8 @@ tests: count: *collectionName maxTimeMS: { $$type: ["int", "long"] } - description: "operation is retried multiple times if timeoutMS is zero - estimatedDocumentCount on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -2218,6 +2298,8 @@ tests: expectError: isTimeoutError: true - description: "operation is retried multiple times for non-zero timeoutMS - distinct on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -2260,6 +2342,8 @@ tests: distinct: *collectionName maxTimeMS: { $$type: ["int", "long"] } - description: "operation is retried multiple times if timeoutMS is zero - distinct on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -2326,6 +2410,8 @@ tests: expectError: isTimeoutError: true - description: "operation is retried multiple times for non-zero timeoutMS - find on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -2367,6 +2453,8 @@ tests: find: *collectionName maxTimeMS: { $$type: ["int", "long"] } - description: "operation is retried multiple times if timeoutMS is zero - find on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -2432,6 +2520,8 @@ tests: expectError: isTimeoutError: true - description: "operation is retried multiple times for non-zero timeoutMS - findOne on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -2473,6 +2563,8 @@ tests: find: *collectionName maxTimeMS: { $$type: ["int", "long"] } - description: "operation is retried multiple times if timeoutMS is zero - findOne on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -2536,6 +2628,8 @@ tests: expectError: isTimeoutError: true - description: "operation is retried multiple times for non-zero timeoutMS - listIndexes on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -2576,6 +2670,8 @@ tests: listIndexes: *collectionName maxTimeMS: { $$type: ["int", "long"] } - description: "operation is retried multiple times if timeoutMS is zero - listIndexes on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -2640,6 +2736,8 @@ tests: expectError: isTimeoutError: true - description: "operation is retried multiple times for non-zero timeoutMS - createChangeStream on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner @@ -2681,6 +2779,8 @@ tests: aggregate: *collectionName maxTimeMS: { $$type: ["int", "long"] } - description: "operation is retried multiple times if timeoutMS is zero - createChangeStream on collection" + runOnRequirements: + - minServerVersion: "4.3.1" # failCommand errorLabels option operations: - name: failPoint object: testRunner diff --git a/test/spec/client-side-operations-timeout/tailable-awaitData.json b/test/spec/client-side-operations-timeout/tailable-awaitData.json index 6da85c7783..535fb69243 100644 --- a/test/spec/client-side-operations-timeout/tailable-awaitData.json +++ b/test/spec/client-side-operations-timeout/tailable-awaitData.json @@ -17,7 +17,7 @@ "client": { "id": "client", "uriOptions": { - "timeoutMS": 10 + "timeoutMS": 200 }, "useMultipleMongoses": false, "observeEvents": [ @@ -130,7 +130,7 @@ "find" ], "blockConnection": true, - "blockTimeMS": 15 + "blockTimeMS": 300 } } } @@ -188,7 +188,7 @@ "getMore" ], "blockConnection": true, - "blockTimeMS": 15 + "blockTimeMS": 150 } } } @@ -199,7 +199,7 @@ "arguments": { "filter": {}, "cursorType": "tailableAwait", - "timeoutMS": 20, + "timeoutMS": 250, "batchSize": 1 }, "saveResultAsEntity": "tailableCursor" @@ -272,7 +272,7 @@ "getMore" ], "blockConnection": true, - "blockTimeMS": 15 + "blockTimeMS": 150 } } } @@ -283,7 +283,7 @@ "arguments": { "filter": {}, "cursorType": "tailableAwait", - "timeoutMS": 20, + "timeoutMS": 250, "batchSize": 1, "maxAwaitTimeMS": 1 }, @@ -354,7 +354,7 @@ "getMore" ], "blockConnection": true, - "blockTimeMS": 15 + "blockTimeMS": 250 } } } diff --git a/test/spec/client-side-operations-timeout/tailable-awaitData.yml b/test/spec/client-side-operations-timeout/tailable-awaitData.yml index 422c6fb537..52b9b3b456 100644 --- a/test/spec/client-side-operations-timeout/tailable-awaitData.yml +++ b/test/spec/client-side-operations-timeout/tailable-awaitData.yml @@ -12,7 +12,7 @@ createEntities: - client: id: &client client uriOptions: - timeoutMS: 10 + timeoutMS: 200 useMultipleMongoses: false observeEvents: - commandStartedEvent @@ -83,7 +83,7 @@ tests: data: failCommands: ["find"] blockConnection: true - blockTimeMS: 15 + blockTimeMS: 300 - name: find object: *collection arguments: @@ -117,13 +117,13 @@ tests: data: failCommands: ["find", "getMore"] blockConnection: true - blockTimeMS: 15 + blockTimeMS: 150 - name: createFindCursor object: *collection arguments: filter: {} cursorType: tailableAwait - timeoutMS: 20 + timeoutMS: 250 batchSize: 1 saveResultAsEntity: &tailableCursor tailableCursor # Iterate twice to force a getMore. The first iteration will return the document from the first batch and the @@ -165,13 +165,13 @@ tests: data: failCommands: ["find", "getMore"] blockConnection: true - blockTimeMS: 15 + blockTimeMS: 150 - name: createFindCursor object: *collection arguments: filter: {} cursorType: tailableAwait - timeoutMS: 20 + timeoutMS: 250 batchSize: 1 maxAwaitTimeMS: 1 saveResultAsEntity: &tailableCursor tailableCursor @@ -199,8 +199,8 @@ tests: collection: *collectionName maxTimeMS: 1 - # The timeoutMS value should be refreshed for getMore's. This is a failure test. The find inherits timeoutMS=10 from - # the collection and the getMore blocks for 15ms, causing iteration to fail with a timeout error. + # The timeoutMS value should be refreshed for getMore's. This is a failure test. The find inherits timeoutMS=200 from + # the collection and the getMore blocks for 250ms, causing iteration to fail with a timeout error. - description: "timeoutMS is refreshed for getMore - failure" operations: - name: failPoint @@ -213,7 +213,7 @@ tests: data: failCommands: ["getMore"] blockConnection: true - blockTimeMS: 15 + blockTimeMS: 250 - name: createFindCursor object: *collection arguments: diff --git a/test/spec/client-side-operations-timeout/tailable-non-awaitData.json b/test/spec/client-side-operations-timeout/tailable-non-awaitData.json index 34ee660963..e88230e4f7 100644 --- a/test/spec/client-side-operations-timeout/tailable-non-awaitData.json +++ b/test/spec/client-side-operations-timeout/tailable-non-awaitData.json @@ -17,7 +17,7 @@ "client": { "id": "client", "uriOptions": { - "timeoutMS": 10 + "timeoutMS": 200 }, "useMultipleMongoses": false, "observeEvents": [ @@ -94,7 +94,7 @@ "find" ], "blockConnection": true, - "blockTimeMS": 15 + "blockTimeMS": 250 } } } @@ -154,7 +154,7 @@ "getMore" ], "blockConnection": true, - "blockTimeMS": 15 + "blockTimeMS": 150 } } } @@ -165,7 +165,7 @@ "arguments": { "filter": {}, "cursorType": "tailable", - "timeoutMS": 20, + "timeoutMS": 200, "batchSize": 1 }, "saveResultAsEntity": "tailableCursor" @@ -239,7 +239,7 @@ "getMore" ], "blockConnection": true, - "blockTimeMS": 15 + "blockTimeMS": 250 } } } diff --git a/test/spec/client-side-operations-timeout/tailable-non-awaitData.yml b/test/spec/client-side-operations-timeout/tailable-non-awaitData.yml index 766b46e658..eb75deaa65 100644 --- a/test/spec/client-side-operations-timeout/tailable-non-awaitData.yml +++ b/test/spec/client-side-operations-timeout/tailable-non-awaitData.yml @@ -12,7 +12,7 @@ createEntities: - client: id: &client client uriOptions: - timeoutMS: 10 + timeoutMS: 200 useMultipleMongoses: false observeEvents: - commandStartedEvent @@ -59,7 +59,7 @@ tests: data: failCommands: ["find"] blockConnection: true - blockTimeMS: 15 + blockTimeMS: 250 - name: find object: *collection arguments: @@ -96,13 +96,13 @@ tests: data: failCommands: ["find", "getMore"] blockConnection: true - blockTimeMS: 15 + blockTimeMS: 150 - name: createFindCursor object: *collection arguments: filter: {} cursorType: tailable - timeoutMS: 20 + timeoutMS: 200 batchSize: 1 saveResultAsEntity: &tailableCursor tailableCursor # Iterate the cursor twice: the first iteration will return the document from the batch in the find and the @@ -131,7 +131,7 @@ tests: maxTimeMS: { $$exists: false } # The timeoutMS option should apply separately to the initial "find" and each getMore. This is a failure test. The - # find inherits timeoutMS=10 from the collection and the getMore command blocks for 15ms, causing iteration to fail + # find inherits timeoutMS=200 from the collection and the getMore command blocks for 250ms, causing iteration to fail # with a timeout error. - description: "timeoutMS is refreshed for getMore - failure" operations: @@ -145,7 +145,7 @@ tests: data: failCommands: ["getMore"] blockConnection: true - blockTimeMS: 15 + blockTimeMS: 250 - name: createFindCursor object: *collection arguments: diff --git a/test/tools/unified-spec-runner/operations.ts b/test/tools/unified-spec-runner/operations.ts index 5b5b704069..31414fa466 100644 --- a/test/tools/unified-spec-runner/operations.ts +++ b/test/tools/unified-spec-runner/operations.ts @@ -215,7 +215,8 @@ operations.set('close', async ({ entities, operation }) => { /* eslint-disable no-empty */ try { const cursor = entities.getEntity('cursor', operation.object); - await cursor.close(); + const timeoutMS = operation.arguments?.timeoutMS; + await cursor.close({ timeoutMS }); return; } catch {} @@ -787,7 +788,9 @@ operations.set('runCursorCommand', async ({ entities, operation }: OperationFunc const { command, ...opts } = operation.arguments!; const cursor = db.runCursorCommand(command, { readPreference: ReadPreference.fromOptions({ readPreference: opts.readPreference }), - session: opts.session + session: opts.session, + timeoutMode: opts.timeoutMode, + timeoutMS: opts.timeoutMS }); if (!Number.isNaN(+opts.batchSize)) cursor.setBatchSize(+opts.batchSize); diff --git a/test/unit/cursor/aggregation_cursor.test.ts b/test/unit/cursor/aggregation_cursor.test.ts index 32ca4125ff..82ae18745b 100644 --- a/test/unit/cursor/aggregation_cursor.test.ts +++ b/test/unit/cursor/aggregation_cursor.test.ts @@ -1,6 +1,12 @@ import { expect } from 'chai'; -import { type AggregationCursor, MongoClient } from '../../mongodb'; +import { + AggregationCursor, + CursorTimeoutMode, + MongoAPIError, + MongoClient, + ns +} from '../../mongodb'; describe('class AggregationCursor', () => { let client: MongoClient; @@ -126,6 +132,38 @@ describe('class AggregationCursor', () => { }); context('when addStage, bespoke stage methods, or array is used to construct pipeline', () => { + context('when CSOT is enabled', () => { + let aggregationCursor: AggregationCursor; + before(function () { + aggregationCursor = client + .db('test') + .collection('test') + .aggregate([], { timeoutMS: 100, timeoutMode: CursorTimeoutMode.ITERATION }); + }); + + context('when a $out stage is added with .addStage()', () => { + it('throws a MongoAPIError', function () { + expect(() => { + aggregationCursor.addStage({ $out: 'test' }); + }).to.throw(MongoAPIError); + }); + }); + context('when a $merge stage is added with .addStage()', () => { + it('throws a MongoAPIError', function () { + expect(() => { + aggregationCursor.addStage({ $merge: {} }); + }).to.throw(MongoAPIError); + }); + }); + context('when a $out stage is added with .out()', () => { + it('throws a MongoAPIError', function () { + expect(() => { + aggregationCursor.out('test'); + }).to.throw(MongoAPIError); + }); + }); + }); + it('sets deeply identical aggregations pipelines', () => { const collection = client.db().collection('test'); @@ -157,4 +195,31 @@ describe('class AggregationCursor', () => { expect(builderGenericStageCursor.pipeline).to.deep.equal(expectedPipeline); }); }); + + describe('constructor()', () => { + context('when CSOT is enabled', () => { + context('when timeoutMode=ITERATION and a $out stage is provided', function () { + it('throws a MongoAPIError', function () { + expect( + () => + new AggregationCursor(client, ns('db.coll'), [{ $out: 'test' }], { + timeoutMS: 100, + timeoutMode: 'iteration' + }) + ).to.throw(MongoAPIError); + }); + }); + context('when timeoutMode=ITERATION and a $merge stage is provided', function () { + it('throws a MongoAPIError', function () { + expect( + () => + new AggregationCursor(client, ns('db.coll'), [{ $merge: 'test' }], { + timeoutMS: 100, + timeoutMode: 'iteration' + }) + ).to.throw(MongoAPIError); + }); + }); + }); + }); });