Skip to content

Commit b32da4b

Browse files
authored
PYTHON-5492 Fix handling of MaxTimeMS message (#2484)
1 parent 2a1523f commit b32da4b

File tree

10 files changed

+73
-44
lines changed

10 files changed

+73
-44
lines changed

pymongo/asynchronous/encryption.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,12 @@
7575
NetworkTimeout,
7676
ServerSelectionTimeoutError,
7777
)
78+
from pymongo.helpers_shared import _get_timeout_details
7879
from pymongo.network_layer import async_socket_sendall
7980
from pymongo.operations import UpdateOne
8081
from pymongo.pool_options import PoolOptions
8182
from pymongo.pool_shared import (
8283
_async_configured_socket,
83-
_get_timeout_details,
8484
_raise_connection_failure,
8585
)
8686
from pymongo.read_concern import ReadConcern

pymongo/asynchronous/pool.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
WaitQueueTimeoutError,
5959
)
6060
from pymongo.hello import Hello, HelloCompat
61+
from pymongo.helpers_shared import _get_timeout_details, format_timeout_details
6162
from pymongo.lock import (
6263
_async_cond_wait,
6364
_async_create_condition,
@@ -79,9 +80,7 @@
7980
SSLErrors,
8081
_CancellationContext,
8182
_configured_protocol_interface,
82-
_get_timeout_details,
8383
_raise_connection_failure,
84-
format_timeout_details,
8584
)
8685
from pymongo.read_preferences import ReadPreference
8786
from pymongo.server_api import _add_to_command

pymongo/asynchronous/server.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
_SDAMStatusMessage,
3939
)
4040
from pymongo.message import _convert_exception, _GetMore, _OpMsg, _Query
41-
from pymongo.pool_shared import _get_timeout_details, format_timeout_details
4241
from pymongo.response import PinnedResponse, Response
4342

4443
if TYPE_CHECKING:
@@ -225,11 +224,7 @@ async def run_operation(
225224
if use_cmd:
226225
first = docs[0]
227226
await operation.client._process_response(first, operation.session) # type: ignore[misc, arg-type]
228-
# Append timeout details to MaxTimeMSExpired responses.
229-
if first.get("code") == 50:
230-
timeout_details = _get_timeout_details(conn.opts) # type:ignore[has-type]
231-
first["errmsg"] += format_timeout_details(timeout_details) # type:ignore[index]
232-
_check_command_response(first, conn.max_wire_version)
227+
_check_command_response(first, conn.max_wire_version, pool_opts=conn.opts) # type:ignore[has-type]
233228
except Exception as exc:
234229
duration = datetime.now() - start
235230
if isinstance(exc, (NotPrimaryError, OperationFailure)):

pymongo/helpers_shared.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
if TYPE_CHECKING:
4848
from pymongo.cursor_shared import _Hint
4949
from pymongo.operations import _IndexList
50+
from pymongo.pool_options import PoolOptions
5051
from pymongo.typings import _DocumentOut
5152

5253

@@ -108,6 +109,34 @@
108109
}
109110

110111

