Skip to content

fix(NODE-7430): throw timeout error when withTransaction retries exceed deadline#4893

Closed
tadjik1 wants to merge 9 commits intomainfrom
NODE-7430
Closed

fix(NODE-7430): throw timeout error when withTransaction retries exceed deadline#4893
tadjik1 wants to merge 9 commits intomainfrom
NODE-7430

Conversation

@tadjik1
Copy link
Member

@tadjik1 tadjik1 commented Mar 16, 2026

Description

Summary of Changes

Align withTransaction timeout behavior with the spec change from DRIVERS-3391: when retry attempts exhaust the timeout (CSOT timeoutMS or legacy 120s), throw a MongoOperationTimeoutError wrapping the last transient error as cause, instead of throwing the raw transient error directly.

Notes for Reviewers

The makeWithTransactionTimeoutError helper mirrors the spec's makeTimeoutError pseudocode function. It creates a MongoOperationTimeoutError and copies error labels from the cause, so callers can check hasErrorLabel('TransientTransactionError') on the timeout error itself.

The 120s cap fix is a one-line change (adding !csotEnabled() guard to the legacy branch of willExceedTransactionDeadline), but it's included here since it's in the same code path.

What is the motivation for this change?

NODE-7430 / DRIVERS-3391

Release Highlight

Release notes highlight

Double check the following

  • Lint is passing (npm run check:lint)
  • Self-review completed using the steps outlined here
  • PR title follows the correct format: type(NODE-xxxx)[!]: description
    • Example: feat(NODE-1234)!: rewriting everything in coffeescript
  • Changes are covered by tests
  • New TODOs have a related JIRA ticket

@tadjik1 tadjik1 marked this pull request as ready for review March 16, 2026 15:21
@tadjik1 tadjik1 requested a review from a team as a code owner March 16, 2026 15:21
Copilot AI review requested due to automatic review settings March 16, 2026 15:21

timeOffset = 0;
const originalNow = performance.now.bind(performance);
sinon.stub(performance, 'now').callsFake(() => originalNow() + timeOffset);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how spec suggests to test, similar implementations in:

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the driver’s withTransaction retry-timeout behavior to align with the DRIVERS-3391 spec change: when retrying runs out the overall deadline (CSOT timeoutMS or the legacy 120s cap), it throws a MongoOperationTimeoutError that wraps the last transient error as cause (and propagates error labels onto the timeout error).

Changes:

  • Update ClientSession.withTransaction to throw a MongoOperationTimeoutError on timeout instead of surfacing the raw transient error.
  • Add a makeWithTransactionTimeoutError helper that wraps the last error as cause and copies error labels.
  • Extend spec/integration tests to cover the new timeout error behavior (and add commandFailedEvent observations in the CSOT unified spec test).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
src/sessions.ts Implements timeout wrapping behavior and adds helper to build labeled MongoOperationTimeoutError.
test/spec/client-side-operations-timeout/convenient-transactions.yml Adds commandFailedEvent observation and a new unified test for timeout surfacing after transient retries.
test/spec/client-side-operations-timeout/convenient-transactions.json JSON equivalent updates for the unified spec test additions.
test/integration/transactions-convenient-api/transactions-convenient-api.prose.test.ts Adds prose integration tests that simulate exceeding the legacy 120s retry window and assert a timeout error is returned.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +162 to +165
// 3. Assert that the error is a timeout error wrapping the TransientTransactionError.
expect(result).to.be.instanceOf(MongoOperationTimeoutError);
expect((result as MongoOperationTimeoutError).cause).to.be.an('error');
}

// 3. Assert that the error is a timeout error wrapping the commit error.
expect(result).to.be.instanceOf(MongoOperationTimeoutError);
expect((result as MongoOperationTimeoutError).cause).to.be.an('error');
Comment on lines +244 to +246
// 3. Assert that the error is a timeout error wrapping the TransientTransactionError.
expect(result).to.be.instanceOf(MongoOperationTimeoutError);
expect((result as MongoOperationTimeoutError).cause).to.be.an('error');
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the driver’s withTransaction retry-timeout behavior to match the updated transactions convenient API spec (DRIVERS-3391 / NODE-7430): when retry attempts run past the allowed deadline (CSOT timeoutMS or legacy 120s), the driver should throw a MongoOperationTimeoutError that wraps the last retryable/transient error as the cause, rather than throwing the transient error directly.

Changes:

  • Update ClientSession.withTransaction to throw a MongoOperationTimeoutError (with labels copied from the cause) when retry deadlines are exceeded, and fix legacy 120s deadline logic when CSOT is enabled.
  • Add/extend unified spec tests for CSOT convenient transactions to cover transient retry exhaustion behavior.
  • Add integration prose tests that stub time to validate the legacy 120-second retry timeout behavior for callback + commit retry paths.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
src/sessions.ts Implements timeout-wrapping behavior for withTransaction retry deadline exhaustion and refines deadline checks for CSOT vs legacy timeout paths.
test/spec/client-side-operations-timeout/convenient-transactions.yml Adds command failed event observation and a new unified spec test covering transient retry exhaustion surfacing as a timeout.
test/spec/client-side-operations-timeout/convenient-transactions.json JSON equivalent updates for the unified spec additions/changes.
test/integration/transactions-convenient-api/transactions-convenient-api.prose.test.ts Adds integration prose tests that validate legacy 120s retry timeout behavior using a stubbed clock.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

src/sessions.ts Outdated
Comment on lines +878 to +884
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;
}
(this.timeoutContext?.csotEnabled() && this.timeoutContext.remainingTimeMS <= 0) ||
(!this.timeoutContext?.csotEnabled() && processTimeMS() - startTime >= MAX_TIMEOUT);

if (hasTimedOut) {
throw makeWithTransactionTimeoutError(commitError);
}
Comment on lines +129 to +136
mode: alwaysOn
data:
failCommands: ["insert"]
blockConnection: true
blockTimeMS: 25
errorCode: 24
errorLabels: ["TransientTransactionError"]

Comment on lines +164 to +167
- commandStartedEvent:
commandName: abortTransaction
- commandFailedEvent:
commandName: abortTransaction
Comment on lines +226 to +229
"mode": "alwaysOn",
"data": {
"failCommands": [
"insert"
Comment on lines +281 to +289
{
"commandStartedEvent": {
"commandName": "abortTransaction"
}
},
{
"commandFailedEvent": {
"commandName": "abortTransaction"
}
@tadjik1 tadjik1 marked this pull request as draft March 18, 2026 11:38
@tadjik1 tadjik1 closed this Mar 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants