Skip to content

Commit a83dea8

Browse files
committed
PYTHON-5505 Make cursor getMore retryable
1 parent 4728868 commit a83dea8

File tree

4 files changed

+72
-6
lines changed

4 files changed

+72
-6
lines changed

pymongo/asynchronous/mongo_client.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2737,6 +2737,7 @@ def __init__(
27372737
):
27382738
self._last_error: Optional[Exception] = None
27392739
self._retrying = False
2740+
self._overload = False
27402741
self._multiple_retries = _csot.get_timeout() is not None
27412742
self._client = mongo_client
27422743

@@ -2808,8 +2809,6 @@ async def run(self) -> T:
28082809

28092810
# Specialized catch on write operation
28102811
if not self._is_read:
2811-
if not self._retryable:
2812-
raise
28132812
if isinstance(exc, ClientBulkWriteException) and exc.error:
28142813
retryable_write_error_exc = isinstance(
28152814
exc.error, PyMongoError
@@ -2820,6 +2819,8 @@ async def run(self) -> T:
28202819
else:
28212820
retryable_write_error_exc = exc.has_error_label("RetryableWriteError")
28222821
overload = exc.has_error_label("Retryable")
2822+
if not self._retryable and not overload:
2823+
raise
28232824
if retryable_write_error_exc or overload:
28242825
assert self._session
28252826
await self._session._unpin()
@@ -2843,6 +2844,7 @@ async def run(self) -> T:
28432844
if self._client.topology_description.topology_type == TOPOLOGY_TYPE.Sharded:
28442845
self._deprioritized_servers.append(self._server)
28452846

2847+
self._overload = overload
28462848
if overload:
28472849
if self._attempt_number > _MAX_RETRIES:
28482850
if exc.has_error_label("NoWritesPerformed") and self._last_error:
@@ -2944,7 +2946,7 @@ async def _read(self) -> T:
29442946
conn,
29452947
read_pref,
29462948
):
2947-
if self._retrying and not self._retryable:
2949+
if self._retrying and not self._retryable and not self._overload:
29482950
self._check_last_error()
29492951
if self._retrying:
29502952
_debug_log(

pymongo/synchronous/mongo_client.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2727,6 +2727,7 @@ def __init__(
27272727
):
27282728
self._last_error: Optional[Exception] = None
27292729
self._retrying = False
2730+
self._overload = False
27302731
self._multiple_retries = _csot.get_timeout() is not None
27312732
self._client = mongo_client
27322733

@@ -2798,8 +2799,6 @@ def run(self) -> T:
27982799

27992800
# Specialized catch on write operation
28002801
if not self._is_read:
2801-
if not self._retryable:
2802-
raise
28032802
if isinstance(exc, ClientBulkWriteException) and exc.error:
28042803
retryable_write_error_exc = isinstance(
28052804
exc.error, PyMongoError
@@ -2810,6 +2809,8 @@ def run(self) -> T:
28102809
else:
28112810
retryable_write_error_exc = exc.has_error_label("RetryableWriteError")
28122811
overload = exc.has_error_label("Retryable")
2812+
if not self._retryable and not overload:
2813+
raise
28132814
if retryable_write_error_exc or overload:
28142815
assert self._session
28152816
self._session._unpin()
@@ -2833,6 +2834,7 @@ def run(self) -> T:
28332834
if self._client.topology_description.topology_type == TOPOLOGY_TYPE.Sharded:
28342835
self._deprioritized_servers.append(self._server)
28352836

2837+
self._overload = overload
28362838
if overload:
28372839
if self._attempt_number > _MAX_RETRIES:
28382840
if exc.has_error_label("NoWritesPerformed") and self._last_error:
@@ -2934,7 +2936,7 @@ def _read(self) -> T:
29342936
conn,
29352937
read_pref,
29362938
):
2937-
if self._retrying and not self._retryable:
2939+
if self._retrying and not self._retryable and not self._overload:
29382940
self._check_last_error()
29392941
if self._retrying:
29402942
_debug_log(

test/asynchronous/test_backpressure.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,37 @@ async def test_retry_overload_error_insert_one(self):
9898

9999
self.assertIn("Retryable", str(error.exception))
100100

101+
@async_client_context.require_failCommand_appName
102+
async def test_retry_overload_error_getMore(self):
103+
coll = self.db.t
104+
await coll.insert_many([{"x": 1} for _ in range(10)])
105+
106+
# Ensure command is retried on overload error.
107+
fail_once = {
108+
"configureFailPoint": "failCommand",
109+
"mode": {"times": _MAX_RETRIES},
110+
"data": {
111+
"failCommands": ["getMore"],
112+
"errorCode": 462, # IngressRequestRateLimitExceeded
113+
"errorLabels": ["Retryable"],
114+
},
115+
}
116+
cursor = coll.find(batch_size=2)
117+
await cursor.next()
118+
async with self.fail_point(fail_once):
119+
await cursor.to_list()
120+
121+
# Ensure command stops retrying after _MAX_RETRIES.
122+
fail_many_times = fail_once.copy()
123+
fail_many_times["mode"] = {"times": _MAX_RETRIES + 1}
124+
cursor = coll.find(batch_size=2)
125+
await cursor.next()
126+
async with self.fail_point(fail_many_times):
127+
with self.assertRaises(PyMongoError) as error:
128+
await cursor.to_list()
129+
130+
self.assertIn("Retryable", str(error.exception))
131+
101132

102133
if __name__ == "__main__":
103134
unittest.main()

test/test_backpressure.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,37 @@ def test_retry_overload_error_insert_one(self):
9898

9999
self.assertIn("Retryable", str(error.exception))
100100

101+
@client_context.require_failCommand_appName
102+
def test_retry_overload_error_getMore(self):
103+
coll = self.db.t
104+
coll.insert_many([{"x": 1} for _ in range(10)])
105+
106+
# Ensure command is retried on overload error.
107+
fail_once = {
108+
"configureFailPoint": "failCommand",
109+
"mode": {"times": _MAX_RETRIES},
110+
"data": {
111+
"failCommands": ["getMore"],
112+
"errorCode": 462, # IngressRequestRateLimitExceeded
113+
"errorLabels": ["Retryable"],
114+
},
115+
}
116+
cursor = coll.find(batch_size=2)
117+
cursor.next()
118+
with self.fail_point(fail_once):
119+
cursor.to_list()
120+
121+
# Ensure command stops retrying after _MAX_RETRIES.
122+
fail_many_times = fail_once.copy()
123+
fail_many_times["mode"] = {"times": _MAX_RETRIES + 1}
124+
cursor = coll.find(batch_size=2)
125+
cursor.next()
126+
with self.fail_point(fail_many_times):
127+
with self.assertRaises(PyMongoError) as error:
128+
cursor.to_list()
129+
130+
self.assertIn("Retryable", str(error.exception))
131+
101132

102133
if __name__ == "__main__":
103134
unittest.main()

0 commit comments

Comments
 (0)