@@ -42,20 +42,6 @@ import {
4242
4343const minWireVersionForShardedTransactions = 8 ;
4444
45- function assertAlive ( session : ClientSession , callback ?: Callback ) : boolean {
46- if ( session . serverSession == null ) {
47- const error = new MongoExpiredSessionError ( ) ;
48- if ( typeof callback === 'function' ) {
49- callback ( error ) ;
50- return false ;
51- }
52-
53- throw error ;
54- }
55-
56- return true ;
57- }
58-
5945/** @public */
6046export interface ClientSessionOptions {
6147 /** Whether causal consistency should be enabled on this session */
@@ -89,6 +75,8 @@ const kSnapshotTime = Symbol('snapshotTime');
8975const kSnapshotEnabled = Symbol ( 'snapshotEnabled' ) ;
9076/** @internal */
9177const kPinnedConnection = Symbol ( 'pinnedConnection' ) ;
78+ /** @internal Accumulates total number of increments to add to txnNumber when applying session to command */
79+ const kTxnNumberIncrement = Symbol ( 'txnNumberIncrement' ) ;
9280
9381/** @public */
9482export interface EndSessionOptions {
@@ -123,13 +111,15 @@ export class ClientSession extends TypedEventEmitter<ClientSessionEvents> {
123111 defaultTransactionOptions : TransactionOptions ;
124112 transaction : Transaction ;
125113 /** @internal */
126- [ kServerSession ] ? : ServerSession ;
114+ [ kServerSession ] : ServerSession | null ;
127115 /** @internal */
128116 [ kSnapshotTime ] ?: Timestamp ;
129117 /** @internal */
130118 [ kSnapshotEnabled ] = false ;
131119 /** @internal */
132120 [ kPinnedConnection ] ?: Connection ;
121+ /** @internal */
122+ [ kTxnNumberIncrement ] : number ;
133123
134124 /**
135125 * Create a client session.
@@ -172,7 +162,10 @@ export class ClientSession extends TypedEventEmitter<ClientSessionEvents> {
172162 this . sessionPool = sessionPool ;
173163 this . hasEnded = false ;
174164 this . clientOptions = clientOptions ;
175- this [ kServerSession ] = undefined ;
165+
166+ this . explicit = ! ! options . explicit ;
167+ this [ kServerSession ] = this . explicit ? this . sessionPool . acquire ( ) : null ;
168+ this [ kTxnNumberIncrement ] = 0 ;
176169
177170 this . supports = {
178171 causalConsistency : options . snapshot !== true && options . causalConsistency !== false
@@ -181,24 +174,29 @@ export class ClientSession extends TypedEventEmitter<ClientSessionEvents> {
181174 this . clusterTime = options . initialClusterTime ;
182175
183176 this . operationTime = undefined ;
184- this . explicit = ! ! options . explicit ;
185177 this . owner = options . owner ;
186178 this . defaultTransactionOptions = Object . assign ( { } , options . defaultTransactionOptions ) ;
187179 this . transaction = new Transaction ( ) ;
188180 }
189181
190182 /** The server id associated with this session */
191183 get id ( ) : ServerSessionId | undefined {
192- return this . serverSession ?. id ;
184+ return this [ kServerSession ] ?. id ;
193185 }
194186
195187 get serverSession ( ) : ServerSession {
196- if ( this [ kServerSession ] == null ) {
197- this [ kServerSession ] = this . sessionPool . acquire ( ) ;
188+ let serverSession = this [ kServerSession ] ;
189+ if ( serverSession == null ) {
190+ if ( this . explicit ) {
191+ throw new MongoRuntimeError ( 'Unexpected null serverSession for an explicit session' ) ;
192+ }
193+ if ( this . hasEnded ) {
194+ throw new MongoRuntimeError ( 'Unexpected null serverSession for an ended implicit session' ) ;
195+ }
196+ serverSession = this . sessionPool . acquire ( ) ;
197+ this [ kServerSession ] = serverSession ;
198198 }
199-
200- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
201- return this [ kServerSession ] ! ;
199+ return serverSession ;
202200 }
203201
204202 /** Whether or not this session is configured for snapshot reads */
@@ -267,9 +265,15 @@ export class ClientSession extends TypedEventEmitter<ClientSessionEvents> {
267265 const completeEndSession = ( ) => {
268266 maybeClearPinnedConnection ( this , finalOptions ) ;
269267
270- // release the server session back to the pool
271- this . sessionPool . release ( this . serverSession ) ;
272- this [ kServerSession ] = undefined ;
268+ const serverSession = this [ kServerSession ] ;
269+ if ( serverSession != null ) {
270+ // release the server session back to the pool
271+ this . sessionPool . release ( serverSession ) ;
272+ // Make sure a new serverSession never makes it on to the ClientSession
273+ Object . defineProperty ( this , kServerSession , {
274+ value : ServerSession . clone ( serverSession )
275+ } ) ;
276+ }
273277
274278 // mark the session as ended, and emit a signal
275279 this . hasEnded = true ;
@@ -279,7 +283,9 @@ export class ClientSession extends TypedEventEmitter<ClientSessionEvents> {
279283 done ( ) ;
280284 } ;
281285
282- if ( this . serverSession && this . inTransaction ( ) ) {
286+ if ( this . inTransaction ( ) ) {
287+ // If we've reached endSession and the transaction is still active
288+ // by default we abort it
283289 this . abortTransaction ( err => {
284290 if ( err ) return done ( err ) ;
285291 completeEndSession ( ) ;
@@ -353,12 +359,16 @@ export class ClientSession extends TypedEventEmitter<ClientSessionEvents> {
353359 return this . id . id . buffer . equals ( session . id . id . buffer ) ;
354360 }
355361
356- /** Increment the transaction number on the internal ServerSession */
362+ /**
363+ * Increment the transaction number on the internal ServerSession
364+ *
365+ * @privateRemarks
366+ * This helper increments a value stored on the client session that will be
367+ * added to the serverSession's txnNumber upon applying it to a command.
368+ * This is because the serverSession is lazily acquired after a connection is obtained
369+ */
357370 incrementTransactionNumber ( ) : void {
358- if ( this . serverSession ) {
359- this . serverSession . txnNumber =
360- typeof this . serverSession . txnNumber === 'number' ? this . serverSession . txnNumber + 1 : 0 ;
361- }
371+ this [ kTxnNumberIncrement ] += 1 ;
362372 }
363373
364374 /** @returns whether this session is currently in a transaction or not */
@@ -376,7 +386,6 @@ export class ClientSession extends TypedEventEmitter<ClientSessionEvents> {
376386 throw new MongoCompatibilityError ( 'Transactions are not allowed with snapshot sessions' ) ;
377387 }
378388
379- assertAlive ( this ) ;
380389 if ( this . inTransaction ( ) ) {
381390 throw new MongoTransactionError ( 'Transaction already in progress' ) ;
382391 }
@@ -627,7 +636,7 @@ function attemptTransaction<TSchema>(
627636 throw err ;
628637 }
629638
630- if ( session . transaction . isActive ) {
639+ if ( session . inTransaction ( ) ) {
631640 return session . abortTransaction ( ) . then ( ( ) => maybeRetryOrThrow ( err ) ) ;
632641 }
633642
@@ -641,11 +650,6 @@ function endTransaction(
641650 commandName : 'abortTransaction' | 'commitTransaction' ,
642651 callback : Callback < Document >
643652) {
644- if ( ! assertAlive ( session , callback ) ) {
645- // checking result in case callback was called
646- return ;
647- }
648-
649653 // handle any initial problematic cases
650654 const txnState = session . transaction . state ;
651655
@@ -750,7 +754,6 @@ function endTransaction(
750754 callback ( error , result ) ;
751755 }
752756
753- // Assumption here that commandName is "commitTransaction" or "abortTransaction"
754757 if ( session . transaction . recoveryToken ) {
755758 command . recoveryToken = session . transaction . recoveryToken ;
756759 }
@@ -832,6 +835,30 @@ export class ServerSession {
832835
833836 return idleTimeMinutes > sessionTimeoutMinutes - 1 ;
834837 }
838+
839+ /**
840+ * @internal
841+ * Cloning meant to keep a readable reference to the server session data
842+ * after ClientSession has ended
843+ */
844+ static clone ( serverSession : ServerSession ) : Readonly < ServerSession > {
845+ const arrayBuffer = new ArrayBuffer ( 16 ) ;
846+ const idBytes = Buffer . from ( arrayBuffer ) ;
847+ idBytes . set ( serverSession . id . id . buffer ) ;
848+
849+ const id = new Binary ( idBytes , serverSession . id . id . sub_type ) ;
850+
851+ // Manual prototype construction to avoid modifying the constructor of this class
852+ return Object . setPrototypeOf (
853+ {
854+ id : { id } ,
855+ lastUse : serverSession . lastUse ,
856+ txnNumber : serverSession . txnNumber ,
857+ isDirty : serverSession . isDirty
858+ } ,
859+ ServerSession . prototype
860+ ) ;
861+ }
835862}
836863
837864/**
@@ -944,11 +971,11 @@ export function applySession(
944971 command : Document ,
945972 options : CommandOptions
946973) : MongoDriverError | undefined {
947- // TODO: merge this with `assertAlive`, did not want to throw a try/catch here
948974 if ( session . hasEnded ) {
949975 return new MongoExpiredSessionError ( ) ;
950976 }
951977
978+ // May acquire serverSession here
952979 const serverSession = session . serverSession ;
953980 if ( serverSession == null ) {
954981 return new MongoRuntimeError ( 'Unable to acquire server session' ) ;
@@ -966,15 +993,16 @@ export function applySession(
966993 serverSession . lastUse = now ( ) ;
967994 command . lsid = serverSession . id ;
968995
969- // first apply non-transaction-specific sessions data
970- const inTransaction = session . inTransaction ( ) || isTransactionCommand ( command ) ;
971- const isRetryableWrite = options ?. willRetryWrite || false ;
996+ const inTxnOrTxnCommand = session . inTransaction ( ) || isTransactionCommand ( command ) ;
997+ const isRetryableWrite = ! ! options . willRetryWrite ;
972998
973- if ( serverSession . txnNumber && ( isRetryableWrite || inTransaction ) ) {
999+ if ( isRetryableWrite || inTxnOrTxnCommand ) {
1000+ serverSession . txnNumber += session [ kTxnNumberIncrement ] ;
1001+ session [ kTxnNumberIncrement ] = 0 ;
9741002 command . txnNumber = Long . fromNumber ( serverSession . txnNumber ) ;
9751003 }
9761004
977- if ( ! inTransaction ) {
1005+ if ( ! inTxnOrTxnCommand ) {
9781006 if ( session . transaction . state !== TxnState . NO_TRANSACTION ) {
9791007 session . transaction . transition ( TxnState . NO_TRANSACTION ) ;
9801008 }
0 commit comments