1+ import { setTimeout } from 'timers/promises' ;
2+
13import { Binary , type Document , Long , type Timestamp } from './bson' ;
24import type { CommandOptions , Connection } from './cmap/connection' ;
35import { ConnectionPoolMetrics } from './cmap/metrics' ;
@@ -731,10 +733,10 @@ export class ClientSession
731733 : processTimeMS ( ) ;
732734
733735 let committed = false ;
734- let result : any ;
736+ let result : T ;
735737
736738 try {
737- while ( ! committed ) {
739+ for ( let retry = 0 ; ! committed ; ++ retry ) {
738740 this . startTransaction ( options ) ; // may throw on error
739741
740742 try {
@@ -770,7 +772,7 @@ export class ClientSession
770772
771773 if (
772774 fnError . hasErrorLabel ( MongoErrorLabel . TransientTransactionError ) &&
773- ( this . timeoutContext != null || processTimeMS ( ) - startTime < MAX_TIMEOUT )
775+ ( this . timeoutContext ?. csotEnabled ( ) || processTimeMS ( ) - startTime < MAX_TIMEOUT )
774776 ) {
775777 continue ;
776778 }
@@ -788,32 +790,57 @@ export class ClientSession
788790 await this . commitTransaction ( ) ;
789791 committed = true ;
790792 } catch ( commitError ) {
791- /*
792- * Note: a maxTimeMS error will have the MaxTimeMSExpired
793- * code (50) and can be reported as a top-level error or
794- * inside writeConcernError, ex.
795- * { ok:0, code: 50, codeName: 'MaxTimeMSExpired' }
796- * { ok:1, writeConcernError: { code: 50, codeName: 'MaxTimeMSExpired' } }
797- */
798- if (
799- ! isMaxTimeMSExpiredError ( commitError ) &&
800- commitError . hasErrorLabel ( MongoErrorLabel . UnknownTransactionCommitResult ) &&
801- ( this . timeoutContext != null || processTimeMS ( ) - startTime < MAX_TIMEOUT )
802- ) {
803- continue ;
804- }
805-
806- if (
807- commitError . hasErrorLabel ( MongoErrorLabel . TransientTransactionError ) &&
808- ( this . timeoutContext != null || processTimeMS ( ) - startTime < MAX_TIMEOUT )
809- ) {
810- break ;
793+ // If CSOT is enabled, we repeatedly retry until timeoutMS expires. This is enforced by providing a
794+ // timeoutContext to each async API, which know how to cancel themselves (i.e., the next retry will
795+ // abort the withTransaction call).
796+ // If CSOT is not enabled, do we still have time remaining or have we timed out?
797+ const hasTimedOut =
798+ ! this . timeoutContext ?. csotEnabled ( ) && processTimeMS ( ) - startTime >= MAX_TIMEOUT ;
799+
800+ if ( ! hasTimedOut ) {
801+ if (
802+ ! isMaxTimeMSExpiredError ( commitError ) &&
803+ commitError . hasErrorLabel ( MongoErrorLabel . UnknownTransactionCommitResult )
804+ ) {
805+ /*
806+ * Note: a maxTimeMS error will have the MaxTimeMSExpired
807+ * code (50) and can be reported as a top-level error or
808+ * inside writeConcernError, ex.
809+ * { ok:0, code: 50, codeName: 'MaxTimeMSExpired' }
810+ * { ok:1, writeConcernError: { code: 50, codeName: 'MaxTimeMSExpired' } }
811+ */
812+ continue ;
813+ }
814+
815+ if ( commitError . hasErrorLabel ( MongoErrorLabel . TransientTransactionError ) ) {
816+ const BACKOFF_INITIAL_MS = 5 ;
817+ const BACKOFF_MAX_MS = 500 ;
818+ const BACKOFF_GROWTH = 1.5 ;
819+ const jitter = Math . random ( ) ;
820+ const backoffMS =
821+ jitter * Math . min ( BACKOFF_INITIAL_MS * BACKOFF_GROWTH ** retry , BACKOFF_MAX_MS ) ;
822+
823+ const willExceedTransactionDeadline =
824+ ( this . timeoutContext ?. csotEnabled ( ) &&
825+ backoffMS > this . timeoutContext . remainingTimeMS ) ||
826+ processTimeMS ( ) + backoffMS > startTime + MAX_TIMEOUT ;
827+
828+ if ( willExceedTransactionDeadline ) {
829+ break ;
830+ }
831+
832+ await setTimeout ( backoffMS ) ;
833+
834+ break ;
835+ }
811836 }
812837
813838 throw commitError ;
814839 }
815840 }
816841 }
842+
843+ // @ts -expect-error Result is always defined if we reach here, the for-loop above convinces TS it is not.
817844 return result ;
818845 } finally {
819846 this . timeoutContext = null ;
0 commit comments