From 339803538d001d77013cff7490746cc10ceae01b Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Thu, 2 Apr 2026 16:00:42 +0100 Subject: [PATCH 01/15] Fixes DRIVERS-3436 --- .../tests/README.md | 20 ++++---- .../transactions-convenient-api.md | 48 ++++++++++++------- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/source/transactions-convenient-api/tests/README.md b/source/transactions-convenient-api/tests/README.md index 6c94f8762b..fc917f0b54 100644 --- a/source/transactions-convenient-api/tests/README.md +++ b/source/transactions-convenient-api/tests/README.md @@ -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 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) 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. + `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 (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. + `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 diff --git a/source/transactions-convenient-api/transactions-convenient-api.md b/source/transactions-convenient-api/transactions-convenient-api.md index d74707b481..48b97b4ec5 100644 --- a/source/transactions-convenient-api/transactions-convenient-api.md +++ b/source/transactions-convenient-api/transactions-convenient-api.md @@ -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 @@ -163,8 +164,9 @@ 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 (see + [propagation section](transactions-convenient-api.md#timeout-error-propagation-mechanism) below) 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. @@ -180,20 +182,21 @@ This method should perform the following sequence of actions: 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 (see + [propagation section](transactions-convenient-api.md#timeout-error-propagation-mechanism) below) 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 error. ##### Pseudo-code @@ -228,11 +231,13 @@ withTransaction(callback, options) { callback(this); } catch (error) { lastError = error; + // step 7.1 if (this.transactionState == STARTING || this.transactionState == IN_PROGRESS) { this.abortTransaction(); } + // step 7.2 if (error.hasErrorLabel("TransientTransactionError")) { if (Date.now() - startTime < timeout) { continue retryTransaction; @@ -241,9 +246,16 @@ withTransaction(callback, options) { } } - throw error; + // step 7.3 + if (error.hasErrorLabel("UnknownTransactionCommitResult")) { + throw error; + } + + // step 7.4 + throw makeTimeoutError(error); } + // step 8 if (this.transactionState == NO_TXN || this.transactionState == COMMITTED || this.transactionState == ABORTED) { @@ -252,6 +264,7 @@ withTransaction(callback, options) { retryCommit: while (true) { try { + // step 9 /* We will rely on ClientSession.commitTransaction() to * apply a majority write concern if commitTransaction is * being retried (see: DRIVERS-601) */ @@ -267,15 +280,18 @@ withTransaction(callback, options) { if (Date.now() - startTime >= timeout) { throw makeTimeoutError(error); } + // step 10.1 if (!isMaxTimeMSExpiredError(error) && error.hasErrorLabel("UnknownTransactionCommitResult")) { continue retryCommit; } + // step 10.2 if (error.hasErrorLabel("TransientTransactionError")) { continue retryTransaction; } + // step 10.3 throw error; } break; // Commit was successful @@ -348,8 +364,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 From dc02bb2ed40108997930c6efa45bf186a43cc2d6 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Thu, 2 Apr 2026 18:31:24 +0100 Subject: [PATCH 02/15] PR feedback --- .../transactions-convenient-api.md | 28 +++---------------- 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/source/transactions-convenient-api/transactions-convenient-api.md b/source/transactions-convenient-api/transactions-convenient-api.md index 48b97b4ec5..3ce6f65426 100644 --- a/source/transactions-convenient-api/transactions-convenient-api.md +++ b/source/transactions-convenient-api/transactions-convenient-api.md @@ -164,9 +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 - [propagation section](transactions-convenient-api.md#timeout-error-propagation-mechanism) 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. @@ -182,9 +180,7 @@ This method should perform the following sequence of actions: 2. If the `commitTransaction` error includes a "TransientTransactionError" label, jump back to step two. - 3. Otherwise, propagate the `commitTransaction` error (see - [propagation section](transactions-convenient-api.md#timeout-error-propagation-mechanism) 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. @@ -196,7 +192,7 @@ 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 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 error. +timeout error is thrown then it SHOULD copy all error label(s) from the previously encountered retriable error. ##### Pseudo-code @@ -231,13 +227,11 @@ withTransaction(callback, options) { callback(this); } catch (error) { lastError = error; - // step 7.1 if (this.transactionState == STARTING || this.transactionState == IN_PROGRESS) { this.abortTransaction(); } - // step 7.2 if (error.hasErrorLabel("TransientTransactionError")) { if (Date.now() - startTime < timeout) { continue retryTransaction; @@ -246,16 +240,9 @@ withTransaction(callback, options) { } } - // step 7.3 - if (error.hasErrorLabel("UnknownTransactionCommitResult")) { - throw error; - } - - // step 7.4 - throw makeTimeoutError(error); + throw error; } - // step 8 if (this.transactionState == NO_TXN || this.transactionState == COMMITTED || this.transactionState == ABORTED) { @@ -264,7 +251,6 @@ withTransaction(callback, options) { retryCommit: while (true) { try { - // step 9 /* We will rely on ClientSession.commitTransaction() to * apply a majority write concern if commitTransaction is * being retried (see: DRIVERS-601) */ @@ -277,21 +263,15 @@ withTransaction(callback, options) { * {ok:1, writeConcernError: {code: 50, codeName: "MaxTimeMSExpired"}} */ lastError = error; - if (Date.now() - startTime >= timeout) { - throw makeTimeoutError(error); - } - // step 10.1 if (!isMaxTimeMSExpiredError(error) && error.hasErrorLabel("UnknownTransactionCommitResult")) { continue retryCommit; } - // step 10.2 if (error.hasErrorLabel("TransientTransactionError")) { continue retryTransaction; } - // step 10.3 throw error; } break; // Commit was successful From 77ab714092705694aa52ebf8dd514b156d739c9d Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Thu, 2 Apr 2026 19:08:52 +0100 Subject: [PATCH 03/15] Update source/transactions-convenient-api/tests/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- source/transactions-convenient-api/tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/transactions-convenient-api/tests/README.md b/source/transactions-convenient-api/tests/README.md index fc917f0b54..56a778a65e 100644 --- a/source/transactions-convenient-api/tests/README.md +++ b/source/transactions-convenient-api/tests/README.md @@ -31,7 +31,7 @@ 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 as described in the - [propagation mechanism](../transactions-convenient-api.md#timeout-error-propagation-mechanism) below) to its caller. + [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. From 847816e802dce0c7101f6f8b1c42bee90d9b0342 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Thu, 2 Apr 2026 19:14:27 +0100 Subject: [PATCH 04/15] Remove error wrapping for UnknownTransactionCommitResult --- source/transactions-convenient-api/tests/README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/source/transactions-convenient-api/tests/README.md b/source/transactions-convenient-api/tests/README.md index 56a778a65e..94ea17d24b 100644 --- a/source/transactions-convenient-api/tests/README.md +++ b/source/transactions-convenient-api/tests/README.md @@ -32,10 +32,9 @@ 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 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, +- If committing raises an error with the `UnknownTransactionCommitResult` label, and the retry timeout has been exceeded, + `withTransaction` should propagate the error 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 From 1d4ec77e723fcfd5f405913156291f5af354c696 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Thu, 2 Apr 2026 19:25:40 +0100 Subject: [PATCH 05/15] Update changelog --- source/transactions-convenient-api/tests/README.md | 6 ++++-- .../transactions-convenient-api.md | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/source/transactions-convenient-api/tests/README.md b/source/transactions-convenient-api/tests/README.md index 94ea17d24b..c4e87a2a79 100644 --- a/source/transactions-convenient-api/tests/README.md +++ b/source/transactions-convenient-api/tests/README.md @@ -32,8 +32,8 @@ 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 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 to its caller. +- If committing raises an error with the `UnknownTransactionCommitResult` label, and the retry timeout has been + exceeded, `withTransaction` should propagate the error 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 @@ -111,6 +111,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). diff --git a/source/transactions-convenient-api/transactions-convenient-api.md b/source/transactions-convenient-api/transactions-convenient-api.md index 3ce6f65426..bf0737ce2b 100644 --- a/source/transactions-convenient-api/transactions-convenient-api.md +++ b/source/transactions-convenient-api/transactions-convenient-api.md @@ -436,6 +436,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. From 3df2210914292a250300262d847a35564b15e203 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Fri, 3 Apr 2026 16:31:52 +0100 Subject: [PATCH 06/15] Add missing case for error propagation --- .../transactions-convenient-api/tests/README.md | 3 ++- .../transactions-convenient-api.md | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/source/transactions-convenient-api/tests/README.md b/source/transactions-convenient-api/tests/README.md index c4e87a2a79..e978343dbd 100644 --- a/source/transactions-convenient-api/tests/README.md +++ b/source/transactions-convenient-api/tests/README.md @@ -33,7 +33,8 @@ transactions. Specifically, three cases should be checked: `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 to its caller. + 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 diff --git a/source/transactions-convenient-api/transactions-convenient-api.md b/source/transactions-convenient-api/transactions-convenient-api.md index bf0737ce2b..2443ad4b3a 100644 --- a/source/transactions-convenient-api/transactions-convenient-api.md +++ b/source/transactions-convenient-api/transactions-convenient-api.md @@ -173,10 +173,15 @@ 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. @@ -265,6 +270,9 @@ withTransaction(callback, options) { lastError = error; if (!isMaxTimeMSExpiredError(error) && error.hasErrorLabel("UnknownTransactionCommitResult")) { + if (Date.now() - startTime >= timeout) { + throw makeTimeoutError(error); + } continue retryCommit; } From d410325f51f48499fd24aea33aaa26fffd0a8178 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Fri, 10 Apr 2026 13:13:54 +0100 Subject: [PATCH 07/15] Update source/transactions-convenient-api/transactions-convenient-api.md Co-authored-by: Valentin Kovalenko --- .../transactions-convenient-api/transactions-convenient-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/transactions-convenient-api/transactions-convenient-api.md b/source/transactions-convenient-api/transactions-convenient-api.md index 2443ad4b3a..e9a62ffc79 100644 --- a/source/transactions-convenient-api/transactions-convenient-api.md +++ b/source/transactions-convenient-api/transactions-convenient-api.md @@ -352,7 +352,7 @@ 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 propagates an error without the "TransientTransactionError" label, in which case `withTransaction` will +- 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 From 8ec218023f590804980e4c773905e7695614f0dd Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Thu, 9 Apr 2026 23:31:42 -0600 Subject: [PATCH 08/15] Refactor the "Timeout Error propagation mechanism" section --- .../transactions-convenient-api.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/source/transactions-convenient-api/transactions-convenient-api.md b/source/transactions-convenient-api/transactions-convenient-api.md index e9a62ffc79..db6285de44 100644 --- a/source/transactions-convenient-api/transactions-convenient-api.md +++ b/source/transactions-convenient-api/transactions-convenient-api.md @@ -191,13 +191,14 @@ This method should perform the following sequence of actions: ###### Timeout Error propagation mechanism -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 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. +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](../client-side-operations-timeout/client-side-operations-timeout.md#errors), +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](../client-side-operations-timeout/client-side-operations-timeout.md#errors). + - 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. ##### Pseudo-code From e416f1ffdfed362d061dbafbaa7c0e248f3d665c Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Thu, 9 Apr 2026 23:53:59 -0600 Subject: [PATCH 09/15] Make the "propagate as is" vs "propagate as per timeout error propagation" distinction more explicit --- .../transactions-convenient-api.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/source/transactions-convenient-api/transactions-convenient-api.md b/source/transactions-convenient-api/transactions-convenient-api.md index db6285de44..9fd4006aaa 100644 --- a/source/transactions-convenient-api/transactions-convenient-api.md +++ b/source/transactions-convenient-api/transactions-convenient-api.md @@ -123,8 +123,7 @@ This method should perform the following sequence of actions: 2. If `transactionAttempt` > 0: - 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 + 1. If elapsed time + `backoffMS` > `TIMEOUT_MS`, then propagate the previously encountered error as per [timeout error propagation](#timeout-error-propagation). 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`. @@ -142,7 +141,7 @@ This method should perform the following sequence of actions: 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 of `withTransaction` and return +4. If `startTransaction` reported an error, propagate that error to the caller of `withTransaction` as is and return immediately. 5. Invoke the callback. Drivers MUST ensure that the ClientSession can be accessed within the callback (e.g. pass @@ -161,10 +160,10 @@ This method should perform the following sequence of actions: 2. If the callback's error includes a "TransientTransactionError" label, jump back to step two. 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` and return + committed a transaction, propagate the callback's error to the caller of `withTransaction` as is and return immediately. - 4. Otherwise, propagate the callback's error to the caller of `withTransaction` and return immediately. + 4. Otherwise, propagate the callback's error to the caller of `withTransaction` as is 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. @@ -177,19 +176,18 @@ This method should perform the following sequence of actions: `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) + caller of `withTransaction` as per [timeout error propagation](#timeout-error-propagation) and return immediately. 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 to the caller of `withTransaction` and return immediately. + 3. Otherwise, propagate the `commitTransaction` error to the caller of `withTransaction` as is and return immediately. 11. The transaction was committed successfully. Return immediately. -###### Timeout Error propagation mechanism +###### 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](../client-side-operations-timeout/client-side-operations-timeout.md#errors), From 6d97107bb88224eb67928aaa1fcb465167ae616b Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Fri, 10 Apr 2026 00:02:07 -0600 Subject: [PATCH 10/15] Add the "propagate...to the caller of `withTransaction`...and return immediately" mantra where it is missing --- .../transactions-convenient-api/transactions-convenient-api.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/transactions-convenient-api/transactions-convenient-api.md b/source/transactions-convenient-api/transactions-convenient-api.md index 9fd4006aaa..db279a8359 100644 --- a/source/transactions-convenient-api/transactions-convenient-api.md +++ b/source/transactions-convenient-api/transactions-convenient-api.md @@ -123,7 +123,8 @@ This method should perform the following sequence of actions: 2. If `transactionAttempt` > 0: - 1. If elapsed time + `backoffMS` > `TIMEOUT_MS`, then propagate the previously encountered error as per [timeout error propagation](#timeout-error-propagation). If the + 1. If elapsed time + `backoffMS` > `TIMEOUT_MS`, then propagate the previously encountered error to the caller of `withTransaction` + as per [timeout error propagation](#timeout-error-propagation) and return immediately. 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`. From 3bbcdc01adeb5ea07f2f60edcdf0576648dd6fe7 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Fri, 10 Apr 2026 13:45:04 +0100 Subject: [PATCH 11/15] Address remaining review feedback for terminology and formatting - Use "Otherwise" in step 10.1.2 instead of "If elapsed time is less than TIMEOUT_MS" - Replace "re-throw" with "report" in "Handling errors inside the callback" section - Use backtick formatting for TransientTransactionError label in step 10.2 - Apply mdformat line wrapping fixes --- .../transactions-convenient-api.md | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/source/transactions-convenient-api/transactions-convenient-api.md b/source/transactions-convenient-api/transactions-convenient-api.md index db279a8359..b6a5bdff98 100644 --- a/source/transactions-convenient-api/transactions-convenient-api.md +++ b/source/transactions-convenient-api/transactions-convenient-api.md @@ -123,8 +123,8 @@ This method should perform the following sequence of actions: 2. If `transactionAttempt` > 0: - 1. If elapsed time + `backoffMS` > `TIMEOUT_MS`, then propagate the previously encountered error to the caller of `withTransaction` - as per [timeout error propagation](#timeout-error-propagation) and return immediately. If the + 1. If elapsed time + `backoffMS` > `TIMEOUT_MS`, then propagate the previously encountered error to the caller of + `withTransaction` as per [timeout error propagation](#timeout-error-propagation) and return immediately. 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`. @@ -177,26 +177,28 @@ This method should perform the following sequence of actions: `MaxTimeMSExpired` 1. If the elapsed time of `withTransaction` exceeded `TIMEOUT_MS`, propagate the `commitTransaction` error to the - caller of `withTransaction` as per [timeout error propagation](#timeout-error-propagation) and return immediately. - 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: + caller of `withTransaction` as per [timeout error propagation](#timeout-error-propagation) and return + immediately. + 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](#majority-write-concern-is-used-when-retrying-committransaction)). - 2. If the `commitTransaction` error includes a "TransientTransactionError" label, jump back to step two. + 2. If the `commitTransaction` error includes a `TransientTransactionError` label, jump back to step two. - 3. Otherwise, propagate the `commitTransaction` error to the caller of `withTransaction` as is and return immediately. + 3. Otherwise, propagate the `commitTransaction` error to the caller of `withTransaction` as is and return + immediately. 11. The transaction was committed successfully. Return immediately. ###### 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](../client-side-operations-timeout/client-side-operations-timeout.md#errors), -then: +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](../client-side-operations-timeout/client-side-operations-timeout.md#errors), 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](../client-side-operations-timeout/client-side-operations-timeout.md#errors). - - 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. + ["Errors" section of the CSOT specification](../client-side-operations-timeout/client-side-operations-timeout.md#errors). + - 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. ##### Pseudo-code @@ -307,11 +309,11 @@ propagate. Command errors may abort the transaction on the server, and an attemp rejected with `NoSuchTransaction` error. For example, `DuplicateKeyError` is an error that aborts a transaction on the server. If the callback catches -`DuplicateKeyError` and does not re-throw it, the driver will attempt to commit the transaction. The server will reject +`DuplicateKeyError` and does not report it, the driver will attempt to commit the transaction. The server will reject the commit attempt with `NoSuchTransaction` error. This error has the "TransientTransactionError" label and the driver will retry the commit. This will result in an infinite loop. -Drivers MUST recommend that the callback re-throw command errors if they need to be handled inside the callback. Drivers +Drivers MUST recommend that the callback report command errors if they need to be handled inside the callback. Drivers SHOULD also recommend using Core Transaction API if a user wants to handle errors in a custom way. ## Test Plan From 893f9d4e680c8ef8ede73af23a1024124c83f1a3 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Fri, 10 Apr 2026 13:49:52 +0100 Subject: [PATCH 12/15] Fix broken anchor links in prose tests README Update references from #timeout-error-propagation-mechanism to #timeout-error-propagation after section rename. --- source/transactions-convenient-api/tests/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/source/transactions-convenient-api/tests/README.md b/source/transactions-convenient-api/tests/README.md index e978343dbd..62cf4ff77b 100644 --- a/source/transactions-convenient-api/tests/README.md +++ b/source/transactions-convenient-api/tests/README.md @@ -31,15 +31,15 @@ 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 as described in the - [propagation mechanism](../transactions-convenient-api.md#timeout-error-propagation-mechanism) to its caller. + [propagation mechanism](../transactions-convenient-api.md#timeout-error-propagation) 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 + [propagation mechanism](../transactions-convenient-api.md#timeout-error-propagation) 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. + [propagation mechanism](../transactions-convenient-api.md#timeout-error-propagation) 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 From 5444a685ab4009083346d01ba649fb0c54cb6be0 Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Fri, 10 Apr 2026 12:22:08 -0600 Subject: [PATCH 13/15] Refactor step 2.1 --- .../transactions-convenient-api.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/source/transactions-convenient-api/transactions-convenient-api.md b/source/transactions-convenient-api/transactions-convenient-api.md index b6a5bdff98..3b5b909903 100644 --- a/source/transactions-convenient-api/transactions-convenient-api.md +++ b/source/transactions-convenient-api/transactions-convenient-api.md @@ -123,10 +123,10 @@ This method should perform the following sequence of actions: 2. If `transactionAttempt` > 0: - 1. If elapsed time + `backoffMS` > `TIMEOUT_MS`, then propagate the previously encountered error to the caller of - `withTransaction` as per [timeout error propagation](#timeout-error-propagation) and return immediately. 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. 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](#timeout-error-propagation) and return immediately. + Otherwise, sleep for `backoffMS`. 1. jitter is a random float between \[0, 1), optionally including 1, depending on what is most natural for the given driver language. From f1c06237358c9043461f82552821f4a03357a8f3 Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Fri, 10 Apr 2026 14:06:27 -0600 Subject: [PATCH 14/15] Fix formatting --- .../transactions-convenient-api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/transactions-convenient-api/transactions-convenient-api.md b/source/transactions-convenient-api/transactions-convenient-api.md index 3b5b909903..91582cdc03 100644 --- a/source/transactions-convenient-api/transactions-convenient-api.md +++ b/source/transactions-convenient-api/transactions-convenient-api.md @@ -125,8 +125,8 @@ This method should perform the following sequence of actions: 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](#timeout-error-propagation) and return immediately. - Otherwise, sleep for `backoffMS`. + `withTransaction` as per [timeout error propagation](#timeout-error-propagation) and return immediately. + Otherwise, sleep for `backoffMS`. 1. jitter is a random float between \[0, 1), optionally including 1, depending on what is most natural for the given driver language. From f674dc7d2d6afe363f37107c9c0bf96fb3139e3c Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Fri, 10 Apr 2026 14:09:19 -0600 Subject: [PATCH 15/15] Fix formatting --- .../transactions-convenient-api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/transactions-convenient-api/transactions-convenient-api.md b/source/transactions-convenient-api/transactions-convenient-api.md index 91582cdc03..c28ffcb9d6 100644 --- a/source/transactions-convenient-api/transactions-convenient-api.md +++ b/source/transactions-convenient-api/transactions-convenient-api.md @@ -123,8 +123,8 @@ This method should perform the following sequence of actions: 2. If `transactionAttempt` > 0: - 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 + 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](#timeout-error-propagation) and return immediately. Otherwise, sleep for `backoffMS`.