@@ -742,20 +742,23 @@ export class ClientSession
742742 // 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).
745- //
745+
746+ // Timeout Error propagation
747+ // When the previously encountered error needs to be propagated because there is no more time for another attempt,
748+ // and it is not already a timeout error, then:
749+ // - A timeout error MUST be propagated instead. It MUST expose the previously encountered error as specified in
750+ // the "Errors" section of the CSOT specification.
751+ // - If exposing the previously encountered error from a timeout error is impossible in a driver, then the driver
752+ // is exempt from the requirement and MUST propagate the previously encountered error as is. The timeout error
753+ // MUST copy all error labels from the previously encountered error.
754+
746755 // The spec describes timeout checks as "elapsed time < TIMEOUT_MS" (where elapsed = now - start).
747- // We precompute `deadline = start + TIMEOUT_MS` so each check becomes simply `now < deadline`.
748- //
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.
756+ // We precompute `deadline = now + remainingTimeMS` so each check becomes simply `now < deadline`.
755757 const csotEnabled = ! ! this . timeoutContext ?. csotEnabled ( ) ;
756- const deadline = this . timeoutContext ?. csotEnabled ( )
757- ? processTimeMS ( ) + this . timeoutContext . remainingTimeMS
758- : processTimeMS ( ) + MAX_TIMEOUT ;
758+ const remainingTimeMS = this . timeoutContext ?. csotEnabled ( )
759+ ? this . timeoutContext . remainingTimeMS
760+ : MAX_TIMEOUT ;
761+ const deadline = processTimeMS ( ) + remainingTimeMS ;
759762
760763 let committed = false ;
761764 let result : T ;
@@ -764,23 +767,23 @@ export class ClientSession
764767
765768 try {
766769 retryTransaction: for (
767- // 1.2 Set `transactionAttempt` to `0`.
768770 let transactionAttempt = 0 , isRetry = false ;
769771 ! committed ;
770772 ++ transactionAttempt , isRetry = transactionAttempt > 0
771773 ) {
772774 // 2. If `transactionAttempt` > 0:
773775 if ( isRetry ) {
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.
776+ // 2.1 Calculate backoffMS to be jitter * min(BACKOFF_INITIAL * 1.5 ** (transactionAttempt - 1), BACKOFF_MAX).
777+ // If elapsed time + backoffMS > TIMEOUT_MS, then propagate the previously encountered error to the caller of
778+ // withTransaction as per timeout error propagation and return immediately. Otherwise, sleep for backoffMS.
779+ // 2.1.1 jitter is a random float between [0, 1), optionally including 1, depending on what is most natural
780+ // for the given driver language.
781+ // 2.1.2 transactionAttempt is the variable defined in step 1.
782+ // 2.1.3 BACKOFF_INITIAL is 5ms
783+ // 2.1.4 BACKOFF_MAX is 500ms
779784 const BACKOFF_INITIAL_MS = 5 ;
780785 const BACKOFF_MAX_MS = 500 ;
781786 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.
784787 const jitter = Math . random ( ) ;
785788 const backoffMS =
786789 jitter *
@@ -805,8 +808,9 @@ export class ClientSession
805808 // 3. Invoke startTransaction on the session and increment transactionAttempt. If TransactionOptions were
806809 // specified in the call to withTransaction, those MUST be used for startTransaction. Note that
807810 // ClientSession.defaultTransactionOptions will be used in the absence of any explicit TransactionOptions.
808- // 4. If startTransaction reported an error, propagate that error to the caller and return immediately.
809- this . startTransaction ( options ) ; // may throw on error
811+ // 4. If startTransaction reported an error, propagate that error to the caller of withTransaction as is and
812+ // return immediately.
813+ this . startTransaction ( options ) ;
810814
811815 try {
812816 // 5. Invoke the callback. Drivers MUST ensure that the ClientSession can be accessed within the callback
@@ -825,16 +829,15 @@ export class ClientSession
825829
826830 // 8. If the ClientSession is in the "no transaction", "transaction aborted", or "transaction committed"
827831 // state, assume the callback intentionally aborted or committed the transaction and return immediately.
828- // Drivers MAY allow the callback to return a value to be propagated as the return value of withTransaction.
829832 if (
830833 this . transaction . state === TxnState . NO_TRANSACTION ||
831834 this . transaction . state === TxnState . TRANSACTION_COMMITTED ||
832835 this . transaction . state === TxnState . TRANSACTION_ABORTED
833836 ) {
834837 return result ;
835838 }
836- // 7. If the callback reported an error:
837839 } catch ( fnError ) {
840+ // 7. If the callback reported an error
838841 if ( ! ( fnError instanceof MongoError ) || fnError instanceof MongoInvalidArgumentError ) {
839842 // This first preemptive abort regardless of TxnState isn't spec,
840843 // and it's unclear whether it's serving a practical purpose, but this logic is OLD
@@ -861,46 +864,45 @@ export class ClientSession
861864 continue retryTransaction;
862865 }
863866
864- // 7.3 If the callback's error includes a "UnknownTransactionCommitResult" label, the callback
865- // must have manually committed a transaction, propagate the error and return immediately.
866- // (This check is redundant with step 8, so we don't write code for it.)
867- // 7.4 Otherwise, propagate the callback's error (see Note 1) and return immediately.
867+ // 7.3 If the callback's error includes a "UnknownTransactionCommitResult" label, the callback must
868+ // have manually committed a transaction, propagate the callback's error to the caller of withTransaction
869+ // as is and return immediately.
870+ // 7.4 Otherwise, propagate the callback's error to the caller of withTransaction as is and return immediately.
868871 throw fnError ;
869872 }
870873
871- // 9. Invoke commitTransaction on the session.
872- // We will rely on ClientSession.commitTransaction() to apply a majority write concern
873- // if commitTransaction is being retried (see: DRIVERS-601).
874874 retryCommit: while ( ! committed ) {
875875 try {
876+ // 9. Invoke commitTransaction on the session.
876877 await this . commitTransaction ( ) ;
877878 committed = true ;
878879 } catch ( commitError ) {
879880 // 10. If commitTransaction reported an error:
880881 lastError = commitError ;
881882
882- // 10.1 If the commitTransaction error includes a UnknownTransactionCommitResult label and the error is not MaxTimeMSExpired
883+ // 10.1 If the commitTransaction error includes a UnknownTransactionCommitResult label and the error is
884+ // not MaxTimeMSExpired
883885 if (
884- ! isMaxTimeMSExpiredError ( commitError ) &&
885- commitError . hasErrorLabel ( MongoErrorLabel . UnknownTransactionCommitResult )
886+ commitError . hasErrorLabel ( MongoErrorLabel . UnknownTransactionCommitResult ) &&
887+ ! isMaxTimeMSExpiredError ( commitError )
886888 ) {
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+ // 10.1.1 If the elapsed time of withTransaction exceeded TIMEOUT_MS, propagate the commitTransaction
890+ // error to the caller of withTransaction as per timeout error propagation and return immediately.
889891 if ( processTimeMS ( ) >= deadline ) {
890892 throw makeTimeoutError ( commitError , csotEnabled ) ;
891893 }
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).
894+ // 10.1.2 Otherwise, jump back to step nine. We will trust commitTransaction to apply a majority write
895+ // concern on retry attempts (see: Majority write concern is used when retrying commitTransaction).
895896 continue retryCommit;
896897 }
897898
898- // 10.2 If the commitTransaction error includes a " TransientTransactionError" label, jump back to step two.
899+ // 10.2 If the commitTransaction error includes a TransientTransactionError label, jump back to step two.
899900 if ( commitError . hasErrorLabel ( MongoErrorLabel . TransientTransactionError ) ) {
900901 continue retryTransaction;
901902 }
902903
903- // 10.3 Otherwise, propagate the commitTransaction error to the caller of withTransaction and return immediately.
904+ // 10.3 Otherwise, propagate the commitTransaction error to the caller of withTransaction as is and return
905+ // immediately.
904906 throw commitError ;
905907 }
906908 }
0 commit comments