@@ -5,10 +5,13 @@ import * as sinon from 'sinon';
55import {
66 type ClientSession ,
77 type Collection ,
8+ type CommandStartedEvent ,
9+ CountOperation ,
810 type Db ,
911 type FindCursor ,
1012 LEGACY_HELLO_COMMAND ,
11- type MongoClient ,
13+ MongoClient ,
14+ MongoError ,
1215 MongoOperationTimeoutError
1316} from '../../mongodb' ;
1417
@@ -166,4 +169,208 @@ describe('CSOT driver tests', () => {
166169 } ) ;
167170 } ) ;
168171 } ) ;
172+
173+ describe ( 'retryable reads' , ( ) => {
174+ let configClient : MongoClient ;
175+ let client : MongoClient ;
176+ const FAIL_COMMAND = {
177+ configureFailPoint : 'failCommand' ,
178+ mode : 'alwaysOn' ,
179+ data : {
180+ failCommands : [ 'count' ] ,
181+ errorCode : 10107 ,
182+ closeConnection : false
183+ }
184+ } ;
185+
186+ const DISABLE_FAIL_COMMAND = {
187+ configureFailPoint : 'failCommand' ,
188+ mode : 'off' ,
189+ data : {
190+ failCommands : [ 'count' ] ,
191+ errorCode : 10107 ,
192+ closeConnection : false
193+ }
194+ } ;
195+
196+ beforeEach ( async function ( ) {
197+ configClient = new MongoClient ( this . configuration . url ( ) ) ;
198+ await configClient . db ( ) . admin ( ) . command ( FAIL_COMMAND ) ;
199+ } ) ;
200+
201+ afterEach ( async function ( ) {
202+ await configClient . db ( ) . admin ( ) . command ( DISABLE_FAIL_COMMAND ) ;
203+ await configClient . close ( ) ;
204+
205+ await client . close ( ) ;
206+ } ) ;
207+
208+ context ( 'when timeoutMS is undefined and retryable operation fails' , ( ) => {
209+ const commandStartedEvents = [ ] ;
210+ let maybeErr : Error | null ;
211+
212+ beforeEach ( async function ( ) {
213+ client = this . configuration . newClient ( this . configuration . url ( ) , {
214+ timeoutMS : undefined ,
215+ monitorCommands : true
216+ } ) ;
217+
218+ client . on ( 'commandStarted' , ( ev : CommandStartedEvent ) => {
219+ if ( Object . hasOwn ( ev . command , 'count' ) ) commandStartedEvents . push ( ev . command ) ;
220+ } ) ;
221+ maybeErr = await client
222+ . db ( 'test' )
223+ . collection ( 'test' )
224+ . count ( )
225+ . then (
226+ ( ) => null ,
227+ e => e
228+ ) ;
229+ } ) ;
230+
231+ it ( 'makes exactly two total attempts and throws an error' , async function ( ) {
232+ expect ( maybeErr ) . to . be . instanceof ( MongoError ) ;
233+ expect ( commandStartedEvents ) . to . have . length ( 2 ) ;
234+ } ) ;
235+ } ) ;
236+
237+ context ( 'when timeoutMS is a number and operation fails' , ( ) => {
238+ const commandStartedEvents = [ ] ;
239+ let start : number , end : number ;
240+ let maybeErr : Error | null ;
241+
242+ beforeEach ( async function ( ) {
243+ client = this . configuration . newClient ( this . configuration . url ( ) , {
244+ timeoutMS : 50 ,
245+ monitorCommands : true
246+ } ) ;
247+
248+ client . on ( 'commandStarted' , ( ev : CommandStartedEvent ) => {
249+ if ( Object . hasOwn ( ev . command , 'count' ) ) commandStartedEvents . push ( ev . command ) ;
250+ } ) ;
251+
252+ start = performance . now ( ) ;
253+ maybeErr = await client
254+ . db ( 'test' )
255+ . collection ( 'test' )
256+ . count ( )
257+ . then (
258+ ( ) => null ,
259+ e => e
260+ ) ;
261+ end = performance . now ( ) ;
262+ } ) ;
263+
264+ it ( 'throws MongoOperationTimeoutError after timeoutMS' , async function ( ) {
265+ expect ( end - start ) . to . be . greaterThanOrEqual ( client . options . timeoutMS ) ;
266+ expect ( maybeErr ) . to . be . instanceof ( MongoOperationTimeoutError ) ;
267+ } ) ;
268+
269+ it ( 'attempts the command more than twice' , async function ( ) {
270+ expect ( commandStartedEvents ) . to . have . length . greaterThan ( 2 ) ;
271+ } ) ;
272+ } ) ;
273+ } ) ;
274+
275+ describe ( 'retryable writes' , ( ) => {
276+ let configClient : MongoClient ;
277+ let client : MongoClient ;
278+ const FAIL_COMMAND = {
279+ configureFailPoint : 'failCommand' ,
280+ mode : 'alwaysOn' ,
281+ data : {
282+ failCommands : [ 'insert' ] ,
283+ errorCode : 10107 ,
284+ closeConnection : false
285+ }
286+ } ;
287+
288+ const DISABLE_FAIL_COMMAND = {
289+ configureFailPoint : 'failCommand' ,
290+ mode : 'off' ,
291+ data : {
292+ failCommands : [ 'insert' ] ,
293+ errorCode : 10107 ,
294+ closeConnection : false
295+ }
296+ } ;
297+
298+ beforeEach ( async function ( ) {
299+ configClient = new MongoClient ( this . configuration . url ( ) ) ;
300+ await configClient . db ( ) . admin ( ) . command ( FAIL_COMMAND ) ;
301+ } ) ;
302+
303+ afterEach ( async function ( ) {
304+ await configClient . db ( ) . admin ( ) . command ( DISABLE_FAIL_COMMAND ) ;
305+ await configClient . close ( ) ;
306+
307+ await client . close ( ) ;
308+ } ) ;
309+
310+ context ( 'when timeoutMS is undefined and retryable operation fails' , ( ) => {
311+ const commandStartedEvents = [ ] ;
312+ let maybeErr : Error | null ;
313+
314+ beforeEach ( async function ( ) {
315+ client = this . configuration . newClient ( this . configuration . url ( ) , {
316+ timeoutMS : undefined ,
317+ monitorCommands : true
318+ } ) ;
319+
320+ client . on ( 'commandStarted' , ( ev : CommandStartedEvent ) => {
321+ if ( Object . hasOwn ( ev . command , 'insert' ) ) commandStartedEvents . push ( ev . command ) ;
322+ } ) ;
323+ maybeErr = await client
324+ . db ( 'test' )
325+ . collection ( 'test' )
326+ . insertOne ( { a : 10 } )
327+ . then (
328+ ( ) => null ,
329+ e => e
330+ ) ;
331+ } ) ;
332+
333+ it ( 'makes exactly two total attempts and throws an error' , async function ( ) {
334+ expect ( maybeErr ) . to . be . instanceof ( MongoError ) ;
335+ expect ( commandStartedEvents ) . to . have . length ( 2 ) ;
336+ } ) ;
337+ } ) ;
338+
339+ context ( 'when timeoutMS is a number and operation fails' , ( ) => {
340+ const commandStartedEvents = [ ] ;
341+ let start : number , end : number ;
342+ let maybeErr : Error | null ;
343+
344+ beforeEach ( async function ( ) {
345+ client = this . configuration . newClient ( this . configuration . url ( ) , {
346+ timeoutMS : 50 ,
347+ monitorCommands : true
348+ } ) ;
349+
350+ client . on ( 'commandStarted' , ( ev : CommandStartedEvent ) => {
351+ if ( Object . hasOwn ( ev . command , 'insert' ) ) commandStartedEvents . push ( ev . command ) ;
352+ } ) ;
353+
354+ start = performance . now ( ) ;
355+ maybeErr = await client
356+ . db ( 'test' )
357+ . collection ( 'test' )
358+ . insertOne ( { a : 10 } )
359+ . then (
360+ ( ) => null ,
361+ e => e
362+ ) ;
363+ end = performance . now ( ) ;
364+ } ) ;
365+
366+ it ( 'throws MongoOperationTimeoutError after timeoutMS' , async function ( ) {
367+ expect ( end - start ) . to . be . greaterThanOrEqual ( client . options . timeoutMS ) ;
368+ expect ( maybeErr ) . to . be . instanceof ( MongoOperationTimeoutError ) ;
369+ } ) ;
370+
371+ it ( 'attempts the command more than twice' , async function ( ) {
372+ expect ( commandStartedEvents ) . to . have . length . greaterThan ( 2 ) ;
373+ } ) ;
374+ } ) ;
375+ } ) ;
169376} ) ;
0 commit comments