@@ -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,18 @@ 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 (
845+ this . timeoutContext ?. csotEnabled ( ) ||
846+ processTimeMS ( ) - startTime < MAX_TIMEOUT
847+ ) {
848+ // 7.ii If the callback's error includes a "TransientTransactionError" label and the elapsed time of `withTransaction`
849+ // is less than TIMEOUT_MS, jump back to step two.
850+ continue retryTransaction;
851+ } else {
852+ // 7.ii (cont.) If timeout has been exceeded, raise a timeout error wrapping the transient error.
853+ throw makeWithTransactionTimeoutError ( fnError ) ;
854+ }
847855 }
848856
849857 // 7.iii If the callback's error includes a "UnknownTransactionCommitResult" label, the callback must have manually committed a transaction,
@@ -865,37 +873,39 @@ export class ClientSession
865873 committed = true ;
866874 // 10. If commitTransaction reported an error:
867875 } 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?
876+ lastError = commitError ;
877+
878+ // Check if the withTransaction timeout has been exceeded.
879+ // With CSOT: check remaining time from the timeout context.
880+ // Without CSOT: check if we've exceeded the 120-second timeout.
872881 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- }
882+ ( this . timeoutContext ?. csotEnabled ( ) && this . timeoutContext . remainingTimeMS <= 0 ) ||
883+ ( ! this . timeoutContext ?. csotEnabled ( ) && processTimeMS ( ) - startTime >= MAX_TIMEOUT ) ;
884+
885+ if ( hasTimedOut ) {
886+ throw makeWithTransactionTimeoutError ( commitError ) ;
887+ }
888+
889+ /*
890+ * Note: a maxTimeMS error will have the MaxTimeMSExpired
891+ * code (50) and can be reported as a top-level error or
892+ * inside writeConcernError, ex.
893+ * { ok:0, code: 50, codeName: 'MaxTimeMSExpired' }
894+ * { ok:1, writeConcernError: { code: 50, codeName: 'MaxTimeMSExpired' } }
895+ */
896+ if (
897+ ! isMaxTimeMSExpiredError ( commitError ) &&
898+ commitError . hasErrorLabel ( MongoErrorLabel . UnknownTransactionCommitResult )
899+ ) {
900+ // 10.i If the `commitTransaction` error includes a "UnknownTransactionCommitResult" label and the error is not
901+ // MaxTimeMSExpired and the elapsed time of `withTransaction` is less than TIMEOUT_MS, jump back to step eight.
902+ continue retryCommit;
903+ }
904+
905+ if ( commitError . hasErrorLabel ( MongoErrorLabel . TransientTransactionError ) ) {
906+ // 10.ii If the commitTransaction error includes a "TransientTransactionError" label
907+ // and the elapsed time of withTransaction is less than TIMEOUT_MS, jump back to step two.
908+ continue retryTransaction;
899909 }
900910
901911 // 10.iii Otherwise, propagate the commitTransaction error to the caller of withTransaction and return immediately.
@@ -912,6 +922,18 @@ export class ClientSession
912922 }
913923}
914924
925+ function makeWithTransactionTimeoutError ( cause : Error ) : MongoOperationTimeoutError {
926+ const timeoutError = new MongoOperationTimeoutError ( 'Timed out during withTransaction' , {
927+ cause
928+ } ) ;
929+ if ( cause instanceof MongoError ) {
930+ for ( const label of cause . errorLabels ) {
931+ timeoutError . addErrorLabel ( label ) ;
932+ }
933+ }
934+ return timeoutError ;
935+ }
936+
915937const NON_DETERMINISTIC_WRITE_CONCERN_ERRORS = new Set ( [
916938 'CannotSatisfyWriteConcern' ,
917939 'UnknownReplWriteConcern' ,
0 commit comments