@@ -6,14 +6,15 @@ import * as sinon from 'sinon';
66
77import {
88 type Collection ,
9+ MAX_RETRIES ,
910 type MongoClient ,
1011 MongoError ,
1112 MongoErrorLabel ,
1213 MongoServerError ,
1314 MongoWriteConcernError ,
1415 Server
1516} from '../../mongodb' ;
16- import { sleep } from '../../tools/utils' ;
17+ import { measureDuration , sleep } from '../../tools/utils' ;
1718
1819describe ( 'Retryable Writes Spec Prose' , ( ) => {
1920 describe ( '1. Test that retryable writes raise an exception when using the MMAPv1 storage engine.' , ( ) => {
@@ -551,5 +552,137 @@ describe('Retryable Writes Spec Prose', () => {
551552 expect ( insertResult . errorLabels ) . to . not . include ( MongoErrorLabel . NoWritesPerformed ) ;
552553 }
553554 ) ;
555+
556+ it (
557+ 'Case 4: Test that drivers set the maximum number of retries for all retryable write errors when an overload error is encountered' ,
558+ { requires : { topology : 'replicaset' , mongodb : '>=6.0' } } ,
559+ async ( ) => {
560+ // 2. Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and
561+ // `SystemOverloadedError` error labels:
562+ // ```javascript
563+ // {
564+ // configureFailPoint: "failCommand",
565+ // mode: {times: 1},
566+ // data: {
567+ // failCommands: ["insert"],
568+ // errorLabels: ["RetryableError", "SystemOverloadedError"],
569+ // errorCode: 91
570+ // }
571+ // }
572+ // ```
573+
574+ // 3. Via the command monitoring CommandFailedEvent, configure a fail point with error code `91` (ShutdownInProgress) and
575+ // the `RetryableWriteError` and `RetryableError` labels:
576+ // ```javascript
577+ // {
578+ // configureFailPoint: "failCommand",
579+ // mode: "alwaysOn",
580+ // data: {
581+ // failCommands: ["insert"],
582+ // errorLabels: ["RetryableError", "RetryableWriteError"],
583+ // errorCode: 91
584+ // }
585+ // }
586+ // ```
587+ // Configure the second fail point command only if the failed event is for the first error configured in step 2.
588+ const serverCommandStub = sinon
589+ . stub ( Server . prototype , 'command' )
590+ . callsFake ( async function ( ) {
591+ // First call: error WITH SystemOverloadedError
592+ // Subsequent calls: error WITHOUT SystemOverloadedError (but still retryable)
593+ const errorLabels =
594+ serverCommandStub . callCount === 1
595+ ? [ MongoErrorLabel . RetryableError , MongoErrorLabel . SystemOverloadedError ]
596+ : [ MongoErrorLabel . RetryableError , MongoErrorLabel . RetryableWriteError ] ;
597+
598+ throw new MongoServerError ( {
599+ message : 'Server Error' ,
600+ errorLabels,
601+ code : 91 ,
602+ ok : 0
603+ } ) ;
604+ } ) ;
605+
606+ // 4. Attempt an `insertOne` operation on any record for any database and collection. Expect the `insertOne` to fail with a
607+ // server error. Assert that `MAX_RETRIES + 1` attempts were made.
608+ const insertResult = await collection . insertOne ( { _id : 1 } ) . catch ( error => error ) ;
609+
610+ expect ( insertResult ) . to . be . instanceOf ( MongoServerError ) ;
611+ expect ( serverCommandStub . callCount ) . to . equal ( MAX_RETRIES + 1 ) ;
612+ }
613+ ) ;
614+
615+ it (
616+ 'Case 5: Test that drivers do not apply backoff to non-overload errors' ,
617+ { requires : { topology : 'replicaset' , mongodb : '>=6.0' } } ,
618+ async function ( ) {
619+ // Configure the random number generator used for jitter to always return a number as close as possible to `1`.
620+ const stub = sinon . stub ( Math , 'random' ) ;
621+ stub . returns ( 0.99 ) ;
622+
623+ // 2. Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and
624+ // `SystemOverloadedError` error labels:
625+ // ```javascript
626+ // {
627+ // configureFailPoint: "failCommand",
628+ // mode: {times: 1},
629+ // data: {
630+ // failCommands: ["insert"],
631+ // errorLabels: ["RetryableError", "SystemOverloadedError"],
632+ // errorCode: 91
633+ // }
634+ // }
635+ // ```
636+
637+ // 3. Via the command monitoring CommandFailedEvent, configure a fail point with error code `91` (ShutdownInProgress) and
638+ // the `RetryableWriteError` and `RetryableError` labels:
639+ // ```javascript
640+ // {
641+ // configureFailPoint: "failCommand",
642+ // mode: "alwaysOn",
643+ // data: {
644+ // failCommands: ["insert"],
645+ // errorLabels: ["RetryableError", "RetryableWriteError"],
646+ // errorCode: 91
647+ // }
648+ // }
649+ // ```
650+ // Configure the second fail point command only if the failed event is for the first error configured in step 2.
651+ const serverCommandStub = sinon
652+ . stub ( Server . prototype , 'command' )
653+ . callsFake ( async function ( ) {
654+ // First call: error WITH SystemOverloadedError
655+ // Subsequent calls: error WITHOUT SystemOverloadedError (but still retryable)
656+ const errorLabels =
657+ serverCommandStub . callCount === 1
658+ ? [ MongoErrorLabel . RetryableError , MongoErrorLabel . SystemOverloadedError ]
659+ : [ MongoErrorLabel . RetryableError , MongoErrorLabel . RetryableWriteError ] ;
660+
661+ throw new MongoServerError ( {
662+ message : 'Server Error' ,
663+ errorLabels,
664+ code : 91 ,
665+ ok : 0
666+ } ) ;
667+ } ) ;
668+
669+ // 4. Attempt an `insertOne` operation on any record for any database and collection. Expect the `insertOne` to fail with a
670+ // server error. Assert that backoff was applied only once for the initial overload error and not for the subsequent
671+ // non-overload retryable errors.
672+ const { duration } = await measureDuration ( async ( ) => {
673+ const insertResult = await collection . insertOne ( { _id : 1 } ) . catch ( error => error ) ;
674+ expect ( insertResult ) . to . be . instanceOf ( MongoServerError ) ;
675+ } ) ;
676+
677+ // The expected backoff for the first (overload) error is: Math.random() * Math.min(10000, 100 * 2^0)
678+ // With Math.random() = 0.99, this gives us: 0.99 * 100 = 99ms
679+ // Subsequent errors are non-overload, so they should have NO backoff applied.
680+ // We add a margin for test execution overhead.
681+ const expectedMinBackoff = 99 ; // First backoff
682+ const expectedMaxBackoff = expectedMinBackoff + 1000 ; // Allow 1 second margin for test overhead
683+
684+ expect ( duration ) . to . be . within ( expectedMinBackoff , expectedMaxBackoff ) ;
685+ }
686+ ) ;
554687 } ) ;
555688} ) ;
0 commit comments