112+
def _get_timeout_details(options: PoolOptions) -> dict[str, float]:
113+
from pymongo import _csot
114+
115+
details = {}
116+
timeout = _csot.get_timeout()
117+
socket_timeout = options.socket_timeout
118+
connect_timeout = options.connect_timeout
119+
if timeout:
120+
details["timeoutMS"] = timeout * 1000
121+
if socket_timeout and not timeout:
122+
details["socketTimeoutMS"] = socket_timeout * 1000
123+
if connect_timeout:
124+
details["connectTimeoutMS"] = connect_timeout * 1000
125+
return details
126+
127+
128+
def format_timeout_details(details: Optional[dict[str, float]]) -> str:
129+
result = ""
130+
if details:
131+
result += " (configured timeouts:"
132+
for timeout in ["socketTimeoutMS", "timeoutMS", "connectTimeoutMS"]:
133+
if timeout in details:
134+
result += f" {timeout}: {details[timeout]}ms,"
135+
result = result[:-1]
136+
result += ")"
137+
return result
138+
139+
111140
def _gen_index_name(keys: _IndexList) -> str:
112141
"""Generate an index name from the set of fields it is over."""
113142
return "_".join(["{}_{}".format(*item) for item in keys])
@@ -188,6 +217,7 @@ def _check_command_response(
188217
max_wire_version: Optional[int],
189218
allowable_errors: Optional[Container[Union[int, str]]] = None,
190219
parse_write_concern_error: bool = False,
220+
pool_opts: Optional[PoolOptions] = None,
191221
) -> None:
192222
"""Check the response to a command for errors."""
193223
if "ok" not in response:
@@ -243,6 +273,10 @@ def _check_command_response(
243273
if code in (11000, 11001, 12582):
244274
raise DuplicateKeyError(errmsg, code, response, max_wire_version)
245275
elif code == 50:
276+
# Append timeout details to MaxTimeMSExpired responses.
277+
if pool_opts:
278+
timeout_details = _get_timeout_details(pool_opts)
279+
errmsg += format_timeout_details(timeout_details)
246280
raise ExecutionTimeout(errmsg, code, response, max_wire_version)
247281
elif code == 43:
248282
raise CursorNotFound(errmsg, code, response, max_wire_version)

pymongo/pool_shared.py

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
NetworkTimeout,
3737
_CertificateError,
3838
)
39+
from pymongo.helpers_shared import _get_timeout_details, format_timeout_details
3940
from pymongo.network_layer import AsyncNetworkingInterface, NetworkingInterface, PyMongoProtocol
4041
from pymongo.pool_options import PoolOptions
4142
from pymongo.ssl_support import PYSSLError, SSLError, _has_sni
@@ -149,32 +150,6 @@ def _raise_connection_failure(
149150
raise AutoReconnect(msg) from error
150151

151152

152-
def _get_timeout_details(options: PoolOptions) -> dict[str, float]:
153-
details = {}
154-
timeout = _csot.get_timeout()
155-
socket_timeout = options.socket_timeout
156-
connect_timeout = options.connect_timeout
157-
if timeout:
158-
details["timeoutMS"] = timeout * 1000
159-
if socket_timeout and not timeout:
160-
details["socketTimeoutMS"] = socket_timeout * 1000
161-
if connect_timeout:
162-
details["connectTimeoutMS"] = connect_timeout * 1000
163-
return details
164-
165-
166-
def format_timeout_details(details: Optional[dict[str, float]]) -> str:
167-
result = ""
168-
if details:
169-
result += " (configured timeouts:"
170-
for timeout in ["socketTimeoutMS", "timeoutMS", "connectTimeoutMS"]:
171-
if timeout in details:
172-
result += f" {timeout}: {details[timeout]}ms,"
173-
result = result[:-1]
174-
result += ")"
175-
return result
176-
177-
178153
class _CancellationContext:
179154
def __init__(self) -> None:
180155
self._cancelled = False

pymongo/synchronous/encryption.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,12 @@
7070
NetworkTimeout,
7171
ServerSelectionTimeoutError,
7272
)
73+
from pymongo.helpers_shared import _get_timeout_details
7374
from pymongo.network_layer import sendall
7475
from pymongo.operations import UpdateOne
7576
from pymongo.pool_options import PoolOptions
7677
from pymongo.pool_shared import (
7778
_configured_socket,
78-
_get_timeout_details,
7979
_raise_connection_failure,
8080
)
8181
from pymongo.read_concern import ReadConcern

pymongo/synchronous/pool.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
WaitQueueTimeoutError,
5656
)
5757
from pymongo.hello import Hello, HelloCompat
58+
from pymongo.helpers_shared import _get_timeout_details, format_timeout_details
5859
from pymongo.lock import (
5960
_cond_wait,
6061
_create_condition,
@@ -76,9 +77,7 @@
7677
SSLErrors,
7778
_CancellationContext,
7879
_configured_socket_interface,
79-
_get_timeout_details,
8080
_raise_connection_failure,
81-
format_timeout_details,
8281
)
8382
from pymongo.read_preferences import ReadPreference
8483
from pymongo.server_api import _add_to_command

