From 514f8b3ecee08180ae0609e0c3dad9bca933c5ac Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 25 Apr 2023 17:29:10 -0400 Subject: [PATCH 01/18] feat(NODE-5019): add runCursorCommand API --- etc/sync-wip-spec.sh | 25 + src/cursor/abstract_cursor.ts | 2 +- src/cursor/run_command_cursor.ts | 135 +++ src/db.ts | 14 + src/index.ts | 3 + .../run-command/run_command.spec.test.ts | 10 +- .../run-command/run_cursor_command.test.ts | 53 ++ test/mongodb.ts | 1 + test/spec/run-command/runCommand.json | 151 ++++ test/spec/run-command/runCommand.yml | 74 ++ test/spec/run-command/runCursorCommand.json | 850 ++++++++++++++++++ test/spec/run-command/runCursorCommand.yml | 380 ++++++++ .../entity-createRunCursorCommand.json | 71 ++ .../invalid/entity-createRunCursorCommand.yml | 31 + .../valid-pass/entity-commandCursor.json | 278 ++++++ .../valid-pass/entity-commandCursor.yml | 115 +++ test/tools/unified-spec-runner/operations.ts | 83 +- 17 files changed, 2259 insertions(+), 17 deletions(-) create mode 100644 etc/sync-wip-spec.sh create mode 100644 src/cursor/run_command_cursor.ts create mode 100644 test/integration/run-command/run_cursor_command.test.ts create mode 100644 test/spec/run-command/runCursorCommand.json create mode 100644 test/spec/run-command/runCursorCommand.yml create mode 100644 test/spec/unified-test-format/invalid/entity-createRunCursorCommand.json create mode 100644 test/spec/unified-test-format/invalid/entity-createRunCursorCommand.yml create mode 100644 test/spec/unified-test-format/valid-pass/entity-commandCursor.json create mode 100644 test/spec/unified-test-format/valid-pass/entity-commandCursor.yml diff --git a/etc/sync-wip-spec.sh b/etc/sync-wip-spec.sh new file mode 100644 index 0000000000..fd6386621f --- /dev/null +++ b/etc/sync-wip-spec.sh @@ -0,0 +1,25 @@ +#! /usr/bin/env bash + +set -o xtrace +set -o errexit + +pushd "$HOME/code/drivers/specifications/source" +make +popd + +SOURCE="$HOME/code/drivers/specifications/source/run-command/tests/unified" + +for file in $SOURCE/*; do + cp "$file" "test/spec/run-command/$(basename "$file")" +done + +# cp "$HOME/code/drivers/specifications/source/unified-test-format/tests/invalid/entity-createRunCursorCommand.yml" test/spec/unified-test-format/invalid/entity-createRunCursorCommand.yml +# cp "$HOME/code/drivers/specifications/source/unified-test-format/tests/invalid/entity-createRunCursorCommand.json" test/spec/unified-test-format/invalid/entity-createRunCursorCommand.json + +cp "$HOME/code/drivers/specifications/source/unified-test-format/tests/valid-pass/entity-commandCursor.yml" test/spec/unified-test-format/valid-pass/entity-commandCursor.yml +cp "$HOME/code/drivers/specifications/source/unified-test-format/tests/valid-pass/entity-commandCursor.json" test/spec/unified-test-format/valid-pass/entity-commandCursor.json + + +if [ -z "$MONGODB_URI" ]; then echo "must set uri" && exit 1; fi +export MONGODB_URI=$MONGODB_URI +npm run check:test -- -g '(RunCommand spec)|(Unified test format runner runCursorCommand)' diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index 3cc8fdf6c2..4c5cb83742 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -608,7 +608,7 @@ export abstract class AbstractCursor< abstract clone(): AbstractCursor; /** @internal */ - abstract _initialize( + protected abstract _initialize( session: ClientSession | undefined, callback: Callback ): void; diff --git a/src/cursor/run_command_cursor.ts b/src/cursor/run_command_cursor.ts new file mode 100644 index 0000000000..741ec0bd4c --- /dev/null +++ b/src/cursor/run_command_cursor.ts @@ -0,0 +1,135 @@ +import type { BSONSerializeOptions, Document, Long } from '../bson'; +import type { Db } from '../db'; +import { MongoAPIError, MongoUnexpectedServerResponseError } from '../error'; +import { executeOperation, ExecutionResult } from '../operations/execute_operation'; +import { GetMoreOperation } from '../operations/get_more'; +import { RunCommandOperation } from '../operations/run_command'; +import type { ReadConcernLike } from '../read_concern'; +import type { ReadPreferenceLike } from '../read_preference'; +import type { ClientSession } from '../sessions'; +import { Callback, ns } from '../utils'; +import { AbstractCursor } from './abstract_cursor'; + +/** @public */ +export type RunCommandCursorOptions = { + readPreference?: ReadPreferenceLike; + session?: ClientSession; +} & BSONSerializeOptions; + +/** @internal */ +type RunCursorCommandResponse = { + cursor: { id: bigint | Long | number; ns: string; firstBatch: Document[] }; + ok: 1; +}; + +/** @public */ +export class RunCommandCursor extends AbstractCursor { + public readonly command: Readonly>; + public readonly getMoreOptions: { + comment?: any; + maxAwaitTimeMS?: number; + batchSize?: number; + } = {}; + + /** + * Controls the `getMore.comment` field + * @param comment - any BSON value + */ + public setComment(comment: any): this { + this.getMoreOptions.comment = comment; + return this; + } + + /** + * Controls the `getMore.maxTimeMS` field. Only valid when cursor is tailable await + * @param maxTimeMS - the number of milliseconds to wait for new data + */ + public setMaxTimeMS(maxTimeMS: number): this { + this.getMoreOptions.maxAwaitTimeMS = maxTimeMS; + return this; + } + + /** + * Controls the `getMore.batchSize` field + * @param maxTimeMS - the number documents to return in the `nextBatch` + */ + public setBatchSize(batchSize: number): this { + this.getMoreOptions.batchSize = batchSize; + return this; + } + + public clone(): never { + throw new MongoAPIError('RunCommandCursor cannot be cloned'); + } + + public override withReadConcern(_: ReadConcernLike): never { + throw new MongoAPIError( + 'RunCommandCursor does not support readConcern it must be attached to the command being run' + ); + } + + public override addCursorFlag(_: string, __: boolean): never { + throw new MongoAPIError( + 'RunCommandCursor does not support cursor flags, they must be attached to the command being run' + ); + } + + public override maxTimeMS(_: number): never { + throw new MongoAPIError( + 'RunCommandCursor does not support maxTimeMS, it must be attached to the command being run' + ); + } + + public override batchSize(_: number): never { + throw new MongoAPIError( + 'RunCommandCursor does not support batchSize, it must be attached to the command being run' + ); + } + + /** @internal */ + private db: Db | undefined; + + /** @internal */ + constructor(db: Db, command: Document, options: RunCommandCursorOptions = {}) { + super(db.s.client, ns(db.namespace), options); + this.db = db; + this.command = Object.freeze({ ...command }); + } + + /** @internal */ + protected _initialize(session: ClientSession, callback: Callback) { + const operation = new RunCommandOperation(this.db, this.command, { + ...this.cursorOptions, + session: session, + readPreference: this.cursorOptions.readPreference + }); + executeOperation(this.client, operation).then( + response => { + if (!response.cursor) { + callback( + new MongoUnexpectedServerResponseError('Expected server to respond with cursor') + ); + return; + } + callback(undefined, { + server: operation.server, + session, + response + }); + }, + err => callback(err) + ); + } + + /** @internal */ + override _getMore(_batchSize: number, callback: Callback) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const getMoreOperation = new GetMoreOperation(this.namespace, this.id!, this.server!, { + ...this.cursorOptions, + session: this.session, + ...this.getMoreOptions + }); + + executeOperation(this.client, getMoreOperation, callback); + } +} diff --git a/src/db.ts b/src/db.ts index 4d3ddeba24..64f727315f 100644 --- a/src/db.ts +++ b/src/db.ts @@ -5,6 +5,7 @@ import { Collection, CollectionOptions } from './collection'; import * as CONSTANTS from './constants'; import { AggregationCursor } from './cursor/aggregation_cursor'; import { ListCollectionsCursor } from './cursor/list_collections_cursor'; +import { RunCommandCursor, type RunCommandCursorOptions } from './cursor/run_command_cursor'; import { MongoAPIError, MongoInvalidArgumentError } from './error'; import type { MongoClient, PkFactory } from './mongo_client'; import type { TODO_NODE_3286 } from './mongo_types'; @@ -523,6 +524,19 @@ export class Db { return new ChangeStream(this, pipeline, resolveOptions(this, options)); } + + /** + * A low level cursor API providing basic driver functionality. + * - ClientSession management + * - ReadPreference for server selection + * - Running getMores automatically when a local batch is exhausted + * + * @param command - The command that will start a cursor on the server. + * @param options - Configurations for running the command, bson options will apply to getMores + */ + runCursorCommand(command: Document, options?: RunCommandCursorOptions): RunCommandCursor { + return new RunCommandCursor(this, command, options); + } } // TODO(NODE-3484): Refactor into MongoDBNamespace diff --git a/src/index.ts b/src/index.ts index e3b1950cfb..e200def71a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ import { AggregationCursor } from './cursor/aggregation_cursor'; import { FindCursor } from './cursor/find_cursor'; import { ListCollectionsCursor } from './cursor/list_collections_cursor'; import { ListIndexesCursor } from './cursor/list_indexes_cursor'; +import type { RunCommandCursor } from './cursor/run_command_cursor'; import { Db } from './db'; import { GridFSBucket } from './gridfs'; import { GridFSBucketReadStream } from './gridfs/download'; @@ -87,6 +88,7 @@ export { ListIndexesCursor, MongoClient, OrderedBulkOperation, + RunCommandCursor, UnorderedBulkOperation }; @@ -275,6 +277,7 @@ export type { ChangeStreamAggregateRawResult, ChangeStreamCursorOptions } from './cursor/change_stream_cursor'; +export type { RunCommandCursorOptions } from './cursor/run_command_cursor'; export type { DbOptions, DbPrivate } from './db'; export type { AutoEncrypter, AutoEncryptionOptions, AutoEncryptionTlsOptions } from './deps'; export type { Encrypter, EncrypterOptions } from './encrypter'; diff --git a/test/integration/run-command/run_command.spec.test.ts b/test/integration/run-command/run_command.spec.test.ts index c2ca5e91b5..b8e43c4d43 100644 --- a/test/integration/run-command/run_command.spec.test.ts +++ b/test/integration/run-command/run_command.spec.test.ts @@ -2,5 +2,13 @@ import { loadSpecTests } from '../../spec'; import { runUnifiedSuite } from '../../tools/unified-spec-runner/runner'; describe('RunCommand spec', () => { - runUnifiedSuite(loadSpecTests('run-command')); + runUnifiedSuite(loadSpecTests('run-command'), test => { + if (test.description.includes('timeoutMS') || test.description.includes('timeoutMode')) { + return 'CSOT not implemented in Node.js yet'; + } + if (test.description === 'does not attach $readPreference to given command on standalone') { + return 'TODO(NODE-5263): Do not send $readPreference to standalone servers'; + } + return false; + }); }); diff --git a/test/integration/run-command/run_cursor_command.test.ts b/test/integration/run-command/run_cursor_command.test.ts new file mode 100644 index 0000000000..9ee568ccea --- /dev/null +++ b/test/integration/run-command/run_cursor_command.test.ts @@ -0,0 +1,53 @@ +import { expect } from 'chai'; + +import { CommandStartedEvent, Db, MongoClient } from '../../mongodb'; + +describe('class RunCommandCursor', () => { + let client: MongoClient; + let db: Db; + let commandsStarted: CommandStartedEvent[]; + + beforeEach(async function () { + client = this.configuration.newClient({}, { monitorCommands: true }); + db = client.db(); + await db.dropDatabase().catch(() => null); + await db + .collection<{ _id: number }>('collection') + .insertMany([{ _id: 0 }, { _id: 1 }, { _id: 2 }]); + commandsStarted = []; + client.on('commandStarted', started => commandsStarted.push(started)); + }); + + afterEach(async function () { + commandsStarted = []; + await client.close(); + }); + + it('should only run init command once', async () => { + const cursor = db.runCursorCommand({ find: 'collection', filter: {}, batchSize: 1 }); + cursor.setBatchSize(1); + const it0 = cursor[Symbol.asyncIterator](); + const it1 = cursor[Symbol.asyncIterator](); + + const next0it0 = await it0.next(); // find, 1 doc + const next0it1 = await it1.next(); // getMore, 1 doc + + expect(next0it0).to.deep.equal({ value: { _id: 0 }, done: false }); + expect(next0it1).to.deep.equal({ value: { _id: 1 }, done: false }); + expect(commandsStarted.map(c => c.commandName)).to.have.lengthOf(2); + + const next1it0 = await it0.next(); // getMore, 1 doc + const next1it1 = await it1.next(); // getMore, 0 doc & exhausted id + + expect(next1it0).to.deep.equal({ value: { _id: 2 }, done: false }); + expect(next1it1).to.deep.equal({ value: undefined, done: true }); + expect(commandsStarted.map(c => c.commandName)).to.have.lengthOf(4); + + const next2it0 = await it0.next(); + const next2it1 = await it1.next(); + + expect(next2it0).to.deep.equal({ value: undefined, done: true }); + expect(next2it1).to.deep.equal({ value: undefined, done: true }); + expect(commandsStarted.map(c => c.commandName)).to.have.lengthOf(4); + }); +}); diff --git a/test/mongodb.ts b/test/mongodb.ts index 18327be63b..85ce656c81 100644 --- a/test/mongodb.ts +++ b/test/mongodb.ts @@ -138,6 +138,7 @@ export * from '../src/cursor/change_stream_cursor'; export * from '../src/cursor/find_cursor'; export * from '../src/cursor/list_collections_cursor'; export * from '../src/cursor/list_indexes_cursor'; +export * from '../src/cursor/run_command_cursor'; export * from '../src/db'; export * from '../src/deps'; export * from '../src/encrypter'; diff --git a/test/spec/run-command/runCommand.json b/test/spec/run-command/runCommand.json index 0ae0e9d66e..245043bc2c 100644 --- a/test/spec/run-command/runCommand.json +++ b/test/spec/run-command/runCommand.json @@ -123,6 +123,60 @@ } ] }, + { + "description": "always gossips the $clusterTime on the sent command", + "operations": [ + { + "name": "runCommand", + "object": "db", + "arguments": { + "commandName": "ping", + "command": { + "ping": 1 + } + }, + "expectResult": { + "ok": 1 + } + }, + { + "name": "runCommand", + "object": "db", + "arguments": { + "commandName": "ping", + "command": { + "ping": 1 + } + }, + "expectResult": { + "ok": 1 + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "commandName": "ping" + } + }, + { + "commandStartedEvent": { + "command": { + "ping": 1, + "$clusterTime": { + "$$exists": true + } + }, + "commandName": "ping" + } + } + ] + } + ] + }, { "description": "attaches the provided session lsid to given command", "operations": [ @@ -163,6 +217,16 @@ }, { "description": "attaches the provided $readPreference to given command", + "runOnRequirements": [ + { + "topologies": [ + "replicaset", + "sharded-replicaset", + "load-balanced", + "sharded" + ] + } + ], "operations": [ { "name": "runCommand", @@ -201,6 +265,93 @@ } ] }, + { + "description": "does not attach $readPreference to given command on standalone", + "runOnRequirements": [ + { + "topologies": [ + "single" + ] + } + ], + "operations": [ + { + "name": "runCommand", + "object": "db", + "arguments": { + "commandName": "ping", + "command": { + "ping": 1 + }, + "readPreference": { + "mode": "nearest" + } + }, + "expectResult": { + "ok": 1 + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "ping": 1, + "$readPreference": { + "$$exists": false + }, + "$db": "db" + }, + "commandName": "ping" + } + } + ] + } + ] + }, + { + "description": "does not attach primary $readPreference to given command", + "operations": [ + { + "name": "runCommand", + "object": "db", + "arguments": { + "commandName": "ping", + "command": { + "ping": 1 + }, + "readPreference": { + "mode": "primary" + } + }, + "expectResult": { + "ok": 1 + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "ping": 1, + "$readPreference": { + "$$exists": false + }, + "$db": "db" + }, + "commandName": "ping" + } + } + ] + } + ] + }, { "description": "does not inherit readConcern specified at the db level", "operations": [ diff --git a/test/spec/run-command/runCommand.yml b/test/spec/run-command/runCommand.yml index 3c5f231361..450490e2a9 100644 --- a/test/spec/run-command/runCommand.yml +++ b/test/spec/run-command/runCommand.yml @@ -67,6 +67,33 @@ tests: $readPreference: { $$exists: false } commandName: ping + - description: always gossips the $clusterTime on the sent command + operations: + # We have to run one command to obtain a clusterTime to gossip + - name: runCommand + object: *db + arguments: + commandName: ping + command: { ping: 1 } + expectResult: { ok: 1 } + - name: runCommand + object: *db + arguments: + commandName: ping + command: { ping: 1 } + expectResult: { ok: 1 } + expectEvents: + - client: *client + events: + - commandStartedEvent: + commandName: ping + # Only check the shape of the second ping which should have the $clusterTime received from the first operation + - commandStartedEvent: + command: + ping: 1 + $clusterTime: { $$exists: true } + commandName: ping + - description: attaches the provided session lsid to given command operations: - name: runCommand @@ -87,6 +114,9 @@ tests: commandName: ping - description: attaches the provided $readPreference to given command + runOnRequirements: + # Exclude single topology, which is most likely a standalone server + - topologies: [ replicaset, sharded-replicaset, load-balanced, sharded ] operations: - name: runCommand object: *db @@ -105,6 +135,50 @@ tests: $db: *db commandName: ping + - description: does not attach $readPreference to given command on standalone + runOnRequirements: + # This test assumes that the single topology contains a standalone server; + # however, it is possible for a single topology to contain a direct + # connection to another server type. + # See: https://github.com/mongodb/specifications/blob/master/source/server-selection/server-selection.rst#topology-type-single + - topologies: [ single ] + operations: + - name: runCommand + object: *db + arguments: + commandName: ping + command: { ping: 1 } + readPreference: { mode: 'nearest' } + expectResult: { ok: 1 } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + ping: 1 + $readPreference: { $$exists: false } + $db: *db + commandName: ping + + - description: does not attach primary $readPreference to given command + operations: + - name: runCommand + object: *db + arguments: + commandName: ping + command: { ping: 1 } + readPreference: { mode: 'primary' } + expectResult: { ok: 1 } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + ping: 1 + $readPreference: { $$exists: false } + $db: *db + commandName: ping + - description: does not inherit readConcern specified at the db level operations: - name: runCommand diff --git a/test/spec/run-command/runCursorCommand.json b/test/spec/run-command/runCursorCommand.json new file mode 100644 index 0000000000..672c8d93e5 --- /dev/null +++ b/test/spec/run-command/runCursorCommand.json @@ -0,0 +1,850 @@ +{ + "description": "runCursorCommand", + "schemaVersion": "1.9", + "createEntities": [ + { + "client": { + "id": "client", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "session": { + "id": "session", + "client": "client" + } + }, + { + "database": { + "id": "db", + "client": "client", + "databaseName": "db" + } + }, + { + "collection": { + "id": "collection", + "database": "db", + "collectionName": "collection" + } + } + ], + "initialData": [ + { + "collectionName": "collection", + "databaseName": "db", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + }, + { + "_id": 5, + "x": 55 + } + ] + } + ], + "tests": [ + { + "description": "successfully executes checkMetadataConsistency cursor creating command", + "runOnRequirements": [ + { + "minServerVersion": "7.0", + "topologies": [ + "sharded" + ] + } + ], + "operations": [ + { + "name": "runCursorCommand", + "object": "db", + "arguments": { + "commandName": "checkMetadataConsistency", + "command": { + "checkMetadataConsistency": 1 + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "command", + "events": [ + { + "commandStartedEvent": { + "command": { + "checkMetadataConsistency": 1, + "$db": "db", + "lsid": { + "$$exists": true + } + }, + "commandName": "checkMetadataConsistency" + } + } + ] + } + ] + }, + { + "description": "errors if the command response is not a cursor", + "operations": [ + { + "name": "createRunCursorCommand", + "object": "db", + "arguments": { + "commandName": "ping", + "command": { + "ping": 1 + } + }, + "expectError": { + "isClientError": true + } + } + ] + }, + { + "description": "creates an implicit session that is reused across getMores", + "operations": [ + { + "name": "runCursorCommand", + "object": "db", + "arguments": { + "commandName": "find", + "command": { + "find": "collection", + "batchSize": 2 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + }, + { + "_id": 5, + "x": 55 + } + ] + }, + { + "name": "assertSameLsidOnLastTwoCommands", + "object": "testRunner", + "arguments": { + "client": "client" + } + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "command", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collection", + "batchSize": 2, + "$db": "db", + "lsid": { + "$$exists": true + } + }, + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "command": { + "getMore": { + "$$type": [ + "int", + "long" + ] + }, + "collection": "collection", + "$db": "db", + "lsid": { + "$$exists": true + } + }, + "commandName": "getMore" + } + } + ] + } + ] + }, + { + "description": "accepts an explicit session that is reused across getMores", + "operations": [ + { + "name": "runCursorCommand", + "object": "db", + "arguments": { + "commandName": "find", + "session": "session", + "command": { + "find": "collection", + "batchSize": 2 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + }, + { + "_id": 5, + "x": 55 + } + ] + }, + { + "name": "assertSameLsidOnLastTwoCommands", + "object": "testRunner", + "arguments": { + "client": "client" + } + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "command", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collection", + "batchSize": 2, + "$db": "db", + "lsid": { + "$$sessionLsid": "session" + } + }, + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "command": { + "getMore": { + "$$type": [ + "int", + "long" + ] + }, + "collection": "collection", + "$db": "db", + "lsid": { + "$$sessionLsid": "session" + } + }, + "commandName": "getMore" + } + } + ] + } + ] + }, + { + "description": "returns pinned connections to the pool when the cursor is exhausted", + "runOnRequirements": [ + { + "topologies": [ + "load-balanced" + ] + } + ], + "operations": [ + { + "name": "createRunCursorCommand", + "object": "db", + "arguments": { + "commandName": "find", + "batchSize": 2, + "command": { + "find": "collection", + "batchSize": 2 + } + }, + "saveResultAsEntity": "cursor" + }, + { + "name": "assertNumberConnectionsCheckedOut", + "object": "testRunner", + "arguments": { + "client": "client", + "connections": 1 + } + }, + { + "name": "iterateUntilDocumentOrError", + "object": "cursor", + "expectResult": { + "_id": 1, + "x": 11 + } + }, + { + "name": "iterateUntilDocumentOrError", + "object": "cursor", + "expectResult": { + "_id": 2, + "x": 22 + } + }, + { + "name": "iterateUntilDocumentOrError", + "object": "cursor", + "expectResult": { + "_id": 3, + "x": 33 + } + }, + { + "name": "iterateUntilDocumentOrError", + "object": "cursor", + "expectResult": { + "_id": 4, + "x": 44 + } + }, + { + "name": "iterateUntilDocumentOrError", + "object": "cursor", + "expectResult": { + "_id": 5, + "x": 55 + } + }, + { + "name": "assertNumberConnectionsCheckedOut", + "object": "testRunner", + "arguments": { + "client": "client", + "connections": 0 + } + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "command", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collection", + "batchSize": 2, + "$db": "db", + "lsid": { + "$$sessionLsid": "session" + } + }, + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "command": { + "getMore": { + "$$type": [ + "int", + "long" + ] + }, + "collection": "collection", + "$db": "db", + "lsid": { + "$$sessionLsid": "session" + } + }, + "commandName": "getMore" + } + } + ] + }, + { + "client": "client", + "eventType": "cmap", + "events": [ + { + "connectionReadyEvent": {} + }, + { + "connectionCheckedOutEvent": {} + }, + { + "connectionCheckedInEvent": {} + } + ] + } + ] + }, + { + "description": "returns pinned connections to the pool when the cursor is closed", + "runOnRequirements": [ + { + "topologies": [ + "load-balanced" + ] + } + ], + "operations": [ + { + "name": "createRunCursorCommand", + "object": "db", + "arguments": { + "commandName": "find", + "command": { + "find": "collection", + "batchSize": 2 + } + }, + "saveResultAsEntity": "cursor" + }, + { + "name": "assertNumberConnectionsCheckedOut", + "object": "testRunner", + "arguments": { + "client": "client", + "connections": 1 + } + }, + { + "name": "close", + "object": "cursor" + }, + { + "name": "assertNumberConnectionsCheckedOut", + "object": "testRunner", + "arguments": { + "client": "client", + "connections": 0 + } + } + ] + }, + { + "description": "supports configuring getMore batchSize", + "operations": [ + { + "name": "runCursorCommand", + "object": "db", + "arguments": { + "commandName": "find", + "batchSize": 5, + "command": { + "find": "collection", + "batchSize": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + }, + { + "_id": 5, + "x": 55 + } + ] + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "command", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collection", + "batchSize": 1, + "$db": "db", + "lsid": { + "$$exists": true + } + }, + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "command": { + "getMore": { + "$$type": [ + "int", + "long" + ] + }, + "collection": "collection", + "batchSize": 5, + "$db": "db", + "lsid": { + "$$exists": true + } + }, + "commandName": "getMore" + } + } + ] + } + ] + }, + { + "description": "supports configuring getMore maxTimeMS", + "operations": [ + { + "name": "runCursorCommand", + "object": "db", + "arguments": { + "commandName": "find", + "maxTimeMS": 300, + "command": { + "find": "collection", + "maxTimeMS": 200, + "batchSize": 1 + } + }, + "ignoreResultAndError": true + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "command", + "ignoreExtraEvents": true, + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collection", + "maxTimeMS": 200, + "batchSize": 1, + "$db": "db", + "lsid": { + "$$exists": true + } + }, + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "command": { + "getMore": { + "$$type": [ + "int", + "long" + ] + }, + "collection": "collection", + "$db": "db", + "maxTimeMS": 300, + "lsid": { + "$$exists": true + } + }, + "commandName": "getMore" + } + } + ] + } + ] + }, + { + "description": "supports configuring getMore comment", + "runOnRequirements": [ + { + "minServerVersion": "4.4" + } + ], + "operations": [ + { + "name": "runCursorCommand", + "object": "db", + "arguments": { + "commandName": "find", + "comment": { + "hello": "getMore" + }, + "command": { + "find": "collection", + "batchSize": 1, + "comment": { + "hello": "find" + } + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + }, + { + "_id": 5, + "x": 55 + } + ] + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "command", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collection", + "batchSize": 1, + "comment": { + "hello": "find" + }, + "$db": "db", + "lsid": { + "$$exists": true + } + }, + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "command": { + "getMore": { + "$$type": [ + "int", + "long" + ] + }, + "collection": "collection", + "comment": { + "hello": "getMore" + }, + "$db": "db", + "lsid": { + "$$exists": true + } + }, + "commandName": "getMore" + } + } + ] + } + ] + }, + { + "description": "does not close the cursor when receiving an empty batch", + "operations": [ + { + "name": "dropCollection", + "object": "db", + "arguments": { + "collection": "cappedCollection" + } + }, + { + "name": "createCollection", + "object": "db", + "arguments": { + "collection": "cappedCollection", + "capped": true, + "size": 4096, + "max": 3 + }, + "saveResultAsEntity": "cappedCollection" + }, + { + "name": "insertMany", + "object": "cappedCollection", + "arguments": { + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + }, + { + "name": "createRunCursorCommand", + "object": "db", + "arguments": { + "cursorType": "tailable", + "commandName": "find", + "batchSize": 2, + "command": { + "find": "cappedCollection", + "tailable": true + } + }, + "saveResultAsEntity": "cursor" + }, + { + "name": "iterateOnce", + "object": "cursor" + }, + { + "name": "iterateOnce", + "object": "cursor" + }, + { + "name": "iterateOnce", + "object": "cursor" + }, + { + "name": "close", + "object": "cursor" + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "command", + "events": [ + { + "commandStartedEvent": { + "command": { + "drop": "cappedCollection" + }, + "commandName": "drop" + } + }, + { + "commandStartedEvent": { + "command": { + "create": "cappedCollection" + }, + "commandName": "create" + } + }, + { + "commandStartedEvent": { + "command": { + "insert": "cappedCollection" + }, + "commandName": "insert" + } + }, + { + "commandStartedEvent": { + "command": { + "find": "cappedCollection", + "$db": "db", + "lsid": { + "$$exists": true + } + }, + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "command": { + "getMore": { + "$$type": [ + "int", + "long" + ] + }, + "collection": "cappedCollection", + "$db": "db", + "lsid": { + "$$exists": true + } + }, + "commandName": "getMore" + } + }, + { + "commandStartedEvent": { + "command": { + "killCursors": "cappedCollection", + "cursors": { + "$$type": "array" + } + }, + "commandName": "killCursors" + } + } + ] + } + ] + } + ] +} diff --git a/test/spec/run-command/runCursorCommand.yml b/test/spec/run-command/runCursorCommand.yml new file mode 100644 index 0000000000..fd025cbb15 --- /dev/null +++ b/test/spec/run-command/runCursorCommand.yml @@ -0,0 +1,380 @@ +description: runCursorCommand + +schemaVersion: '1.9' + +createEntities: + - client: + id: &client client + useMultipleMongoses: false + observeEvents: [commandStartedEvent] + - session: + id: &session session + client: *client + - database: + id: &db db + client: *client + databaseName: *db + - collection: + id: &collection collection + database: *db + collectionName: *collection + +initialData: + - collectionName: collection + databaseName: *db + documents: &documents + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - { _id: 4, x: 44 } + - { _id: 5, x: 55 } + +tests: + # This is what this API was invented to do. + - description: successfully executes checkMetadataConsistency cursor creating command + runOnRequirements: + - minServerVersion: '7.0' + topologies: [sharded] + operations: + - name: runCursorCommand + object: *db + arguments: + commandName: checkMetadataConsistency + command: { checkMetadataConsistency: 1 } + expectEvents: + - client: *client + eventType: command + events: + - commandStartedEvent: + command: + checkMetadataConsistency: 1 + $db: *db + lsid: { $$exists: true } + commandName: checkMetadataConsistency + + - description: errors if the command response is not a cursor + operations: + - name: createRunCursorCommand + object: *db + arguments: + commandName: ping + command: { ping: 1 } + expectError: + isClientError: true + + + # Driver Sessions + - description: creates an implicit session that is reused across getMores + operations: + - name: runCursorCommand + object: *db + arguments: + commandName: find + command: { find: *collection, batchSize: 2 } + expectResult: *documents + - name: assertSameLsidOnLastTwoCommands + object: testRunner + arguments: + client: *client + expectEvents: + - client: *client + eventType: command + events: + - commandStartedEvent: + command: + find: *collection + batchSize: 2 + $db: *db + lsid: { $$exists: true } + commandName: find + - commandStartedEvent: + command: + getMore: { $$type: [int, long] } + collection: *collection + $db: *db + lsid: { $$exists: true } + commandName: getMore + + - description: accepts an explicit session that is reused across getMores + operations: + - name: runCursorCommand + object: *db + arguments: + commandName: find + session: *session + command: { find: *collection, batchSize: 2 } + expectResult: *documents + - name: assertSameLsidOnLastTwoCommands + object: testRunner + arguments: + client: *client + expectEvents: + - client: *client + eventType: command + events: + - commandStartedEvent: + command: + find: *collection + batchSize: 2 + $db: *db + lsid: { $$sessionLsid: *session } + commandName: find + - commandStartedEvent: + command: + getMore: { $$type: [int, long] } + collection: *collection + $db: *db + lsid: { $$sessionLsid: *session } + commandName: getMore + + # Load Balancers + - description: returns pinned connections to the pool when the cursor is exhausted + runOnRequirements: + - topologies: [ load-balanced ] + operations: + - name: createRunCursorCommand + object: *db + arguments: + commandName: find + batchSize: 2 + command: { find: *collection, batchSize: 2 } + saveResultAsEntity: &cursor cursor + - name: assertNumberConnectionsCheckedOut + object: testRunner + arguments: + client: *client + connections: 1 + - name: iterateUntilDocumentOrError + object: *cursor + expectResult: { _id: 1, x: 11 } + - name: iterateUntilDocumentOrError + object: *cursor + expectResult: { _id: 2, x: 22 } + - name: iterateUntilDocumentOrError + object: *cursor + expectResult: { _id: 3, x: 33 } + - name: iterateUntilDocumentOrError + object: *cursor + expectResult: { _id: 4, x: 44 } + - name: iterateUntilDocumentOrError + object: *cursor + expectResult: { _id: 5, x: 55 } + - name: assertNumberConnectionsCheckedOut + object: testRunner + arguments: + client: *client + connections: 0 + expectEvents: + - client: *client + eventType: command + events: + - commandStartedEvent: + command: + find: *collection + batchSize: 2 + $db: *db + lsid: { $$sessionLsid: *session } + commandName: find + - commandStartedEvent: + command: + getMore: { $$type: [int, long] } + collection: *collection + $db: *db + lsid: { $$sessionLsid: *session } + commandName: getMore + - client: *client + eventType: cmap + events: + - connectionReadyEvent: {} + - connectionCheckedOutEvent: {} + - connectionCheckedInEvent: {} + + - description: returns pinned connections to the pool when the cursor is closed + runOnRequirements: + - topologies: [ load-balanced ] + operations: + - name: createRunCursorCommand + object: *db + arguments: + commandName: find + command: { find: *collection, batchSize: 2 } + saveResultAsEntity: *cursor + - name: assertNumberConnectionsCheckedOut + object: testRunner + arguments: + client: *client + connections: 1 + - name: close + object: *cursor + - name: assertNumberConnectionsCheckedOut + object: testRunner + arguments: + client: *client + connections: 0 + + # Iterating the Cursor / Executing GetMores + - description: supports configuring getMore batchSize + operations: + - name: runCursorCommand + object: *db + arguments: + commandName: find + batchSize: 5 + command: { find: *collection, batchSize: 1 } + expectResult: *documents + expectEvents: + - client: *client + eventType: command + events: + - commandStartedEvent: + command: + find: *collection + batchSize: 1 + $db: *db + lsid: { $$exists: true } + commandName: find + - commandStartedEvent: + command: + getMore: { $$type: [int, long] } + collection: *collection + batchSize: 5 + $db: *db + lsid: { $$exists: true } + commandName: getMore + + - description: supports configuring getMore maxTimeMS + operations: + - name: runCursorCommand + object: *db + arguments: + commandName: find + maxTimeMS: 300 + command: { find: *collection, maxTimeMS: 200, batchSize: 1 } + ignoreResultAndError: true + expectEvents: + - client: *client + eventType: command + # The getMore should receive an error here because we do not have the right kind of cursor + # So drivers should run a killCursors, but neither the error nor the killCursors command is relevant to this test + ignoreExtraEvents: true + events: + - commandStartedEvent: + command: + find: *collection + maxTimeMS: 200 + batchSize: 1 + $db: *db + lsid: { $$exists: true } + commandName: find + - commandStartedEvent: + command: + getMore: { $$type: [int, long] } + collection: *collection + $db: *db + maxTimeMS: 300 + lsid: { $$exists: true } + commandName: getMore + + - description: supports configuring getMore comment + runOnRequirements: + - minServerVersion: '4.4' + operations: + - name: runCursorCommand + object: *db + arguments: + commandName: find + comment: { hello: 'getMore' } + command: { find: *collection, batchSize: 1, comment: { hello: 'find' } } + expectResult: *documents + expectEvents: + - client: *client + eventType: command + events: + - commandStartedEvent: + command: + find: *collection + batchSize: 1 + comment: { hello: 'find' } + $db: *db + lsid: { $$exists: true } + commandName: find + - commandStartedEvent: + command: + getMore: { $$type: [int, long] } + collection: *collection + comment: { hello: 'getMore' } + $db: *db + lsid: { $$exists: true } + commandName: getMore + + # Tailable cursor + - description: does not close the cursor when receiving an empty batch + operations: + - name: dropCollection + object: *db + arguments: + collection: &cappedCollection cappedCollection + - name: createCollection + object: *db + arguments: + collection: *cappedCollection + capped: true + size: 4096 + max: 3 + saveResultAsEntity: *cappedCollection + - name: insertMany + object: *cappedCollection + arguments: + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - name: createRunCursorCommand + object: *db + arguments: + cursorType: tailable + commandName: find + batchSize: 2 + command: { find: *cappedCollection, tailable: true } + saveResultAsEntity: &cursor cursor + - name: iterateOnce + object: *cursor + - name: iterateOnce + object: *cursor + - name: iterateOnce + object: *cursor + - name: close + object: *cursor + expectEvents: + - client: *client + eventType: command + events: + - commandStartedEvent: + command: + drop: *cappedCollection + commandName: drop + - commandStartedEvent: + command: + create: *cappedCollection + commandName: create + - commandStartedEvent: + command: + insert: *cappedCollection + commandName: insert + - commandStartedEvent: + command: + find: *cappedCollection + $db: *db + lsid: { $$exists: true } + commandName: find + - commandStartedEvent: + command: + getMore: { $$type: [int, long] } + collection: *cappedCollection + $db: *db + lsid: { $$exists: true } + commandName: getMore + - commandStartedEvent: + command: + killCursors: *cappedCollection + cursors: { $$type: array } + commandName: killCursors diff --git a/test/spec/unified-test-format/invalid/entity-createRunCursorCommand.json b/test/spec/unified-test-format/invalid/entity-createRunCursorCommand.json new file mode 100644 index 0000000000..dea17e5a10 --- /dev/null +++ b/test/spec/unified-test-format/invalid/entity-createRunCursorCommand.json @@ -0,0 +1,71 @@ +{ + "description": "runCursorCommand", + "schemaVersion": "1.3", + "createEntities": [ + { + "client": { + "id": "client", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "db", + "client": "client", + "databaseName": "db" + } + }, + { + "collection": { + "id": "collection", + "database": "db", + "collectionName": "collection" + } + } + ], + "initialData": [ + { + "collectionName": "collection", + "databaseName": "db", + "documents": [ + { + "_id": 1, + "x": 11 + } + ] + } + ], + "tests": [ + { + "description": "runCursorCommand cannot be combined with expectResult", + "operations": [ + { + "name": "runCursorCommand", + "object": "db", + "arguments": { + "commandName": "find", + "batchSize": 2, + "command": { + "find": "collection", + "filter": {}, + "batchSize": 2 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + } + ], + "expectResult": { + "_id": 1, + "x": 11 + } + } + ] +} diff --git a/test/spec/unified-test-format/invalid/entity-createRunCursorCommand.yml b/test/spec/unified-test-format/invalid/entity-createRunCursorCommand.yml new file mode 100644 index 0000000000..b745ed282e --- /dev/null +++ b/test/spec/unified-test-format/invalid/entity-createRunCursorCommand.yml @@ -0,0 +1,31 @@ +description: runCursorCommand +schemaVersion: '1.3' +createEntities: + - client: + id: &client client + useMultipleMongoses: false + observeEvents: [commandStartedEvent] + - database: + id: &db db + client: *client + databaseName: *db + - collection: + id: &collection collection + database: *db + collectionName: *collection +initialData: + - collectionName: collection + databaseName: *db + documents: &documents + - { _id: 1, x: 11 } +tests: + - description: runCursorCommand cannot be combined with expectResult + operations: + - name: runCursorCommand + object: *db + arguments: + commandName: find + batchSize: 2 + command: { find: *collection, filter: {}, batchSize: 2 } + expectResult: *documents + expectResult: { _id: 1, x: 11 } diff --git a/test/spec/unified-test-format/valid-pass/entity-commandCursor.json b/test/spec/unified-test-format/valid-pass/entity-commandCursor.json new file mode 100644 index 0000000000..a4a392ed98 --- /dev/null +++ b/test/spec/unified-test-format/valid-pass/entity-commandCursor.json @@ -0,0 +1,278 @@ +{ + "description": "entity-commandCursor", + "schemaVersion": "1.3", + "createEntities": [ + { + "client": { + "id": "client", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "db", + "client": "client", + "databaseName": "db" + } + }, + { + "collection": { + "id": "collection", + "database": "db", + "collectionName": "collection" + } + } + ], + "initialData": [ + { + "collectionName": "collection", + "databaseName": "db", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + }, + { + "_id": 5, + "x": 55 + } + ] + } + ], + "tests": [ + { + "description": "runCursorCommand creates and exhausts cursor by running getMores", + "operations": [ + { + "name": "runCursorCommand", + "object": "db", + "arguments": { + "commandName": "find", + "batchSize": 2, + "command": { + "find": "collection", + "filter": {}, + "batchSize": 2 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + }, + { + "_id": 5, + "x": 55 + } + ] + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collection", + "filter": {}, + "batchSize": 2, + "$db": "db", + "lsid": { + "$$exists": true + } + }, + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "command": { + "getMore": { + "$$type": [ + "int", + "long" + ] + }, + "collection": "collection", + "$db": "db", + "lsid": { + "$$exists": true + } + }, + "commandName": "getMore" + } + }, + { + "commandStartedEvent": { + "command": { + "getMore": { + "$$type": [ + "int", + "long" + ] + }, + "collection": "collection", + "$db": "db", + "lsid": { + "$$exists": true + } + }, + "commandName": "getMore" + } + } + ] + } + ] + }, + { + "description": "createRunCursorCommand creates a cursor and stores it as an entity that can be iterated one document at a time", + "operations": [ + { + "name": "createRunCursorCommand", + "object": "db", + "arguments": { + "commandName": "find", + "batchSize": 2, + "command": { + "find": "collection", + "filter": {}, + "batchSize": 2 + } + }, + "saveResultAsEntity": "myRunCommandCursor" + }, + { + "name": "iterateUntilDocumentOrError", + "object": "myRunCommandCursor", + "expectResult": { + "_id": 1, + "x": 11 + } + }, + { + "name": "iterateUntilDocumentOrError", + "object": "myRunCommandCursor", + "expectResult": { + "_id": 2, + "x": 22 + } + }, + { + "name": "iterateUntilDocumentOrError", + "object": "myRunCommandCursor", + "expectResult": { + "_id": 3, + "x": 33 + } + }, + { + "name": "iterateUntilDocumentOrError", + "object": "myRunCommandCursor", + "expectResult": { + "_id": 4, + "x": 44 + } + }, + { + "name": "iterateUntilDocumentOrError", + "object": "myRunCommandCursor", + "expectResult": { + "_id": 5, + "x": 55 + } + } + ] + }, + { + "description": "createRunCursorCommand's cursor can be closed and will perform a killCursors operation", + "operations": [ + { + "name": "createRunCursorCommand", + "object": "db", + "arguments": { + "commandName": "find", + "batchSize": 2, + "command": { + "find": "collection", + "filter": {}, + "batchSize": 2 + } + }, + "saveResultAsEntity": "myRunCommandCursor" + }, + { + "name": "iterateUntilDocumentOrError", + "object": "myRunCommandCursor", + "expectResult": { + "_id": 1, + "x": 11 + } + }, + { + "name": "close", + "object": "myRunCommandCursor" + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collection", + "filter": {}, + "batchSize": 2, + "$db": "db", + "lsid": { + "$$exists": true + } + }, + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "command": { + "killCursors": "collection", + "cursors": { + "$$type": "array" + } + }, + "commandName": "killCursors" + } + } + ] + } + ] + } + ] +} diff --git a/test/spec/unified-test-format/valid-pass/entity-commandCursor.yml b/test/spec/unified-test-format/valid-pass/entity-commandCursor.yml new file mode 100644 index 0000000000..18448bb421 --- /dev/null +++ b/test/spec/unified-test-format/valid-pass/entity-commandCursor.yml @@ -0,0 +1,115 @@ +description: entity-commandCursor +schemaVersion: '1.3' +createEntities: + - client: + id: &client client + useMultipleMongoses: false + observeEvents: [commandStartedEvent] + - database: + id: &db db + client: *client + databaseName: *db + - collection: + id: &collection collection + database: *db + collectionName: *collection +initialData: + - collectionName: collection + databaseName: *db + documents: &documents + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - { _id: 4, x: 44 } + - { _id: 5, x: 55 } +tests: + - description: runCursorCommand creates and exhausts cursor by running getMores + operations: + - name: runCursorCommand + object: *db + arguments: + commandName: find + batchSize: 2 + command: { find: *collection, filter: {}, batchSize: 2 } + expectResult: *documents + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + find: *collection + filter: {} + batchSize: 2 + $db: *db + lsid: { $$exists: true } + commandName: find + - commandStartedEvent: + command: + getMore: { $$type: [int, long] } + collection: *collection + $db: *db + lsid: { $$exists: true } + commandName: getMore + - commandStartedEvent: + command: + getMore: { $$type: [int, long] } + collection: *collection + $db: *db + lsid: { $$exists: true } + commandName: getMore + + - description: createRunCursorCommand creates a cursor and stores it as an entity that can be iterated one document at a time + operations: + - name: createRunCursorCommand + object: *db + arguments: + commandName: find + batchSize: 2 + command: { find: *collection, filter: {}, batchSize: 2 } + saveResultAsEntity: &myRunCommandCursor myRunCommandCursor + - name: iterateUntilDocumentOrError + object: *myRunCommandCursor + expectResult: { _id: 1, x: 11 } + - name: iterateUntilDocumentOrError + object: *myRunCommandCursor + expectResult: { _id: 2, x: 22 } + - name: iterateUntilDocumentOrError + object: *myRunCommandCursor + expectResult: { _id: 3, x: 33 } + - name: iterateUntilDocumentOrError + object: *myRunCommandCursor + expectResult: { _id: 4, x: 44 } + - name: iterateUntilDocumentOrError + object: *myRunCommandCursor + expectResult: { _id: 5, x: 55 } + + - description: createRunCursorCommand's cursor can be closed and will perform a killCursors operation + operations: + - name: createRunCursorCommand + object: *db + arguments: + commandName: find + batchSize: 2 + command: { find: *collection, filter: {}, batchSize: 2 } + saveResultAsEntity: myRunCommandCursor + - name: iterateUntilDocumentOrError + object: *myRunCommandCursor + expectResult: { _id: 1, x: 11 } + - name: close + object: *myRunCommandCursor + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + find: *collection + filter: {} + batchSize: 2 + $db: *db + lsid: { $$exists: true } + commandName: find + - commandStartedEvent: + command: + killCursors: *collection + cursors: { $$type: array } + commandName: killCursors diff --git a/test/tools/unified-spec-runner/operations.ts b/test/tools/unified-spec-runner/operations.ts index a2b4e555c8..36e1c3c325 100644 --- a/test/tools/unified-spec-runner/operations.ts +++ b/test/tools/unified-spec-runner/operations.ts @@ -349,27 +349,40 @@ operations.set('insertMany', async ({ entities, operation }) => { return collection.insertMany(documents, opts); }); -operations.set('iterateUntilDocumentOrError', async ({ entities, operation }) => { - function getChangeStream(): UnifiedChangeStream | null { - try { - const changeStream = entities.getEntity('stream', operation.object); - return changeStream; - } catch (e) { - return null; - } +function getChangeStream({ entities, operation }): UnifiedChangeStream | null { + try { + const changeStream = entities.getEntity('stream', operation.object); + return changeStream; + } catch (e) { + return null; } - - const changeStream = getChangeStream(); +} +operations.set('iterateUntilDocumentOrError', async ({ entities, operation }) => { + const changeStream = getChangeStream({ entities, operation }); if (changeStream == null) { // iterateUntilDocumentOrError is used for changes streams and regular cursors. // we have no other way to distinguish which scenario we are testing when we run an // iterateUntilDocumentOrError operation, so we first try to get the changeStream and // if that fails, we know we need to get a cursor const cursor = entities.getEntity('cursor', operation.object); - return await cursor.next(); + return cursor.next(); + } + + return changeStream.next(); +}); + +operations.set('iterateOnce', async ({ entities, operation }) => { + const changeStream = getChangeStream({ entities, operation }); + if (changeStream == null) { + // iterateOnce is used for changes streams and regular cursors. + // we have no other way to distinguish which scenario we are testing when we run an + // iterateOnce operation, so we first try to get the changeStream and + // if that fails, we know we need to get a cursor + const cursor = entities.getEntity('cursor', operation.object); + return cursor.tryNext(); } - return await changeStream.next(); + return changeStream.tryNext(); }); operations.set('listCollections', async ({ entities, operation }) => { @@ -659,6 +672,45 @@ operations.set('runCommand', async ({ entities, operation }: OperationFunctionPa return db.command(command, options); }); +operations.set('runCursorCommand', async ({ entities, operation }: OperationFunctionParams) => { + const db = entities.getEntity('db', operation.object); + const { command, ...opts } = operation.arguments!; + const cursor = db.runCursorCommand(command, { + readPreference: ReadPreference.fromOptions(opts), + session: opts.session + }); + + if (!Number.isNaN(+opts.batchSize)) cursor.setBatchSize(+opts.batchSize); + if (!Number.isNaN(+opts.maxTimeMS)) cursor.setMaxTimeMS(+opts.maxTimeMS); + if (opts.comment !== undefined) cursor.setComment(opts.comment); + + return cursor.toArray(); +}); + +operations.set( + 'createRunCursorCommand', + async ({ entities, operation }: OperationFunctionParams) => { + const collection = entities.getEntity('db', operation.object); + const { command, ...opts } = operation.arguments!; + const cursor = collection.runCursorCommand(command, { + readPreference: ReadPreference.fromOptions(opts), + session: opts.session + }); + + if (!Number.isNaN(+opts.batchSize)) cursor.setBatchSize(+opts.batchSize); + if (!Number.isNaN(+opts.maxTimeMS)) cursor.setMaxTimeMS(+opts.maxTimeMS); + if (opts.comment !== undefined) cursor.setComment(opts.comment); + + // The spec dictates that we create the cursor and force the find command + // to execute, but the first document must still be returned for the first iteration. + const result = await cursor.tryNext(); + const kDocuments = getSymbolFrom(cursor, 'documents'); + if (result) cursor[kDocuments].unshift(result); + + return cursor; + } +); + operations.set('updateMany', async ({ entities, operation }) => { const collection = entities.getEntity('collection', operation.object); const { filter, update, ...options } = operation.arguments!; @@ -741,9 +793,10 @@ export async function executeOperationAndCheck( const opFunc = operations.get(operation.name); expect(opFunc, `Unknown operation: ${operation.name}`).to.exist; - if (operation.arguments?.session) { - const session = entities.getEntity('session', operation.arguments.session, false); - operation.arguments.session = session; + if (typeof operation.arguments?.session === 'string') { + // Cannot do this b/c operations pass through unsanitized options to command construction: + // operation.arguments.__sessionId = operation.arguments.session; + operation.arguments.session = entities.getEntity('session', operation.arguments.session); } let result; From 3298b88ccedb795664813790a1ec4c9ca7fbfde3 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Wed, 17 May 2023 14:06:20 -0400 Subject: [PATCH 02/18] fix: clone --- src/cursor/run_command_cursor.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/cursor/run_command_cursor.ts b/src/cursor/run_command_cursor.ts index 741ec0bd4c..9c43735105 100644 --- a/src/cursor/run_command_cursor.ts +++ b/src/cursor/run_command_cursor.ts @@ -1,4 +1,4 @@ -import type { BSONSerializeOptions, Document, Long } from '../bson'; +import { BSONSerializeOptions, Document, Long, resolveBSONOptions } from '../bson'; import type { Db } from '../db'; import { MongoAPIError, MongoUnexpectedServerResponseError } from '../error'; import { executeOperation, ExecutionResult } from '../operations/execute_operation'; @@ -58,8 +58,11 @@ export class RunCommandCursor extends AbstractCursor { return this; } - public clone(): never { - throw new MongoAPIError('RunCommandCursor cannot be cloned'); + public clone(): RunCommandCursor { + return new RunCommandCursor(this.db, this.command, { + readPreference: this.readPreference, + ...resolveBSONOptions(this.cursorOptions) + }); } public override withReadConcern(_: ReadConcernLike): never { @@ -87,7 +90,7 @@ export class RunCommandCursor extends AbstractCursor { } /** @internal */ - private db: Db | undefined; + private db: Db; /** @internal */ constructor(db: Db, command: Document, options: RunCommandCursorOptions = {}) { From 3da03ad87eb1b31632fa5bff0e0b24cd1ab3c0a2 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Wed, 17 May 2023 15:30:58 -0400 Subject: [PATCH 03/18] sync --- test/spec/run-command/runCursorCommand.json | 8 ++-- test/spec/run-command/runCursorCommand.yml | 8 ++-- .../valid-pass/entity-commandCursor.json | 8 ++-- .../valid-pass/entity-commandCursor.yml | 8 ++-- test/tools/unified-spec-runner/operations.ts | 37 +++++++++---------- 5 files changed, 33 insertions(+), 36 deletions(-) diff --git a/test/spec/run-command/runCursorCommand.json b/test/spec/run-command/runCursorCommand.json index 672c8d93e5..d5c7d23798 100644 --- a/test/spec/run-command/runCursorCommand.json +++ b/test/spec/run-command/runCursorCommand.json @@ -108,7 +108,7 @@ "description": "errors if the command response is not a cursor", "operations": [ { - "name": "createRunCursorCommand", + "name": "createCommandCursor", "object": "db", "arguments": { "commandName": "ping", @@ -302,7 +302,7 @@ ], "operations": [ { - "name": "createRunCursorCommand", + "name": "createCommandCursor", "object": "db", "arguments": { "commandName": "find", @@ -437,7 +437,7 @@ ], "operations": [ { - "name": "createRunCursorCommand", + "name": "createCommandCursor", "object": "db", "arguments": { "commandName": "find", @@ -742,7 +742,7 @@ } }, { - "name": "createRunCursorCommand", + "name": "createCommandCursor", "object": "db", "arguments": { "cursorType": "tailable", diff --git a/test/spec/run-command/runCursorCommand.yml b/test/spec/run-command/runCursorCommand.yml index fd025cbb15..1c221d91be 100644 --- a/test/spec/run-command/runCursorCommand.yml +++ b/test/spec/run-command/runCursorCommand.yml @@ -54,7 +54,7 @@ tests: - description: errors if the command response is not a cursor operations: - - name: createRunCursorCommand + - name: createCommandCursor object: *db arguments: commandName: ping @@ -132,7 +132,7 @@ tests: runOnRequirements: - topologies: [ load-balanced ] operations: - - name: createRunCursorCommand + - name: createCommandCursor object: *db arguments: commandName: find @@ -193,7 +193,7 @@ tests: runOnRequirements: - topologies: [ load-balanced ] operations: - - name: createRunCursorCommand + - name: createCommandCursor object: *db arguments: commandName: find @@ -328,7 +328,7 @@ tests: documents: - { _id: 1, x: 11 } - { _id: 2, x: 22 } - - name: createRunCursorCommand + - name: createCommandCursor object: *db arguments: cursorType: tailable diff --git a/test/spec/unified-test-format/valid-pass/entity-commandCursor.json b/test/spec/unified-test-format/valid-pass/entity-commandCursor.json index a4a392ed98..72b74b4a9a 100644 --- a/test/spec/unified-test-format/valid-pass/entity-commandCursor.json +++ b/test/spec/unified-test-format/valid-pass/entity-commandCursor.json @@ -153,10 +153,10 @@ ] }, { - "description": "createRunCursorCommand creates a cursor and stores it as an entity that can be iterated one document at a time", + "description": "createCommandCursor creates a cursor and stores it as an entity that can be iterated one document at a time", "operations": [ { - "name": "createRunCursorCommand", + "name": "createCommandCursor", "object": "db", "arguments": { "commandName": "find", @@ -212,10 +212,10 @@ ] }, { - "description": "createRunCursorCommand's cursor can be closed and will perform a killCursors operation", + "description": "createCommandCursor's cursor can be closed and will perform a killCursors operation", "operations": [ { - "name": "createRunCursorCommand", + "name": "createCommandCursor", "object": "db", "arguments": { "commandName": "find", diff --git a/test/spec/unified-test-format/valid-pass/entity-commandCursor.yml b/test/spec/unified-test-format/valid-pass/entity-commandCursor.yml index 18448bb421..3becf2095a 100644 --- a/test/spec/unified-test-format/valid-pass/entity-commandCursor.yml +++ b/test/spec/unified-test-format/valid-pass/entity-commandCursor.yml @@ -58,9 +58,9 @@ tests: lsid: { $$exists: true } commandName: getMore - - description: createRunCursorCommand creates a cursor and stores it as an entity that can be iterated one document at a time + - description: createCommandCursor creates a cursor and stores it as an entity that can be iterated one document at a time operations: - - name: createRunCursorCommand + - name: createCommandCursor object: *db arguments: commandName: find @@ -83,9 +83,9 @@ tests: object: *myRunCommandCursor expectResult: { _id: 5, x: 55 } - - description: createRunCursorCommand's cursor can be closed and will perform a killCursors operation + - description: createCommandCursor's cursor can be closed and will perform a killCursors operation operations: - - name: createRunCursorCommand + - name: createCommandCursor object: *db arguments: commandName: find diff --git a/test/tools/unified-spec-runner/operations.ts b/test/tools/unified-spec-runner/operations.ts index 36e1c3c325..f3907ba105 100644 --- a/test/tools/unified-spec-runner/operations.ts +++ b/test/tools/unified-spec-runner/operations.ts @@ -687,29 +687,26 @@ operations.set('runCursorCommand', async ({ entities, operation }: OperationFunc return cursor.toArray(); }); -operations.set( - 'createRunCursorCommand', - async ({ entities, operation }: OperationFunctionParams) => { - const collection = entities.getEntity('db', operation.object); - const { command, ...opts } = operation.arguments!; - const cursor = collection.runCursorCommand(command, { - readPreference: ReadPreference.fromOptions(opts), - session: opts.session - }); +operations.set('createCommandCursor', async ({ entities, operation }: OperationFunctionParams) => { + const collection = entities.getEntity('db', operation.object); + const { command, ...opts } = operation.arguments!; + const cursor = collection.runCursorCommand(command, { + readPreference: ReadPreference.fromOptions(opts), + session: opts.session + }); - if (!Number.isNaN(+opts.batchSize)) cursor.setBatchSize(+opts.batchSize); - if (!Number.isNaN(+opts.maxTimeMS)) cursor.setMaxTimeMS(+opts.maxTimeMS); - if (opts.comment !== undefined) cursor.setComment(opts.comment); + if (!Number.isNaN(+opts.batchSize)) cursor.setBatchSize(+opts.batchSize); + if (!Number.isNaN(+opts.maxTimeMS)) cursor.setMaxTimeMS(+opts.maxTimeMS); + if (opts.comment !== undefined) cursor.setComment(opts.comment); - // The spec dictates that we create the cursor and force the find command - // to execute, but the first document must still be returned for the first iteration. - const result = await cursor.tryNext(); - const kDocuments = getSymbolFrom(cursor, 'documents'); - if (result) cursor[kDocuments].unshift(result); + // The spec dictates that we create the cursor and force the find command + // to execute, but the first document must still be returned for the first iteration. + const result = await cursor.tryNext(); + const kDocuments = getSymbolFrom(cursor, 'documents'); + if (result) cursor[kDocuments].unshift(result); - return cursor; - } -); + return cursor; +}); operations.set('updateMany', async ({ entities, operation }) => { const collection = entities.getEntity('collection', operation.object); From e0626ad0e4c8485169fd98e51b1bf51c166c2200 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Thu, 18 May 2023 16:08:38 -0400 Subject: [PATCH 04/18] rm invalid tests --- .../entity-createRunCursorCommand.json | 71 ------------------- .../invalid/entity-createRunCursorCommand.yml | 31 -------- 2 files changed, 102 deletions(-) delete mode 100644 test/spec/unified-test-format/invalid/entity-createRunCursorCommand.json delete mode 100644 test/spec/unified-test-format/invalid/entity-createRunCursorCommand.yml diff --git a/test/spec/unified-test-format/invalid/entity-createRunCursorCommand.json b/test/spec/unified-test-format/invalid/entity-createRunCursorCommand.json deleted file mode 100644 index dea17e5a10..0000000000 --- a/test/spec/unified-test-format/invalid/entity-createRunCursorCommand.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "description": "runCursorCommand", - "schemaVersion": "1.3", - "createEntities": [ - { - "client": { - "id": "client", - "useMultipleMongoses": false, - "observeEvents": [ - "commandStartedEvent" - ] - } - }, - { - "database": { - "id": "db", - "client": "client", - "databaseName": "db" - } - }, - { - "collection": { - "id": "collection", - "database": "db", - "collectionName": "collection" - } - } - ], - "initialData": [ - { - "collectionName": "collection", - "databaseName": "db", - "documents": [ - { - "_id": 1, - "x": 11 - } - ] - } - ], - "tests": [ - { - "description": "runCursorCommand cannot be combined with expectResult", - "operations": [ - { - "name": "runCursorCommand", - "object": "db", - "arguments": { - "commandName": "find", - "batchSize": 2, - "command": { - "find": "collection", - "filter": {}, - "batchSize": 2 - } - }, - "expectResult": [ - { - "_id": 1, - "x": 11 - } - ] - } - ], - "expectResult": { - "_id": 1, - "x": 11 - } - } - ] -} diff --git a/test/spec/unified-test-format/invalid/entity-createRunCursorCommand.yml b/test/spec/unified-test-format/invalid/entity-createRunCursorCommand.yml deleted file mode 100644 index b745ed282e..0000000000 --- a/test/spec/unified-test-format/invalid/entity-createRunCursorCommand.yml +++ /dev/null @@ -1,31 +0,0 @@ -description: runCursorCommand -schemaVersion: '1.3' -createEntities: - - client: - id: &client client - useMultipleMongoses: false - observeEvents: [commandStartedEvent] - - database: - id: &db db - client: *client - databaseName: *db - - collection: - id: &collection collection - database: *db - collectionName: *collection -initialData: - - collectionName: collection - databaseName: *db - documents: &documents - - { _id: 1, x: 11 } -tests: - - description: runCursorCommand cannot be combined with expectResult - operations: - - name: runCursorCommand - object: *db - arguments: - commandName: find - batchSize: 2 - command: { find: *collection, filter: {}, batchSize: 2 } - expectResult: *documents - expectResult: { _id: 1, x: 11 } From 4c23490dd3e63bcab3db14b3492578d45b0cadd5 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Thu, 18 May 2023 16:09:31 -0400 Subject: [PATCH 05/18] remove comment --- test/tools/unified-spec-runner/operations.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/tools/unified-spec-runner/operations.ts b/test/tools/unified-spec-runner/operations.ts index f3907ba105..8bd1c2a7cf 100644 --- a/test/tools/unified-spec-runner/operations.ts +++ b/test/tools/unified-spec-runner/operations.ts @@ -791,8 +791,6 @@ export async function executeOperationAndCheck( expect(opFunc, `Unknown operation: ${operation.name}`).to.exist; if (typeof operation.arguments?.session === 'string') { - // Cannot do this b/c operations pass through unsanitized options to command construction: - // operation.arguments.__sessionId = operation.arguments.session; operation.arguments.session = entities.getEntity('session', operation.arguments.session); } From e778767df7312a29a93b6f8bd59f20cb5833035d Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Thu, 18 May 2023 16:13:20 -0400 Subject: [PATCH 06/18] extract RP --- test/tools/unified-spec-runner/operations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tools/unified-spec-runner/operations.ts b/test/tools/unified-spec-runner/operations.ts index 8bd1c2a7cf..e0a7fc9879 100644 --- a/test/tools/unified-spec-runner/operations.ts +++ b/test/tools/unified-spec-runner/operations.ts @@ -691,7 +691,7 @@ operations.set('createCommandCursor', async ({ entities, operation }: OperationF const collection = entities.getEntity('db', operation.object); const { command, ...opts } = operation.arguments!; const cursor = collection.runCursorCommand(command, { - readPreference: ReadPreference.fromOptions(opts), + readPreference: ReadPreference.fromOptions({ readPreference: opts.readPreference }), session: opts.session }); From 408bbe74430418a5c431ec7d9afd41e0f8671b3e Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Thu, 18 May 2023 16:49:16 -0400 Subject: [PATCH 07/18] fix: clusterTime tests --- test/spec/run-command/runCommand.json | 8 ++++++++ test/spec/run-command/runCommand.yml | 3 +++ 2 files changed, 11 insertions(+) diff --git a/test/spec/run-command/runCommand.json b/test/spec/run-command/runCommand.json index 245043bc2c..ddd4044c4b 100644 --- a/test/spec/run-command/runCommand.json +++ b/test/spec/run-command/runCommand.json @@ -125,6 +125,14 @@ }, { "description": "always gossips the $clusterTime on the sent command", + "runOnRequirements": [ + { + "topologies": [ + "replicaset", + "sharded" + ] + } + ], "operations": [ { "name": "runCommand", diff --git a/test/spec/run-command/runCommand.yml b/test/spec/run-command/runCommand.yml index 450490e2a9..68d722ac3a 100644 --- a/test/spec/run-command/runCommand.yml +++ b/test/spec/run-command/runCommand.yml @@ -68,6 +68,9 @@ tests: commandName: ping - description: always gossips the $clusterTime on the sent command + runOnRequirements: + # Only replicasets and sharded clusters have a $clusterTime + - topologies: [ replicaset, sharded ] operations: # We have to run one command to obtain a clusterTime to gossip - name: runCommand From 4ae5514f45f22ece5cc6426f6e4ec5876bbaea26 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Thu, 18 May 2023 17:34:28 -0400 Subject: [PATCH 08/18] sync --- test/spec/run-command/runCommand.json | 6 +++--- test/spec/run-command/runCommand.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/spec/run-command/runCommand.json b/test/spec/run-command/runCommand.json index ddd4044c4b..007e514bd7 100644 --- a/test/spec/run-command/runCommand.json +++ b/test/spec/run-command/runCommand.json @@ -408,7 +408,7 @@ "insert": "collection", "documents": [ { - "_id": 1 + "foo": "bar" } ], "ordered": true @@ -514,7 +514,7 @@ "insert": "collection", "documents": [ { - "_id": 2 + "foo": "transaction" } ], "ordered": true @@ -542,7 +542,7 @@ "insert": "collection", "documents": [ { - "_id": 2 + "foo": "transaction" } ], "ordered": true, diff --git a/test/spec/run-command/runCommand.yml b/test/spec/run-command/runCommand.yml index 68d722ac3a..eaa12eff23 100644 --- a/test/spec/run-command/runCommand.yml +++ b/test/spec/run-command/runCommand.yml @@ -210,7 +210,7 @@ tests: commandName: insert command: insert: *collection - documents: [ { _id: 1 } ] + documents: [ { foo: 'bar' } ] ordered: true expectResult: { ok: 1 } expectEvents: @@ -263,7 +263,7 @@ tests: commandName: insert command: insert: *collection - documents: [ { _id: 2 } ] + documents: [ { foo: 'transaction' } ] ordered: true expectResult: { $$unsetOrMatches: { insertedId: { $$unsetOrMatches: 1 } } } expectEvents: @@ -272,7 +272,7 @@ tests: - commandStartedEvent: command: insert: *collection - documents: [ { _id: 2 } ] + documents: [ { foo: 'transaction' } ] ordered: true lsid: { $$sessionLsid: *session } txnNumber: 1 From f3f9de101604217a65cb6dc73c265bea7016d581 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 19 May 2023 11:42:07 -0400 Subject: [PATCH 09/18] test: add unit --- src/cursor/run_command_cursor.ts | 2 +- test/unit/cursor/run_command_cursor.test.ts | 91 +++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 test/unit/cursor/run_command_cursor.test.ts diff --git a/src/cursor/run_command_cursor.ts b/src/cursor/run_command_cursor.ts index 9c43735105..505ced2f9d 100644 --- a/src/cursor/run_command_cursor.ts +++ b/src/cursor/run_command_cursor.ts @@ -108,7 +108,7 @@ export class RunCommandCursor extends AbstractCursor { }); executeOperation(this.client, operation).then( response => { - if (!response.cursor) { + if (response.cursor == null) { callback( new MongoUnexpectedServerResponseError('Expected server to respond with cursor') ); diff --git a/test/unit/cursor/run_command_cursor.test.ts b/test/unit/cursor/run_command_cursor.test.ts new file mode 100644 index 0000000000..7468ba9157 --- /dev/null +++ b/test/unit/cursor/run_command_cursor.test.ts @@ -0,0 +1,91 @@ +import { expect } from 'chai'; + +import { MongoAPIError, MongoClient, RunCommandCursor } from '../../mongodb'; + +describe('class AbstractCursor', () => { + let client: MongoClient; + beforeEach(async function () { + client = new MongoClient('mongodb://iLoveJavascript'); + }); + + context('constructor()', () => { + it('freezes and stores the command on the cursor instance', () => { + const cursor = client.db().runCursorCommand({ a: 1 }); + expect(cursor).to.have.property('command').that.is.frozen; + }); + + it('creates getMoreOptions property with no defaults', () => { + const cursor = client.db().runCursorCommand({ a: 1 }); + expect(cursor).to.have.property('getMoreOptions').that.deep.equals({}); + }); + }); + + context('setComment()', () => { + it('stores the comment value in getMoreOptions', () => { + const cursor = client.db().runCursorCommand({ a: 1 }); + cursor.setComment('iLoveJS'); + expect(cursor).to.have.nested.property('getMoreOptions.comment', 'iLoveJS'); + }); + }); + + context('setMaxTimeMS()', () => { + it('stores the maxTimeMS value in getMoreOptions.maxAwaitTimeMS', () => { + const cursor = client.db().runCursorCommand({ a: 1 }); + cursor.setMaxTimeMS(2); + expect(cursor).to.have.nested.property('getMoreOptions.maxAwaitTimeMS', 2); + }); + + it('does not throw on incorrect maxTimeMS type', () => { + const cursor = client.db().runCursorCommand({ a: 1 }); + // @ts-expect-error: testing for incorrect type + cursor.setMaxTimeMS('abc'); + expect(cursor).to.have.nested.property('getMoreOptions.maxAwaitTimeMS', 'abc'); + }); + }); + + context('setBatchSize()', () => { + it('stores the batchSize value in getMoreOptions', () => { + const cursor = client.db().runCursorCommand({ a: 1 }); + cursor.setBatchSize(2); + expect(cursor).to.have.nested.property('getMoreOptions.batchSize', 2); + }); + + it('does not throw on incorrect batchSize type', () => { + const cursor = client.db().runCursorCommand({ a: 1 }); + // @ts-expect-error: testing for incorrect type + cursor.setBatchSize('abc'); + expect(cursor).to.have.nested.property('getMoreOptions.batchSize', 'abc'); + }); + }); + + context('clone()', () => { + it('returns a new RunCursorCommand instance', () => { + const cursor = client.db().runCursorCommand({ a: 1 }); + const clonedCursor = cursor.clone(); + expect(clonedCursor).to.be.instanceOf(RunCommandCursor); + expect(clonedCursor).to.not.equal(cursor); + }); + }); + + context('Non applicable AbstractCursor methods', () => { + it('withReadConcern throws', () => { + expect(() => + client.db().runCursorCommand({ a: 1 }).withReadConcern({ level: 'local' }) + ).to.throw(MongoAPIError); + }); + + it('addCursorFlag throws', () => { + expect(() => client.db().runCursorCommand({ a: 1 }).addCursorFlag('tailable', true)).to.throw( + MongoAPIError + ); + }); + + it('maxTimeMS throws', () => { + expect(() => client.db().runCursorCommand({ a: 1 }).maxTimeMS(2)).to.throw(MongoAPIError); + }); + + it('batchSize throws', () => { + expect(() => client.db().runCursorCommand({ a: 1 }).batchSize(2)).to.throw(MongoAPIError); + }); + }); +}); From a91d7924c3e3bbcb09070481cdd8c9d05bdfd1c6 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 19 May 2023 17:46:59 -0400 Subject: [PATCH 10/18] lb fixes --- test/spec/run-command/runCursorCommand.json | 24 ++++++++++++++++++++- test/spec/run-command/runCursorCommand.yml | 15 ++++++++++--- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/test/spec/run-command/runCursorCommand.json b/test/spec/run-command/runCursorCommand.json index d5c7d23798..1ddf89f37d 100644 --- a/test/spec/run-command/runCursorCommand.json +++ b/test/spec/run-command/runCursorCommand.json @@ -7,7 +7,10 @@ "id": "client", "useMultipleMongoses": false, "observeEvents": [ - "commandStartedEvent" + "commandStartedEvent", + "connectionReadyEvent", + "connectionCheckedOutEvent", + "connectionCheckedInEvent" ] } }, @@ -307,6 +310,7 @@ "arguments": { "commandName": "find", "batchSize": 2, + "session": "session", "command": { "find": "collection", "batchSize": 2 @@ -389,6 +393,24 @@ "commandName": "find" } }, + { + "commandStartedEvent": { + "command": { + "getMore": { + "$$type": [ + "int", + "long" + ] + }, + "collection": "collection", + "$db": "db", + "lsid": { + "$$sessionLsid": "session" + } + }, + "commandName": "getMore" + } + }, { "commandStartedEvent": { "command": { diff --git a/test/spec/run-command/runCursorCommand.yml b/test/spec/run-command/runCursorCommand.yml index 1c221d91be..5e222185c6 100644 --- a/test/spec/run-command/runCursorCommand.yml +++ b/test/spec/run-command/runCursorCommand.yml @@ -6,7 +6,7 @@ createEntities: - client: id: &client client useMultipleMongoses: false - observeEvents: [commandStartedEvent] + observeEvents: [commandStartedEvent, connectionReadyEvent, connectionCheckedOutEvent, connectionCheckedInEvent] - session: id: &session session client: *client @@ -137,6 +137,7 @@ tests: arguments: commandName: find batchSize: 2 + session: *session command: { find: *collection, batchSize: 2 } saveResultAsEntity: &cursor cursor - name: assertNumberConnectionsCheckedOut @@ -174,14 +175,22 @@ tests: batchSize: 2 $db: *db lsid: { $$sessionLsid: *session } - commandName: find + commandName: find # 2 documents - commandStartedEvent: command: getMore: { $$type: [int, long] } collection: *collection $db: *db lsid: { $$sessionLsid: *session } - commandName: getMore + commandName: getMore # 2 documents + - commandStartedEvent: + command: + getMore: { $$type: [int, long] } + collection: *collection + $db: *db + lsid: { $$sessionLsid: *session } + commandName: getMore # 1 document + # Total documents: 5 - client: *client eventType: cmap events: From f7d8653e9036876cd0665f183df041b21e09e405 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Mon, 22 May 2023 10:37:48 -0400 Subject: [PATCH 11/18] suite name --- test/unit/cursor/run_command_cursor.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/cursor/run_command_cursor.test.ts b/test/unit/cursor/run_command_cursor.test.ts index 7468ba9157..e551b52ca2 100644 --- a/test/unit/cursor/run_command_cursor.test.ts +++ b/test/unit/cursor/run_command_cursor.test.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import { MongoAPIError, MongoClient, RunCommandCursor } from '../../mongodb'; -describe('class AbstractCursor', () => { +describe('class RunCommandCursor', () => { let client: MongoClient; beforeEach(async function () { client = new MongoClient('mongodb://iLoveJavascript'); From f52c98c33dcb73835e58078f444400fc71b5cd4d Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Mon, 22 May 2023 14:22:31 -0400 Subject: [PATCH 12/18] run on serverless --- .evergreen/run-serverless-tests.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.evergreen/run-serverless-tests.sh b/.evergreen/run-serverless-tests.sh index 86b9800fbe..35e45c8328 100755 --- a/.evergreen/run-serverless-tests.sh +++ b/.evergreen/run-serverless-tests.sh @@ -25,4 +25,5 @@ npx mocha \ test/integration/transactions/transactions.test.ts \ test/integration/versioned-api/versioned_api.spec.test.js \ test/integration/load-balancers/load_balancers.spec.test.js \ - test/integration/client-side-encryption/client_side_encryption.spec.test.ts + test/integration/client-side-encryption/client_side_encryption.spec.test.ts \ + test/integration/run-command/run_command.spec.test.ts From ecef528ffe7382bd50181b7b5a67065e6fbf0cff Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Mon, 22 May 2023 14:39:29 -0400 Subject: [PATCH 13/18] sync skip to serverless --- test/spec/run-command/runCursorCommand.json | 5 +++++ test/spec/run-command/runCursorCommand.yml | 2 ++ 2 files changed, 7 insertions(+) diff --git a/test/spec/run-command/runCursorCommand.json b/test/spec/run-command/runCursorCommand.json index 1ddf89f37d..4f1ec8a01a 100644 --- a/test/spec/run-command/runCursorCommand.json +++ b/test/spec/run-command/runCursorCommand.json @@ -728,6 +728,11 @@ }, { "description": "does not close the cursor when receiving an empty batch", + "runOnRequirements": [ + { + "serverless": "forbid" + } + ], "operations": [ { "name": "dropCollection", diff --git a/test/spec/run-command/runCursorCommand.yml b/test/spec/run-command/runCursorCommand.yml index 5e222185c6..1f9bf532c3 100644 --- a/test/spec/run-command/runCursorCommand.yml +++ b/test/spec/run-command/runCursorCommand.yml @@ -318,6 +318,8 @@ tests: # Tailable cursor - description: does not close the cursor when receiving an empty batch + runOnRequirements: + - serverless: forbid operations: - name: dropCollection object: *db From 6bee14bd85ecc7d62ea738c3cf350f77537b7f73 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Mon, 22 May 2023 15:27:22 -0400 Subject: [PATCH 14/18] comments --- src/cursor/run_command_cursor.ts | 8 ++++++-- src/db.ts | 2 +- test/integration/run-command/run_command.spec.test.ts | 3 --- test/integration/run-command/run_cursor_command.test.ts | 2 +- test/tools/unified-spec-runner/operations.ts | 4 ++-- test/unit/cursor/run_command_cursor.test.ts | 4 ++-- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/cursor/run_command_cursor.ts b/src/cursor/run_command_cursor.ts index 505ced2f9d..92fcef4213 100644 --- a/src/cursor/run_command_cursor.ts +++ b/src/cursor/run_command_cursor.ts @@ -65,27 +65,31 @@ export class RunCommandCursor extends AbstractCursor { }); } + /** Unsupported for RunCommandCursor: readConcern must be configured directly on command document */ public override withReadConcern(_: ReadConcernLike): never { throw new MongoAPIError( 'RunCommandCursor does not support readConcern it must be attached to the command being run' ); } + /** Unsupported for RunCommandCursor: various cursor flags must be configured directly on command document */ public override addCursorFlag(_: string, __: boolean): never { throw new MongoAPIError( 'RunCommandCursor does not support cursor flags, they must be attached to the command being run' ); } + /** Unsupported for RunCommandCursor: maxTimeMS must be configured directly on command document */ public override maxTimeMS(_: number): never { throw new MongoAPIError( - 'RunCommandCursor does not support maxTimeMS, it must be attached to the command being run' + 'maxTimeMS must be configured on the command document directly, to configure getMore.maxTimeMS use cursor.setMaxTimeMS()' ); } + /** Unsupported for RunCommandCursor: batchSize must be configured directly on command document */ public override batchSize(_: number): never { throw new MongoAPIError( - 'RunCommandCursor does not support batchSize, it must be attached to the command being run' + 'batchSize must be configured on the command document directly, to configure getMore.batchSize use cursor.setBatchSize()' ); } diff --git a/src/db.ts b/src/db.ts index 64f727315f..c73753e73f 100644 --- a/src/db.ts +++ b/src/db.ts @@ -526,7 +526,7 @@ export class Db { } /** - * A low level cursor API providing basic driver functionality. + * A low level cursor API providing basic driver functionality: * - ClientSession management * - ReadPreference for server selection * - Running getMores automatically when a local batch is exhausted diff --git a/test/integration/run-command/run_command.spec.test.ts b/test/integration/run-command/run_command.spec.test.ts index b8e43c4d43..a4a8a522d5 100644 --- a/test/integration/run-command/run_command.spec.test.ts +++ b/test/integration/run-command/run_command.spec.test.ts @@ -3,9 +3,6 @@ import { runUnifiedSuite } from '../../tools/unified-spec-runner/runner'; describe('RunCommand spec', () => { runUnifiedSuite(loadSpecTests('run-command'), test => { - if (test.description.includes('timeoutMS') || test.description.includes('timeoutMode')) { - return 'CSOT not implemented in Node.js yet'; - } if (test.description === 'does not attach $readPreference to given command on standalone') { return 'TODO(NODE-5263): Do not send $readPreference to standalone servers'; } diff --git a/test/integration/run-command/run_cursor_command.test.ts b/test/integration/run-command/run_cursor_command.test.ts index 9ee568ccea..ab2bc68350 100644 --- a/test/integration/run-command/run_cursor_command.test.ts +++ b/test/integration/run-command/run_cursor_command.test.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import { CommandStartedEvent, Db, MongoClient } from '../../mongodb'; -describe('class RunCommandCursor', () => { +describe('runCursorCommand API', () => { let client: MongoClient; let db: Db; let commandsStarted: CommandStartedEvent[]; diff --git a/test/tools/unified-spec-runner/operations.ts b/test/tools/unified-spec-runner/operations.ts index e0a7fc9879..18fb039498 100644 --- a/test/tools/unified-spec-runner/operations.ts +++ b/test/tools/unified-spec-runner/operations.ts @@ -676,7 +676,7 @@ operations.set('runCursorCommand', async ({ entities, operation }: OperationFunc const db = entities.getEntity('db', operation.object); const { command, ...opts } = operation.arguments!; const cursor = db.runCursorCommand(command, { - readPreference: ReadPreference.fromOptions(opts), + readPreference: ReadPreference.fromOptions({ readPreference: opts.readPreference }), session: opts.session }); @@ -790,7 +790,7 @@ export async function executeOperationAndCheck( const opFunc = operations.get(operation.name); expect(opFunc, `Unknown operation: ${operation.name}`).to.exist; - if (typeof operation.arguments?.session === 'string') { + if (operation.arguments?.session) { operation.arguments.session = entities.getEntity('session', operation.arguments.session); } diff --git a/test/unit/cursor/run_command_cursor.test.ts b/test/unit/cursor/run_command_cursor.test.ts index e551b52ca2..8322d0ddef 100644 --- a/test/unit/cursor/run_command_cursor.test.ts +++ b/test/unit/cursor/run_command_cursor.test.ts @@ -35,7 +35,7 @@ describe('class RunCommandCursor', () => { expect(cursor).to.have.nested.property('getMoreOptions.maxAwaitTimeMS', 2); }); - it('does not throw on incorrect maxTimeMS type', () => { + it('does not validate maxTimeMS type', () => { const cursor = client.db().runCursorCommand({ a: 1 }); // @ts-expect-error: testing for incorrect type cursor.setMaxTimeMS('abc'); @@ -50,7 +50,7 @@ describe('class RunCommandCursor', () => { expect(cursor).to.have.nested.property('getMoreOptions.batchSize', 2); }); - it('does not throw on incorrect batchSize type', () => { + it('does not validate batchSize type', () => { const cursor = client.db().runCursorCommand({ a: 1 }); // @ts-expect-error: testing for incorrect type cursor.setBatchSize('abc'); From 6ace185ab80e05188dbf4ff5a16f6f71485ff9c9 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Mon, 22 May 2023 17:10:46 -0400 Subject: [PATCH 15/18] refactor test and csot --- src/cursor/run_command_cursor.ts | 10 +- .../client-side-operations-timeout/.gitkeep | 0 .../run-command/run_cursor_command.test.ts | 53 +- .../runCursorCommand.json | 583 ++++++++++++++++++ .../runCursorCommand.yml | 304 +++++++++ test/unit/cursor/run_command_cursor.test.ts | 15 +- 6 files changed, 921 insertions(+), 44 deletions(-) create mode 100644 test/integration/client-side-operations-timeout/.gitkeep create mode 100644 test/spec/client-side-operations-timeout/runCursorCommand.json create mode 100644 test/spec/client-side-operations-timeout/runCursorCommand.yml diff --git a/src/cursor/run_command_cursor.ts b/src/cursor/run_command_cursor.ts index 92fcef4213..c132e24961 100644 --- a/src/cursor/run_command_cursor.ts +++ b/src/cursor/run_command_cursor.ts @@ -1,4 +1,4 @@ -import { BSONSerializeOptions, Document, Long, resolveBSONOptions } from '../bson'; +import type { BSONSerializeOptions, Document, Long } from '../bson'; import type { Db } from '../db'; import { MongoAPIError, MongoUnexpectedServerResponseError } from '../error'; import { executeOperation, ExecutionResult } from '../operations/execute_operation'; @@ -58,11 +58,9 @@ export class RunCommandCursor extends AbstractCursor { return this; } - public clone(): RunCommandCursor { - return new RunCommandCursor(this.db, this.command, { - readPreference: this.readPreference, - ...resolveBSONOptions(this.cursorOptions) - }); + /** Unsupported for RunCommandCursor */ + public override clone(): never { + throw new MongoAPIError('Clone not supported, create a new cursor with db.runCursorCommand'); } /** Unsupported for RunCommandCursor: readConcern must be configured directly on command document */ diff --git a/test/integration/client-side-operations-timeout/.gitkeep b/test/integration/client-side-operations-timeout/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/integration/run-command/run_cursor_command.test.ts b/test/integration/run-command/run_cursor_command.test.ts index ab2bc68350..d90640c15d 100644 --- a/test/integration/run-command/run_cursor_command.test.ts +++ b/test/integration/run-command/run_cursor_command.test.ts @@ -1,11 +1,10 @@ import { expect } from 'chai'; -import { CommandStartedEvent, Db, MongoClient } from '../../mongodb'; +import { Db, MongoClient } from '../../mongodb'; describe('runCursorCommand API', () => { let client: MongoClient; let db: Db; - let commandsStarted: CommandStartedEvent[]; beforeEach(async function () { client = this.configuration.newClient({}, { monitorCommands: true }); @@ -14,40 +13,38 @@ describe('runCursorCommand API', () => { await db .collection<{ _id: number }>('collection') .insertMany([{ _id: 0 }, { _id: 1 }, { _id: 2 }]); - commandsStarted = []; - client.on('commandStarted', started => commandsStarted.push(started)); }); afterEach(async function () { - commandsStarted = []; await client.close(); }); - it('should only run init command once', async () => { + it('returns each document only once across multiple iterators', async () => { const cursor = db.runCursorCommand({ find: 'collection', filter: {}, batchSize: 1 }); cursor.setBatchSize(1); - const it0 = cursor[Symbol.asyncIterator](); - const it1 = cursor[Symbol.asyncIterator](); - const next0it0 = await it0.next(); // find, 1 doc - const next0it1 = await it1.next(); // getMore, 1 doc - - expect(next0it0).to.deep.equal({ value: { _id: 0 }, done: false }); - expect(next0it1).to.deep.equal({ value: { _id: 1 }, done: false }); - expect(commandsStarted.map(c => c.commandName)).to.have.lengthOf(2); - - const next1it0 = await it0.next(); // getMore, 1 doc - const next1it1 = await it1.next(); // getMore, 0 doc & exhausted id - - expect(next1it0).to.deep.equal({ value: { _id: 2 }, done: false }); - expect(next1it1).to.deep.equal({ value: undefined, done: true }); - expect(commandsStarted.map(c => c.commandName)).to.have.lengthOf(4); - - const next2it0 = await it0.next(); - const next2it1 = await it1.next(); - - expect(next2it0).to.deep.equal({ value: undefined, done: true }); - expect(next2it1).to.deep.equal({ value: undefined, done: true }); - expect(commandsStarted.map(c => c.commandName)).to.have.lengthOf(4); + const a = cursor[Symbol.asyncIterator](); + const b = cursor[Symbol.asyncIterator](); + + // Interleaving calls to A and B + const results = [ + await a.next(), // find, first doc + await b.next(), // getMore, second doc + + await a.next(), // getMore, third doc + await b.next(), // getMore, no doc & exhausted id, a.k.a. done + + await a.next(), // done + await b.next() // done + ]; + + expect(results).to.deep.equal([ + { value: { _id: 0 }, done: false }, + { value: { _id: 1 }, done: false }, + { value: { _id: 2 }, done: false }, + { value: undefined, done: true }, + { value: undefined, done: true }, + { value: undefined, done: true } + ]); }); }); diff --git a/test/spec/client-side-operations-timeout/runCursorCommand.json b/test/spec/client-side-operations-timeout/runCursorCommand.json new file mode 100644 index 0000000000..2ffc32322b --- /dev/null +++ b/test/spec/client-side-operations-timeout/runCursorCommand.json @@ -0,0 +1,583 @@ +{ + "description": "runCursorCommand", + "schemaVersion": "1.9", + "runOnRequirements": [ + { + "minServerVersion": "4.4" + } + ], + "createEntities": [ + { + "client": { + "id": "failPointClient", + "useMultipleMongoses": false + } + }, + { + "client": { + "id": "commandClient", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent" + ] + } + }, + { + "client": { + "id": "client", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent" + ], + "ignoreCommandMonitoringEvents": [ + "killCursors" + ] + } + }, + { + "database": { + "id": "commandDb", + "client": "commandClient", + "databaseName": "commandDb" + } + }, + { + "database": { + "id": "db", + "client": "client", + "databaseName": "db" + } + }, + { + "collection": { + "id": "collection", + "database": "db", + "collectionName": "collection" + } + } + ], + "initialData": [ + { + "collectionName": "collection", + "databaseName": "db", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + }, + { + "_id": 5, + "x": 55 + } + ] + } + ], + "tests": [ + { + "description": "errors if timeoutMode is set without timeoutMS", + "operations": [ + { + "name": "runCursorCommand", + "object": "db", + "arguments": { + "commandName": "find", + "command": { + "find": "collection" + }, + "timeoutMode": "cursorLifetime" + }, + "expectError": { + "isClientError": true + } + } + ] + }, + { + "description": "error if timeoutMode is cursorLifetime and cursorType is tailableAwait", + "operations": [ + { + "name": "runCursorCommand", + "object": "db", + "arguments": { + "commandName": "find", + "command": { + "find": "collection" + }, + "timeoutMode": "cursorLifetime", + "cursorType": "tailableAwait" + }, + "expectError": { + "isClientError": true + } + } + ] + }, + { + "description": "Non-tailable cursor lifetime remaining timeoutMS applied to getMore if timeoutMode is unset", + "runOnRequirements": [ + { + "serverless": "forbid" + } + ], + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "failPointClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "find", + "getMore" + ], + "blockConnection": true, + "blockTimeMS": 45 + } + } + } + }, + { + "name": "runCursorCommand", + "object": "db", + "arguments": { + "commandName": "find", + "timeoutMS": 60, + "command": { + "find": "collection", + "batchSize": 2 + } + }, + "expectError": { + "isTimeoutError": true + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "collection", + "maxTimeMS": { + "$$type": [ + "int", + "long" + ] + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "getMore", + "command": { + "getMore": { + "$$type": [ + "int", + "long" + ] + }, + "collection": "collection", + "maxTimeMS": { + "$$exists": true + } + } + } + } + ] + } + ] + }, + { + "description": "Non=tailable cursor iteration timeoutMS is refreshed for getMore if timeoutMode is iteration - failure", + "runOnRequirements": [ + { + "serverless": "forbid" + } + ], + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "failPointClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "getMore" + ], + "blockConnection": true, + "blockTimeMS": 15 + } + } + } + }, + { + "name": "runCursorCommand", + "object": "db", + "arguments": { + "commandName": "find", + "command": { + "find": "collection", + "batchSize": 2 + }, + "timeoutMode": "iteration", + "timeoutMS": 20, + "batchSize": 2 + }, + "expectError": { + "isTimeoutError": true + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "databaseName": "db", + "command": { + "find": "collection", + "maxTimeMS": { + "$$exists": false + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "getMore", + "databaseName": "db", + "command": { + "getMore": { + "$$type": [ + "int", + "long" + ] + }, + "collection": "collection", + "maxTimeMS": { + "$$exists": false + } + } + } + } + ] + } + ] + }, + { + "description": "Tailable cursor iteration timeoutMS is refreshed for getMore - failure", + "runOnRequirements": [ + { + "serverless": "forbid" + } + ], + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "failPointClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "getMore" + ], + "blockConnection": true, + "blockTimeMS": 15 + } + } + } + }, + { + "name": "dropCollection", + "object": "db", + "arguments": { + "collection": "cappedCollection" + } + }, + { + "name": "createCollection", + "object": "db", + "arguments": { + "collection": "cappedCollection", + "capped": true, + "size": 4096, + "max": 3 + }, + "saveResultAsEntity": "cappedCollection" + }, + { + "name": "insertMany", + "object": "cappedCollection", + "arguments": { + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + }, + { + "name": "createCommandCursor", + "object": "db", + "arguments": { + "commandName": "find", + "command": { + "find": "cappedCollection", + "batchSize": 1, + "tailable": true + }, + "timeoutMode": "iteration", + "timeoutMS": 20, + "batchSize": 1, + "cursorType": "tailable" + }, + "saveResultAsEntity": "tailableCursor" + }, + { + "name": "iterateUntilDocumentOrError", + "object": "tailableCursor" + }, + { + "name": "iterateUntilDocumentOrError", + "object": "tailableCursor", + "expectError": { + "isTimeoutError": true + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "commandName": "drop" + } + }, + { + "commandStartedEvent": { + "commandName": "create" + } + }, + { + "commandStartedEvent": { + "commandName": "insert" + } + }, + { + "commandStartedEvent": { + "commandName": "find", + "databaseName": "db", + "command": { + "find": "cappedCollection", + "tailable": true, + "awaitData": { + "$$exists": false + }, + "maxTimeMS": { + "$$exists": false + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "getMore", + "databaseName": "db", + "command": { + "getMore": { + "$$type": [ + "int", + "long" + ] + }, + "collection": "cappedCollection", + "maxTimeMS": { + "$$exists": false + } + } + } + } + ] + } + ] + }, + { + "description": "Tailable cursor awaitData iteration timeoutMS is refreshed for getMore - failure", + "runOnRequirements": [ + { + "serverless": "forbid" + } + ], + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "failPointClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "getMore" + ], + "blockConnection": true, + "blockTimeMS": 15 + } + } + } + }, + { + "name": "dropCollection", + "object": "db", + "arguments": { + "collection": "cappedCollection" + } + }, + { + "name": "createCollection", + "object": "db", + "arguments": { + "collection": "cappedCollection", + "capped": true, + "size": 4096, + "max": 3 + }, + "saveResultAsEntity": "cappedCollection" + }, + { + "name": "insertMany", + "object": "cappedCollection", + "arguments": { + "documents": [ + { + "foo": "bar" + }, + { + "fizz": "buzz" + } + ] + } + }, + { + "name": "createCommandCursor", + "object": "db", + "arguments": { + "command": { + "find": "cappedCollection", + "tailable": true, + "awaitData": true + }, + "cursorType": "tailableAwait", + "batchSize": 1 + }, + "saveResultAsEntity": "tailableCursor" + }, + { + "name": "iterateUntilDocumentOrError", + "object": "tailableCursor" + }, + { + "name": "iterateUntilDocumentOrError", + "object": "tailableCursor", + "expectError": { + "isTimeoutError": true + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "commandName": "drop" + } + }, + { + "commandStartedEvent": { + "commandName": "create" + } + }, + { + "commandStartedEvent": { + "commandName": "insert" + } + }, + { + "commandStartedEvent": { + "commandName": "find", + "databaseName": "db", + "command": { + "find": "cappedCollection", + "tailable": true, + "awaitData": true, + "maxTimeMS": { + "$$exists": true + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "getMore", + "databaseName": "db", + "command": { + "getMore": { + "$$type": [ + "int", + "long" + ] + }, + "collection": "cappedCollection" + } + } + } + ] + } + ] + } + ] +} diff --git a/test/spec/client-side-operations-timeout/runCursorCommand.yml b/test/spec/client-side-operations-timeout/runCursorCommand.yml new file mode 100644 index 0000000000..09cc959b04 --- /dev/null +++ b/test/spec/client-side-operations-timeout/runCursorCommand.yml @@ -0,0 +1,304 @@ +description: runCursorCommand + +schemaVersion: '1.9' + +runOnRequirements: + - minServerVersion: "4.4" + +createEntities: + - client: + id: &failPointClient failPointClient + useMultipleMongoses: false + - client: + id: &commandClient commandClient + useMultipleMongoses: false + observeEvents: [commandStartedEvent, commandSucceededEvent] + - client: + id: &client client + useMultipleMongoses: false + observeEvents: [commandStartedEvent] + ignoreCommandMonitoringEvents: [killCursors] + - database: # For tests that need success event assertions + id: &commandDb commandDb + client: *commandClient + databaseName: *commandDb + - database: + id: &db db + client: *client + databaseName: *db + - collection: + id: &collection collection + database: *db + collectionName: *collection + +initialData: + - collectionName: *collection + databaseName: *db + documents: &documents + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + - { _id: 4, x: 44 } + - { _id: 5, x: 55 } + +tests: + - description: errors if timeoutMode is set without timeoutMS + operations: + - name: runCursorCommand + object: *db + arguments: + commandName: find + command: { find: *collection } + timeoutMode: cursorLifetime + expectError: + isClientError: true + + - description: error if timeoutMode is cursorLifetime and cursorType is tailableAwait + operations: + - name: runCursorCommand + object: *db + arguments: + commandName: find + command: { find: *collection } + timeoutMode: cursorLifetime + cursorType: tailableAwait + expectError: + isClientError: true + + # If timeoutMode is unset, it should default to CURSOR_LIFETIME and the time remaining after the find succeeds should be applied to the getMore + - description: Non-tailable cursor lifetime remaining timeoutMS applied to getMore if timeoutMode is unset + runOnRequirements: + - serverless: forbid + operations: + # Block find/getMore for 15ms. + - name: failPoint + object: testRunner + arguments: + client: *failPointClient + failPoint: + configureFailPoint: failCommand + mode: { times: 2 } + data: + failCommands: [find, getMore] + blockConnection: true + blockTimeMS: 45 + # Run a find with timeoutMS less than double our failPoint blockTimeMS and + # batchSize less than the total document count will cause a find and a getMore to be sent. + # Both will block for 45ms so together they will go over the timeout. + - name: runCursorCommand + object: *db + arguments: + commandName: find + timeoutMS: 60 + command: { find: *collection, batchSize: 2 } + expectError: + isTimeoutError: true + expectEvents: + - client: *client + events: + - commandStartedEvent: + commandName: find + command: + find: *collection + maxTimeMS: { $$type: [int, long] } + - commandStartedEvent: + commandName: getMore + command: + getMore: { $$type: [int, long] } + collection: *collection + maxTimeMS: { $$exists: true } + + # 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. + - description: Non=tailable cursor iteration timeoutMS is refreshed for getMore if timeoutMode is iteration - failure + runOnRequirements: + - serverless: forbid + operations: + - name: failPoint + object: testRunner + arguments: + client: *failPointClient + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["getMore"] + blockConnection: true + blockTimeMS: 15 + - name: runCursorCommand + object: *db + arguments: + commandName: find + command: { find: *collection, batchSize: 2 } + timeoutMode: iteration + timeoutMS: 20 + batchSize: 2 + expectError: + isTimeoutError: true + expectEvents: + - client: *client + events: + - commandStartedEvent: + commandName: find + databaseName: *db + command: + find: *collection + maxTimeMS: { $$exists: false } + - commandStartedEvent: + commandName: getMore + databaseName: *db + command: + getMore: { $$type: ["int", "long"] } + collection: *collection + 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 + # with a timeout error. + - description: Tailable cursor iteration timeoutMS is refreshed for getMore - failure + runOnRequirements: + - serverless: forbid + operations: + - name: failPoint + object: testRunner + arguments: + client: *failPointClient + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["getMore"] + blockConnection: true + blockTimeMS: 15 + - name: dropCollection + object: *db + arguments: + collection: &cappedCollection cappedCollection + - name: createCollection + object: *db + arguments: + collection: *cappedCollection + capped: true + size: 4096 + max: 3 + saveResultAsEntity: *cappedCollection + - name: insertMany + object: *cappedCollection + arguments: + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - name: createCommandCursor + object: *db + arguments: + commandName: find + command: { find: *cappedCollection, batchSize: 1, tailable: true } + timeoutMode: iteration + timeoutMS: 20 + batchSize: 1 + cursorType: tailable + saveResultAsEntity: &tailableCursor tailableCursor + # Iterate the cursor twice: the first iteration will return the document from the batch in the find and the + # second will do a getMore. + - name: iterateUntilDocumentOrError + object: *tailableCursor + - name: iterateUntilDocumentOrError + object: *tailableCursor + expectError: + isTimeoutError: true + expectEvents: + - client: *client + events: + - commandStartedEvent: + commandName: drop + - commandStartedEvent: + commandName: create + - commandStartedEvent: + commandName: insert + - commandStartedEvent: + commandName: find + databaseName: *db + command: + find: *cappedCollection + tailable: true + awaitData: { $$exists: false } + maxTimeMS: { $$exists: false } + - commandStartedEvent: + commandName: getMore + databaseName: *db + command: + getMore: { $$type: ["int", "long"] } + collection: *cappedCollection + maxTimeMS: { $$exists: false } + + # 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. + - description: Tailable cursor awaitData iteration timeoutMS is refreshed for getMore - failure + runOnRequirements: + - serverless: forbid + operations: + - name: failPoint + object: testRunner + arguments: + client: *failPointClient + failPoint: + configureFailPoint: failCommand + mode: { times: 1 } + data: + failCommands: ["getMore"] + blockConnection: true + blockTimeMS: 15 + - name: dropCollection + object: *db + arguments: + collection: &cappedCollection cappedCollection + - name: createCollection + object: *db + arguments: + collection: *cappedCollection + capped: true + size: 4096 + max: 3 + saveResultAsEntity: *cappedCollection + - name: insertMany + object: *cappedCollection + arguments: + documents: [ { foo: bar }, { fizz: buzz } ] + - name: createCommandCursor + object: *db + arguments: + command: { find: *cappedCollection, tailable: true, awaitData: true } + cursorType: tailableAwait + batchSize: 1 + saveResultAsEntity: &tailableCursor tailableCursor + # Iterate twice to force a getMore. + - name: iterateUntilDocumentOrError + object: *tailableCursor + - name: iterateUntilDocumentOrError + object: *tailableCursor + expectError: + isTimeoutError: true + expectEvents: + - client: *client + events: + - commandStartedEvent: + commandName: drop + - commandStartedEvent: + commandName: create + - commandStartedEvent: + commandName: insert + - commandStartedEvent: + commandName: find + databaseName: *db + command: + find: *cappedCollection + tailable: true + awaitData: true + maxTimeMS: { $$exists: true } + - commandStartedEvent: + commandName: getMore + databaseName: *db + command: + getMore: { $$type: ["int", "long"] } + collection: *cappedCollection diff --git a/test/unit/cursor/run_command_cursor.test.ts b/test/unit/cursor/run_command_cursor.test.ts index 8322d0ddef..d09966059d 100644 --- a/test/unit/cursor/run_command_cursor.test.ts +++ b/test/unit/cursor/run_command_cursor.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; -import { MongoAPIError, MongoClient, RunCommandCursor } from '../../mongodb'; +import { MongoAPIError, MongoClient } from '../../mongodb'; describe('class RunCommandCursor', () => { let client: MongoClient; @@ -58,15 +58,6 @@ describe('class RunCommandCursor', () => { }); }); - context('clone()', () => { - it('returns a new RunCursorCommand instance', () => { - const cursor = client.db().runCursorCommand({ a: 1 }); - const clonedCursor = cursor.clone(); - expect(clonedCursor).to.be.instanceOf(RunCommandCursor); - expect(clonedCursor).to.not.equal(cursor); - }); - }); - context('Non applicable AbstractCursor methods', () => { it('withReadConcern throws', () => { expect(() => @@ -87,5 +78,9 @@ describe('class RunCommandCursor', () => { it('batchSize throws', () => { expect(() => client.db().runCursorCommand({ a: 1 }).batchSize(2)).to.throw(MongoAPIError); }); + + it('clone throws', () => { + expect(() => client.db().runCursorCommand({ a: 1 }).clone()).to.throw(MongoAPIError); + }); }); }); From 482f8166426b6328c57fc766bf982877c52c94e7 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 23 May 2023 16:23:07 -0400 Subject: [PATCH 16/18] sync --- etc/sync-wip-spec.sh | 25 ------------------- .../runCursorCommand.json | 14 +++++------ .../runCursorCommand.yml | 22 ++++++++-------- 3 files changed, 18 insertions(+), 43 deletions(-) delete mode 100644 etc/sync-wip-spec.sh diff --git a/etc/sync-wip-spec.sh b/etc/sync-wip-spec.sh deleted file mode 100644 index fd6386621f..0000000000 --- a/etc/sync-wip-spec.sh +++ /dev/null @@ -1,25 +0,0 @@ -#! /usr/bin/env bash - -set -o xtrace -set -o errexit - -pushd "$HOME/code/drivers/specifications/source" -make -popd - -SOURCE="$HOME/code/drivers/specifications/source/run-command/tests/unified" - -for file in $SOURCE/*; do - cp "$file" "test/spec/run-command/$(basename "$file")" -done - -# cp "$HOME/code/drivers/specifications/source/unified-test-format/tests/invalid/entity-createRunCursorCommand.yml" test/spec/unified-test-format/invalid/entity-createRunCursorCommand.yml -# cp "$HOME/code/drivers/specifications/source/unified-test-format/tests/invalid/entity-createRunCursorCommand.json" test/spec/unified-test-format/invalid/entity-createRunCursorCommand.json - -cp "$HOME/code/drivers/specifications/source/unified-test-format/tests/valid-pass/entity-commandCursor.yml" test/spec/unified-test-format/valid-pass/entity-commandCursor.yml -cp "$HOME/code/drivers/specifications/source/unified-test-format/tests/valid-pass/entity-commandCursor.json" test/spec/unified-test-format/valid-pass/entity-commandCursor.json - - -if [ -z "$MONGODB_URI" ]; then echo "must set uri" && exit 1; fi -export MONGODB_URI=$MONGODB_URI -npm run check:test -- -g '(RunCommand spec)|(Unified test format runner runCursorCommand)' diff --git a/test/spec/client-side-operations-timeout/runCursorCommand.json b/test/spec/client-side-operations-timeout/runCursorCommand.json index 2ffc32322b..5fc0be3399 100644 --- a/test/spec/client-side-operations-timeout/runCursorCommand.json +++ b/test/spec/client-side-operations-timeout/runCursorCommand.json @@ -149,7 +149,7 @@ "getMore" ], "blockConnection": true, - "blockTimeMS": 45 + "blockTimeMS": 60 } } } @@ -159,7 +159,7 @@ "object": "db", "arguments": { "commandName": "find", - "timeoutMS": 60, + "timeoutMS": 100, "command": { "find": "collection", "batchSize": 2 @@ -232,7 +232,7 @@ "getMore" ], "blockConnection": true, - "blockTimeMS": 15 + "blockTimeMS": 60 } } } @@ -247,7 +247,7 @@ "batchSize": 2 }, "timeoutMode": "iteration", - "timeoutMS": 20, + "timeoutMS": 100, "batchSize": 2 }, "expectError": { @@ -316,7 +316,7 @@ "getMore" ], "blockConnection": true, - "blockTimeMS": 15 + "blockTimeMS": 60 } } } @@ -366,7 +366,7 @@ "tailable": true }, "timeoutMode": "iteration", - "timeoutMS": 20, + "timeoutMS": 100, "batchSize": 1, "cursorType": "tailable" }, @@ -464,7 +464,7 @@ "getMore" ], "blockConnection": true, - "blockTimeMS": 15 + "blockTimeMS": 60 } } } diff --git a/test/spec/client-side-operations-timeout/runCursorCommand.yml b/test/spec/client-side-operations-timeout/runCursorCommand.yml index 09cc959b04..16a648e028 100644 --- a/test/spec/client-side-operations-timeout/runCursorCommand.yml +++ b/test/spec/client-side-operations-timeout/runCursorCommand.yml @@ -81,15 +81,15 @@ tests: data: failCommands: [find, getMore] blockConnection: true - blockTimeMS: 45 + blockTimeMS: 60 # Run a find with timeoutMS less than double our failPoint blockTimeMS and # batchSize less than the total document count will cause a find and a getMore to be sent. - # Both will block for 45ms so together they will go over the timeout. + # Both will block for 60ms so together they will go over the timeout. - name: runCursorCommand object: *db arguments: commandName: find - timeoutMS: 60 + timeoutMS: 100 command: { find: *collection, batchSize: 2 } expectError: isTimeoutError: true @@ -109,8 +109,8 @@ tests: maxTimeMS: { $$exists: true } # 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=100 and "getMore" + # commands are blocked for 60ms, causing iteration to fail with a timeout error. - description: Non=tailable cursor iteration timeoutMS is refreshed for getMore if timeoutMode is iteration - failure runOnRequirements: - serverless: forbid @@ -125,14 +125,14 @@ tests: data: failCommands: ["getMore"] blockConnection: true - blockTimeMS: 15 + blockTimeMS: 60 - name: runCursorCommand object: *db arguments: commandName: find command: { find: *collection, batchSize: 2 } timeoutMode: iteration - timeoutMS: 20 + timeoutMS: 100 batchSize: 2 expectError: isTimeoutError: true @@ -154,7 +154,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=100 from the collection and the getMore command blocks for 60ms, causing iteration to fail # with a timeout error. - description: Tailable cursor iteration timeoutMS is refreshed for getMore - failure runOnRequirements: @@ -170,7 +170,7 @@ tests: data: failCommands: ["getMore"] blockConnection: true - blockTimeMS: 15 + blockTimeMS: 60 - name: dropCollection object: *db arguments: @@ -195,7 +195,7 @@ tests: commandName: find command: { find: *cappedCollection, batchSize: 1, tailable: true } timeoutMode: iteration - timeoutMS: 20 + timeoutMS: 100 batchSize: 1 cursorType: tailable saveResultAsEntity: &tailableCursor tailableCursor @@ -248,7 +248,7 @@ tests: data: failCommands: ["getMore"] blockConnection: true - blockTimeMS: 15 + blockTimeMS: 60 - name: dropCollection object: *db arguments: From b95b5c13622bded2b923b6c4cd83c756f4f21a44 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Tue, 23 May 2023 17:00:32 -0400 Subject: [PATCH 17/18] rename option --- src/cursor/run_command_cursor.ts | 4 ++-- src/db.ts | 4 ++-- src/index.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cursor/run_command_cursor.ts b/src/cursor/run_command_cursor.ts index c132e24961..95dcef4983 100644 --- a/src/cursor/run_command_cursor.ts +++ b/src/cursor/run_command_cursor.ts @@ -11,7 +11,7 @@ import { Callback, ns } from '../utils'; import { AbstractCursor } from './abstract_cursor'; /** @public */ -export type RunCommandCursorOptions = { +export type RunCursorCommandOptions = { readPreference?: ReadPreferenceLike; session?: ClientSession; } & BSONSerializeOptions; @@ -95,7 +95,7 @@ export class RunCommandCursor extends AbstractCursor { private db: Db; /** @internal */ - constructor(db: Db, command: Document, options: RunCommandCursorOptions = {}) { + constructor(db: Db, command: Document, options: RunCursorCommandOptions = {}) { super(db.s.client, ns(db.namespace), options); this.db = db; this.command = Object.freeze({ ...command }); diff --git a/src/db.ts b/src/db.ts index c73753e73f..548d183094 100644 --- a/src/db.ts +++ b/src/db.ts @@ -5,7 +5,7 @@ import { Collection, CollectionOptions } from './collection'; import * as CONSTANTS from './constants'; import { AggregationCursor } from './cursor/aggregation_cursor'; import { ListCollectionsCursor } from './cursor/list_collections_cursor'; -import { RunCommandCursor, type RunCommandCursorOptions } from './cursor/run_command_cursor'; +import { RunCommandCursor, type RunCursorCommandOptions } from './cursor/run_command_cursor'; import { MongoAPIError, MongoInvalidArgumentError } from './error'; import type { MongoClient, PkFactory } from './mongo_client'; import type { TODO_NODE_3286 } from './mongo_types'; @@ -534,7 +534,7 @@ export class Db { * @param command - The command that will start a cursor on the server. * @param options - Configurations for running the command, bson options will apply to getMores */ - runCursorCommand(command: Document, options?: RunCommandCursorOptions): RunCommandCursor { + runCursorCommand(command: Document, options?: RunCursorCommandOptions): RunCommandCursor { return new RunCommandCursor(this, command, options); } } diff --git a/src/index.ts b/src/index.ts index e200def71a..7917e23539 100644 --- a/src/index.ts +++ b/src/index.ts @@ -277,7 +277,7 @@ export type { ChangeStreamAggregateRawResult, ChangeStreamCursorOptions } from './cursor/change_stream_cursor'; -export type { RunCommandCursorOptions } from './cursor/run_command_cursor'; +export type { RunCursorCommandOptions } from './cursor/run_command_cursor'; export type { DbOptions, DbPrivate } from './db'; export type { AutoEncrypter, AutoEncryptionOptions, AutoEncryptionTlsOptions } from './deps'; export type { Encrypter, EncrypterOptions } from './encrypter'; From d1a3d43f830c36d06f38181fbd0def4edb3471fc Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Wed, 24 May 2023 11:28:36 -0400 Subject: [PATCH 18/18] consolidate cs/cursor getting --- test/tools/unified-spec-runner/entities.ts | 10 +++++ test/tools/unified-spec-runner/operations.ts | 42 ++++---------------- 2 files changed, 17 insertions(+), 35 deletions(-) diff --git a/test/tools/unified-spec-runner/entities.ts b/test/tools/unified-spec-runner/entities.ts index 83c689b6fb..ffc1d41221 100644 --- a/test/tools/unified-spec-runner/entities.ts +++ b/test/tools/unified-spec-runner/entities.ts @@ -417,6 +417,16 @@ export class EntitiesMap extends Map { return new EntitiesMap(Array.from(this.entries()).filter(([, e]) => e instanceof ctor)); } + getChangeStreamOrCursor(key: string): UnifiedChangeStream | AbstractCursor { + try { + const cs = this.getEntity('stream', key); + return cs; + } catch { + const cursor = this.getEntity('cursor', key); + return cursor; + } + } + getEntity(type: 'client', key: string, assertExists?: boolean): UnifiedMongoClient; getEntity(type: 'db', key: string, assertExists?: boolean): Db; getEntity(type: 'collection', key: string, assertExists?: boolean): Collection; diff --git a/test/tools/unified-spec-runner/operations.ts b/test/tools/unified-spec-runner/operations.ts index 18fb039498..3118a51889 100644 --- a/test/tools/unified-spec-runner/operations.ts +++ b/test/tools/unified-spec-runner/operations.ts @@ -21,9 +21,9 @@ import { } from '../../mongodb'; import { getSymbolFrom, sleep } from '../../tools/utils'; import { TestConfiguration } from '../runner/config'; -import { EntitiesMap, UnifiedChangeStream } from './entities'; +import { EntitiesMap } from './entities'; import { expectErrorCheck, resultCheck } from './match'; -import type { ExpectedEvent, ExpectedLogMessage, OperationDescription } from './schema'; +import type { ExpectedEvent, OperationDescription } from './schema'; import { getMatchingEventCount, translateOptions } from './unified-utils'; interface OperationFunctionParams { @@ -349,40 +349,14 @@ operations.set('insertMany', async ({ entities, operation }) => { return collection.insertMany(documents, opts); }); -function getChangeStream({ entities, operation }): UnifiedChangeStream | null { - try { - const changeStream = entities.getEntity('stream', operation.object); - return changeStream; - } catch (e) { - return null; - } -} operations.set('iterateUntilDocumentOrError', async ({ entities, operation }) => { - const changeStream = getChangeStream({ entities, operation }); - if (changeStream == null) { - // iterateUntilDocumentOrError is used for changes streams and regular cursors. - // we have no other way to distinguish which scenario we are testing when we run an - // iterateUntilDocumentOrError operation, so we first try to get the changeStream and - // if that fails, we know we need to get a cursor - const cursor = entities.getEntity('cursor', operation.object); - return cursor.next(); - } - - return changeStream.next(); + const iterable = entities.getChangeStreamOrCursor(operation.object); + return iterable.next(); }); operations.set('iterateOnce', async ({ entities, operation }) => { - const changeStream = getChangeStream({ entities, operation }); - if (changeStream == null) { - // iterateOnce is used for changes streams and regular cursors. - // we have no other way to distinguish which scenario we are testing when we run an - // iterateOnce operation, so we first try to get the changeStream and - // if that fails, we know we need to get a cursor - const cursor = entities.getEntity('cursor', operation.object); - return cursor.tryNext(); - } - - return changeStream.tryNext(); + const iterable = entities.getChangeStreamOrCursor(operation.object); + return iterable.tryNext(); }); operations.set('listCollections', async ({ entities, operation }) => { @@ -701,9 +675,7 @@ operations.set('createCommandCursor', async ({ entities, operation }: OperationF // The spec dictates that we create the cursor and force the find command // to execute, but the first document must still be returned for the first iteration. - const result = await cursor.tryNext(); - const kDocuments = getSymbolFrom(cursor, 'documents'); - if (result) cursor[kDocuments].unshift(result); + await cursor.hasNext(); return cursor; });