@@ -21,13 +21,14 @@ import {
21
21
MongoInvalidArgumentError ,
22
22
MongoMissingCredentialsError ,
23
23
MongoNetworkError ,
24
+ MongoOperationTimeoutError ,
24
25
MongoRuntimeError ,
25
26
MongoServerError
26
27
} from '../error' ;
27
28
import { CancellationToken , TypedEventEmitter } from '../mongo_types' ;
28
29
import type { Server } from '../sdam/server' ;
29
30
import { Timeout , TimeoutError } from '../timeout' ;
30
- import { type Callback , List , makeCounter , now , promiseWithResolvers } from '../utils' ;
31
+ import { type Callback , csotMin , List , makeCounter , promiseWithResolvers } from '../utils' ;
31
32
import { connect } from './connect' ;
32
33
import { Connection , type ConnectionEvents , type ConnectionOptions } from './connection' ;
33
34
import {
@@ -102,7 +103,6 @@ export interface ConnectionPoolOptions extends Omit<ConnectionOptions, 'id' | 'g
102
103
export interface WaitQueueMember {
103
104
resolve : ( conn : Connection ) => void ;
104
105
reject : ( err : AnyError ) => void ;
105
- timeout : Timeout ;
106
106
[ kCancelled ] ?: boolean ;
107
107
checkoutTime : number ;
108
108
}
@@ -355,37 +355,57 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
355
355
* will be held by the pool. This means that if a connection is checked out it MUST be checked back in or
356
356
* explicitly destroyed by the new owner.
357
357
*/
358
- async checkOut ( ) : Promise < Connection > {
359
- const checkoutTime = now ( ) ;
358
+ async checkOut ( options ?: { timeout ?: Timeout } ) : Promise < Connection > {
360
359
this . emitAndLog (
361
360
ConnectionPool . CONNECTION_CHECK_OUT_STARTED ,
362
361
new ConnectionCheckOutStartedEvent ( this )
363
362
) ;
364
363
365
364
const waitQueueTimeoutMS = this . options . waitQueueTimeoutMS ;
365
+ const serverSelectionTimeoutMS = this [ kServer ] . topology . s . serverSelectionTimeoutMS ;
366
366
367
367
const { promise, resolve, reject } = promiseWithResolvers < Connection > ( ) ;
368
368
369
- const timeout = Timeout . expires ( waitQueueTimeoutMS ) ;
369
+ let timeout : Timeout | null = null ;
370
+ if ( options ?. timeout ) {
371
+ // CSOT enabled
372
+ // Determine if we're using the timeout passed in or a new timeout
373
+ if ( options . timeout . duration > 0 || serverSelectionTimeoutMS > 0 ) {
374
+ // This check determines whether or not Topology.selectServer used the configured
375
+ // `timeoutMS` or `serverSelectionTimeoutMS` value for its timeout
376
+ if (
377
+ options . timeout . duration === serverSelectionTimeoutMS ||
378
+ csotMin ( options . timeout . duration , serverSelectionTimeoutMS ) < serverSelectionTimeoutMS
379
+ ) {
380
+ // server selection used `timeoutMS`, so we should use the existing timeout as the timeout
381
+ // here
382
+ timeout = options . timeout ;
383
+ } else {
384
+ // server selection used `serverSelectionTimeoutMS`, so we construct a new timeout with
385
+ // the time remaining to ensure that Topology.selectServer and ConnectionPool.checkOut
386
+ // cumulatively don't spend more than `serverSelectionTimeoutMS` blocking
387
+ timeout = Timeout . expires ( serverSelectionTimeoutMS - options . timeout . timeElapsed ) ;
388
+ }
389
+ }
390
+ } else {
391
+ timeout = Timeout . expires ( waitQueueTimeoutMS ) ;
392
+ }
370
393
371
394
const waitQueueMember : WaitQueueMember = {
372
395
resolve,
373
- reject,
374
- timeout,
375
- checkoutTime
396
+ reject
376
397
} ;
377
398
378
399
this [ kWaitQueue ] . push ( waitQueueMember ) ;
379
400
process . nextTick ( ( ) => this . processWaitQueue ( ) ) ;
380
401
381
402
try {
382
- return await Promise . race ( [ promise , waitQueueMember . timeout ] ) ;
403
+ timeout ?. throwIfExpired ( ) ;
404
+ return await ( timeout ? Promise . race ( [ promise , timeout ] ) : promise ) ;
383
405
} catch ( error ) {
384
406
if ( TimeoutError . is ( error ) ) {
385
407
waitQueueMember [ kCancelled ] = true ;
386
408
387
- waitQueueMember . timeout . clear ( ) ;
388
-
389
409
this . emitAndLog (
390
410
ConnectionPool . CONNECTION_CHECK_OUT_FAILED ,
391
411
new ConnectionCheckOutFailedEvent ( this , 'timeout' , waitQueueMember . checkoutTime )
@@ -396,9 +416,16 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
396
416
: 'Timed out while checking out a connection from connection pool' ,
397
417
this . address
398
418
) ;
419
+ if ( options ?. timeout ) {
420
+ throw new MongoOperationTimeoutError ( 'Timed out during connection checkout' , {
421
+ cause : timeoutError
422
+ } ) ;
423
+ }
399
424
throw timeoutError ;
400
425
}
401
426
throw error ;
427
+ } finally {
428
+ if ( timeout !== options ?. timeout ) timeout ?. clear ( ) ;
402
429
}
403
430
}
404
431
@@ -764,7 +791,6 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
764
791
ConnectionPool . CONNECTION_CHECK_OUT_FAILED ,
765
792
new ConnectionCheckOutFailedEvent ( this , reason , waitQueueMember . checkoutTime , error )
766
793
) ;
767
- waitQueueMember . timeout . clear ( ) ;
768
794
this [ kWaitQueue ] . shift ( ) ;
769
795
waitQueueMember . reject ( error ) ;
770
796
continue ;
@@ -785,7 +811,6 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
785
811
ConnectionPool . CONNECTION_CHECKED_OUT ,
786
812
new ConnectionCheckedOutEvent ( this , connection , waitQueueMember . checkoutTime )
787
813
) ;
788
- waitQueueMember . timeout . clear ( ) ;
789
814
790
815
this [ kWaitQueue ] . shift ( ) ;
791
816
waitQueueMember . resolve ( connection ) ;
@@ -828,8 +853,6 @@ export class ConnectionPool extends TypedEventEmitter<ConnectionPoolEvents> {
828
853
) ;
829
854
waitQueueMember . resolve ( connection ) ;
830
855
}
831
-
832
- waitQueueMember . timeout . clear ( ) ;
833
856
}
834
857
process . nextTick ( ( ) => this . processWaitQueue ( ) ) ;
835
858
} ) ;
0 commit comments