pymongo/synchronous/server.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
_SDAMStatusMessage,
3838
)
3939
from pymongo.message import _convert_exception, _GetMore, _OpMsg, _Query
40-
from pymongo.pool_shared import _get_timeout_details, format_timeout_details
4140
from pymongo.response import PinnedResponse, Response
4241
from pymongo.synchronous.helpers import _handle_reauth
4342

@@ -225,11 +224,7 @@ def run_operation(
225224
if use_cmd:
226225
first = docs[0]
227226
operation.client._process_response(first, operation.session) # type: ignore[misc, arg-type]
228-
# Append timeout details to MaxTimeMSExpired responses.
229-
if first.get("code") == 50:
230-
timeout_details = _get_timeout_details(conn.opts) # type:ignore[has-type]
231-
first["errmsg"] += format_timeout_details(timeout_details) # type:ignore[index]
232-
_check_command_response(first, conn.max_wire_version)
227+
_check_command_response(first, conn.max_wire_version, pool_opts=conn.opts) # type:ignore[has-type]
233228
except Exception as exc:
234229
duration = datetime.now() - start
235230
if isinstance(exc, (NotPrimaryError, OperationFailure)):

test/asynchronous/test_cursor.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343

4444
from bson import decode_all
4545
from bson.code import Code
46+
from bson.raw_bson import RawBSONDocument
4647
from pymongo import ASCENDING, DESCENDING
4748
from pymongo.asynchronous.cursor import AsyncCursor, CursorType
4849
from pymongo.asynchronous.helpers import anext
@@ -199,6 +200,21 @@ async def test_max_time_ms(self):
199200
finally:
200201
await client.admin.command("configureFailPoint", "maxTimeAlwaysTimeOut", mode="off")
201202

203+
async def test_maxtime_ms_message(self):
204+
db = self.db
205+
await db.t.insert_one({"x": 1})
206+
with self.assertRaises(Exception) as error:
207+
await db.t.find_one({"$where": delay(2)}, max_time_ms=1)
208+
209+
self.assertIn("(configured timeouts: connectTimeoutMS: 20000.0ms", str(error.exception))
210+
211+
client = await self.async_rs_client(document_class=RawBSONDocument)
212+
await client.db.t.insert_one({"x": 1})
213+
with self.assertRaises(Exception) as error:
214+
await client.db.t.find_one({"$where": delay(2)}, max_time_ms=1)
215+
216+
self.assertIn("(configured timeouts: connectTimeoutMS: 20000.0ms", str(error.exception))
217+
202218
async def test_max_await_time_ms(self):
203219
db = self.db
204220
await db.pymongo_test.drop()

test/test_cursor.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343

4444
from bson import decode_all
4545
from bson.code import Code
46+
from bson.raw_bson import RawBSONDocument
4647
from pymongo import ASCENDING, DESCENDING
4748
from pymongo.collation import Collation
4849
from pymongo.errors import ExecutionTimeout, InvalidOperation, OperationFailure, PyMongoError
@@ -197,6 +198,21 @@ def test_max_time_ms(self):
197198
finally:
198199
client.admin.command("configureFailPoint", "maxTimeAlwaysTimeOut", mode="off")
199200

201+
def test_maxtime_ms_message(self):
202+
db = self.db
203+
db.t.insert_one({"x": 1})
204+
with self.assertRaises(Exception) as error:
205+
db.t.find_one({"$where": delay(2)}, max_time_ms=1)
206+
207+
self.assertIn("(configured timeouts: connectTimeoutMS: 20000.0ms", str(error.exception))
208+
209+
client = self.rs_client(document_class=RawBSONDocument)
210+
client.db.t.insert_one({"x": 1})
211+
with self.assertRaises(Exception) as error:
212+
client.db.t.find_one({"$where": delay(2)}, max_time_ms=1)
213+
214+
self.assertIn("(configured timeouts: connectTimeoutMS: 20000.0ms", str(error.exception))
215+
200216
def test_max_await_time_ms(self):
201217
db = self.db
202218
db.pymongo_test.drop()

0 commit comments

Comments
 (0)