1616from __future__ import annotations
1717
1818import asyncio
19+ import random
1920import sys
21+ import time
2022from io import BytesIO
2123from test .asynchronous .utils_spec_runner import AsyncSpecRunner
2224
@@ -441,7 +443,7 @@ async def set_fail_point(self, command_args):
441443 await self .configure_fail_point (client , command_args )
442444
443445 @async_client_context .require_transactions
444- async def test_callback_raises_custom_error (self ):
446+ async def test_1_callback_raises_custom_error (self ):
445447 class _MyException (Exception ):
446448 pass
447449
@@ -453,7 +455,7 @@ async def raise_error(_):
453455 await s .with_transaction (raise_error )
454456
455457 @async_client_context .require_transactions
456- async def test_callback_returns_value (self ):
458+ async def test_2_callback_returns_value (self ):
457459 async def callback (_ ):
458460 return "Foo"
459461
@@ -481,7 +483,7 @@ def callback(_):
481483 self .assertEqual (await s .with_transaction (callback ), "Foo" )
482484
483485 @async_client_context .require_transactions
484- async def test_callback_not_retried_after_timeout (self ):
486+ async def test_3_1_callback_not_retried_after_timeout (self ):
485487 listener = OvertCommandListener ()
486488 client = await self .async_rs_client (event_listeners = [listener ])
487489 coll = client [self .db .name ].test
@@ -509,7 +511,7 @@ async def callback(session):
509511
510512 @async_client_context .require_test_commands
511513 @async_client_context .require_transactions
512- async def test_callback_not_retried_after_commit_timeout (self ):
514+ async def test_3_2_callback_not_retried_after_commit_timeout (self ):
513515 listener = OvertCommandListener ()
514516 client = await self .async_rs_client (event_listeners = [listener ])
515517 coll = client [self .db .name ].test
@@ -543,7 +545,7 @@ async def callback(session):
543545
544546 @async_client_context .require_test_commands
545547 @async_client_context .require_transactions
546- async def test_commit_not_retried_after_timeout (self ):
548+ async def test_3_3_commit_not_retried_after_timeout (self ):
547549 listener = OvertCommandListener ()
548550 client = await self .async_rs_client (event_listeners = [listener ])
549551 coll = client [self .db .name ].test
@@ -613,6 +615,72 @@ async def callback(session):
613615 await s .with_transaction (callback )
614616 self .assertFalse (s .in_transaction )
615617
618+ @async_client_context .require_test_commands
619+ @async_client_context .require_transactions
620+ async def test_4_retry_backoff_is_enforced (self ):
621+ client = async_client_context .client
622+ coll = client [self .db .name ].test
623+ # patch random to make it deterministic -- once to effectively have
624+ # no backoff and the second time with "max" backoff (always waiting the longest
625+ # possible time)
626+ _original_random_random = random .random
627+
628+ def always_one ():
629+ return 1
630+
631+ def always_zero ():
632+ return 0
633+
634+ random .random = always_zero
635+ # set fail point to trigger transaction failure and trigger backoff
636+ await self .set_fail_point (
637+ {
638+ "configureFailPoint" : "failCommand" ,
639+ "mode" : {"times" : 13 },
640+ "data" : {
641+ "failCommands" : ["commitTransaction" ],
642+ "errorCode" : 251 ,
643+ },
644+ }
645+ )
646+ self .addAsyncCleanup (
647+ self .set_fail_point , {"configureFailPoint" : "failCommand" , "mode" : "off" }
648+ )
649+
650+ async def callback (session ):
651+ await coll .insert_one ({}, session = session )
652+
653+ start = time .monotonic ()
654+ async with self .client .start_session () as s :
655+ await s .with_transaction (callback )
656+ end = time .monotonic ()
657+ no_backoff_time = end - start
658+
659+ random .random = always_one
660+ # set fail point to trigger transaction failure and trigger backoff
661+ await self .set_fail_point (
662+ {
663+ "configureFailPoint" : "failCommand" ,
664+ "mode" : {
665+ "times" : 13
666+ }, # sufficiently high enough such that the time effect of backoff is noticeable
667+ "data" : {
668+ "failCommands" : ["commitTransaction" ],
669+ "errorCode" : 251 ,
670+ },
671+ }
672+ )
673+ self .addAsyncCleanup (
674+ self .set_fail_point , {"configureFailPoint" : "failCommand" , "mode" : "off" }
675+ )
676+ start = time .monotonic ()
677+ async with self .client .start_session () as s :
678+ await s .with_transaction (callback )
679+ end = time .monotonic ()
680+ self .assertLess (abs (end - start - (no_backoff_time + 2.2 )), 1 ) # sum of 13 backoffs is 2.2
681+
682+ random .random = _original_random_random
683+
616684
617685class TestOptionsInsideTransactionProse (AsyncTransactionsBase ):
618686 @async_client_context .require_transactions
0 commit comments