Skip to content

Commit 96c4d3f

Browse files
committed
add second test?
1 parent fe21d09 commit 96c4d3f

File tree

4 files changed

+110
-8
lines changed

4 files changed

+110
-8
lines changed

pymongo/asynchronous/client_session.py

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

483483

484+
def _would_exceed_time_limit(start_time: float, backoff: float) -> bool:
485+
"""Is the backoff within the with_transaction retry limit?"""
486+
return time.monotonic() + backoff - start_time >= _WITH_TRANSACTION_RETRY_TIME_LIMIT
487+
488+
484489
_T = TypeVar("_T")
485490

486491
if TYPE_CHECKING:
@@ -708,12 +713,13 @@ async def callback(session, custom_arg, custom_kwarg=None):
708713
"""
709714
start_time = time.monotonic()
710715
retry = 0
711-
self._transaction_retry_backoffs = []
716+
last_error = None
712717
while True:
713718
if retry: # Implement exponential backoff on retry.
714719
jitter = random.random() # noqa: S311
715720
backoff = jitter * min(_BACKOFF_INITIAL * (1.25**retry), _BACKOFF_MAX)
716-
self._transaction_retry_backoffs.append(backoff)
721+
if _would_exceed_time_limit(start_time, backoff):
722+
raise last_error
717723
await asyncio.sleep(backoff)
718724
retry += 1
719725
await self.start_transaction(
@@ -723,6 +729,7 @@ async def callback(session, custom_arg, custom_kwarg=None):
723729
ret = await callback(self)
724730
# Catch KeyboardInterrupt, CancelledError, etc. and cleanup.
725731
except BaseException as exc:
732+
last_error = exc
726733
if self.in_transaction:
727734
await self.abort_transaction()
728735
if (

pymongo/synchronous/client_session.py

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

481481

482+
def _would_exceed_time_limit(start_time: float, backoff: float) -> bool:
483+
"""Is the backoff within the with_transaction retry limit?"""
484+
return time.monotonic() + backoff - start_time >= _WITH_TRANSACTION_RETRY_TIME_LIMIT
485+
486+
482487
_T = TypeVar("_T")
483488

484489
if TYPE_CHECKING:
@@ -706,17 +711,21 @@ def callback(session, custom_arg, custom_kwarg=None):
706711
"""
707712
start_time = time.monotonic()
708713
retry = 0
714+
last_error = None
709715
while True:
710716
if retry: # Implement exponential backoff on retry.
711717
jitter = random.random() # noqa: S311
712718
backoff = jitter * min(_BACKOFF_INITIAL * (1.25**retry), _BACKOFF_MAX)
719+
if _would_exceed_time_limit(start_time, backoff):
720+
raise last_error
713721
time.sleep(backoff)
714722
retry += 1
715723
self.start_transaction(read_concern, write_concern, read_preference, max_commit_time_ms)
716724
try:
717725
ret = callback(self)
718726
# Catch KeyboardInterrupt, CancelledError, etc. and cleanup.
719727
except BaseException as exc:
728+
last_error = exc
720729
if self.in_transaction:
721730
self.abort_transaction()
722731
if (

test/asynchronous/test_transactions.py

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from __future__ import annotations
1717

1818
import asyncio
19+
import random
1920
import sys
2021
import time
2122
from io import BytesIO
@@ -45,6 +46,7 @@
4546
)
4647
from pymongo.asynchronous.command_cursor import AsyncCommandCursor
4748
from pymongo.asynchronous.cursor import AsyncCursor
49+
from pymongo.asynchronous.helpers import anext
4850
from pymongo.errors import (
4951
AutoReconnect,
5052
CollectionInvalid,
@@ -618,13 +620,51 @@ async def callback(session):
618620
await s.with_transaction(callback)
619621
self.assertFalse(s.in_transaction)
620622

623+
@async_client_context.require_test_commands
624+
@async_client_context.require_transactions
625+
async def test_transaction_backoff_is_random(self):
626+
client = async_client_context.client
627+
coll = client[self.db.name].test
628+
# set fail point to trigger transaction failure and trigger backoff
629+
await self.set_fail_point(
630+
{
631+
"configureFailPoint": "failCommand",
632+
"mode": {
633+
"times": 30
634+
}, # sufficiently high enough such that the time effect of backoff is noticeable
635+
"data": {
636+
"failCommands": ["commitTransaction"],
637+
"errorCode": 24,
638+
},
639+
}
640+
)
641+
self.addAsyncCleanup(
642+
self.set_fail_point, {"configureFailPoint": "failCommand", "mode": "off"}
643+
)
644+
645+
start = time.monotonic()
646+
647+
async def callback(session):
648+
await coll.insert_one({}, session=session)
649+
650+
async with self.client.start_session() as s:
651+
await s.with_transaction(callback)
652+
653+
end = time.monotonic()
654+
self.assertLess(end - start, 5) # backoff alone is ~3.5 seconds
655+
621656
@async_client_context.require_test_commands
622657
@async_client_context.require_transactions
623658
async def test_transaction_backoff(self):
624659
client = async_client_context.client
625660
coll = client[self.db.name].test
626-
# optionally set _backoff_initial to a higher value
627-
_set_backoff_initial(client_session._BACKOFF_MAX)
661+
# patch random to make it deterministic
662+
_original_random_random = random.random
663+
664+
def always_one():
665+
return 1
666+
667+
random.random = always_one
628668
# set fail point to trigger transaction failure and trigger backoff
629669
await self.set_fail_point(
630670
{
@@ -651,7 +691,11 @@ async def callback(session):
651691
await s.with_transaction(callback)
652692

653693
end = time.monotonic()
654-
self.assertGreaterEqual(end - start, 1.25) # 1 second
694+
self.assertGreaterEqual(
695+
end - start, 3.5629515313825695
696+
) # sum of backoffs is 3.5629515313825695
697+
698+
random.random = _original_random_random
655699

656700

657701
class TestOptionsInsideTransactionProse(AsyncTransactionsBase):

test/test_transactions.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from __future__ import annotations
1717

1818
import asyncio
19+
import random
1920
import sys
2021
import time
2122
from io import BytesIO
@@ -56,6 +57,7 @@
5657
)
5758
from pymongo.synchronous.command_cursor import CommandCursor
5859
from pymongo.synchronous.cursor import Cursor
60+
from pymongo.synchronous.helpers import next
5961

6062
_IS_SYNC = True
6163

@@ -606,13 +608,49 @@ def callback(session):
606608
s.with_transaction(callback)
607609
self.assertFalse(s.in_transaction)
608610

611+
@client_context.require_test_commands
612+
@client_context.require_transactions
613+
def test_transaction_backoff_is_random(self):
614+
client = client_context.client
615+
coll = client[self.db.name].test
616+
# set fail point to trigger transaction failure and trigger backoff
617+
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.addCleanup(self.set_fail_point, {"configureFailPoint": "failCommand", "mode": "off"})
630+
631+
start = time.monotonic()
632+
633+
def callback(session):
634+
coll.insert_one({}, session=session)
635+
636+
with self.client.start_session() as s:
637+
s.with_transaction(callback)
638+
639+
end = time.monotonic()
640+
self.assertLess(end - start, 5) # backoff alone is ~3.5 seconds
641+
609642
@client_context.require_test_commands
610643
@client_context.require_transactions
611644
def test_transaction_backoff(self):
612645
client = client_context.client
613646
coll = client[self.db.name].test
614-
# optionally set _backoff_initial to a higher value
615-
_set_backoff_initial(client_session._BACKOFF_MAX)
647+
# patch random to make it deterministic
648+
_original_random_random = random.random
649+
650+
def always_one():
651+
return 1
652+
653+
random.random = always_one
616654
# set fail point to trigger transaction failure and trigger backoff
617655
self.set_fail_point(
618656
{
@@ -637,7 +675,11 @@ def callback(session):
637675
s.with_transaction(callback)
638676

639677
end = time.monotonic()
640-
self.assertGreaterEqual(end - start, 1.25) # 1 second
678+
self.assertGreaterEqual(
679+
end - start, 3.5629515313825695
680+
) # sum of backoffs is 3.5629515313825695
681+
682+
random.random = _original_random_random
641683

642684

643685
class TestOptionsInsideTransactionProse(TransactionsBase):

0 commit comments

Comments
 (0)