Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions source/transactions-convenient-api/tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,23 @@ Write a callback that returns a custom value (e.g. boolean, string, object). Exe
Drivers should test that `withTransaction` enforces a non-configurable timeout before retrying both commits and entire
transactions. Specifically, three cases should be checked:

- If the callback raises an error with the TransientTransactionError label and the retry timeout has been exceeded,
`withTransaction` should propagate the error (see Note 1 below) to its caller.
- If committing raises an error with the UnknownTransactionCommitResult label, and the retry timeout has been exceeded,
`withTransaction` should propagate the error (see Note 1 below) to its caller.
- If committing raises an error with the TransientTransactionError label and the retry timeout has been exceeded,
`withTransaction` should propagate the error (see Note 1 below) 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.
- If the callback raises an error with the `TransientTransactionError` label and the retry timeout has been exceeded,
`withTransaction` should propagate the error as described in the
[propagation mechanism](../transactions-convenient-api.md#timeout-error-propagation-mechanism) to its caller.
- If committing raises an error with the `UnknownTransactionCommitResult` label, and the retry timeout has been
exceeded, `withTransaction` should propagate the error as described in the
[propagation mechanism](../transactions-convenient-api.md#timeout-error-propagation-mechanism) to its caller
- If committing raises an error with the `TransientTransactionError` label and the retry timeout has been exceeded,
`withTransaction` should propagate the error as described in the
[propagation mechanism](../transactions-convenient-api.md#timeout-error-propagation-mechanism) 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.

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.

______________________________________________________________________

**Note 1:** The error SHOULD be propagated as a timeout error if the language allows to expose the underlying error as a
cause of a timeout error.
The drivers should assert that the timeout error propagated has the same labels as the error it wraps.

### Retry Backoff is Enforced

Expand Down Expand Up @@ -112,6 +112,8 @@ Drivers should test that retries within `withTransaction` do not occur immediate

## Changelog

- 2026-04-02: [DRIVERS-3436](https://github.com/mongodb/specifications/pull/1920) Refine withTransaction timeout error
wrapping semantics and label propagation in spec and prose tests
- 2026-03-03: Clarify exponential backoff jitter upper bound.
- 2026-02-17: Clarify expected error when timeout is reached
[DRIVERS-3391](https://jira.mongodb.org/browse/DRIVERS-3391).
Expand Down
51 changes: 29 additions & 22 deletions source/transactions-convenient-api/transactions-convenient-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,9 @@ This method should perform the following sequence of actions:

2. If `transactionAttempt` > 0:

1. If elapsed time + `backoffMS` > `TIMEOUT_MS`, then raise the previously encountered error (see Note 1 below). If
the elapsed time of `withTransaction` is less than TIMEOUT_MS, calculate the backoffMS to be
1. If elapsed time + `backoffMS` > `TIMEOUT_MS`, then propagate the previously encountered error (see
[propagation section](transactions-convenient-api.md#timeout-error-propagation-mechanism) below). 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`.

1. jitter is a random float between \[0, 1), optionally including 1, depending on what is most natural for the
Expand Down Expand Up @@ -163,8 +164,7 @@ This method should perform the following sequence of actions:
committed a transaction, propagate the callback's error to the caller of `withTransaction` and return
immediately.

4. Otherwise, propagate the callback's error (see Note 1 below) to the caller of `withTransaction` and return
immediately.
4. Otherwise, propagate the callback's error to the caller of `withTransaction` and return immediately.

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.
Expand All @@ -173,27 +173,31 @@ This method should perform the following sequence of actions:

10. If `commitTransaction` reported an error:

1. If the `commitTransaction` error includes a "UnknownTransactionCommitResult" label and the error is not
MaxTimeMSExpired and 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](#majority-write-concern-is-used-when-retrying-committransaction)).
1. If the `commitTransaction` error includes a `UnknownTransactionCommitResult` label and the error is not
`MaxTimeMSExpired`

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](transactions-convenient-api.md#timeout-error-propagation-mechanism) below)
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](#majority-write-concern-is-used-when-retrying-committransaction)).

2. If the `commitTransaction` error includes a "TransientTransactionError" label, jump back to step two.

3. Otherwise, propagate the `commitTransaction` error (see Note 1 below) to the caller of `withTransaction` and
return immediately.
3. Otherwise, propagate the `commitTransaction` error to the caller of `withTransaction` and return immediately.

11. The transaction was committed successfully. Return immediately.

______________________________________________________________________
###### Timeout Error propagation mechanism

**Note 1:** When the `TIMEOUT_MS` (calculated in step [1.3](#sequence-of-actions)) is reached we MUST report a timeout
error wrapping the last error that was encountered which triggered the retry behavior. If `timeoutMS` is set, then
timeout error is a special type which is defined in CSOT
When the `TIMEOUT_MS` (calculated in step [1.3](#sequence-of-actions)) 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](https://github.com/mongodb/specifications/blob/master/source/client-side-operations-timeout/client-side-operations-timeout.md#errors)
, If `timeoutMS` is not set, then propagate it as timeout error if the language allows to expose the underlying error as
a cause of a timeout error (see `makeTimeoutError` below in [pseudo-code](#pseudo-code)). If timeout error is thrown
then it SHOULD expose error label(s) from the transient error.
, 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](#pseudo-code)). If
timeout error is thrown then it SHOULD copy all error label(s) from the previously encountered retriable error.

##### Pseudo-code

Expand Down Expand Up @@ -264,11 +268,11 @@ withTransaction(callback, options) {
* {ok:1, writeConcernError: {code: 50, codeName: "MaxTimeMSExpired"}}
*/
lastError = error;
if (Date.now() - startTime >= timeout) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This was removed because in sections 7.4 & 10.3 of the [sequence-of-actions|https://github.com/mongodb/specifications/blob/master/source/transactions-convenient-api/transactions-convenient-api.md#sequence-of-actions] the errors are not retriable, and they are mainly the reason for returning immediately from withTransaction. So there's no need to wrap them in a TimeoutError (if Timeout is encountered)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

However, in section 10.1.1 (UnknownTransactionCommitResult encountered during commit) The error is retriable if the timeout is not exeeded. So the wrapping into a timeout error needs to happen if we are throwing

throw makeTimeoutError(error);
}
if (!isMaxTimeMSExpiredError(error) &&
error.hasErrorLabel("UnknownTransactionCommitResult")) {
if (Date.now() - startTime >= timeout) {
throw makeTimeoutError(error);
}
continue retryCommit;
}

Expand Down Expand Up @@ -348,8 +352,8 @@ An earlier design also considered using the callback's return value to indicate
of two ways:

- The callback aborts the transaction directly and returns to `withTransaction`, which will then return to its caller.
- The callback raises an error without the "TransientTransactionError" label, in which case `withTransaction` will abort
the transaction and return to its caller.
- The callback propagates an error without the "TransientTransactionError" label, in which case `withTransaction` will
abort the transaction and return to its caller.

### Applications are responsible for passing ClientSession for operations within a transaction

Expand Down Expand Up @@ -440,6 +444,9 @@ provides an implementation of a technique already described in the MongoDB 4.0 d

## Changelog

- 2026-04-02: [DRIVERS-3436](https://github.com/mongodb/specifications/pull/1920) Refine withTransaction timeout error
wrapping semantics and label propagation in spec and prose tests.

- 2026-03-03: Clarify exponential backoff jitter upper bound.

- 2026-02-20: Fix initial backoff and growth value parameters in "Design Rationale" section.
Expand Down
Loading