diff --git a/drivers-evergreen-tools b/drivers-evergreen-tools index 1e3ddd0078b..3052fce9dfc 160000 --- a/drivers-evergreen-tools +++ b/drivers-evergreen-tools @@ -1 +1 @@ -Subproject commit 1e3ddd0078bf6a62369fc34edc5447beb0d7eff7 +Subproject commit 3052fce9dfc582cc9ab7a16f5b6b04647ce7ebb0 diff --git a/src/collection.ts b/src/collection.ts index 1bdc89b2628..00fbe4374c3 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -491,10 +491,7 @@ export class Collection { * @param options - Optional settings for the command */ async drop(options?: DropCollectionOptions): Promise { - return await executeOperation( - this.client, - new DropCollectionOperation(this.s.db, this.collectionName, options) - ); + return await this.s.db.dropCollection(this.collectionName, options); } /** diff --git a/src/db.ts b/src/db.ts index bcadc4937e7..8f150954bd9 100644 --- a/src/db.ts +++ b/src/db.ts @@ -6,7 +6,7 @@ import * as CONSTANTS from './constants'; import { AggregationCursor } from './cursor/aggregation_cursor'; import { ListCollectionsCursor } from './cursor/list_collections_cursor'; import { RunCommandCursor, type RunCursorCommandOptions } from './cursor/run_command_cursor'; -import { MongoInvalidArgumentError } from './error'; +import { MONGODB_ERROR_CODES, MongoInvalidArgumentError, MongoServerError } from './error'; import type { MongoClient, PkFactory } from './mongo_client'; import type { Abortable, TODO_NODE_3286 } from './mongo_types'; import type { AggregateOptions } from './operations/aggregate'; @@ -411,6 +411,43 @@ export class Db { * @param options - Optional settings for the command */ async dropCollection(name: string, options?: DropCollectionOptions): Promise { + options = resolveOptions(this, options); + const encryptedFieldsMap = this.client.s.options.autoEncryption?.encryptedFieldsMap; + let encryptedFields: Document | undefined = + options?.encryptedFields ?? encryptedFieldsMap?.[`${this.databaseName}.${name}`]; + + if (!encryptedFields && encryptedFieldsMap) { + // If the MongoClient was configured with an encryptedFieldsMap, + // and no encryptedFields config was available in it or explicitly + // passed as an argument, the spec tells us to look one up using + // listCollections(). + const listCollectionsResult = await this.listCollections( + { name }, + { nameOnly: false } + ).toArray(); + encryptedFields = listCollectionsResult?.[0]?.options?.encryptedFields; + } + + if (encryptedFields) { + const escCollection = encryptedFields.escCollection || `enxcol_.${name}.esc`; + const ecocCollection = encryptedFields.ecocCollection || `enxcol_.${name}.ecoc`; + + for (const collectionName of [escCollection, ecocCollection]) { + // Drop auxilliary collections, ignoring potential NamespaceNotFound errors. + const dropOp = new DropCollectionOperation(this, collectionName); + try { + await executeOperation(this.client, dropOp); + } catch (err) { + if ( + !(err instanceof MongoServerError) || + err.code !== MONGODB_ERROR_CODES.NamespaceNotFound + ) { + throw err; + } + } + } + } + return await executeOperation( this.client, new DropCollectionOperation(this, name, resolveOptions(this, options)) diff --git a/src/operations/drop.ts b/src/operations/drop.ts index 0ead5a4927a..e73c823e9d9 100644 --- a/src/operations/drop.ts +++ b/src/operations/drop.ts @@ -34,53 +34,6 @@ export class DropCollectionOperation extends CommandOperation { server: Server, session: ClientSession | undefined, timeoutContext: TimeoutContext - ): Promise { - const db = this.db; - const options = this.options; - const name = this.name; - - const encryptedFieldsMap = db.client.s.options.autoEncryption?.encryptedFieldsMap; - let encryptedFields: Document | undefined = - options.encryptedFields ?? encryptedFieldsMap?.[`${db.databaseName}.${name}`]; - - if (!encryptedFields && encryptedFieldsMap) { - // If the MongoClient was configured with an encryptedFieldsMap, - // and no encryptedFields config was available in it or explicitly - // passed as an argument, the spec tells us to look one up using - // listCollections(). - const listCollectionsResult = await db - .listCollections({ name }, { nameOnly: false }) - .toArray(); - encryptedFields = listCollectionsResult?.[0]?.options?.encryptedFields; - } - - if (encryptedFields) { - const escCollection = encryptedFields.escCollection || `enxcol_.${name}.esc`; - const ecocCollection = encryptedFields.ecocCollection || `enxcol_.${name}.ecoc`; - - for (const collectionName of [escCollection, ecocCollection]) { - // Drop auxilliary collections, ignoring potential NamespaceNotFound errors. - const dropOp = new DropCollectionOperation(db, collectionName); - try { - await dropOp.executeWithoutEncryptedFieldsCheck(server, session, timeoutContext); - } catch (err) { - if ( - !(err instanceof MongoServerError) || - err.code !== MONGODB_ERROR_CODES.NamespaceNotFound - ) { - throw err; - } - } - } - } - - return await this.executeWithoutEncryptedFieldsCheck(server, session, timeoutContext); - } - - private async executeWithoutEncryptedFieldsCheck( - server: Server, - session: ClientSession | undefined, - timeoutContext: TimeoutContext ): Promise { await super.executeCommand(server, session, { drop: this.name }, timeoutContext); return true; diff --git a/src/operations/execute_operation.ts b/src/operations/execute_operation.ts index ed713999991..e1ce0491f27 100644 --- a/src/operations/execute_operation.ts +++ b/src/operations/execute_operation.ts @@ -26,7 +26,7 @@ import type { Topology } from '../sdam/topology'; import type { ClientSession } from '../sessions'; import { TimeoutContext } from '../timeout'; import { abortable, supportsRetryableWrites } from '../utils'; -import { AbstractOperation, Aspect } from './operation'; +import { AbstractOperation, Aspect, ModernOperation } from './operation'; const MMAPv1_RETRY_WRITES_ERROR_CODE = MONGODB_ERROR_CODES.IllegalOperation; const MMAPv1_RETRY_WRITES_ERROR_MESSAGE = @@ -85,6 +85,8 @@ export async function executeOperation< throw new MongoInvalidArgumentError('ClientSession must be from the same MongoClient'); } + operation.session ??= session; + const readPreference = operation.readPreference ?? ReadPreference.primary; const inTransaction = !!session?.inTransaction(); @@ -231,6 +233,8 @@ async function tryOperation< let previousOperationError: MongoError | undefined; let previousServer: ServerDescription | undefined; + const isModernOperation = operation instanceof ModernOperation; + for (let tries = 0; tries < maxTries; tries++) { if (previousOperationError) { if (hasWriteAspect && previousOperationError.code === MMAPv1_RETRY_WRITES_ERROR_CODE) { @@ -280,7 +284,17 @@ async function tryOperation< if (tries > 0 && operation.hasAspect(Aspect.COMMAND_BATCHING)) { operation.resetBatch(); } - return await operation.execute(server, session, timeoutContext); + + if (!isModernOperation) { + return await operation.execute(server, session, timeoutContext); + } + + try { + const result = await server.modernCommand(operation, timeoutContext); + return operation.handleOk(result) as TResult; + } catch (error) { + operation.handleError(error); + } } catch (operationError) { if (!(operationError instanceof MongoError)) throw operationError; if ( diff --git a/src/operations/operation.ts b/src/operations/operation.ts index 190f2a522bd..d09568ca494 100644 --- a/src/operations/operation.ts +++ b/src/operations/operation.ts @@ -1,7 +1,8 @@ +import { type Connection, type MongoError } from '..'; import { type BSONSerializeOptions, type Document, resolveBSONOptions } from '../bson'; import { type Abortable } from '../mongo_types'; import { ReadPreference, type ReadPreferenceLike } from '../read_preference'; -import type { Server } from '../sdam/server'; +import type { Server, ServerCommandOptions } from '../sdam/server'; import type { ClientSession } from '../sessions'; import { type TimeoutContext } from '../timeout'; import type { MongoDBNamespace } from '../utils'; @@ -108,6 +109,10 @@ export abstract class AbstractOperation { return this._session; } + set session(session: ClientSession) { + this._session = session; + } + clearSession() { this._session = undefined; } @@ -125,6 +130,37 @@ export abstract class AbstractOperation { } } +export abstract class ModernOperation extends AbstractOperation { + /** this will never be used - but we must implement it to satisfy AbstractOperation's interface */ + override execute( + _server: Server, + _session: ClientSession | undefined, + _timeoutContext: TimeoutContext + ): Promise { + throw new Error('cannot execute!!'); + } + + abstract buildCommand(connection: Connection, session?: ClientSession): Document; + + abstract buildOptions(timeoutContext: TimeoutContext): ServerCommandOptions; + + /** + * Optional - if the operation performs error handling, such as wrapping or renaming the error, + * this method can be overridden. + */ + handleOk(response: Document) { + return response; + } + + /** + * Optional - if the operation performs post-processing + * on the result document, this method can be overridden. + */ + handleError(error: MongoError): void { + throw error; + } +} + export function defineAspects( operation: { aspects?: Set }, aspects: symbol | symbol[] | Set diff --git a/src/operations/search_indexes/drop.ts b/src/operations/search_indexes/drop.ts index 3b87bfad442..a0ef1314fde 100644 --- a/src/operations/search_indexes/drop.ts +++ b/src/operations/search_indexes/drop.ts @@ -1,13 +1,14 @@ +import { type Connection, type MongoError } from '../..'; import type { Document } from '../../bson'; import type { Collection } from '../../collection'; import { MONGODB_ERROR_CODES, MongoServerError } from '../../error'; -import type { Server } from '../../sdam/server'; +import type { Server, ServerCommandOptions } from '../../sdam/server'; import type { ClientSession } from '../../sessions'; import { type TimeoutContext } from '../../timeout'; -import { AbstractOperation } from '../operation'; +import { AbstractOperation, ModernOperation } from '../operation'; /** @internal */ -export class DropSearchIndexOperation extends AbstractOperation { +export class DropSearchIndexOperation extends ModernOperation { private readonly collection: Collection; private readonly name: string; @@ -15,17 +16,14 @@ export class DropSearchIndexOperation extends AbstractOperation { super(); this.collection = collection; this.name = name; + this.ns = collection.fullNamespace; } override get commandName() { return 'dropSearchIndex' as const; } - override async execute( - server: Server, - session: ClientSession | undefined, - timeoutContext: TimeoutContext - ): Promise { + override buildCommand(_connection: Connection, _session?: ClientSession): Document { const namespace = this.collection.fullNamespace; const command: Document = { @@ -35,15 +33,18 @@ export class DropSearchIndexOperation extends AbstractOperation { if (typeof this.name === 'string') { command.name = this.name; } + return command; + } + + override buildOptions(timeoutContext: TimeoutContext): ServerCommandOptions { + return { session: this.session, timeoutContext }; + } - try { - await server.command(namespace, command, { session, timeoutContext }); - } catch (error) { - const isNamespaceNotFoundError = - error instanceof MongoServerError && error.code === MONGODB_ERROR_CODES.NamespaceNotFound; - if (!isNamespaceNotFoundError) { - throw error; - } + override handleError(error: MongoError): void { + const isNamespaceNotFoundError = + error instanceof MongoServerError && error.code === MONGODB_ERROR_CODES.NamespaceNotFound; + if (!isNamespaceNotFoundError) { + throw error; } } } diff --git a/src/sdam/server.ts b/src/sdam/server.ts index 4d7052e3270..121bbef4f95 100644 --- a/src/sdam/server.ts +++ b/src/sdam/server.ts @@ -38,8 +38,9 @@ import { import type { ServerApi } from '../mongo_client'; import { type Abortable, TypedEventEmitter } from '../mongo_types'; import type { GetMoreOptions } from '../operations/get_more'; +import { type ModernOperation } from '../operations/operation'; import type { ClientSession } from '../sessions'; -import { type TimeoutContext } from '../timeout'; +import { Timeout, type TimeoutContext } from '../timeout'; import { isTransactionCommand } from '../transactions'; import { abortable, @@ -277,6 +278,100 @@ export class Server extends TypedEventEmitter { } } + public async modernCommand( + operation: ModernOperation, + timeoutContext: TimeoutContext + ): Promise { + if (this.s.state === STATE_CLOSING || this.s.state === STATE_CLOSED) { + throw new MongoServerClosedError(); + } + const session = operation.session; + + let conn = session?.pinnedConnection; + + this.incrementOperationCount(); + if (conn == null) { + try { + conn = await this.pool.checkOut({ timeoutContext }); + } catch (checkoutError) { + this.decrementOperationCount(); + if (!(checkoutError instanceof PoolClearedError)) this.handleError(checkoutError); + throw checkoutError; + } + } + + const cmd = operation.buildCommand(conn, session); + const options = operation.buildOptions(timeoutContext); + const ns = operation.ns; + + if (this.loadBalanced && isPinnableCommand(cmd, session)) { + session?.pin(conn); + } + + options.directConnection = this.topology.s.options.directConnection; + + // There are cases where we need to flag the read preference not to get sent in + // the command, such as pre-5.0 servers attempting to perform an aggregate write + // with a non-primary read preference. In this case the effective read preference + // (primary) is not the same as the provided and must be removed completely. + if (options.omitReadPreference) { + delete options.readPreference; + } + + if (this.description.iscryptd) { + options.omitMaxTimeMS = true; + } + + let reauthPromise: Promise | null = null; + + try { + try { + const res = await conn.command(ns, cmd, options); + throwIfWriteConcernError(res); + return res; + } catch (commandError) { + throw this.decorateCommandError(conn, cmd, options, commandError); + } + } catch (operationError) { + if ( + operationError instanceof MongoError && + operationError.code === MONGODB_ERROR_CODES.Reauthenticate + ) { + reauthPromise = this.pool.reauthenticate(conn); + reauthPromise.then(undefined, error => { + reauthPromise = null; + squashError(error); + }); + + await abortable(reauthPromise, options); + reauthPromise = null; // only reachable if reauth succeeds + + try { + const res = await conn.command(ns, cmd, options); + throwIfWriteConcernError(res); + return res; + } catch (commandError) { + throw this.decorateCommandError(conn, cmd, options, commandError); + } + } else { + throw operationError; + } + } finally { + this.decrementOperationCount(); + if (session?.pinnedConnection !== conn) { + if (reauthPromise != null) { + // The reauth promise only exists if it hasn't thrown. + const checkBackIn = () => { + this.pool.checkIn(conn); + }; + void reauthPromise.then(checkBackIn, checkBackIn); + } else { + this.pool.checkIn(conn); + } + } + } + } + public async command( ns: MongoDBNamespace, command: Document,