From 47c94b1dc64e39a0f2c139ba14889158b40422d2 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Wed, 4 Feb 2026 03:31:44 +0000 Subject: [PATCH 1/8] Add prose test to clarify backpressure thrown timeout error logic --- .../tests/README.md | 46 +++++++++++++++++++ .../transactions-convenient-api.md | 21 +++++---- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/source/client-side-operations-timeout/tests/README.md b/source/client-side-operations-timeout/tests/README.md index e883aad593..eb4b522ea9 100644 --- a/source/client-side-operations-timeout/tests/README.md +++ b/source/client-side-operations-timeout/tests/README.md @@ -565,6 +565,11 @@ the ClientSession `defaultTimeoutMS` option, and once more with the timeout spec Tests in this section MUST only run against replica sets and sharded clusters with server versions 4.4 or higher. +It is recommended that drivers run these tests with [jitter](../../transactions-convenient-api/transactions-convenient-api.md#clientsessionwithtransaction) disabled +(set to 0) to reduce the likelihood of flakiness due to varying +[backoff times](../../transactions-convenient-api/transactions-convenient-api.md#backoff-benefits). + + #### timeoutMS is refreshed for abortTransaction if the callback fails 1. Using `internalClient`, drop the `db.coll` collection. @@ -602,6 +607,47 @@ Tests in this section MUST only run against replica sets and sharded clusters wi 1. `command_started` and `command_failed` events for an `insert` command. 2. `command_started` and `command_failed` events for an `abortTransaction` command. +#### withTransaction applies a single timeout across retries due to transient errors + +1. Using `internalClient`, drop the collection `db.coll`. + +2. Using `internalClient`, set the following fail point: + + ```javascript + { + configureFailPoint: "failCommand", + mode: "alwaysOn", + data: { + failCommands: ["insert"], + blockConnection: true, + blockTimeMS: 25, + errorCode: 24, + errorLabels: ["TransientTransactionError"] + } + } + ``` + +3. Create a new MongoClient `client` with default settings. + +4. Using `client`, create a new explicit `session` with `defaultTimeout=200ms`. + +5. Initialize an `attempt` counter to `0`. + +6. Using `session`, execute `withTransaction` with a callback that:- + - increment the `attempt` counter + - Inserts the document `{ x: 1 }` into `db.coll` + +7. Expect this to fail with a CSOT timeout error (this depends on the driver's error/exception type) + `MongoOperationTimeoutException` for Java). + +8. Verify that there has been at least 2 attempts to execute the `insert` command as part of the `withTransaction` call. (`attempt > 1`) + +**Rationale:** This test verifies that when `withTransaction` encounters transient transaction errors +(such as lock acquisition failures with error code 24), the retry attempts share the same timeout budget rather than resetting it. +Each retry consumes time from the original 200ms timeout, and the cumulative delay from retries exceeds the available time budget, resulting in a timeout error. +This test als ensures that the driver does not throw the lock acquisition error directly to the user, 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. + ### 11. Multi-batch bulkWrites This test MUST only run against server versions 8.0+. This test must be skipped on Atlas Serverless. diff --git a/source/transactions-convenient-api/transactions-convenient-api.md b/source/transactions-convenient-api/transactions-convenient-api.md index 025fe4a939..86221a5743 100644 --- a/source/transactions-convenient-api/transactions-convenient-api.md +++ b/source/transactions-convenient-api/transactions-convenient-api.md @@ -220,9 +220,12 @@ withTransaction(callback, options) { this.abortTransaction(); } - if (error.hasErrorLabel("TransientTransactionError") && - Date.now() - startTime < timeout) { - continue retryTransaction; + if (error.hasErrorLabel("TransientTransactionError")) { + if (Date.now() - startTime < timeout) { + continue retryTransaction; + } else { + throw getCSOTTimeoutIfSet() != null ? createCSOTMongoTimeoutException(error) : createLegacyMongoTimeoutException(e); + } } throw error; @@ -247,15 +250,17 @@ withTransaction(callback, options) { * {ok:0, code: 50, codeName: "MaxTimeMSExpired"} * {ok:1, writeConcernError: {code: 50, codeName: "MaxTimeMSExpired"}} */ + lastError = error; + if (Date.now() - startTime >= timeout) { + throw getCSOTTimeoutIfSet() != null ? createCSOTMongoTimeoutException(error) : createLegacyMongoTimeoutException(e); + } + if (!isMaxTimeMSExpiredError(error) && - error.hasErrorLabel("UnknownTransactionCommitResult") && - Date.now() - startTime < timeout) { + error.hasErrorLabel("UnknownTransactionCommitResult")) { continue retryCommit; } - if (error.hasErrorLabel("TransientTransactionError") && - Date.now() - startTime < timeout) { - lastError = error; + if (error.hasErrorLabel("TransientTransactionError")) { continue retryTransaction; } From 14870f235f804e9b600a84bc551bdb78d472dd61 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Wed, 4 Feb 2026 03:48:30 +0000 Subject: [PATCH 2/8] Fix spellcheck --- source/client-side-operations-timeout/tests/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/client-side-operations-timeout/tests/README.md b/source/client-side-operations-timeout/tests/README.md index eb4b522ea9..2003aa19df 100644 --- a/source/client-side-operations-timeout/tests/README.md +++ b/source/client-side-operations-timeout/tests/README.md @@ -645,7 +645,8 @@ It is recommended that drivers run these tests with [jitter](../../transactions- **Rationale:** This test verifies that when `withTransaction` encounters transient transaction errors (such as lock acquisition failures with error code 24), the retry attempts share the same timeout budget rather than resetting it. Each retry consumes time from the original 200ms timeout, and the cumulative delay from retries exceeds the available time budget, resulting in a timeout error. -This test als ensures that the driver does not throw the lock acquisition error directly to the user, but instead surfaces a timeout error after exhausting the retry attempts within the specified timeout. +This test also ensures that the driver does not throw the lock acquisition error directly to the user, 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. ### 11. Multi-batch bulkWrites From 03f12b387919f269b9ad14a71b24dac955c0b464 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Wed, 4 Feb 2026 03:52:32 +0000 Subject: [PATCH 3/8] Markdown formatting --- .../tests/README.md | 54 ++++++++++--------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/source/client-side-operations-timeout/tests/README.md b/source/client-side-operations-timeout/tests/README.md index 2003aa19df..90034fef6f 100644 --- a/source/client-side-operations-timeout/tests/README.md +++ b/source/client-side-operations-timeout/tests/README.md @@ -565,11 +565,11 @@ the ClientSession `defaultTimeoutMS` option, and once more with the timeout spec Tests in this section MUST only run against replica sets and sharded clusters with server versions 4.4 or higher. -It is recommended that drivers run these tests with [jitter](../../transactions-convenient-api/transactions-convenient-api.md#clientsessionwithtransaction) disabled -(set to 0) to reduce the likelihood of flakiness due to varying +It is recommended that drivers run these tests with +[jitter](../../transactions-convenient-api/transactions-convenient-api.md#clientsessionwithtransaction) disabled (set to +0\) to reduce the likelihood of flakiness due to varying [backoff times](../../transactions-convenient-api/transactions-convenient-api.md#backoff-benefits). - #### timeoutMS is refreshed for abortTransaction if the callback fails 1. Using `internalClient`, drop the `db.coll` collection. @@ -613,19 +613,19 @@ It is recommended that drivers run these tests with [jitter](../../transactions- 2. Using `internalClient`, set the following fail point: - ```javascript - { - configureFailPoint: "failCommand", - mode: "alwaysOn", - data: { - failCommands: ["insert"], - blockConnection: true, - blockTimeMS: 25, - errorCode: 24, - errorLabels: ["TransientTransactionError"] - } - } - ``` + ```javascript + { + configureFailPoint: "failCommand", + mode: "alwaysOn", + data: { + failCommands: ["insert"], + blockConnection: true, + blockTimeMS: 25, + errorCode: 24, + errorLabels: ["TransientTransactionError"] + } + } + ``` 3. Create a new MongoClient `client` with default settings. @@ -633,21 +633,23 @@ It is recommended that drivers run these tests with [jitter](../../transactions- 5. Initialize an `attempt` counter to `0`. -6. Using `session`, execute `withTransaction` with a callback that:- +6. Using `session`, execute `withTransaction` with a callback that:- + - increment the `attempt` counter - Inserts the document `{ x: 1 }` into `db.coll` -7. Expect this to fail with a CSOT timeout error (this depends on the driver's error/exception type) - `MongoOperationTimeoutException` for Java). +7. Expect this to fail with a CSOT timeout error (this depends on the driver's error/exception type) + `MongoOperationTimeoutException` for Java). -8. Verify that there has been at least 2 attempts to execute the `insert` command as part of the `withTransaction` call. (`attempt > 1`) +8. Verify that there has been at least 2 attempts to execute the `insert` command as part of the `withTransaction` call. + (`attempt > 1`) -**Rationale:** This test verifies that when `withTransaction` encounters transient transaction errors -(such as lock acquisition failures with error code 24), the retry attempts share the same timeout budget rather than resetting it. -Each retry consumes time from the original 200ms timeout, and the cumulative delay from retries exceeds the available time budget, resulting in a timeout error. -This test also ensures that the driver does not throw the lock acquisition error directly to the user, 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. +**Rationale:** This test verifies that when `withTransaction` encounters transient transaction errors (such as lock +acquisition failures with error code 24), the retry attempts share the same timeout budget rather than resetting it. +Each retry consumes time from the original 200ms timeout, and the cumulative delay from retries exceeds the available +time budget, resulting in a timeout error. This test also ensures that the driver does not throw the lock acquisition +error directly to the user, 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. ### 11. Multi-batch bulkWrites From 5ef3afd78f8dba1f4a058cc9516e1a3a1df50691 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 9 Feb 2026 15:08:11 +0000 Subject: [PATCH 4/8] Apply suggestion from @baileympearson Co-authored-by: Bailey Pearson --- .../transactions-convenient-api/transactions-convenient-api.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/source/transactions-convenient-api/transactions-convenient-api.md b/source/transactions-convenient-api/transactions-convenient-api.md index 86221a5743..44dc88065b 100644 --- a/source/transactions-convenient-api/transactions-convenient-api.md +++ b/source/transactions-convenient-api/transactions-convenient-api.md @@ -252,9 +252,8 @@ withTransaction(callback, options) { */ lastError = error; if (Date.now() - startTime >= timeout) { - throw getCSOTTimeoutIfSet() != null ? createCSOTMongoTimeoutException(error) : createLegacyMongoTimeoutException(e); + throw makeTimeoutError(error); } - if (!isMaxTimeMSExpiredError(error) && error.hasErrorLabel("UnknownTransactionCommitResult")) { continue retryCommit; From 87b3cf8d3d7e27cf4afc62db7c7c3fc7a180d648 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Wed, 11 Feb 2026 17:36:46 +0000 Subject: [PATCH 5/8] - PR feedback. - Rewrote the prose test as unified test --- .../tests/README.md | 49 -------- .../tests/convenient-transactions.json | 107 +++++++++++++++++- .../tests/convenient-transactions.yml | 65 +++++++++++ .../transactions-convenient-api.md | 8 +- 4 files changed, 177 insertions(+), 52 deletions(-) diff --git a/source/client-side-operations-timeout/tests/README.md b/source/client-side-operations-timeout/tests/README.md index 90034fef6f..e883aad593 100644 --- a/source/client-side-operations-timeout/tests/README.md +++ b/source/client-side-operations-timeout/tests/README.md @@ -565,11 +565,6 @@ the ClientSession `defaultTimeoutMS` option, and once more with the timeout spec Tests in this section MUST only run against replica sets and sharded clusters with server versions 4.4 or higher. -It is recommended that drivers run these tests with -[jitter](../../transactions-convenient-api/transactions-convenient-api.md#clientsessionwithtransaction) disabled (set to -0\) to reduce the likelihood of flakiness due to varying -[backoff times](../../transactions-convenient-api/transactions-convenient-api.md#backoff-benefits). - #### timeoutMS is refreshed for abortTransaction if the callback fails 1. Using `internalClient`, drop the `db.coll` collection. @@ -607,50 +602,6 @@ It is recommended that drivers run these tests with 1. `command_started` and `command_failed` events for an `insert` command. 2. `command_started` and `command_failed` events for an `abortTransaction` command. -#### withTransaction applies a single timeout across retries due to transient errors - -1. Using `internalClient`, drop the collection `db.coll`. - -2. Using `internalClient`, set the following fail point: - - ```javascript - { - configureFailPoint: "failCommand", - mode: "alwaysOn", - data: { - failCommands: ["insert"], - blockConnection: true, - blockTimeMS: 25, - errorCode: 24, - errorLabels: ["TransientTransactionError"] - } - } - ``` - -3. Create a new MongoClient `client` with default settings. - -4. Using `client`, create a new explicit `session` with `defaultTimeout=200ms`. - -5. Initialize an `attempt` counter to `0`. - -6. Using `session`, execute `withTransaction` with a callback that:- - - - increment the `attempt` counter - - Inserts the document `{ x: 1 }` into `db.coll` - -7. Expect this to fail with a CSOT timeout error (this depends on the driver's error/exception type) - `MongoOperationTimeoutException` for Java). - -8. Verify that there has been at least 2 attempts to execute the `insert` command as part of the `withTransaction` call. - (`attempt > 1`) - -**Rationale:** This test verifies that when `withTransaction` encounters transient transaction errors (such as lock -acquisition failures with error code 24), the retry attempts share the same timeout budget rather than resetting it. -Each retry consumes time from the original 200ms timeout, and the cumulative delay from retries exceeds the available -time budget, resulting in a timeout error. This test also ensures that the driver does not throw the lock acquisition -error directly to the user, 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. - ### 11. Multi-batch bulkWrites This test MUST only run against server versions 8.0+. This test must be skipped on Atlas Serverless. diff --git a/source/client-side-operations-timeout/tests/convenient-transactions.json b/source/client-side-operations-timeout/tests/convenient-transactions.json index f9d03429db..3400b82ba9 100644 --- a/source/client-side-operations-timeout/tests/convenient-transactions.json +++ b/source/client-side-operations-timeout/tests/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/source/client-side-operations-timeout/tests/convenient-transactions.yml b/source/client-side-operations-timeout/tests/convenient-transactions.yml index 55b72481df..6d8244934c 100644 --- a/source/client-side-operations-timeout/tests/convenient-transactions.yml +++ b/source/client-side-operations-timeout/tests/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 applies a single timeout across retries due to transient errors" + 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 diff --git a/source/transactions-convenient-api/transactions-convenient-api.md b/source/transactions-convenient-api/transactions-convenient-api.md index 44dc88065b..06e7c2306e 100644 --- a/source/transactions-convenient-api/transactions-convenient-api.md +++ b/source/transactions-convenient-api/transactions-convenient-api.md @@ -203,7 +203,7 @@ withTransaction(callback, options) { BACKOFF_MAX); if (Date.now() + backoff - startTime >= timeout) { - throw lastError; + throw makeTimeoutError(lastError); } sleep(backoff); } @@ -224,7 +224,7 @@ withTransaction(callback, options) { if (Date.now() - startTime < timeout) { continue retryTransaction; } else { - throw getCSOTTimeoutIfSet() != null ? createCSOTMongoTimeoutException(error) : createLegacyMongoTimeoutException(e); + throw makeTimeoutError(error) } } @@ -270,6 +270,10 @@ withTransaction(callback, options) { break; // Transaction was successful } } + +function makeTimeoutError(error) { + return getCSOTTimeoutIfSet() != null ? createCSOTMongoTimeoutException(error) : createLegacyMongoTimeoutException(error); +} ``` ### ClientSession must provide access to a MongoClient From 160d1fbeeb33e985c3950367180e2ad246205566 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Wed, 11 Feb 2026 18:06:14 +0000 Subject: [PATCH 6/8] update test description --- .../tests/convenient-transactions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/client-side-operations-timeout/tests/convenient-transactions.yml b/source/client-side-operations-timeout/tests/convenient-transactions.yml index 6d8244934c..8157c5e4d8 100644 --- a/source/client-side-operations-timeout/tests/convenient-transactions.yml +++ b/source/client-side-operations-timeout/tests/convenient-transactions.yml @@ -118,7 +118,7 @@ tests: # 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 applies a single timeout across retries due to transient errors" + - description: "withTransaction surfaces a timeout after exhausting transient transaction retries, retaining the last transient error as the timeout cause." operations: - name: failPoint object: testRunner From 8aad52ac0bb91c6dc47c61e23ff180b445afd91d Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Tue, 17 Feb 2026 19:30:09 +0000 Subject: [PATCH 7/8] PR feedback --- .../tests/README.md | 14 +++++++---- .../transactions-convenient-api.md | 24 +++++++++++++++---- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/source/transactions-convenient-api/tests/README.md b/source/transactions-convenient-api/tests/README.md index 834ab47a25..2fdc52edf6 100644 --- a/source/transactions-convenient-api/tests/README.md +++ b/source/transactions-convenient-api/tests/README.md @@ -30,17 +30,21 @@ Drivers should test that `withTransaction` enforces a non-configurable timeout b 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 to its caller. + `withTransaction` should propagate the error[1](#footnote-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. + `withTransaction` should propagate the error[1](#footnote-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 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[1](#footnote-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. 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. +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. + ### Retry Backoff is Enforced Drivers should test that retries within `withTransaction` do not occur immediately. @@ -106,6 +110,8 @@ Drivers should test that retries within `withTransaction` do not occur immediate ## Changelog +- 2026-02-17: Clarify expected error when timeout is reached + [DRIVERS-3391](https://jira.mongodb.org/browse/DRIVERS-3391). - 2026-01-07: Fixed Retry Backoff is Enforced test accordingly to the updated spec. - 2025-11-18: Added Backoff test. - 2024-09-06: Migrated from reStructuredText to Markdown. diff --git a/source/transactions-convenient-api/transactions-convenient-api.md b/source/transactions-convenient-api/transactions-convenient-api.md index 06e7c2306e..f7ebc65ac2 100644 --- a/source/transactions-convenient-api/transactions-convenient-api.md +++ b/source/transactions-convenient-api/transactions-convenient-api.md @@ -123,9 +123,10 @@ 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. 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. If elapsed time + `backoffMS` > `TIMEOUT_MS`, then raise the previously encountered + error[1](#footnote-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`. 1. jitter is a random float between \[0, 1) @@ -162,7 +163,8 @@ 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 to the caller of `withTransaction` and return immediately. + 4. Otherwise, propagate the callback's error[1](#footnote-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. @@ -178,10 +180,19 @@ 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 to the caller of `withTransaction` and return immediately. + 3. Otherwise, propagate the `commitTransaction` error[1](#footnote-error) to the caller of + `withTransaction` and return immediately. 11. The transaction was committed successfully. Return immediately. +1When 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 +[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. + ##### Pseudo-code This method can be expressed by the following pseudo-code: @@ -427,6 +438,9 @@ provides an implementation of a technique already described in the MongoDB 4.0 d ## Changelog +- 2026-02-17: Clarify expected error when timeout is reached + [DRIVERS-3391](https://jira.mongodb.org/browse/DRIVERS-3391). + - 2025-11-20: withTransaction applies exponential backoff when retrying. - 2024-09-06: Migrated from reStructuredText to Markdown. From 634a14da52d019d979c44be509276e915c8201eb Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Tue, 17 Feb 2026 19:54:05 +0000 Subject: [PATCH 8/8] Fixing markdown-html-check --- .../tests/README.md | 16 +++++++------ .../transactions-convenient-api.md | 23 ++++++++++--------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/source/transactions-convenient-api/tests/README.md b/source/transactions-convenient-api/tests/README.md index 2fdc52edf6..bd9fd225df 100644 --- a/source/transactions-convenient-api/tests/README.md +++ b/source/transactions-convenient-api/tests/README.md @@ -30,20 +30,22 @@ Drivers should test that `withTransaction` enforces a non-configurable timeout b 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[1](#footnote-error) to its caller. + `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[1](#footnote-error) to its caller. + `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[1](#footnote-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. + `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 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. -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. +______________________________________________________________________ + +**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. ### 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 f7ebc65ac2..6d1baff47d 100644 --- a/source/transactions-convenient-api/transactions-convenient-api.md +++ b/source/transactions-convenient-api/transactions-convenient-api.md @@ -123,10 +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[1](#footnote-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`. + 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 + `jitter * min(BACKOFF_INITIAL * 1.5 ** (transactionAttempt - 1), BACKOFF_MAX)`. sleep for `backoffMS`. 1. jitter is a random float between \[0, 1) @@ -163,8 +162,8 @@ 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[1](#footnote-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. 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,14 +179,16 @@ 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[1](#footnote-error) to the caller of - `withTransaction` and return immediately. + 3. Otherwise, propagate the `commitTransaction` error (see Note 1 below) to the caller of `withTransaction` and + return immediately. 11. The transaction was committed successfully. Return immediately. -1When 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 +______________________________________________________________________ + +**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 [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