Skip to content

Commit ab5215e

Browse files
committed
add second test?
1 parent 44e7f59 commit ab5215e

File tree

4 files changed

+108
-6
lines changed

4 files changed

+108
-6
lines changed

pymongo/asynchronous/client_session.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,11 @@ def _within_time_limit(start_time: float) -> bool:
487487
return time.monotonic() - start_time < _WITH_TRANSACTION_RETRY_TIME_LIMIT
488488

489489

490+
def _would_exceed_time_limit(start_time: float, backoff: float) -> bool:
491+
"""Is the backoff within the with_transaction retry limit?"""
492+
return time.monotonic() + backoff - start_time >= _WITH_TRANSACTION_RETRY_TIME_LIMIT
493+
494+
490495
_T = TypeVar("_T")
491496

492497
if TYPE_CHECKING:
@@ -710,10 +715,13 @@ async def callback(session, custom_arg, custom_kwarg=None):
710715
"""
711716
start_time = time.monotonic()
712717
retry = 0
718+
last_error = None
713719
while True:
714720
if retry: # Implement exponential backoff on retry.
715721
jitter = random.random() # noqa: S311
716722
backoff = jitter * min(_BACKOFF_INITIAL * (1.25**retry), _BACKOFF_MAX)
723+
if _would_exceed_time_limit(start_time, backoff):
724+
raise last_error
717725
await asyncio.sleep(backoff)
718726
retry += 1
719727
await self.start_transaction(
@@ -723,6 +731,7 @@ async def callback(session, custom_arg, custom_kwarg=None):
723731
ret = await callback(self)
724732
# Catch KeyboardInterrupt, CancelledError, etc. and cleanup.
725733
except BaseException as exc:
734+
last_error = exc
726735
if self.in_transaction:
727736
await self.abort_transaction()
728737
if (

pymongo/synchronous/client_session.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,11 @@ def _within_time_limit(start_time: float) -> bool:
485485
return time.monotonic() - start_time < _WITH_TRANSACTION_RETRY_TIME_LIMIT
486486

487487

488+
def _would_exceed_time_limit(start_time: float, backoff: float) -> bool:
489+
"""Is the backoff within the with_transaction retry limit?"""
490+
return time.monotonic() + backoff - start_time >= _WITH_TRANSACTION_RETRY_TIME_LIMIT
491+
492+
488493
_T = TypeVar("_T")
489494

490495
if TYPE_CHECKING:
@@ -708,17 +713,21 @@ def callback(session, custom_arg, custom_kwarg=None):
708713
"""
709714
start_time = time.monotonic()
710715
retry = 0
716+
last_error = None
711717
while True:
712718
if retry: # Implement exponential backoff on retry.
713719
jitter = random.random() # noqa: S311
714720
backoff = jitter * min(_BACKOFF_INITIAL * (1.25**retry), _BACKOFF_MAX)
721+
if _would_exceed_time_limit(start_time, backoff):
722+
raise last_error
715723
time.sleep(backoff)
716724
retry += 1
717725
self.start_transaction(read_concern, write_concern, read_preference, max_commit_time_ms)
718726
try:
719727
ret = callback(self)
720728
# Catch KeyboardInterrupt, CancelledError, etc. and cleanup.
721729
except BaseException as exc:
730+
last_error = exc
722731
if self.in_transaction:
723732
self.abort_transaction()
724733
if (

test/asynchronous/test_transactions.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""Execute Transactions Spec tests."""
1616
from __future__ import annotations
1717

18+
import random
1819
import sys
1920
import time
2021
from io import BytesIO
@@ -607,13 +608,51 @@ async def callback(session):
607608
await s.with_transaction(callback)
608609
self.assertFalse(s.in_transaction)
609610

611+
@async_client_context.require_test_commands
612+
@async_client_context.require_transactions
613+
async def test_transaction_backoff_is_random(self):
614+
client = async_client_context.client
615+
coll = client[self.db.name].test
616+
# set fail point to trigger transaction failure and trigger backoff
617+
await self.set_fail_point(
618+
{
619+
"configureFailPoint": "failCommand",
620+
"mode": {
621+
"times": 30
622+
}, # sufficiently high enough such that the time effect of backoff is noticeable
623+
"data": {
624+
"failCommands": ["commitTransaction"],
625+
"errorCode": 24,
626+
},
627+
}
628+
)
629+
self.addAsyncCleanup(
630+
self.set_fail_point, {"configureFailPoint": "failCommand", "mode": "off"}
631+
)
632+
633+
start = time.monotonic()
634+
635+
async def callback(session):
636+
await coll.insert_one({}, session=session)
637+
638+
async with self.client.start_session() as s:
639+
await s.with_transaction(callback)
640+
641+
end = time.monotonic()
642+
self.assertLess(end - start, 5) # backoff alone is ~3.5 seconds
643+
610644
@async_client_context.require_test_commands
611645
@async_client_context.require_transactions
612646
async def test_transaction_backoff(self):
613647
client = async_client_context.client
614648
coll = client[self.db.name].test
615-
# optionally set _backoff_initial to a higher value
616-
_set_backoff_initial(client_session._BACKOFF_MAX)
649+
# patch random to make it deterministic
650+
_original_random_random = random.random
651+
652+
def always_one():
653+
return 1
654+
655+
random.random = always_one
617656
# set fail point to trigger transaction failure and trigger backoff
618657
await self.set_fail_point(
619658
{
@@ -640,7 +679,11 @@ async def callback(session):
640679
await s.with_transaction(callback)
641680

642681
end = time.monotonic()
643-
self.assertGreaterEqual(end - start, 1.25) # 1 second
682+
self.assertGreaterEqual(
683+
end - start, 3.5629515313825695
684+
) # sum of backoffs is 3.5629515313825695
685+
686+
random.random = _original_random_random
644687

645688

646689
class TestOptionsInsideTransactionProse(AsyncTransactionsBase):

test/test_transactions.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""Execute Transactions Spec tests."""
1616
from __future__ import annotations
1717

18+
import random
1819
import sys
1920
import time
2021
from io import BytesIO
@@ -595,13 +596,49 @@ def callback(session):
595596
s.with_transaction(callback)
596597
self.assertFalse(s.in_transaction)
597598

599+
@client_context.require_test_commands
600+
@client_context.require_transactions
601+
def test_transaction_backoff_is_random(self):
602+
client = client_context.client
603+
coll = client[self.db.name].test
604+
# set fail point to trigger transaction failure and trigger backoff
605+
self.set_fail_point(
606+
{
607+
"configureFailPoint": "failCommand",
608+
"mode": {
609+
"times": 30
610+
}, # sufficiently high enough such that the time effect of backoff is noticeable
611+
"data": {
612+
"failCommands": ["commitTransaction"],
613+
"errorCode": 24,
614+
},
615+
}
616+
)
617+
self.addCleanup(self.set_fail_point, {"configureFailPoint": "failCommand", "mode": "off"})
618+
619+
start = time.monotonic()
620+
621+
def callback(session):
622+
coll.insert_one({}, session=session)
623+
624+
with self.client.start_session() as s:
625+
s.with_transaction(callback)
626+
627+
end = time.monotonic()
628+
self.assertLess(end - start, 5) # backoff alone is ~3.5 seconds
629+
598630
@client_context.require_test_commands
599631
@client_context.require_transactions
600632
def test_transaction_backoff(self):
601633
client = client_context.client
602634
coll = client[self.db.name].test
603-
# optionally set _backoff_initial to a higher value
604-
_set_backoff_initial(client_session._BACKOFF_MAX)
635+
# patch random to make it deterministic
636+
_original_random_random = random.random
637+
638+
def always_one():
639+
return 1
640+
641+
random.random = always_one
605642
# set fail point to trigger transaction failure and trigger backoff
606643
self.set_fail_point(
607644
{
@@ -626,7 +663,11 @@ def callback(session):
626663
s.with_transaction(callback)
627664

628665
end = time.monotonic()
629-
self.assertGreaterEqual(end - start, 1.25) # 1 second
666+
self.assertGreaterEqual(
667+
end - start, 3.5629515313825695
668+
) # sum of backoffs is 3.5629515313825695
669+
670+
random.random = _original_random_random
630671

631672

632673
class TestOptionsInsideTransactionProse(TransactionsBase):

0 commit comments

Comments
 (0)