@@ -346,21 +346,21 @@ describe('Retryable Writes Spec Prose', () => {
346346 ) ;
347347 } ) ;
348348
349- describe ( '6. Test that drivers return the original error after encountering multiple WriteConcernErrors with a RetryableWriteError label' , ( ) => {
349+ describe ( '6. Test error propagation after encountering multiple errors' , ( ) => {
350+ // These tests MUST:
351+ // - be implemented by any driver that implements the Command Monitoring specification.
352+ // - only run against replica sets as mongos does not propagate the NoWritesPerformed label to the drivers.
353+ // - be run against server versions 6.0 and above.
354+ // - be implemented by any driver that has implemented the Client Backpressure specification.
355+
356+ // Additionally, this test requires drivers to set a fail point after an `insertOne` operation but before the subsequent
357+ // retry. Drivers that are unable to set a failCommand after the CommandFailedEvent SHOULD use mocking or write a unit test
358+ // to cover the same sequence of events.
359+
350360 let client : MongoClient ;
351361 let collection : Collection < { _id : 1 } > ;
352362
353363 beforeEach ( async function ( ) {
354- // This test MUST:
355- // - be implemented by any driver that implements the Command Monitoring specification,
356- // - only run against replica sets as mongos does not propagate the NoWritesPerformed label to the drivers.
357- // - be run against server versions 6.0 and above.
358- // - be implemented by any driver that has implemented the Client Backpressure specification.
359-
360- // Additionally, this test requires drivers to set a fail point after an `insertOne` operation but before the subsequent
361- // retry. Drivers that are unable to set a failCommand after the CommandFailedEvent SHOULD use mocking or write a unit test
362- // to cover the same sequence of events.
363-
364364 // 1. Create a client with `retryWrites=true`.
365365 client = this . configuration . newClient ( { monitorCommands : true , retryWrites : true } ) ;
366366 await client
@@ -372,26 +372,68 @@ describe('Retryable Writes Spec Prose', () => {
372372 } ) ;
373373
374374 afterEach ( async function ( ) {
375- // 5. Disable the fail point:
376- // ```javascript
377- // {
378- // configureFailPoint: "failCommand",
379- // mode: "off"
380- // }
381- // ```
382-
383- // (we don't use a failPoint, so we use sinon.restore instead)
375+ // 5. Disable the fail point (we don't use a failPoint, so we use sinon.restore instead)
384376 sinon . restore ( ) ;
385377 await client . close ( ) ;
386378 } ) ;
387379
388380 it (
389- 'when a retry attempt fails with an error labeled NoWritesPerformed, drivers MUST return the original error ' ,
390- { requires : { topology : 'replicaset' , mongodb : '>=4.2.9 ' } } ,
381+ 'Case 1: Test that drivers return the correct error when receiving only errors without NoWritesPerformed ' ,
382+ { requires : { topology : 'replicaset' , mongodb : '>=6.0 ' } } ,
391383 async ( ) => {
392384 // 2. Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and
393385 // `SystemOverloadedError` error labels:
386+ // ```javascript
387+ // {
388+ // configureFailPoint: "failCommand",
389+ // mode: {times: 1},
390+ // data: {
391+ // failCommands: ["insert"],
392+ // errorLabels: ["RetryableError", "SystemOverloadedError"],
393+ // errorCode: 91
394+ // }
395+ // }
396+ // ```
397+
398+ // 3. Via the command monitoring CommandFailedEvent, configure a fail point with error code `10107` (NotWritablePrimary):
399+ // ```javascript
400+ // {
401+ // configureFailPoint: "failCommand",
402+ // mode: "alwaysOn",
403+ // data: {
404+ // failCommands: ["insert"],
405+ // errorCode: 10107,
406+ // errorLabels: ["RetryableError", "SystemOverloadedError"]
407+ // }
408+ // }
409+ // ```
410+ // Configure the `10107` fail point command only if the the failed event is for the `91` error configured in step 2.
411+ const serverCommandStub = sinon
412+ . stub ( Server . prototype , 'command' )
413+ . callsFake ( async function ( ) {
414+ throw new MongoServerError ( {
415+ message : 'Server Error' ,
416+ errorLabels : [ MongoErrorLabel . RetryableError , MongoErrorLabel . SystemOverloadedError ] ,
417+ code : serverCommandStub . callCount === 1 ? 91 : 10107 ,
418+ ok : 0
419+ } ) ;
420+ } ) ;
421+
422+ // 4. Attempt an `insertOne` operation on any record for any database and collection. Expect the `insertOne` to fail with a
423+ // server error. Assert that the error code of the server error is `10107`.
424+ const insertResult = await collection . insertOne ( { _id : 1 } ) . catch ( error => error ) ;
394425
426+ expect ( insertResult ) . to . be . instanceOf ( MongoServerError ) ;
427+ expect ( insertResult ) . to . have . property ( 'code' , 10107 ) ;
428+ }
429+ ) ;
430+
431+ it (
432+ 'Case 2: Test that drivers return the correct error when receiving only errors with NoWritesPerformed' ,
433+ { requires : { topology : 'replicaset' , mongodb : '>=6.0' } } ,
434+ async ( ) => {
435+ // 2. Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError`,
436+ // `SystemOverloadedError`, and `NoWritesPerformed` error labels:
395437 // ```javascript
396438 // {
397439 // configureFailPoint: "failCommand",
@@ -406,21 +448,18 @@ describe('Retryable Writes Spec Prose', () => {
406448
407449 // 3. Via the command monitoring CommandFailedEvent, configure a fail point with error code `10107` (NotWritablePrimary)
408450 // and a NoWritesPerformed label:
409-
410451 // ```javascript
411452 // {
412453 // configureFailPoint: "failCommand",
413454 // mode: "alwaysOn",
414455 // data: {
415456 // failCommands: ["insert"],
416457 // errorCode: 10107,
417- // errorLabels: ["RetryableError", "SystemOverloadedError", , "NoWritesPerformed"]
458+ // errorLabels: ["RetryableError", "SystemOverloadedError", "NoWritesPerformed"]
418459 // }
419460 // }
420461 // ```
421-
422- // Drivers SHOULD only configure the `10107` fail point command if the the failed event is for the `91` error configured
423- // in step 2.
462+ // Configure the `10107` fail point command only if the the failed event is for the `91` error configured in step 2.
424463 const serverCommandStub = sinon
425464 . stub ( Server . prototype , 'command' )
426465 . callsFake ( async function ( ) {
@@ -436,14 +475,78 @@ describe('Retryable Writes Spec Prose', () => {
436475 } ) ;
437476 } ) ;
438477
478+ // 4. Attempt an `insertOne` operation on any record for any database and collection. Expect the `insertOne` to fail with a
479+ // server error. Assert that the error code of the server error is 91.
439480 const insertResult = await collection . insertOne ( { _id : 1 } ) . catch ( error => error ) ;
440481
441482 expect ( serverCommandStub . callCount ) . to . equal ( 6 ) ;
442483
484+ expect ( insertResult ) . to . be . instanceOf ( MongoServerError ) ;
485+ expect ( insertResult ) . to . have . property ( 'code' , 91 ) ;
486+ }
487+ ) ;
488+
489+ it (
490+ 'Case 3: Test that drivers return the correct error when receiving some errors with NoWritesPerformed and some without NoWritesPerformed' ,
491+ { requires : { topology : 'replicaset' , mongodb : '>=6.0' } } ,
492+ async ( ) => {
493+ // 2. Configure the client to listen to CommandFailedEvents. In the attached listener, configure a fail point with error
494+ // code `91` (NotWritablePrimary) and the `NoWritesPerformed`, `RetryableError` and `SystemOverloadedError` labels:
495+ // ```javascript
496+ // {
497+ // configureFailPoint: "failCommand",
498+ // mode: "alwaysOn",
499+ // data: {
500+ // failCommands: ["insert"],
501+ // errorLabels: ["RetryableError", "SystemOverloadedError", "NoWritesPerformed"],
502+ // errorCode: 91
503+ // }
504+ // }
505+ // ```
506+
507+ // 3. Configure a fail point with error code `91` (ShutdownInProgress) with the `RetryableError` and
508+ // `SystemOverloadedError` error labels but without the `NoWritesPerformed` error label:
509+ // ```javascript
510+ // {
511+ // configureFailPoint: "failCommand",
512+ // mode: {times: 1},
513+ // data: {
514+ // failCommands: ["insert"],
515+ // errorLabels: ["RetryableError", "SystemOverloadedError"],
516+ // errorCode: 91
517+ // }
518+ // }
519+ // ```
520+ const serverCommandStub = sinon
521+ . stub ( Server . prototype , 'command' )
522+ . callsFake ( async function ( ) {
523+ // First call: error WITHOUT NoWritesPerformed
524+ // Subsequent calls: error WITH NoWritesPerformed
525+ const errorLabels =
526+ serverCommandStub . callCount === 1
527+ ? [ MongoErrorLabel . RetryableError , MongoErrorLabel . SystemOverloadedError ]
528+ : [
529+ MongoErrorLabel . RetryableError ,
530+ MongoErrorLabel . SystemOverloadedError ,
531+ MongoErrorLabel . NoWritesPerformed
532+ ] ;
533+
534+ throw new MongoServerError ( {
535+ message : 'Server Error' ,
536+ errorLabels,
537+ code : 91 ,
538+ ok : 0
539+ } ) ;
540+ } ) ;
541+
443542 // 4. Attempt an `insertOne` operation on any record for any database and collection. Expect the `insertOne` to fail with a
444- // server error. Assert that the error code of the server error is 91.
543+ // server error. Assert that the error code of the server error is 91. Assert that the error does not contain the
544+ // error label `NoWritesPerformed`.
545+ const insertResult = await collection . insertOne ( { _id : 1 } ) . catch ( error => error ) ;
546+
445547 expect ( insertResult ) . to . be . instanceOf ( MongoServerError ) ;
446548 expect ( insertResult ) . to . have . property ( 'code' , 91 ) ;
549+ expect ( insertResult . errorLabels ) . to . not . include ( MongoErrorLabel . NoWritesPerformed ) ;
447550 }
448551 ) ;
449552 } ) ;
0 commit comments