@@ -739,16 +739,19 @@ export class ClientSession
739739 : null ;
740740
741741 // 1. Define the following:
742- // 1.1 Record the current monotonic time, which will be used to enforce the timeout before later retry attempts.
742+ // 1.1 Record the current monotonic time, which will be used to enforce the 120-second / CSOT timeout before later retry attempts.
743743 // 1.2 Set `transactionAttempt` to `0`.
744744 // 1.3 Set `TIMEOUT_MS` to be `timeoutMS` if given, otherwise MAX_TIMEOUT (120-seconds).
745745 //
746746 // The spec describes timeout checks as "elapsed time < TIMEOUT_MS" (where elapsed = now - start).
747747 // We precompute `deadline = start + TIMEOUT_MS` so each check becomes simply `now < deadline`.
748748 //
749- // Note 1: When TIMEOUT_MS is reached, we MUST report a timeout error wrapping the last error that
750- // triggered retry. With CSOT this is a MongoOperationTimeoutError; without CSOT the raw error
751- // is propagated directly. See makeTimeoutError() below.
749+ // Timeout Error propagation mechanism
750+ // When the TIMEOUT_MS (calculated in step 1.3) is reached we MUST report a timeout error wrapping the previously
751+ // encountered error. If timeoutMS is set, then timeout error is a special type which is defined in CSOT
752+ // specification, If timeoutMS is not set, then propagate it as timeout error if the language allows to expose the
753+ // previously encountered error as a cause of a timeout error (see makeTimeoutError below in pseudo-code). If
754+ // timeout error is thrown then it SHOULD copy all error label(s) from the previously encountered retriable error.
752755 const csotEnabled = ! ! this . timeoutContext ?. csotEnabled ( ) ;
753756 const deadline = this . timeoutContext ?. csotEnabled ( )
754757 ? processTimeMS ( ) + this . timeoutContext . remainingTimeMS
@@ -768,12 +771,16 @@ export class ClientSession
768771 ) {
769772 // 2. If `transactionAttempt` > 0:
770773 if ( isRetry ) {
771- // 2.1 Calculate backoffMS. If elapsed time + backoffMS > TIMEOUT_MS
772- // (i.e., now + backoff >= deadline), raise the previously encountered error (see Note 1).
773- // Otherwise, sleep for backoffMS.
774+ // 2.1 If elapsed time + backoffMS > TIMEOUT_MS, then propagate the previously encountered
775+ // error (see propagation section above). If the elapsed time of withTransaction is less
776+ // than TIMEOUT_MS, calculate the backoffMS to be
777+ // jitter * min(BACKOFF_INITIAL * 1.5 ** (transactionAttempt - 1), BACKOFF_MAX).
778+ // sleep for backoffMS.
774779 const BACKOFF_INITIAL_MS = 5 ;
775780 const BACKOFF_MAX_MS = 500 ;
776781 const BACKOFF_GROWTH = 1.5 ;
782+ // 2.1.1 Jitter is a random float between [0, 1), optionally including 1, depending on what is most natural
783+ // for the given driver language.
777784 const jitter = Math . random ( ) ;
778785 const backoffMS =
779786 jitter *
@@ -795,20 +802,25 @@ export class ClientSession
795802 await setTimeout ( backoffMS ) ;
796803 }
797804
798- // 3. Invoke startTransaction on the session and increment transactionAttempt.
805+ // 3. Invoke startTransaction on the session and increment transactionAttempt. If TransactionOptions were
806+ // specified in the call to withTransaction, those MUST be used for startTransaction. Note that
807+ // ClientSession.defaultTransactionOptions will be used in the absence of any explicit TransactionOptions.
799808 // 4. If startTransaction reported an error, propagate that error to the caller and return immediately.
800809 this . startTransaction ( options ) ; // may throw on error
801810
802811 try {
803- // 5. Invoke the callback.
804- // 6. Control returns to withTransaction. Determine the current state and whether the callback reported an error.
812+ // 5. Invoke the callback. Drivers MUST ensure that the ClientSession can be accessed within the callback
813+ // (e.g. pass ClientSession as the first parameter, rely on lexical scoping). Drivers MAY pass additional
814+ // parameters as needed (e.g. user data solicited by withTransaction).
805815 const promise = fn ( this ) ;
806816 if ( ! isPromiseLike ( promise ) ) {
807817 throw new MongoInvalidArgumentError (
808818 'Function provided to `withTransaction` must return a Promise'
809819 ) ;
810820 }
811821
822+ // 6. Control returns to withTransaction. Determine the current state of the ClientSession and whether the
823+ // callback reported an error (e.g. thrown exception, error output parameter).
812824 result = await promise ;
813825
814826 // 8. If the ClientSession is in the "no transaction", "transaction aborted", or "transaction committed"
@@ -843,6 +855,9 @@ export class ClientSession
843855
844856 // 7.2 If the callback's error includes a "TransientTransactionError" label, jump back to step two.
845857 if ( fnError . hasErrorLabel ( MongoErrorLabel . TransientTransactionError ) ) {
858+ if ( processTimeMS ( ) >= deadline ) {
859+ throw makeTimeoutError ( lastError , csotEnabled ) ;
860+ }
846861 continue retryTransaction;
847862 }
848863
@@ -864,29 +879,28 @@ export class ClientSession
864879 // 10. If commitTransaction reported an error:
865880 lastError = commitError ;
866881
867- // If elapsed time >= TIMEOUT_MS (i.e., now >= deadline), raise a timeout error (see Note 1).
868- if ( processTimeMS ( ) >= deadline ) {
869- throw makeTimeoutError ( commitError , csotEnabled ) ;
870- }
871-
872- // 10.1 If the error includes "UnknownTransactionCommitResult" and is not MaxTimeMSExpired
873- // and elapsed time < TIMEOUT_MS (guaranteed — deadline check above), jump back to step nine.
874- // Note: a maxTimeMS error will have the MaxTimeMSExpired code (50) and can be reported
875- // as a top-level error or inside writeConcernError.
882+ // 10.1 If the commitTransaction error includes a UnknownTransactionCommitResult label and the error is not MaxTimeMSExpired
876883 if (
877884 ! isMaxTimeMSExpiredError ( commitError ) &&
878885 commitError . hasErrorLabel ( MongoErrorLabel . UnknownTransactionCommitResult )
879886 ) {
887+ // 10.1.1 If the elapsed time of withTransaction exceeded TIMEOUT_MS, propagate the commitTransaction error to the caller
888+ // of withTransaction and return immediately (see propagation section above)
889+ if ( processTimeMS ( ) >= deadline ) {
890+ throw makeTimeoutError ( commitError , csotEnabled ) ;
891+ }
892+ // 10.1.2 If the elapsed time of withTransaction is less than TIMEOUT_MS, jump back to step nine. We will trust
893+ // commitTransaction to apply a majority write concern on retry attempts (see: Majority write concern is used
894+ // when retrying commitTransaction).
880895 continue retryCommit;
881896 }
882897
883- // 10.2 If the error includes "TransientTransactionError" and elapsed time < TIMEOUT_MS
884- // (guaranteed — deadline check above), jump back to step two.
898+ // 10.2 If the commitTransaction error includes a "TransientTransactionError" label, jump back to step two.
885899 if ( commitError . hasErrorLabel ( MongoErrorLabel . TransientTransactionError ) ) {
886900 continue retryTransaction;
887901 }
888902
889- // 10.3 Otherwise, propagate the commitTransaction error (see Note 1) and return immediately.
903+ // 10.3 Otherwise, propagate the commitTransaction error to the caller of withTransaction and return immediately.
890904 throw commitError ;
891905 }
892906 }
0 commit comments