Skip to content

Commit 285dd6b

Browse files
committed
Add retryable writes prose test
1 parent eacc0c1 commit 285dd6b

2 files changed

Lines changed: 114 additions & 0 deletions

File tree

test/asynchronous/test_retryable_writes.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import sys
2222
import threading
2323
from test.asynchronous.utils import async_set_fail_point, flaky
24+
from unittest import mock
2425

2526
from pymongo.common import MAX_ADAPTIVE_RETRIES
2627

@@ -835,6 +836,62 @@ def failed(event: CommandFailedEvent) -> None:
835836
started_inserts = [e for e in listener.started_events if e.command_name == "insert"]
836837
self.assertEqual(len(started_inserts), MAX_ADAPTIVE_RETRIES + 1)
837838

839+
async def test_backoff_is_not_applied_for_non_overload_errors(self):
840+
if _IS_SYNC:
841+
mock_target = "pymongo.synchronous.helpers._RetryPolicy.backoff"
842+
else:
843+
mock_target = "pymongo.asynchronous.helpers._RetryPolicy.backoff"
844+
845+
# Create a client.
846+
listener = OvertCommandListener()
847+
848+
# Configure the client to listen to CommandFailedEvents. In the attached listener, configure a fail point with error
849+
# code `91` (ShutdownInProgress) and `RetryableError` and `SystemOverloadedError` labels.
850+
overload_fail_point = {
851+
"configureFailPoint": "failCommand",
852+
"mode": {"times": 1},
853+
"data": {
854+
"failCommands": ["insert"],
855+
"errorLabels": ["RetryableError", "SystemOverloadedError"],
856+
"errorCode": 91,
857+
},
858+
}
859+
860+
# Configure a fail point with error code `91` (ShutdownInProgress) with only the `RetryableError` error label.
861+
non_overload_fail_point = {
862+
"configureFailPoint": "failCommand",
863+
"mode": "alwaysOn",
864+
"data": {
865+
"failCommands": ["insert"],
866+
"errorCode": 91,
867+
"errorLabels": ["RetryableError", "RetryableWriteError"],
868+
},
869+
}
870+
871+
def failed(event: CommandFailedEvent) -> None:
872+
# Configure the fail point command only if the failed event is for the 91 error configured in step 2.
873+
if listener.failed_events:
874+
return
875+
assert event.failure["code"] == 91
876+
self.configure_fail_point_sync(non_overload_fail_point)
877+
self.addCleanup(self.configure_fail_point_sync, {}, off=True)
878+
listener.failed_events.append(event)
879+
880+
listener.failed = failed
881+
882+
client = await self.async_rs_client(event_listeners=[listener])
883+
884+
self.configure_fail_point_sync(overload_fail_point)
885+
self.addCleanup(self.configure_fail_point_sync, {}, off=True)
886+
887+
# Perform a findOne operation with coll. Expect the operation to fail.
888+
with mock.patch(mock_target, return_value=0) as mock_backoff:
889+
with self.assertRaises(PyMongoError):
890+
await client.test.test.insert_one({})
891+
892+
# Assert that backoff was applied only once for the initial overload error and not for the subsequent non-overload retryable errors.
893+
self.assertEqual(mock_backoff.call_count, 1)
894+
838895

839896
if __name__ == "__main__":
840897
unittest.main()

test/test_retryable_writes.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import sys
2222
import threading
2323
from test.utils import flaky, set_fail_point
24+
from unittest import mock
2425

2526
from pymongo.common import MAX_ADAPTIVE_RETRIES
2627

@@ -831,6 +832,62 @@ def failed(event: CommandFailedEvent) -> None:
831832
started_inserts = [e for e in listener.started_events if e.command_name == "insert"]
832833
self.assertEqual(len(started_inserts), MAX_ADAPTIVE_RETRIES + 1)
833834

835+
def test_backoff_is_not_applied_for_non_overload_errors(self):
836+
if _IS_SYNC:
837+
mock_target = "pymongo.synchronous.helpers._RetryPolicy.backoff"
838+
else:
839+
mock_target = "pymongo.helpers._RetryPolicy.backoff"
840+
841+
# Create a client.
842+
listener = OvertCommandListener()
843+
844+
# Configure the client to listen to CommandFailedEvents. In the attached listener, configure a fail point with error
845+
# code `91` (ShutdownInProgress) and `RetryableError` and `SystemOverloadedError` labels.
846+
overload_fail_point = {
847+
"configureFailPoint": "failCommand",
848+
"mode": {"times": 1},
849+
"data": {
850+
"failCommands": ["insert"],
851+
"errorLabels": ["RetryableError", "SystemOverloadedError"],
852+
"errorCode": 91,
853+
},
854+
}
855+
856+
# Configure a fail point with error code `91` (ShutdownInProgress) with only the `RetryableError` error label.
857+
non_overload_fail_point = {
858+
"configureFailPoint": "failCommand",
859+
"mode": "alwaysOn",
860+
"data": {
861+
"failCommands": ["insert"],
862+
"errorCode": 91,
863+
"errorLabels": ["RetryableError", "RetryableWriteError"],
864+
},
865+
}
866+
867+
def failed(event: CommandFailedEvent) -> None:
868+
# Configure the fail point command only if the failed event is for the 91 error configured in step 2.
869+
if listener.failed_events:
870+
return
871+
assert event.failure["code"] == 91
872+
self.configure_fail_point_sync(non_overload_fail_point)
873+
self.addCleanup(self.configure_fail_point_sync, {}, off=True)
874+
listener.failed_events.append(event)
875+
876+
listener.failed = failed
877+
878+
client = self.rs_client(event_listeners=[listener])
879+
880+
self.configure_fail_point_sync(overload_fail_point)
881+
self.addCleanup(self.configure_fail_point_sync, {}, off=True)
882+
883+
# Perform a findOne operation with coll. Expect the operation to fail.
884+
with mock.patch(mock_target, return_value=0) as mock_backoff:
885+
with self.assertRaises(PyMongoError):
886+
client.test.test.insert_one({})
887+
888+
# Assert that backoff was applied only once for the initial overload error and not for the subsequent non-overload retryable errors.
889+
self.assertEqual(mock_backoff.call_count, 1)
890+
834891

835892
if __name__ == "__main__":
836893
unittest.main()

0 commit comments

Comments
 (0)