@@ -17,6 +17,7 @@ import {
1717 MongoErrorLabel ,
1818 MongoExpiredSessionError ,
1919 MongoInvalidArgumentError ,
20+ MongoOperationTimeoutError ,
2021 MongoRuntimeError ,
2122 MongoServerError ,
2223 MongoTransactionError ,
@@ -777,14 +778,15 @@ export class ClientSession
777778 const willExceedTransactionDeadline =
778779 ( this . timeoutContext ?. csotEnabled ( ) &&
779780 backoffMS > this . timeoutContext . remainingTimeMS ) ||
780- processTimeMS ( ) + backoffMS > startTime + MAX_TIMEOUT ;
781+ ( ! this . timeoutContext ?. csotEnabled ( ) &&
782+ processTimeMS ( ) + backoffMS > startTime + MAX_TIMEOUT ) ;
781783
782784 if ( willExceedTransactionDeadline ) {
783- throw (
785+ throw makeWithTransactionTimeoutError (
784786 lastError ??
785- new MongoRuntimeError (
786- `Transaction retry did not record an error: should never occur. Please file a bug.`
787- )
787+ new MongoRuntimeError (
788+ `Transaction retry did not record an error: should never occur. Please file a bug.`
789+ )
788790 ) ;
789791 }
790792
@@ -827,6 +829,8 @@ export class ClientSession
827829 throw fnError ;
828830 }
829831
832+ lastError = fnError ;
833+
830834 if (
831835 this . transaction . state === TxnState . STARTING_TRANSACTION ||
832836 this . transaction . state === TxnState . TRANSACTION_IN_PROGRESS
@@ -836,14 +840,15 @@ export class ClientSession
836840 await this . abortTransaction ( ) ;
837841 }
838842
839- if (
840- fnError . hasErrorLabel ( MongoErrorLabel . TransientTransactionError ) &&
841- ( this . timeoutContext ?. csotEnabled ( ) || processTimeMS ( ) - startTime < MAX_TIMEOUT )
842- ) {
843- // 7.ii If the callback's error includes a "TransientTransactionError" label and the elapsed time of `withTransaction`
844- // is less than 120 seconds, jump back to step two.
845- lastError = fnError ;
846- continue retryTransaction;
843+ if ( fnError . hasErrorLabel ( MongoErrorLabel . TransientTransactionError ) ) {
844+ if ( this . timeoutContext ?. csotEnabled ( ) || processTimeMS ( ) - startTime < MAX_TIMEOUT ) {
845+ // 7.ii If the callback's error includes a "TransientTransactionError" label and the elapsed time of `withTransaction`
846+ // is less than TIMEOUT_MS, jump back to step two.
847+ continue retryTransaction;
848+ } else {
849+ // 7.ii (cont.) If timeout has been exceeded, raise a timeout error wrapping the transient error.
850+ throw makeWithTransactionTimeoutError ( fnError ) ;
851+ }
847852 }
848853
849854 // 7.iii If the callback's error includes a "UnknownTransactionCommitResult" label, the callback must have manually committed a transaction,
@@ -865,37 +870,39 @@ export class ClientSession
865870 committed = true ;
866871 // 10. If commitTransaction reported an error:
867872 } catch ( commitError ) {
868- // If CSOT is enabled, we repeatedly retry until timeoutMS expires. This is enforced by providing a
869- // timeoutContext to each async API, which know how to cancel themselves (i.e., the next retry will
870- // abort the withTransaction call).
871- // If CSOT is not enabled, do we still have time remaining or have we timed out?
873+ lastError = commitError ;
874+
875+ // Check if the withTransaction timeout has been exceeded.
876+ // With CSOT: check remaining time from the timeout context.
877+ // Without CSOT: check if we've exceeded the 120-second timeout.
872878 const hasTimedOut =
873- ! this . timeoutContext ?. csotEnabled ( ) && processTimeMS ( ) - startTime >= MAX_TIMEOUT ;
874-
875- if ( ! hasTimedOut ) {
876- /*
877- * Note: a maxTimeMS error will have the MaxTimeMSExpired
878- * code (50) and can be reported as a top-level error or
879- * inside writeConcernError, ex.
880- * { ok:0, code: 50, codeName: 'MaxTimeMSExpired' }
881- * { ok:1, writeConcernError: { code: 50, codeName: 'MaxTimeMSExpired' } }
882- */
883- if (
884- ! isMaxTimeMSExpiredError ( commitError ) &&
885- commitError . hasErrorLabel ( MongoErrorLabel . UnknownTransactionCommitResult )
886- ) {
887- // 10.i If the `commitTransaction` error includes a "UnknownTransactionCommitResult" label and the error is not
888- // MaxTimeMSExpired and the elapsed time of `withTransaction` is less than 120 seconds, jump back to step eight.
889- continue retryCommit;
890- }
891-
892- if ( commitError . hasErrorLabel ( MongoErrorLabel . TransientTransactionError ) ) {
893- // 10.ii If the commitTransaction error includes a "TransientTransactionError" label
894- // and the elapsed time of withTransaction is less than 120 seconds, jump back to step two.
895- lastError = commitError ;
896-
897- continue retryTransaction;
898- }
879+ ( this . timeoutContext ?. csotEnabled ( ) && this . timeoutContext . remainingTimeMS <= 0 ) ||
880+ ( ! this . timeoutContext ?. csotEnabled ( ) && processTimeMS ( ) - startTime >= MAX_TIMEOUT ) ;
881+
882+ if ( hasTimedOut ) {
883+ throw makeWithTransactionTimeoutError ( commitError ) ;
884+ }
885+
886+ /*
887+ * Note: a maxTimeMS error will have the MaxTimeMSExpired
888+ * code (50) and can be reported as a top-level error or
889+ * inside writeConcernError, ex.
890+ * { ok:0, code: 50, codeName: 'MaxTimeMSExpired' }
891+ * { ok:1, writeConcernError: { code: 50, codeName: 'MaxTimeMSExpired' } }
892+ */
893+ if (
894+ ! isMaxTimeMSExpiredError ( commitError ) &&
895+ commitError . hasErrorLabel ( MongoErrorLabel . UnknownTransactionCommitResult )
896+ ) {
897+ // 10.i If the `commitTransaction` error includes a "UnknownTransactionCommitResult" label and the error is not
898+ // MaxTimeMSExpired and the elapsed time of `withTransaction` is less than TIMEOUT_MS, jump back to step eight.
899+ continue retryCommit;
900+ }
901+
902+ if ( commitError . hasErrorLabel ( MongoErrorLabel . TransientTransactionError ) ) {
903+ // 10.ii If the commitTransaction error includes a "TransientTransactionError" label
904+ // and the elapsed time of withTransaction is less than TIMEOUT_MS, jump back to step two.
905+ continue retryTransaction;
899906 }
900907
901908 // 10.iii Otherwise, propagate the commitTransaction error to the caller of withTransaction and return immediately.
@@ -912,6 +919,18 @@ export class ClientSession
912919 }
913920}
914921
922+ function makeWithTransactionTimeoutError ( cause : Error ) : MongoOperationTimeoutError {
923+ const timeoutError = new MongoOperationTimeoutError ( 'Timed out during withTransaction' , {
924+ cause
925+ } ) ;
926+ if ( cause instanceof MongoError ) {
927+ for ( const label of cause . errorLabels ) {
928+ timeoutError . addErrorLabel ( label ) ;
929+ }
930+ }
931+ return timeoutError ;
932+ }
933+
915934const NON_DETERMINISTIC_WRITE_CONCERN_ERRORS = new Set ( [
916935 'CannotSatisfyWriteConcern' ,
917936 'UnknownReplWriteConcern' ,
0 commit comments