From cc6856f9f64b1023beb6c0aad075c2e0f544f2f1 Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Mon, 16 Mar 2026 11:53:13 +0100 Subject: [PATCH 1/4] fix(NODE-7430): throw timeout error when `withTransaction` retries exceed deadline --- src/sessions.ts | 172 +++++++++--------- .../transactions-convenient-api.prose.test.ts | 164 ++++++++++++++++- .../convenient-transactions.json | 107 ++++++++++- .../convenient-transactions.yml | 65 +++++++ 4 files changed, 423 insertions(+), 85 deletions(-) diff --git a/src/sessions.ts b/src/sessions.ts index ea209b63d10..09fa63c4349 100644 --- a/src/sessions.ts +++ b/src/sessions.ts @@ -17,6 +17,7 @@ import { MongoErrorLabel, MongoExpiredSessionError, MongoInvalidArgumentError, + MongoOperationTimeoutError, MongoRuntimeError, MongoServerError, MongoTransactionError, @@ -725,7 +726,7 @@ export class ClientSession timeoutMS?: number; } ): Promise { - const MAX_TIMEOUT = 120000; + const MAX_TIMEOUT = 120_000; const timeoutMS = options?.timeoutMS ?? this.timeoutMS ?? null; this.timeoutContext = @@ -737,10 +738,21 @@ export class ClientSession }) : null; - // 1. Record the current monotonic time, which will be used to enforce the 120-second timeout before later retry attempts. - const startTime = this.timeoutContext?.csotEnabled() // This is strictly to appease TS. We must narrow the context to a CSOT context before accessing `.start`. - ? this.timeoutContext.start - : processTimeMS(); + // 1. Define the following: + // 1.1 Record the current monotonic time, which will be used to enforce the timeout before later retry attempts. + // 1.2 Set `transactionAttempt` to `0`. + // 1.3 Set `TIMEOUT_MS` to be `timeoutMS` if given, otherwise MAX_TIMEOUT (120-seconds). + // + // The spec describes timeout checks as "elapsed time < TIMEOUT_MS" (where elapsed = now - start). + // We precompute `deadline = start + TIMEOUT_MS` so each check becomes simply `now < deadline`. + // + // Note 1: When TIMEOUT_MS is reached, we MUST report a timeout error wrapping the last error that + // triggered retry. With CSOT this is a MongoOperationTimeoutError; without CSOT the raw error + // is propagated directly. See makeTimeoutError() below. + const csotEnabled = !!this.timeoutContext?.csotEnabled(); + const deadline = this.timeoutContext?.csotEnabled() + ? processTimeMS() + this.timeoutContext.remainingTimeMS + : processTimeMS() + MAX_TIMEOUT; let committed = false; let result: T; @@ -749,20 +761,16 @@ export class ClientSession try { retryTransaction: for ( - // 2. Set `transactionAttempt` to `0`. + // 1.2 Set `transactionAttempt` to `0`. let transactionAttempt = 0, isRetry = false; !committed; ++transactionAttempt, isRetry = transactionAttempt > 0 ) { // 2. If `transactionAttempt` > 0: if (isRetry) { - // 2.i If elapsed time + `backoffMS` > `TIMEOUT_MS`, then raise the previously encountered error. If the elapsed time of - // `withTransaction` is less than TIMEOUT_MS, calculate the backoffMS to be - // `jitter * min(BACKOFF_INITIAL * 1.5 ** (transactionAttempt - 1), BACKOFF_MAX)`. sleep for `backoffMS`. - // 2.i.i jitter is a random float between \[0, 1) - // 2.i.ii `transactionAttempt` is the variable defined in step 1. - // 2.i.iii `BACKOFF_INITIAL` is 5ms - // 2.i.iv `BACKOFF_MAX` is 500ms + // 2.1 Calculate backoffMS. If elapsed time + backoffMS > TIMEOUT_MS + // (i.e., now + backoff >= deadline), raise the previously encountered error (see Note 1). + // Otherwise, sleep for backoffMS. const BACKOFF_INITIAL_MS = 5; const BACKOFF_MAX_MS = 500; const BACKOFF_GROWTH = 1.5; @@ -774,30 +782,26 @@ export class ClientSession BACKOFF_MAX_MS ); - const willExceedTransactionDeadline = - (this.timeoutContext?.csotEnabled() && - backoffMS > this.timeoutContext.remainingTimeMS) || - processTimeMS() + backoffMS > startTime + MAX_TIMEOUT; - - if (willExceedTransactionDeadline) { - throw ( + if (processTimeMS() + backoffMS >= deadline) { + throw makeTimeoutError( lastError ?? - new MongoRuntimeError( - `Transaction retry did not record an error: should never occur. Please file a bug.` - ) + new MongoRuntimeError( + `Transaction retry did not record an error: should never occur. Please file a bug.` + ), + csotEnabled ); } await setTimeout(backoffMS); } - // 3. Invoke startTransaction on the session - // 4. If `startTransaction` reported an error, propagate that error to the caller of `withTransaction` and return immediately. + // 3. Invoke startTransaction on the session and increment transactionAttempt. + // 4. If startTransaction reported an error, propagate that error to the caller and return immediately. this.startTransaction(options); // may throw on error try { // 5. Invoke the callback. - // 6. Control returns to withTransaction. (continued below) + // 6. Control returns to withTransaction. Determine the current state and whether the callback reported an error. const promise = fn(this); if (!isPromiseLike(promise)) { throw new MongoInvalidArgumentError( @@ -807,17 +811,16 @@ export class ClientSession result = await promise; - // 6. (cont.) Determine the current state of the ClientSession (continued below) + // 8. If the ClientSession is in the "no transaction", "transaction aborted", or "transaction committed" + // state, assume the callback intentionally aborted or committed the transaction and return immediately. + // Drivers MAY allow the callback to return a value to be propagated as the return value of withTransaction. if ( this.transaction.state === TxnState.NO_TRANSACTION || this.transaction.state === TxnState.TRANSACTION_COMMITTED || this.transaction.state === TxnState.TRANSACTION_ABORTED ) { - // 8. If the ClientSession is in the "no transaction", "transaction aborted", or "transaction committed" state, - // assume the callback intentionally aborted or committed the transaction and return immediately. return result; } - // 5. (cont.) and whether the callback reported an error // 7. If the callback reported an error: } catch (fnError) { if (!(fnError instanceof MongoError) || fnError instanceof MongoInvalidArgumentError) { @@ -827,83 +830,69 @@ export class ClientSession throw fnError; } + lastError = fnError; + + // 7.1 If the ClientSession is in the "starting transaction" or "transaction in progress" + // state, invoke abortTransaction on the session. if ( this.transaction.state === TxnState.STARTING_TRANSACTION || this.transaction.state === TxnState.TRANSACTION_IN_PROGRESS ) { - // 7.i If the ClientSession is in the "starting transaction" or "transaction in progress" state, - // invoke abortTransaction on the session await this.abortTransaction(); } - if ( - fnError.hasErrorLabel(MongoErrorLabel.TransientTransactionError) && - (this.timeoutContext?.csotEnabled() || processTimeMS() - startTime < MAX_TIMEOUT) - ) { - // 7.ii If the callback's error includes a "TransientTransactionError" label and the elapsed time of `withTransaction` - // is less than 120 seconds, jump back to step two. - lastError = fnError; + // 7.2 If the callback's error includes a "TransientTransactionError" label, jump back to step two. + if (fnError.hasErrorLabel(MongoErrorLabel.TransientTransactionError)) { continue retryTransaction; } - // 7.iii If the callback's error includes a "UnknownTransactionCommitResult" label, the callback must have manually committed a transaction, - // propagate the callback's error to the caller of withTransaction and return immediately. - // The 7.iii check is redundant with 6.iv, so we don't write code for it - // 7.iv Otherwise, propagate the callback's error to the caller of withTransaction and return immediately. + // 7.3 If the callback's error includes a "UnknownTransactionCommitResult" label, the callback + // must have manually committed a transaction, propagate the error and return immediately. + // (This check is redundant with step 8, so we don't write code for it.) + // 7.4 Otherwise, propagate the callback's error (see Note 1) and return immediately. throw fnError; } + // 9. Invoke commitTransaction on the session. + // We will rely on ClientSession.commitTransaction() to apply a majority write concern + // if commitTransaction is being retried (see: DRIVERS-601). retryCommit: while (!committed) { try { - /* - * We will rely on ClientSession.commitTransaction() to - * apply a majority write concern if commitTransaction is - * being retried (see: DRIVERS-601) - */ - // 9. Invoke commitTransaction on the session. await this.commitTransaction(); committed = true; - // 10. If commitTransaction reported an error: } catch (commitError) { - // If CSOT is enabled, we repeatedly retry until timeoutMS expires. This is enforced by providing a - // timeoutContext to each async API, which know how to cancel themselves (i.e., the next retry will - // abort the withTransaction call). - // If CSOT is not enabled, do we still have time remaining or have we timed out? - const hasTimedOut = - !this.timeoutContext?.csotEnabled() && processTimeMS() - startTime >= MAX_TIMEOUT; - - if (!hasTimedOut) { - /* - * Note: a maxTimeMS error will have the MaxTimeMSExpired - * code (50) and can be reported as a top-level error or - * inside writeConcernError, ex. - * { ok:0, code: 50, codeName: 'MaxTimeMSExpired' } - * { ok:1, writeConcernError: { code: 50, codeName: 'MaxTimeMSExpired' } } - */ - if ( - !isMaxTimeMSExpiredError(commitError) && - commitError.hasErrorLabel(MongoErrorLabel.UnknownTransactionCommitResult) - ) { - // 10.i If the `commitTransaction` error includes a "UnknownTransactionCommitResult" label and the error is not - // MaxTimeMSExpired and the elapsed time of `withTransaction` is less than 120 seconds, jump back to step eight. - continue retryCommit; - } - - if (commitError.hasErrorLabel(MongoErrorLabel.TransientTransactionError)) { - // 10.ii If the commitTransaction error includes a "TransientTransactionError" label - // and the elapsed time of withTransaction is less than 120 seconds, jump back to step two. - lastError = commitError; - - continue retryTransaction; - } + // 10. If commitTransaction reported an error: + lastError = commitError; + + // If elapsed time >= TIMEOUT_MS (i.e., now >= deadline), raise a timeout error (see Note 1). + if (processTimeMS() >= deadline) { + throw makeTimeoutError(commitError, csotEnabled); + } + + // 10.1 If the error includes "UnknownTransactionCommitResult" and is not MaxTimeMSExpired + // and elapsed time < TIMEOUT_MS (guaranteed — deadline check above), jump back to step nine. + // Note: a maxTimeMS error will have the MaxTimeMSExpired code (50) and can be reported + // as a top-level error or inside writeConcernError. + if ( + !isMaxTimeMSExpiredError(commitError) && + commitError.hasErrorLabel(MongoErrorLabel.UnknownTransactionCommitResult) + ) { + continue retryCommit; } - // 10.iii Otherwise, propagate the commitTransaction error to the caller of withTransaction and return immediately. + // 10.2 If the error includes "TransientTransactionError" and elapsed time < TIMEOUT_MS + // (guaranteed — deadline check above), jump back to step two. + if (commitError.hasErrorLabel(MongoErrorLabel.TransientTransactionError)) { + continue retryTransaction; + } + + // 10.3 Otherwise, propagate the commitTransaction error (see Note 1) and return immediately. throw commitError; } } } + // 11. The transaction was committed successfully. Return immediately. // @ts-expect-error Result is always defined if we reach here, the for-loop above convinces TS it is not. return result; } finally { @@ -912,6 +901,25 @@ export class ClientSession } } +function makeTimeoutError(cause: Error, csotEnabled: boolean): Error { + // Async APIs know how to cancel themselves and might return CSOT error + if (cause instanceof MongoOperationTimeoutError) { + return cause; + } + if (csotEnabled) { + const timeoutError = new MongoOperationTimeoutError('Timed out during withTransaction', { + cause + }); + if (cause instanceof MongoError) { + for (const label of cause.errorLabels) { + timeoutError.addErrorLabel(label); + } + } + return timeoutError; + } + return cause; +} + const NON_DETERMINISTIC_WRITE_CONCERN_ERRORS = new Set([ 'CannotSatisfyWriteConcern', 'UnknownReplWriteConcern', diff --git a/test/integration/transactions-convenient-api/transactions-convenient-api.prose.test.ts b/test/integration/transactions-convenient-api/transactions-convenient-api.prose.test.ts index 1f4e67687de..6e304eab4e7 100644 --- a/test/integration/transactions-convenient-api/transactions-convenient-api.prose.test.ts +++ b/test/integration/transactions-convenient-api/transactions-convenient-api.prose.test.ts @@ -2,8 +2,13 @@ import { expect } from 'chai'; import { test } from 'mocha'; import * as sinon from 'sinon'; -import { type ClientSession, type Collection, type MongoClient } from '../../mongodb'; -import { configureFailPoint, type FailCommandFailPoint, measureDuration } from '../../tools/utils'; +import { type ClientSession, type Collection, type MongoClient, MongoError } from '../../mongodb'; +import { + clearFailPoint, + configureFailPoint, + type FailCommandFailPoint, + measureDuration +} from '../../tools/utils'; const failCommand: FailCommandFailPoint = { configureFailPoint: 'failCommand', @@ -85,3 +90,158 @@ describe('Retry Backoff is Enforced', function () { } ); }); + +describe('Retry Timeout is Enforced', function () { + // Drivers should test that withTransaction enforces a non-configurable timeout before retrying + // both commits and entire transactions. + // + // We stub performance.now() to simulate elapsed time exceeding the 120-second retry limit, + // as recommended by the spec: "This might be done by internally modifying the timeout value + // used by withTransaction with some private API or using a mock timer." + // + // Without CSOT, the original error is propagated directly. + // With CSOT, the error is wrapped in a MongoOperationTimeoutError. + + let client: MongoClient; + let collection: Collection; + let timeOffset: number; + + beforeEach(async function () { + client = this.configuration.newClient(); + collection = client.db('foo').collection('bar'); + + timeOffset = 0; + const originalNow = performance.now.bind(performance); + sinon.stub(performance, 'now').callsFake(() => originalNow() + timeOffset); + }); + + afterEach(async function () { + sinon.restore(); + await clearFailPoint(this.configuration); + await client?.close(); + }); + + // Case 1: If the callback raises an error with the TransientTransactionError label and the retry + // timeout has been exceeded, withTransaction should propagate the error to its caller. + test( + 'callback TransientTransactionError propagated when retry timeout exceeded', + { + requires: { + mongodb: '>=4.4', + topology: '!single' + } + }, + async function () { + // 1. Configure a failpoint that fails insert with TransientTransactionError. + await configureFailPoint(this.configuration, { + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['insert'], + errorCode: 24, + errorLabels: ['TransientTransactionError'] + } + }); + + // 2. Run withTransaction. The callback advances the clock past the 120-second retry + // limit before the insert fails, so the timeout is detected immediately. + const { result } = await measureDuration(() => { + return client.withSession(async s => { + await s.withTransaction(async session => { + timeOffset = 120_000; + await collection.insertOne({}, { session }); + }); + }); + }); + + // 3. Assert that the error is the original TransientTransactionError (propagated directly + // in the legacy non-CSOT path). + expect(result).to.be.instanceOf(MongoError); + expect((result as MongoError).hasErrorLabel('TransientTransactionError')).to.be.true; + } + ); + + // Case 2: If committing raises an error with the UnknownTransactionCommitResult label, and the + // retry timeout has been exceeded, withTransaction should propagate the error to + // its caller. + test( + 'commit UnknownTransactionCommitResult propagated when retry timeout exceeded', + { + requires: { + mongodb: '>=4.4', + topology: '!single' + } + }, + async function () { + // 1. Configure a failpoint that fails commitTransaction with UnknownTransactionCommitResult. + await configureFailPoint(this.configuration, { + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['commitTransaction'], + errorCode: 64, + errorLabels: ['UnknownTransactionCommitResult'] + } + }); + + // 2. Run withTransaction. The callback advances the clock past the 120-second retry + // limit. The insert succeeds, but the commit fails and the timeout is detected. + const { result } = await measureDuration(() => { + return client.withSession(async s => { + await s.withTransaction(async session => { + timeOffset = 120_000; + await collection.insertOne({}, { session }); + }); + }); + }); + + // 3. Assert that the error is the original commit error (propagated directly + // in the legacy non-CSOT path). + expect(result).to.be.instanceOf(MongoError); + expect((result as MongoError).hasErrorLabel('UnknownTransactionCommitResult')).to.be.true; + } + ); + + // Case 3: If committing raises an error with the TransientTransactionError label and the retry + // timeout has been exceeded, withTransaction should propagate the error to its + // caller. This case may occur if the commit was internally retried against a new primary after a + // failover and the second primary returned a NoSuchTransaction error response. + test( + 'commit TransientTransactionError propagated when retry timeout exceeded', + { + requires: { + mongodb: '>=4.4', + topology: '!single' + } + }, + async function () { + // 1. Configure a failpoint that fails commitTransaction with TransientTransactionError + // (errorCode 251 = NoSuchTransaction). + await configureFailPoint(this.configuration, { + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['commitTransaction'], + errorCode: 251, + errorLabels: ['TransientTransactionError'] + } + }); + + // 2. Run withTransaction. The callback advances the clock past the 120-second retry + // limit. The insert succeeds, but the commit fails and the timeout is detected. + const { result } = await measureDuration(() => { + return client.withSession(async s => { + await s.withTransaction(async session => { + timeOffset = 120_000; + await collection.insertOne({}, { session }); + }); + }); + }); + + // 3. Assert that the error is the original commit error (propagated directly + // in the legacy non-CSOT path). + expect(result).to.be.instanceOf(MongoError); + expect((result as MongoError).hasErrorLabel('TransientTransactionError')).to.be.true; + } + ); +}); diff --git a/test/spec/client-side-operations-timeout/convenient-transactions.json b/test/spec/client-side-operations-timeout/convenient-transactions.json index f9d03429db9..3400b82ba92 100644 --- a/test/spec/client-side-operations-timeout/convenient-transactions.json +++ b/test/spec/client-side-operations-timeout/convenient-transactions.json @@ -27,7 +27,8 @@ "awaitMinPoolSizeMS": 10000, "useMultipleMongoses": false, "observeEvents": [ - "commandStartedEvent" + "commandStartedEvent", + "commandFailedEvent" ] } }, @@ -188,6 +189,11 @@ } } }, + { + "commandFailedEvent": { + "commandName": "insert" + } + }, { "commandStartedEvent": { "commandName": "abortTransaction", @@ -206,6 +212,105 @@ ] } ] + }, + { + "description": "withTransaction surfaces a timeout after exhausting transient transaction retries, retaining the last transient error as the timeout cause.", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "failPointClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": "alwaysOn", + "data": { + "failCommands": [ + "insert" + ], + "blockConnection": true, + "blockTimeMS": 25, + "errorCode": 24, + "errorLabels": [ + "TransientTransactionError" + ] + } + } + } + }, + { + "name": "withTransaction", + "object": "session", + "arguments": { + "callback": [ + { + "name": "insertOne", + "object": "collection", + "arguments": { + "document": { + "_id": 1 + }, + "session": "session" + }, + "expectError": { + "isError": true + } + } + ] + }, + "expectError": { + "isTimeoutError": true + } + } + ], + "expectEvents": [ + { + "client": "client", + "ignoreExtraEvents": true, + "events": [ + { + "commandStartedEvent": { + "commandName": "insert" + } + }, + { + "commandFailedEvent": { + "commandName": "insert" + } + }, + { + "commandStartedEvent": { + "commandName": "abortTransaction" + } + }, + { + "commandFailedEvent": { + "commandName": "abortTransaction" + } + }, + { + "commandStartedEvent": { + "commandName": "insert" + } + }, + { + "commandFailedEvent": { + "commandName": "insert" + } + }, + { + "commandStartedEvent": { + "commandName": "abortTransaction" + } + }, + { + "commandFailedEvent": { + "commandName": "abortTransaction" + } + } + ] + } + ] } ] } diff --git a/test/spec/client-side-operations-timeout/convenient-transactions.yml b/test/spec/client-side-operations-timeout/convenient-transactions.yml index 55b72481dfb..8157c5e4d85 100644 --- a/test/spec/client-side-operations-timeout/convenient-transactions.yml +++ b/test/spec/client-side-operations-timeout/convenient-transactions.yml @@ -19,6 +19,7 @@ createEntities: useMultipleMongoses: false observeEvents: - commandStartedEvent + - commandFailedEvent - database: id: &database database client: *client @@ -104,9 +105,73 @@ tests: command: insert: *collectionName maxTimeMS: { $$type: ["int", "long"] } + - commandFailedEvent: + commandName: insert - commandStartedEvent: commandName: abortTransaction databaseName: admin command: abortTransaction: 1 maxTimeMS: { $$type: [ "int", "long" ] } + + # This test verifies that when withTransaction encounters transient transaction errors it does not + # throw the transient transaction error when the timeout is exceeded, but instead surfaces a timeout error after + # exhausting the retry attempts within the specified timeout. + # The timeout error thrown contains as a cause the last transient error encountered. + - description: "withTransaction surfaces a timeout after exhausting transient transaction retries, retaining the last transient error as the timeout cause." + operations: + - name: failPoint + object: testRunner + arguments: + client: *failPointClient + failPoint: + configureFailPoint: failCommand + mode: alwaysOn + data: + failCommands: ["insert"] + blockConnection: true + blockTimeMS: 25 + errorCode: 24 + errorLabels: ["TransientTransactionError"] + + - name: withTransaction + object: *session + arguments: + callback: + - name: insertOne + object: *collection + arguments: + document: { _id: 1 } + session: *session + expectError: + isError: true + expectError: + isTimeoutError: true + + # Verify that multiple insert (at least 2) attempts occurred due to TransientTransactionError retries + # The exact number of events depends on timing and retry backoff, but there should be at least: + # - 2 commandStartedEvent for insert (initial + at least one retry) + # - 2 commandFailedEvent for insert (corresponding failures) + expectEvents: + - client: *client + ignoreExtraEvents: true + events: + # First insert attempt + - commandStartedEvent: + commandName: insert + - commandFailedEvent: + commandName: insert + - commandStartedEvent: + commandName: abortTransaction + - commandFailedEvent: + commandName: abortTransaction + + # Second insert attempt (retry due to TransientTransactionError) + - commandStartedEvent: + commandName: insert + - commandFailedEvent: + commandName: insert + - commandStartedEvent: + commandName: abortTransaction + - commandFailedEvent: + commandName: abortTransaction From 65c8500f5e8f5f246b0acafbd8cada2b42f0244d Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Fri, 10 Apr 2026 13:29:06 +0200 Subject: [PATCH 2/4] update to latest spec --- src/sessions.ts | 58 ++++++++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/src/sessions.ts b/src/sessions.ts index 09fa63c4349..ec4363b4e10 100644 --- a/src/sessions.ts +++ b/src/sessions.ts @@ -739,16 +739,19 @@ export class ClientSession : null; // 1. Define the following: - // 1.1 Record the current monotonic time, which will be used to enforce the timeout before later retry attempts. + // 1.1 Record the current monotonic time, which will be used to enforce the 120-second / CSOT timeout before later retry attempts. // 1.2 Set `transactionAttempt` to `0`. // 1.3 Set `TIMEOUT_MS` to be `timeoutMS` if given, otherwise MAX_TIMEOUT (120-seconds). // // The spec describes timeout checks as "elapsed time < TIMEOUT_MS" (where elapsed = now - start). // We precompute `deadline = start + TIMEOUT_MS` so each check becomes simply `now < deadline`. // - // Note 1: When TIMEOUT_MS is reached, we MUST report a timeout error wrapping the last error that - // triggered retry. With CSOT this is a MongoOperationTimeoutError; without CSOT the raw error - // is propagated directly. See makeTimeoutError() below. + // Timeout Error propagation mechanism + // When the TIMEOUT_MS (calculated in step 1.3) is reached we MUST report a timeout error wrapping the previously + // encountered error. If timeoutMS is set, then timeout error is a special type which is defined in CSOT + // specification, If timeoutMS is not set, then propagate it as timeout error if the language allows to expose the + // previously encountered error as a cause of a timeout error (see makeTimeoutError below in pseudo-code). If + // timeout error is thrown then it SHOULD copy all error label(s) from the previously encountered retriable error. const csotEnabled = !!this.timeoutContext?.csotEnabled(); const deadline = this.timeoutContext?.csotEnabled() ? processTimeMS() + this.timeoutContext.remainingTimeMS @@ -768,12 +771,16 @@ export class ClientSession ) { // 2. If `transactionAttempt` > 0: if (isRetry) { - // 2.1 Calculate backoffMS. If elapsed time + backoffMS > TIMEOUT_MS - // (i.e., now + backoff >= deadline), raise the previously encountered error (see Note 1). - // Otherwise, sleep for backoffMS. + // 2.1 If elapsed time + backoffMS > TIMEOUT_MS, then propagate the previously encountered + // error (see propagation section above). If the elapsed time of withTransaction is less + // than TIMEOUT_MS, calculate the backoffMS to be + // jitter * min(BACKOFF_INITIAL * 1.5 ** (transactionAttempt - 1), BACKOFF_MAX). + // sleep for backoffMS. const BACKOFF_INITIAL_MS = 5; const BACKOFF_MAX_MS = 500; const BACKOFF_GROWTH = 1.5; + // 2.1.1 Jitter is a random float between [0, 1), optionally including 1, depending on what is most natural + // for the given driver language. const jitter = Math.random(); const backoffMS = jitter * @@ -795,13 +802,16 @@ export class ClientSession await setTimeout(backoffMS); } - // 3. Invoke startTransaction on the session and increment transactionAttempt. + // 3. Invoke startTransaction on the session and increment transactionAttempt. If TransactionOptions were + // specified in the call to withTransaction, those MUST be used for startTransaction. Note that + // ClientSession.defaultTransactionOptions will be used in the absence of any explicit TransactionOptions. // 4. If startTransaction reported an error, propagate that error to the caller and return immediately. this.startTransaction(options); // may throw on error try { - // 5. Invoke the callback. - // 6. Control returns to withTransaction. Determine the current state and whether the callback reported an error. + // 5. Invoke the callback. Drivers MUST ensure that the ClientSession can be accessed within the callback + // (e.g. pass ClientSession as the first parameter, rely on lexical scoping). Drivers MAY pass additional + // parameters as needed (e.g. user data solicited by withTransaction). const promise = fn(this); if (!isPromiseLike(promise)) { throw new MongoInvalidArgumentError( @@ -809,6 +819,8 @@ export class ClientSession ); } + // 6. Control returns to withTransaction. Determine the current state of the ClientSession and whether the + // callback reported an error (e.g. thrown exception, error output parameter). result = await promise; // 8. If the ClientSession is in the "no transaction", "transaction aborted", or "transaction committed" @@ -843,6 +855,9 @@ export class ClientSession // 7.2 If the callback's error includes a "TransientTransactionError" label, jump back to step two. if (fnError.hasErrorLabel(MongoErrorLabel.TransientTransactionError)) { + if (processTimeMS() >= deadline) { + throw makeTimeoutError(lastError, csotEnabled); + } continue retryTransaction; } @@ -864,29 +879,28 @@ export class ClientSession // 10. If commitTransaction reported an error: lastError = commitError; - // If elapsed time >= TIMEOUT_MS (i.e., now >= deadline), raise a timeout error (see Note 1). - if (processTimeMS() >= deadline) { - throw makeTimeoutError(commitError, csotEnabled); - } - - // 10.1 If the error includes "UnknownTransactionCommitResult" and is not MaxTimeMSExpired - // and elapsed time < TIMEOUT_MS (guaranteed — deadline check above), jump back to step nine. - // Note: a maxTimeMS error will have the MaxTimeMSExpired code (50) and can be reported - // as a top-level error or inside writeConcernError. + // 10.1 If the commitTransaction error includes a UnknownTransactionCommitResult label and the error is not MaxTimeMSExpired if ( !isMaxTimeMSExpiredError(commitError) && commitError.hasErrorLabel(MongoErrorLabel.UnknownTransactionCommitResult) ) { + // 10.1.1 If the elapsed time of withTransaction exceeded TIMEOUT_MS, propagate the commitTransaction error to the caller + // of withTransaction and return immediately (see propagation section above) + if (processTimeMS() >= deadline) { + throw makeTimeoutError(commitError, csotEnabled); + } + // 10.1.2 If the elapsed time of withTransaction is less than TIMEOUT_MS, jump back to step nine. We will trust + // commitTransaction to apply a majority write concern on retry attempts (see: Majority write concern is used + // when retrying commitTransaction). continue retryCommit; } - // 10.2 If the error includes "TransientTransactionError" and elapsed time < TIMEOUT_MS - // (guaranteed — deadline check above), jump back to step two. + // 10.2 If the commitTransaction error includes a "TransientTransactionError" label, jump back to step two. if (commitError.hasErrorLabel(MongoErrorLabel.TransientTransactionError)) { continue retryTransaction; } - // 10.3 Otherwise, propagate the commitTransaction error (see Note 1) and return immediately. + // 10.3 Otherwise, propagate the commitTransaction error to the caller of withTransaction and return immediately. throw commitError; } } From 19f51b5d7761350981e7d2e00f4e20a2269615d4 Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Mon, 13 Apr 2026 10:47:48 +0200 Subject: [PATCH 3/4] update to latest spec --- src/sessions.ts | 84 ++++--- .../transactions-convenient-api.prose.test.ts | 234 ++++++++++++------ 2 files changed, 206 insertions(+), 112 deletions(-) diff --git a/src/sessions.ts b/src/sessions.ts index ec4363b4e10..36cc4b9860d 100644 --- a/src/sessions.ts +++ b/src/sessions.ts @@ -742,20 +742,23 @@ export class ClientSession // 1.1 Record the current monotonic time, which will be used to enforce the 120-second / CSOT timeout before later retry attempts. // 1.2 Set `transactionAttempt` to `0`. // 1.3 Set `TIMEOUT_MS` to be `timeoutMS` if given, otherwise MAX_TIMEOUT (120-seconds). - // + + // Timeout Error propagation + // When the previously encountered error needs to be propagated because there is no more time for another attempt, + // and it is not already a timeout error, then: + // - A timeout error MUST be propagated instead. It MUST expose the previously encountered error as specified in + // the "Errors" section of the CSOT specification. + // - If exposing the previously encountered error from a timeout error is impossible in a driver, then the driver + // is exempt from the requirement and MUST propagate the previously encountered error as is. The timeout error + // MUST copy all error labels from the previously encountered error. + // The spec describes timeout checks as "elapsed time < TIMEOUT_MS" (where elapsed = now - start). - // We precompute `deadline = start + TIMEOUT_MS` so each check becomes simply `now < deadline`. - // - // Timeout Error propagation mechanism - // When the TIMEOUT_MS (calculated in step 1.3) is reached we MUST report a timeout error wrapping the previously - // encountered error. If timeoutMS is set, then timeout error is a special type which is defined in CSOT - // specification, If timeoutMS is not set, then propagate it as timeout error if the language allows to expose the - // previously encountered error as a cause of a timeout error (see makeTimeoutError below in pseudo-code). If - // timeout error is thrown then it SHOULD copy all error label(s) from the previously encountered retriable error. + // We precompute `deadline = now + remainingTimeMS` so each check becomes simply `now < deadline`. const csotEnabled = !!this.timeoutContext?.csotEnabled(); - const deadline = this.timeoutContext?.csotEnabled() - ? processTimeMS() + this.timeoutContext.remainingTimeMS - : processTimeMS() + MAX_TIMEOUT; + const remainingTimeMS = this.timeoutContext?.csotEnabled() + ? this.timeoutContext.remainingTimeMS + : MAX_TIMEOUT; + const deadline = processTimeMS() + remainingTimeMS; let committed = false; let result: T; @@ -764,23 +767,23 @@ export class ClientSession try { retryTransaction: for ( - // 1.2 Set `transactionAttempt` to `0`. let transactionAttempt = 0, isRetry = false; !committed; ++transactionAttempt, isRetry = transactionAttempt > 0 ) { // 2. If `transactionAttempt` > 0: if (isRetry) { - // 2.1 If elapsed time + backoffMS > TIMEOUT_MS, then propagate the previously encountered - // error (see propagation section above). If the elapsed time of withTransaction is less - // than TIMEOUT_MS, calculate the backoffMS to be - // jitter * min(BACKOFF_INITIAL * 1.5 ** (transactionAttempt - 1), BACKOFF_MAX). - // sleep for backoffMS. + // 2.1 Calculate backoffMS to be jitter * min(BACKOFF_INITIAL * 1.5 ** (transactionAttempt - 1), BACKOFF_MAX). + // If elapsed time + backoffMS > TIMEOUT_MS, then propagate the previously encountered error to the caller of + // withTransaction as per timeout error propagation and return immediately. Otherwise, sleep for backoffMS. + // 2.1.1 jitter is a random float between [0, 1), optionally including 1, depending on what is most natural + // for the given driver language. + // 2.1.2 transactionAttempt is the variable defined in step 1. + // 2.1.3 BACKOFF_INITIAL is 5ms + // 2.1.4 BACKOFF_MAX is 500ms const BACKOFF_INITIAL_MS = 5; const BACKOFF_MAX_MS = 500; const BACKOFF_GROWTH = 1.5; - // 2.1.1 Jitter is a random float between [0, 1), optionally including 1, depending on what is most natural - // for the given driver language. const jitter = Math.random(); const backoffMS = jitter * @@ -805,8 +808,9 @@ export class ClientSession // 3. Invoke startTransaction on the session and increment transactionAttempt. If TransactionOptions were // specified in the call to withTransaction, those MUST be used for startTransaction. Note that // ClientSession.defaultTransactionOptions will be used in the absence of any explicit TransactionOptions. - // 4. If startTransaction reported an error, propagate that error to the caller and return immediately. - this.startTransaction(options); // may throw on error + // 4. If startTransaction reported an error, propagate that error to the caller of withTransaction as is and + // return immediately. + this.startTransaction(options); try { // 5. Invoke the callback. Drivers MUST ensure that the ClientSession can be accessed within the callback @@ -825,7 +829,6 @@ export class ClientSession // 8. If the ClientSession is in the "no transaction", "transaction aborted", or "transaction committed" // state, assume the callback intentionally aborted or committed the transaction and return immediately. - // Drivers MAY allow the callback to return a value to be propagated as the return value of withTransaction. if ( this.transaction.state === TxnState.NO_TRANSACTION || this.transaction.state === TxnState.TRANSACTION_COMMITTED || @@ -833,8 +836,8 @@ export class ClientSession ) { return result; } - // 7. If the callback reported an error: } catch (fnError) { + // 7. If the callback reported an error if (!(fnError instanceof MongoError) || fnError instanceof MongoInvalidArgumentError) { // This first preemptive abort regardless of TxnState isn't spec, // and it's unclear whether it's serving a practical purpose, but this logic is OLD @@ -861,46 +864,45 @@ export class ClientSession continue retryTransaction; } - // 7.3 If the callback's error includes a "UnknownTransactionCommitResult" label, the callback - // must have manually committed a transaction, propagate the error and return immediately. - // (This check is redundant with step 8, so we don't write code for it.) - // 7.4 Otherwise, propagate the callback's error (see Note 1) and return immediately. + // 7.3 If the callback's error includes a "UnknownTransactionCommitResult" label, the callback must + // have manually committed a transaction, propagate the callback's error to the caller of withTransaction + // as is and return immediately. + // 7.4 Otherwise, propagate the callback's error to the caller of withTransaction as is and return immediately. throw fnError; } - // 9. Invoke commitTransaction on the session. - // We will rely on ClientSession.commitTransaction() to apply a majority write concern - // if commitTransaction is being retried (see: DRIVERS-601). retryCommit: while (!committed) { try { + // 9. Invoke commitTransaction on the session. await this.commitTransaction(); committed = true; } catch (commitError) { // 10. If commitTransaction reported an error: lastError = commitError; - // 10.1 If the commitTransaction error includes a UnknownTransactionCommitResult label and the error is not MaxTimeMSExpired + // 10.1 If the commitTransaction error includes a UnknownTransactionCommitResult label and the error is + // not MaxTimeMSExpired if ( - !isMaxTimeMSExpiredError(commitError) && - commitError.hasErrorLabel(MongoErrorLabel.UnknownTransactionCommitResult) + commitError.hasErrorLabel(MongoErrorLabel.UnknownTransactionCommitResult) && + !isMaxTimeMSExpiredError(commitError) ) { - // 10.1.1 If the elapsed time of withTransaction exceeded TIMEOUT_MS, propagate the commitTransaction error to the caller - // of withTransaction and return immediately (see propagation section above) + // 10.1.1 If the elapsed time of withTransaction exceeded TIMEOUT_MS, propagate the commitTransaction + // error to the caller of withTransaction as per timeout error propagation and return immediately. if (processTimeMS() >= deadline) { throw makeTimeoutError(commitError, csotEnabled); } - // 10.1.2 If the elapsed time of withTransaction is less than TIMEOUT_MS, jump back to step nine. We will trust - // commitTransaction to apply a majority write concern on retry attempts (see: Majority write concern is used - // when retrying commitTransaction). + // 10.1.2 Otherwise, jump back to step nine. We will trust commitTransaction to apply a majority write + // concern on retry attempts (see: Majority write concern is used when retrying commitTransaction). continue retryCommit; } - // 10.2 If the commitTransaction error includes a "TransientTransactionError" label, jump back to step two. + // 10.2 If the commitTransaction error includes a TransientTransactionError label, jump back to step two. if (commitError.hasErrorLabel(MongoErrorLabel.TransientTransactionError)) { continue retryTransaction; } - // 10.3 Otherwise, propagate the commitTransaction error to the caller of withTransaction and return immediately. + // 10.3 Otherwise, propagate the commitTransaction error to the caller of withTransaction as is and return + // immediately. throw commitError; } } diff --git a/test/integration/transactions-convenient-api/transactions-convenient-api.prose.test.ts b/test/integration/transactions-convenient-api/transactions-convenient-api.prose.test.ts index 6e304eab4e7..f1df06032ef 100644 --- a/test/integration/transactions-convenient-api/transactions-convenient-api.prose.test.ts +++ b/test/integration/transactions-convenient-api/transactions-convenient-api.prose.test.ts @@ -1,5 +1,4 @@ import { expect } from 'chai'; -import { test } from 'mocha'; import * as sinon from 'sinon'; import { type ClientSession, type Collection, type MongoClient, MongoError } from '../../mongodb'; @@ -10,98 +9,106 @@ import { measureDuration } from '../../tools/utils'; -const failCommand: FailCommandFailPoint = { - configureFailPoint: 'failCommand', - mode: { - times: 13 - }, - data: { - failCommands: ['commitTransaction'], - errorCode: 251 // no such transaction - } -}; - -describe('Retry Backoff is Enforced', function () { - // 1. let client be a MongoClient +// Callback Raises a Custom Error +// Write a callback that raises a custom exception or error that does not include either +// UnknownTransactionCommitResult or TransientTransactionError error labels. Execute this callback +// using withTransaction and assert that the callback's error bypasses any retry logic within +// withTransaction and is propagated to the caller of withTransaction. +describe('Callback Raises a Custom Error', function () { let client: MongoClient; - // 2. let coll be a collection - let collection: Collection; - beforeEach(async function () { client = this.configuration.newClient(); - collection = client.db('foo').collection('bar'); }); afterEach(async function () { - sinon.restore(); await client?.close(); }); - test( - 'works', + it( + 'callback error without retry labels is propagated to the caller of withTransaction', { requires: { - mongodb: '>=4.4', // failCommand - topology: '!single' // transactions can't run on standalone servers + mongodb: '>=4.4', + topology: '!single' } }, async function () { - const randomStub = sinon.stub(Math, 'random'); + // 1. Write a callback that raises a custom error without UnknownTransactionCommitResult + // or TransientTransactionError error labels. + const customError = new Error('My custom error'); - // 3.i Configure the random number generator used for jitter to always return 0 - randomStub.returns(0); + // 2. Execute this callback using withTransaction. + const thrownError = await client + .withSession(async session => { + await session.withTransaction(async () => { + throw customError; + }); + }) + .catch(error => error); - // 3.ii Configure a fail point that forces 13 retries - await configureFailPoint(this.configuration, failCommand); + // 3. Assert that the callback's error bypasses any retry logic within withTransaction + // and is propagated to the caller of withTransaction. + expect(thrownError).to.equal(customError); + } + ); +}); - // 3.iii - const callback = async (s: ClientSession) => { - await collection.insertOne({}, { session: s }); - }; +// Callback Returns a Value +// Write a callback that returns a custom value (e.g. boolean, string, object). Execute this +// callback using withTransaction and assert that the callback's return value is propagated to +// the caller of withTransaction. +describe('Callback Returns a Value', function () { + let client: MongoClient; + let collection: Collection; - // 3.iv Let no_backoff_time be the duration of the withTransaction API call - const { duration: noBackoffTime } = await measureDuration(() => { - return client.withSession(async s => { - await s.withTransaction(callback); - }); - }); + beforeEach(async function () { + client = this.configuration.newClient(); + collection = client.db('foo').collection('bar'); + }); - // 4.i Configure the random number generator used for jitter to always return 1. - randomStub.returns(1); + afterEach(async function () { + await client?.close(); + }); - // 4.ii Configure a fail point that forces 13 retries like in step 3.2. - await configureFailPoint(this.configuration, failCommand); + it( + 'callback return value is propagated to the caller of withTransaction', + { + requires: { + mongodb: '>=4.4', + topology: '!single' + } + }, + async function () { + // 1. Write a callback that returns a custom value after performing an operation. + const returnValue = { message: 'Foo' }; - // 4.iii Use the same callback defined in 3.3. - // 4.iv Let with_backoff_time be the duration of the withTransaction API call - const { duration: fullBackoffDuration } = await measureDuration(() => { - return client.withSession(async s => { - await s.withTransaction(callback); + // 2. Execute this callback using withTransaction. + const result = await client.withSession(async session => { + return session.withTransaction(async s => { + await collection.insertOne({}, { session: s }); + return returnValue; }); }); - // 5. Compare the two time between the two runs. - // The sum of 13 backoffs is roughly 1.8 seconds. There is a half-second window to account for potential variance between the two runs. - expect(fullBackoffDuration).to.be.within( - noBackoffTime + 1800 - 500, - noBackoffTime + 1800 + 500 - ); + // 3. Assert that the callback's return value is propagated to the caller of withTransaction. + expect(result).to.equal(returnValue); } ); }); +// Retry Timeout is Enforced +// Drivers should test that withTransaction enforces a non-configurable timeout before retrying +// both commits and entire transactions. Specifically, three cases should be checked. +// +// If possible, drivers should implement these tests without requiring the test runner to block for +// the full duration of the retry timeout. This might be done by internally modifying the timeout +// value used by withTransaction with some private API or using a mock timer. +// +// We stub performance.now() to simulate elapsed time exceeding the 120-second retry limit. +// Without CSOT, the original error is propagated directly. +// With CSOT, the error is wrapped in a MongoOperationTimeoutError. describe('Retry Timeout is Enforced', function () { - // Drivers should test that withTransaction enforces a non-configurable timeout before retrying - // both commits and entire transactions. - // - // We stub performance.now() to simulate elapsed time exceeding the 120-second retry limit, - // as recommended by the spec: "This might be done by internally modifying the timeout value - // used by withTransaction with some private API or using a mock timer." - // - // Without CSOT, the original error is propagated directly. - // With CSOT, the error is wrapped in a MongoOperationTimeoutError. - let client: MongoClient; let collection: Collection; let timeOffset: number; @@ -123,7 +130,7 @@ describe('Retry Timeout is Enforced', function () { // Case 1: If the callback raises an error with the TransientTransactionError label and the retry // timeout has been exceeded, withTransaction should propagate the error to its caller. - test( + it( 'callback TransientTransactionError propagated when retry timeout exceeded', { requires: { @@ -162,9 +169,8 @@ describe('Retry Timeout is Enforced', function () { ); // Case 2: If committing raises an error with the UnknownTransactionCommitResult label, and the - // retry timeout has been exceeded, withTransaction should propagate the error to - // its caller. - test( + // retry timeout has been exceeded, withTransaction should propagate the error to its caller. + it( 'commit UnknownTransactionCommitResult propagated when retry timeout exceeded', { requires: { @@ -203,10 +209,10 @@ describe('Retry Timeout is Enforced', function () { ); // Case 3: If committing raises an error with the TransientTransactionError label and the retry - // timeout has been exceeded, withTransaction should propagate the error to its - // caller. This case may occur if the commit was internally retried against a new primary after a - // failover and the second primary returned a NoSuchTransaction error response. - test( + // timeout has been exceeded, withTransaction should propagate the error to its caller. This case + // may occur if the commit was internally retried against a new primary after a failover and the + // second primary returned a NoSuchTransaction error response. + it( 'commit TransientTransactionError propagated when retry timeout exceeded', { requires: { @@ -245,3 +251,89 @@ describe('Retry Timeout is Enforced', function () { } ); }); + +// Retry Backoff is Enforced +// Drivers should test that retries within withTransaction do not occur immediately. +describe('Retry Backoff is Enforced', function () { + // 1. Let client be a MongoClient. + let client: MongoClient; + + // 2. Let coll be a collection. + let collection: Collection; + + const failCommand: FailCommandFailPoint = { + configureFailPoint: 'failCommand', + mode: { times: 13 }, + data: { + failCommands: ['commitTransaction'], + errorCode: 251 // NoSuchTransaction + } + }; + + beforeEach(async function () { + client = this.configuration.newClient(); + collection = client.db('foo').collection('bar'); + }); + + afterEach(async function () { + sinon.restore(); + await client?.close(); + }); + + it( + 'retries within withTransaction apply exponential backoff with jitter', + { + requires: { + mongodb: '>=4.4', + topology: '!single' + } + }, + async function () { + const randomStub = sinon.stub(Math, 'random'); + + // 3. Run transactions without backoff: + // 3.1 Configure the random number generator used for jitter to always return 0 + // -- this effectively disables backoff. + randomStub.returns(0); + + // 3.2 Configure a fail point that forces 13 retries. + await configureFailPoint(this.configuration, failCommand); + + // 3.3 Define the callback for the transaction. + const callback = async (s: ClientSession) => { + await collection.insertOne({}, { session: s }); + }; + + // 3.4 Let no_backoff_time be the duration of the withTransaction API call. + const { duration: noBackoffTime } = await measureDuration(() => { + return client.withSession(async s => { + await s.withTransaction(callback); + }); + }); + + // 4. Now run the command with backoff: + // 4.1 Configure the random number generator used for jitter to always return + // a number as close as possible to 1. + randomStub.returns(1); + + // 4.2 Configure a fail point that forces 13 retries like in step 3.2. + await configureFailPoint(this.configuration, failCommand); + + // 4.3 Use the same callback defined in 3.3. + // 4.4 Let with_backoff_time be the duration of the withTransaction API call. + const { duration: fullBackoffDuration } = await measureDuration(() => { + return client.withSession(async s => { + await s.withTransaction(callback); + }); + }); + + // 5. Compare the durations of the two runs. + // The sum of 13 backoffs is roughly 1.8 seconds. There is a half-second window to + // account for potential variance between the two runs. + expect(fullBackoffDuration).to.be.within( + noBackoffTime + 1800 - 500, + noBackoffTime + 1800 + 500 + ); + } + ); +}); From 51bf0f7402691e283ac338c500ff780f64c5696c Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Mon, 13 Apr 2026 11:17:09 +0200 Subject: [PATCH 4/4] sync spec tests for relevant feature --- .../sessions-inherit-timeoutMS.json | 2 +- .../sessions-inherit-timeoutMS.yml | 2 +- .../sessions-override-operation-timeoutMS.json | 2 +- .../sessions-override-operation-timeoutMS.yml | 2 +- .../sessions-override-timeoutMS.json | 2 +- .../sessions-override-timeoutMS.yml | 2 +- test/spec/client-side-operations-timeout/tailable-awaitData.yml | 1 + .../transactions-convenient-api/unified/callback-retry.json | 2 +- .../spec/transactions-convenient-api/unified/callback-retry.yml | 2 +- .../unified/commit-transienttransactionerror-4.2.json | 2 +- .../unified/commit-transienttransactionerror-4.2.yml | 2 +- .../unified/commit-transienttransactionerror.json | 2 +- .../unified/commit-transienttransactionerror.yml | 2 +- 13 files changed, 13 insertions(+), 12 deletions(-) diff --git a/test/spec/client-side-operations-timeout/sessions-inherit-timeoutMS.json b/test/spec/client-side-operations-timeout/sessions-inherit-timeoutMS.json index b8725e04bb6..50feabf6098 100644 --- a/test/spec/client-side-operations-timeout/sessions-inherit-timeoutMS.json +++ b/test/spec/client-side-operations-timeout/sessions-inherit-timeoutMS.json @@ -331,4 +331,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/test/spec/client-side-operations-timeout/sessions-inherit-timeoutMS.yml b/test/spec/client-side-operations-timeout/sessions-inherit-timeoutMS.yml index caa7cc404e1..4cb9f1c646d 100644 --- a/test/spec/client-side-operations-timeout/sessions-inherit-timeoutMS.yml +++ b/test/spec/client-side-operations-timeout/sessions-inherit-timeoutMS.yml @@ -172,4 +172,4 @@ tests: abortTransaction: 1 maxTimeMS: { $$type: [ "int", "long" ] } - commandFailedEvent: - commandName: abortTransaction \ No newline at end of file + commandName: abortTransaction diff --git a/test/spec/client-side-operations-timeout/sessions-override-operation-timeoutMS.json b/test/spec/client-side-operations-timeout/sessions-override-operation-timeoutMS.json index f550e830b72..78e873f948c 100644 --- a/test/spec/client-side-operations-timeout/sessions-override-operation-timeoutMS.json +++ b/test/spec/client-side-operations-timeout/sessions-override-operation-timeoutMS.json @@ -333,4 +333,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/test/spec/client-side-operations-timeout/sessions-override-operation-timeoutMS.yml b/test/spec/client-side-operations-timeout/sessions-override-operation-timeoutMS.yml index daacf540be7..c91f7366dca 100644 --- a/test/spec/client-side-operations-timeout/sessions-override-operation-timeoutMS.yml +++ b/test/spec/client-side-operations-timeout/sessions-override-operation-timeoutMS.yml @@ -173,4 +173,4 @@ tests: abortTransaction: 1 maxTimeMS: { $$type: ["int", "long"] } - commandFailedEvent: - commandName: abortTransaction \ No newline at end of file + commandName: abortTransaction diff --git a/test/spec/client-side-operations-timeout/sessions-override-timeoutMS.json b/test/spec/client-side-operations-timeout/sessions-override-timeoutMS.json index b9f3e4c3001..c4a446c5315 100644 --- a/test/spec/client-side-operations-timeout/sessions-override-timeoutMS.json +++ b/test/spec/client-side-operations-timeout/sessions-override-timeoutMS.json @@ -329,4 +329,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/test/spec/client-side-operations-timeout/sessions-override-timeoutMS.yml b/test/spec/client-side-operations-timeout/sessions-override-timeoutMS.yml index 356a29ef10e..815d7490525 100644 --- a/test/spec/client-side-operations-timeout/sessions-override-timeoutMS.yml +++ b/test/spec/client-side-operations-timeout/sessions-override-timeoutMS.yml @@ -170,4 +170,4 @@ tests: abortTransaction: 1 maxTimeMS: { $$type: [ "int", "long" ] } - commandFailedEvent: - commandName: abortTransaction \ No newline at end of file + commandName: abortTransaction diff --git a/test/spec/client-side-operations-timeout/tailable-awaitData.yml b/test/spec/client-side-operations-timeout/tailable-awaitData.yml index f374dbf4d60..c4d5a82f52e 100644 --- a/test/spec/client-side-operations-timeout/tailable-awaitData.yml +++ b/test/spec/client-side-operations-timeout/tailable-awaitData.yml @@ -370,3 +370,4 @@ tests: databaseName: *databaseName command: maxTimeMS: { $$lte: 100 } + diff --git a/test/spec/transactions-convenient-api/unified/callback-retry.json b/test/spec/transactions-convenient-api/unified/callback-retry.json index 1e07a2a656c..277dfa18ed6 100644 --- a/test/spec/transactions-convenient-api/unified/callback-retry.json +++ b/test/spec/transactions-convenient-api/unified/callback-retry.json @@ -1,6 +1,6 @@ { "description": "callback-retry", - "schemaVersion": "1.3", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "4.0", diff --git a/test/spec/transactions-convenient-api/unified/callback-retry.yml b/test/spec/transactions-convenient-api/unified/callback-retry.yml index b17469893de..2a89de08064 100644 --- a/test/spec/transactions-convenient-api/unified/callback-retry.yml +++ b/test/spec/transactions-convenient-api/unified/callback-retry.yml @@ -1,6 +1,6 @@ description: callback-retry -schemaVersion: '1.3' +schemaVersion: '1.4' runOnRequirements: - minServerVersion: '4.0' diff --git a/test/spec/transactions-convenient-api/unified/commit-transienttransactionerror-4.2.json b/test/spec/transactions-convenient-api/unified/commit-transienttransactionerror-4.2.json index 07f190ffb43..0f5a782452c 100644 --- a/test/spec/transactions-convenient-api/unified/commit-transienttransactionerror-4.2.json +++ b/test/spec/transactions-convenient-api/unified/commit-transienttransactionerror-4.2.json @@ -1,6 +1,6 @@ { "description": "commit-transienttransactionerror-4.2", - "schemaVersion": "1.3", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "4.1.6", diff --git a/test/spec/transactions-convenient-api/unified/commit-transienttransactionerror-4.2.yml b/test/spec/transactions-convenient-api/unified/commit-transienttransactionerror-4.2.yml index 6612c4ae2f6..36cf90b91e5 100644 --- a/test/spec/transactions-convenient-api/unified/commit-transienttransactionerror-4.2.yml +++ b/test/spec/transactions-convenient-api/unified/commit-transienttransactionerror-4.2.yml @@ -1,6 +1,6 @@ description: commit-transienttransactionerror-4.2 -schemaVersion: '1.3' +schemaVersion: '1.4' runOnRequirements: - minServerVersion: '4.1.6' diff --git a/test/spec/transactions-convenient-api/unified/commit-transienttransactionerror.json b/test/spec/transactions-convenient-api/unified/commit-transienttransactionerror.json index 9584bb61b5b..dd5158d8134 100644 --- a/test/spec/transactions-convenient-api/unified/commit-transienttransactionerror.json +++ b/test/spec/transactions-convenient-api/unified/commit-transienttransactionerror.json @@ -1,6 +1,6 @@ { "description": "commit-transienttransactionerror", - "schemaVersion": "1.3", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "4.0", diff --git a/test/spec/transactions-convenient-api/unified/commit-transienttransactionerror.yml b/test/spec/transactions-convenient-api/unified/commit-transienttransactionerror.yml index 21732e1e17f..befd8f1031a 100644 --- a/test/spec/transactions-convenient-api/unified/commit-transienttransactionerror.yml +++ b/test/spec/transactions-convenient-api/unified/commit-transienttransactionerror.yml @@ -1,6 +1,6 @@ description: commit-transienttransactionerror -schemaVersion: '1.3' +schemaVersion: '1.4' runOnRequirements: - minServerVersion: '4.0'