11/* eslint-disable @typescript-eslint/no-non-null-assertion */
22import { expect } from 'chai' ;
33import * as sinon from 'sinon' ;
4+ import * as timersPromises from 'timers/promises' ;
45
56import {
67 type Collection ,
78 type CommandFailedEvent ,
89 type CommandSucceededEvent ,
910 type MongoClient ,
1011 MongoErrorLabel ,
11- MongoServerError ,
12- Server
12+ MongoServerError
1313} from '../../mongodb' ;
14- import { measureDuration } from '../../tools/utils' ;
1514import { filterForCommands } from '../shared' ;
1615
1716describe ( 'Retryable Reads Spec Prose' , ( ) => {
@@ -361,51 +360,66 @@ describe('Retryable Reads Spec Prose', () => {
361360 const TEST_METADATA : MongoDBMetadataUI = {
362361 requires : { mongodb : '>=4.4' }
363362 } ;
364-
365- let client : MongoClient ;
363+ const APP_NAME = 'retryable-reads-prose-4' ;
366364
367365 beforeEach ( async function ( ) {
368366 // 1. Create a client.
369367 client = this . configuration . newClient ( {
370368 monitorCommands : true ,
371- retryReads : true
369+ retryReads : true ,
370+ appName : APP_NAME
372371 } ) ;
373372 await client . connect ( ) ;
374373 } ) ;
375374
376- afterEach ( async function ( ) {
377- sinon . restore ( ) ;
378- await client ?. close ( ) ;
375+ afterEach ( async ( ) => {
376+ await client
377+ ?. db ( 'admin' )
378+ . command ( { configureFailPoint : 'failCommand' , mode : 'off' } )
379+ . catch ( ( ) => null ) ;
379380 } ) ;
380381
381382 it (
382383 'should retry MAX_RETRIES times for all retryable errors after encountering an overload error' ,
383384 TEST_METADATA ,
384385 async ( ) => {
385386 // 2. Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and
386- // `SystemOverloadedError` error labels:
387-
388- // 3. Via the command monitoring CommandFailedEvent, configure a fail point with error code `91` (ShutdownInProgress) and
389- // the `RetryableError` label:
390-
391- // We use mocking to simulate the failpoint sequence:
392- // - First call: error WITH SystemOverloadedError
393- // - Subsequent calls: error WITHOUT SystemOverloadedError (but still retryable)
394- const serverCommandStub = sinon
395- . stub ( Server . prototype , 'command' )
396- . callsFake ( async function ( ) {
397- const errorLabels =
398- serverCommandStub . callCount === 1
399- ? [ MongoErrorLabel . RetryableError , MongoErrorLabel . SystemOverloadedError ]
400- : [ MongoErrorLabel . RetryableError ] ;
401-
402- throw new MongoServerError ( {
403- message : 'Server Error' ,
404- errorLabels,
405- code : 91 ,
406- ok : 0
407- } ) ;
387+ // `SystemOverloadedError` error labels.
388+ await client . db ( 'admin' ) . command ( {
389+ configureFailPoint : 'failCommand' ,
390+ mode : { times : 1 } ,
391+ data : {
392+ failCommands : [ 'find' ] ,
393+ errorLabels : [ 'RetryableError' , 'SystemOverloadedError' ] ,
394+ errorCode : 91 ,
395+ appName : APP_NAME
396+ }
397+ } ) ;
398+
399+ // 3. Via the command monitoring CommandFailedEvent, configure a fail point with error code `91`
400+ // (ShutdownInProgress) and the `RetryableError` label. Configure the second fail point command
401+ // only if the failed event is for the first error configured in step 2.
402+ let secondFailpointConfigured = false ;
403+ client . on ( 'commandFailed' , async ( event : CommandFailedEvent ) => {
404+ if ( secondFailpointConfigured ) return ;
405+ if ( event . commandName !== 'find' ) return ;
406+ secondFailpointConfigured = true ;
407+ await client . db ( 'admin' ) . command ( {
408+ configureFailPoint : 'failCommand' ,
409+ mode : 'alwaysOn' ,
410+ data : {
411+ failCommands : [ 'find' ] ,
412+ errorLabels : [ 'RetryableError' ] ,
413+ errorCode : 91 ,
414+ appName : APP_NAME
415+ }
408416 } ) ;
417+ } ) ;
418+
419+ const findStartedEvents : Array < Record < string , any > > = [ ] ;
420+ client . on ( 'commandStarted' , ev => {
421+ if ( ev . commandName === 'find' ) findStartedEvents . push ( ev ) ;
422+ } ) ;
409423
410424 // 4. Attempt a `findOne` operation on any record for any database and collection. Expect the `findOne` to fail with a
411425 // server error. Assert that `MAX_RETRIES + 1` attempts were made.
@@ -419,7 +433,9 @@ describe('Retryable Reads Spec Prose', () => {
419433 expect ( error . code ) . to . equal ( 91 ) ;
420434 expect ( error . hasErrorLabel ( MongoErrorLabel . RetryableError ) ) . to . be . true ;
421435 // MAX_RETRIES + 1 (default maxAdaptiveRetries is 2).
422- expect ( serverCommandStub . callCount ) . to . equal ( 3 ) ;
436+ expect ( findStartedEvents ) . to . have . lengthOf ( 3 ) ;
437+
438+ // 5. Disable the fail point — handled by the surrounding afterEach.
423439 }
424440 ) ;
425441 } ) ;
@@ -431,82 +447,92 @@ describe('Retryable Reads Spec Prose', () => {
431447 const TEST_METADATA : MongoDBMetadataUI = {
432448 requires : { mongodb : '>=4.4' }
433449 } ;
434-
435- let client : MongoClient ;
450+ const APP_NAME = 'retryable-reads-prose-5' ;
436451
437452 beforeEach ( async function ( ) {
438453 // 1. Create a client.
439454 client = this . configuration . newClient ( {
440455 monitorCommands : true ,
441- retryReads : true
456+ retryReads : true ,
457+ appName : APP_NAME
442458 } ) ;
443459 await client . connect ( ) ;
444460 } ) ;
445461
446- afterEach ( async function ( ) {
462+ afterEach ( async ( ) => {
447463 sinon . restore ( ) ;
448- await client ?. close ( ) ;
464+ await client
465+ ?. db ( 'admin' )
466+ . command ( { configureFailPoint : 'failCommand' , mode : 'off' } )
467+ . catch ( ( ) => null ) ;
449468 } ) ;
450469
451470 it (
452471 'should apply backoff only once for the initial overload error and not for subsequent non-overload retryable errors' ,
453472 TEST_METADATA ,
454473 async function ( ) {
455- // Configure the random number generator used for jitter to always return a number as close as possible to `1`.
456- const randomStub = sinon . stub ( Math , 'random' ) ;
457- randomStub . returns ( 0.99 ) ;
474+ // Spy on `timers/promises.setTimeout` — the only sleep on the retry path
475+ // (src/operations/execute_operation.ts:337) — to count how many times backoff was applied.
476+ // We use a spy (not a stub) so the real sleep still happens, giving the commandFailed
477+ // listener below time to configure the second failpoint before the driver dispatches its
478+ // next retry.
479+ const setTimeoutSpy = sinon . spy ( timersPromises , 'setTimeout' ) ;
458480
459481 // 2. Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and
460- // `SystemOverloadedError` error labels:
461-
462- // 3. Via the command monitoring CommandFailedEvent, configure a fail point with error code `91` (ShutdownInProgress) and
463- // the `RetryableError` label:
464-
465- // We use mocking to simulate the failpoint sequence:
466- // - First call: error WITH SystemOverloadedError
467- // - Subsequent calls: error WITHOUT SystemOverloadedError (but still retryable)
468- const serverCommandStub = sinon
469- . stub ( Server . prototype , 'command' )
470- . callsFake ( async function ( ) {
471- const errorLabels =
472- serverCommandStub . callCount === 1
473- ? [ MongoErrorLabel . RetryableError , MongoErrorLabel . SystemOverloadedError ]
474- : [ MongoErrorLabel . RetryableError ] ;
475-
476- throw new MongoServerError ( {
477- message : 'Server Error' ,
478- errorLabels,
479- code : 91 ,
480- ok : 0
481- } ) ;
482+ // `SystemOverloadedError` error labels.
483+ await client . db ( 'admin' ) . command ( {
484+ configureFailPoint : 'failCommand' ,
485+ mode : { times : 1 } ,
486+ data : {
487+ failCommands : [ 'find' ] ,
488+ errorLabels : [ 'RetryableError' , 'SystemOverloadedError' ] ,
489+ errorCode : 91 ,
490+ appName : APP_NAME
491+ }
492+ } ) ;
493+
494+ // 3. Via the command monitoring CommandFailedEvent, configure a fail point with error code `91`
495+ // (ShutdownInProgress) and the `RetryableError` label. Configure the second fail point command
496+ // only if the failed event is for the first error configured in step 2.
497+ let secondFailpointConfigured = false ;
498+ client . on ( 'commandFailed' , async ( event : CommandFailedEvent ) => {
499+ if ( secondFailpointConfigured ) return ;
500+ if ( event . commandName !== 'find' ) return ;
501+ secondFailpointConfigured = true ;
502+ await client . db ( 'admin' ) . command ( {
503+ configureFailPoint : 'failCommand' ,
504+ mode : 'alwaysOn' ,
505+ data : {
506+ failCommands : [ 'find' ] ,
507+ errorLabels : [ 'RetryableError' ] ,
508+ errorCode : 91 ,
509+ appName : APP_NAME
510+ }
482511 } ) ;
512+ } ) ;
513+
514+ const findStartedEvents : Array < Record < string , any > > = [ ] ;
515+ client . on ( 'commandStarted' , ev => {
516+ if ( ev . commandName === 'find' ) findStartedEvents . push ( ev ) ;
517+ } ) ;
483518
484519 // 4. Attempt a `findOne` operation on any record for any database and collection. Expect the `findOne` to fail with a
485520 // server error. Assert that backoff was applied only once for the initial overload error and not for the subsequent
486521 // non-overload retryable errors.
487- const { duration } = await measureDuration ( async ( ) => {
488- const error = await client
489- . db ( 'test' )
490- . collection ( 'test' )
491- . findOne ( { } )
492- . catch ( e => e ) ;
493- expect ( error ) . to . be . instanceOf ( MongoServerError ) ;
494- expect ( error . code ) . to . equal ( 91 ) ;
495- } ) ;
496-
497- // Ensure the full retry sequence executed (i.e. the test really exercised the
498- // post-overload non-overload retries, not just bailed after the first attempt).
499- // MAX_RETRIES + 1 (default maxAdaptiveRetries is 2).
500- expect ( serverCommandStub . callCount ) . to . equal ( 3 ) ;
522+ const error = await client
523+ . db ( 'test' )
524+ . collection ( 'test' )
525+ . findOne ( { } )
526+ . catch ( e => e ) ;
501527
502- // The expected backoff for the first (overload) error is: Math.random() * Math.min(10000, 100 * 2^0)
503- // With Math.random() = 0.99, this gives ~99ms. `measureDuration` uses `Math.floor(performance.now())`
504- // on both ends and setTimeout can fire a couple ms early, so we allow slack on the lower bound .
505- // If the driver incorrectly applied backoff to all retries, total would be 0.99*(100+200) = ~297ms.
506- const expectedMinBackoff = 90 ; // First backoff
507- const expectedMaxBackoff = expectedMinBackoff + 1000 ; // Allow 1 second margin for test overhead
528+ expect ( error ) . to . be . instanceOf ( MongoServerError ) ;
529+ expect ( error . code ) . to . equal ( 91 ) ;
530+ // MAX_RETRIES + 1 (default maxAdaptiveRetries is 2) — the full retry sequence ran .
531+ expect ( findStartedEvents ) . to . have . lengthOf ( 3 ) ;
532+ // Backoff was applied exactly once — for the initial overload error only.
533+ expect ( setTimeoutSpy . callCount ) . to . equal ( 1 ) ;
508534
509- expect ( duration ) . to . be . within ( expectedMinBackoff , expectedMaxBackoff ) ;
535+ // 5. Disable the fail point — handled by the surrounding afterEach.
510536 }
511537 ) ;
512538 } ) ;
0 commit comments