@@ -30,7 +30,7 @@ import { ReadConcernLevel } from './read_concern';
3030import { ReadPreference } from './read_preference' ;
3131import { type AsyncDisposable , configureResourceManagement } from './resource_management' ;
3232import { _advanceClusterTime , type ClusterTime , TopologyType } from './sdam/common' ;
33- import { type TimeoutContext } from './timeout' ;
33+ import { TimeoutContext } from './timeout' ;
3434import {
3535 isTransactionCommand ,
3636 Transaction ,
@@ -280,12 +280,13 @@ export class ClientSession
280280 async endSession ( options ?: EndSessionOptions ) : Promise < void > {
281281 try {
282282 if ( this . inTransaction ( ) ) {
283- if ( typeof options ?. timeoutMS === 'number' ) {
284- await endTransaction ( this , 'abortTransaction' , { timeoutMS : options . timeoutMS } ) ;
285- } else {
286- await endTransaction ( this , 'abortTransaction' ) ;
287- }
283+ await this . abortTransaction ( { ...options , throwTimeout : true } ) ;
288284 }
285+ } catch ( error ) {
286+ // spec indicates that we should ignore all errors for `endSessions`
287+ if ( MongoOperationTimeoutError . is ( error ) ) throw error ;
288+ squashError ( error ) ;
289+ } finally {
289290 if ( ! this . hasEnded ) {
290291 const serverSession = this [ kServerSession ] ;
291292 if ( serverSession != null ) {
@@ -301,11 +302,6 @@ export class ClientSession
301302 this . hasEnded = true ;
302303 this . emit ( 'ended' , this ) ;
303304 }
304- } catch ( error ) {
305- // spec indicates that we should ignore all errors for `endSessions`
306- if ( MongoOperationTimeoutError . is ( error ) ) throw error ;
307- squashError ( error ) ;
308- } finally {
309305 maybeClearPinnedConnection ( this , { force : true , ...options } ) ;
310306 }
311307 }
@@ -460,7 +456,7 @@ export class ClientSession
460456 *
461457 * @param options - Optional options, can be used to override `defaultTimeoutMS`.
462458 */
463- async commitTransaction ( ) : Promise < void > {
459+ async commitTransaction ( options ?: { timeoutMS ?: number } ) : Promise < void > {
464460 if ( this . transaction . state === TxnState . NO_TRANSACTION ) {
465461 throw new MongoTransactionError ( 'No transaction started' ) ;
466462 }
@@ -510,8 +506,19 @@ export class ClientSession
510506 bypassPinningCheck : true
511507 } ) ;
512508
509+ const timeoutMS =
510+ typeof options ?. timeoutMS === 'number'
511+ ? options . timeoutMS
512+ : typeof this . timeoutMS === 'number'
513+ ? this . timeoutMS
514+ : null ;
515+
516+ const timeoutContext = this . timeoutContext ?. csotEnabled ( )
517+ ? this . timeoutContext
518+ : TimeoutContext . create ( { timeoutMS, ...this . clientOptions } ) ;
519+
513520 try {
514- await executeOperation ( this . client , operation ) ;
521+ await executeOperation ( this . client , operation , timeoutContext ) ;
515522 return ;
516523 } catch ( firstCommitError ) {
517524 if ( firstCommitError instanceof MongoError && isRetryableWriteError ( firstCommitError ) ) {
@@ -521,7 +528,7 @@ export class ClientSession
521528 this . unpin ( { force : true } ) ;
522529
523530 try {
524- await executeOperation ( this . client , operation ) ;
531+ await executeOperation ( this . client , operation , timeoutContext ) ;
525532 return ;
526533 } catch ( retryCommitError ) {
527534 // If the retry failed, we process that error instead of the original
@@ -556,7 +563,10 @@ export class ClientSession
556563 *
557564 * @param options - Optional options, can be used to override `defaultTimeoutMS`.
558565 */
559- async abortTransaction ( ) : Promise < void > {
566+ async abortTransaction ( options ?: { timeoutMS ?: number } ) : Promise < void > ;
567+ /** @internal */
568+ async abortTransaction ( options ?: { timeoutMS ?: number ; throwTimeout ?: true } ) : Promise < void > ;
569+ async abortTransaction ( options ?: { timeoutMS ?: number ; throwTimeout ?: true } ) : Promise < void > {
560570 if ( this . transaction . state === TxnState . NO_TRANSACTION ) {
561571 throw new MongoTransactionError ( 'No transaction started' ) ;
562572 }
@@ -601,18 +611,34 @@ export class ClientSession
601611 bypassPinningCheck : true
602612 } ) ;
603613
614+ const timeoutMS =
615+ typeof options ?. timeoutMS === 'number'
616+ ? options . timeoutMS
617+ : this . timeoutContext ?. csotEnabled ( )
618+ ? this . timeoutContext . timeoutMS // refresh timeoutMS for abort operation
619+ : typeof this . timeoutMS === 'number'
620+ ? this . timeoutMS
621+ : null ;
622+
623+ const timeoutContext = TimeoutContext . create ( { timeoutMS, ...this . clientOptions } ) ;
624+
604625 try {
605- await executeOperation ( this . client , operation ) ;
626+ await executeOperation ( this . client , operation , timeoutContext ) ;
606627 this . unpin ( ) ;
607628 return ;
608629 } catch ( firstAbortError ) {
609630 this . unpin ( ) ;
610631
632+ if ( options ?. throwTimeout && MongoOperationTimeoutError . is ( firstAbortError ) )
633+ throw firstAbortError ;
634+
611635 if ( firstAbortError instanceof MongoError && isRetryableWriteError ( firstAbortError ) ) {
612636 try {
613- await executeOperation ( this . client , operation ) ;
637+ await executeOperation ( this . client , operation , timeoutContext ) ;
614638 return ;
615639 } catch ( secondAbortError ) {
640+ if ( options ?. throwTimeout && MongoOperationTimeoutError . is ( secondAbortError ) )
641+ throw secondAbortError ;
616642 // we do not retry the retry
617643 }
618644 }
@@ -670,93 +696,102 @@ export class ClientSession
670696 options ?: TransactionOptions
671697 ) : Promise < T > {
672698 const MAX_TIMEOUT = 120000 ;
673- const startTime = now ( ) ;
674-
675- let committed = false ;
676- let result : any ;
677699
678- while ( ! committed ) {
679- this . startTransaction ( options ) ; // may throw on error
700+ this . timeoutContext =
701+ options != null && 'timeoutMS' in options && typeof options . timeoutMS === 'number'
702+ ? TimeoutContext . create ( { timeoutMS : options . timeoutMS , ...this . clientOptions } )
703+ : null ;
680704
681- try {
682- const promise = fn ( this ) ;
683- if ( ! isPromiseLike ( promise ) ) {
684- throw new MongoInvalidArgumentError (
685- 'Function provided to `withTransaction` must return a Promise'
686- ) ;
687- }
705+ const startTime = this . timeoutContext ?. csotEnabled ( ) ? this . timeoutContext . start : now ( ) ;
688706
689- result = await promise ;
707+ let committed = false ;
708+ let result : any ;
690709
691- if (
692- this . transaction . state === TxnState . NO_TRANSACTION ||
693- this . transaction . state === TxnState . TRANSACTION_COMMITTED ||
694- this . transaction . state === TxnState . TRANSACTION_ABORTED
695- ) {
696- // Assume callback intentionally ended the transaction
697- return result ;
698- }
699- } catch ( fnError ) {
700- if ( ! ( fnError instanceof MongoError ) || fnError instanceof MongoInvalidArgumentError ) {
701- await this . abortTransaction ( ) ;
702- throw fnError ;
703- }
710+ try {
711+ while ( ! committed ) {
712+ this . startTransaction ( options ) ; // may throw on error
704713
705- if (
706- this . transaction . state === TxnState . STARTING_TRANSACTION ||
707- this . transaction . state === TxnState . TRANSACTION_IN_PROGRESS
708- ) {
709- await this . abortTransaction ( ) ;
710- }
714+ try {
715+ const promise = fn ( this ) ;
716+ if ( ! isPromiseLike ( promise ) ) {
717+ throw new MongoInvalidArgumentError (
718+ 'Function provided to `withTransaction` must return a Promise'
719+ ) ;
720+ }
711721
712- if (
713- fnError . hasErrorLabel ( MongoErrorLabel . TransientTransactionError ) &&
714- now ( ) - startTime < MAX_TIMEOUT
715- ) {
716- continue ;
717- }
722+ result = await promise ;
718723
719- throw fnError ;
720- }
724+ if (
725+ this . transaction . state === TxnState . NO_TRANSACTION ||
726+ this . transaction . state === TxnState . TRANSACTION_COMMITTED ||
727+ this . transaction . state === TxnState . TRANSACTION_ABORTED
728+ ) {
729+ // Assume callback intentionally ended the transaction
730+ return result ;
731+ }
732+ } catch ( fnError ) {
733+ if ( ! ( fnError instanceof MongoError ) || fnError instanceof MongoInvalidArgumentError ) {
734+ await this . abortTransaction ( ) ;
735+ throw fnError ;
736+ }
721737
722- while ( ! committed ) {
723- try {
724- /*
725- * We will rely on ClientSession.commitTransaction() to
726- * apply a majority write concern if commitTransaction is
727- * being retried (see: DRIVERS-601)
728- */
729- await this . commitTransaction ( ) ;
730- committed = true ;
731- } catch ( commitError ) {
732- /*
733- * Note: a maxTimeMS error will have the MaxTimeMSExpired
734- * code (50) and can be reported as a top-level error or
735- * inside writeConcernError, ex.
736- * { ok:0, code: 50, codeName: 'MaxTimeMSExpired' }
737- * { ok:1, writeConcernError: { code: 50, codeName: 'MaxTimeMSExpired' } }
738- */
739738 if (
740- ! isMaxTimeMSExpiredError ( commitError ) &&
741- commitError . hasErrorLabel ( MongoErrorLabel . UnknownTransactionCommitResult ) &&
742- now ( ) - startTime < MAX_TIMEOUT
739+ this . transaction . state === TxnState . STARTING_TRANSACTION ||
740+ this . transaction . state === TxnState . TRANSACTION_IN_PROGRESS
743741 ) {
744- continue ;
742+ await this . abortTransaction ( ) ;
745743 }
746744
747745 if (
748- commitError . hasErrorLabel ( MongoErrorLabel . TransientTransactionError ) &&
749- now ( ) - startTime < MAX_TIMEOUT
746+ fnError . hasErrorLabel ( MongoErrorLabel . TransientTransactionError ) &&
747+ ( this . timeoutContext != null || now ( ) - startTime < MAX_TIMEOUT )
750748 ) {
751- break ;
749+ continue ;
752750 }
753751
754- throw commitError ;
752+ throw fnError ;
753+ }
754+
755+ while ( ! committed ) {
756+ try {
757+ /*
758+ * We will rely on ClientSession.commitTransaction() to
759+ * apply a majority write concern if commitTransaction is
760+ * being retried (see: DRIVERS-601)
761+ */
762+ await this . commitTransaction ( ) ;
763+ committed = true ;
764+ } catch ( commitError ) {
765+ /*
766+ * Note: a maxTimeMS error will have the MaxTimeMSExpired
767+ * code (50) and can be reported as a top-level error or
768+ * inside writeConcernError, ex.
769+ * { ok:0, code: 50, codeName: 'MaxTimeMSExpired' }
770+ * { ok:1, writeConcernError: { code: 50, codeName: 'MaxTimeMSExpired' } }
771+ */
772+ if (
773+ ! isMaxTimeMSExpiredError ( commitError ) &&
774+ commitError . hasErrorLabel ( MongoErrorLabel . UnknownTransactionCommitResult ) &&
775+ ( this . timeoutContext != null || now ( ) - startTime < MAX_TIMEOUT )
776+ ) {
777+ continue ;
778+ }
779+
780+ if (
781+ commitError . hasErrorLabel ( MongoErrorLabel . TransientTransactionError ) &&
782+ ( this . timeoutContext != null || now ( ) - startTime < MAX_TIMEOUT )
783+ ) {
784+ break ;
785+ }
786+
787+ throw commitError ;
788+ }
755789 }
756790 }
791+ return result ;
792+ } finally {
793+ this . timeoutContext = null ;
757794 }
758-
759- return result ;
760795 }
761796}
762797
0 commit comments