Skip to content

Commit c434861

Browse files
authored
PYTHON-3291 Add PyMongoError.timeout to identify timeout related errors (#1008)
1 parent 484374e commit c434861

File tree

5 files changed

+67
-15
lines changed

5 files changed

+67
-15
lines changed

doc/changelog.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ PyMongo 4.2 brings a number of improvements including:
1313
changes may be made before the final release. See :ref:`automatic-queryable-client-side-encryption` for example usage.
1414
- Provisional (beta) support for :func:`pymongo.timeout` to apply a single timeout
1515
to an entire block of pymongo operations.
16+
- Added the :attr:`pymongo.errors.PyMongoError.timeout` property which is ``True`` when
17+
the error was caused by a timeout.
1618
- Added ``check_exists`` option to :meth:`~pymongo.database.Database.create_collection`
1719
that when True (the default) runs an additional ``listCollections`` command to verify that the
1820
collection does not exist already.

pymongo/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,11 @@ def timeout(seconds: Optional[float]) -> ContextManager:
149149
# The deadline has now expired, the next operation will raise
150150
# a timeout exception.
151151
client.db.coll2.insert_one({})
152-
except (ServerSelectionTimeoutError, ExecutionTimeout, WTimeoutError,
153-
NetworkTimeout) as exc:
154-
print(f"block timed out: {exc!r}")
152+
except PyMongoError as exc:
153+
if exc.timeout:
154+
print(f"block timed out: {exc!r}")
155+
else:
156+
print(f"failed with non-timeout error: {exc!r}")
155157
156158
When nesting :func:`~pymongo.timeout`, the newly computed deadline is capped to at most
157159
the existing deadline. The deadline can only be shortened, not extended.

pymongo/errors.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ def _remove_error_label(self, label):
5252
"""Remove the given label from this error."""
5353
self._error_labels.discard(label)
5454

55+
@property
56+
def timeout(self) -> bool:
57+
"""True if this error was caused by a timeout.
58+
59+
.. versionadded:: 4.2
60+
"""
61+
return False
62+
5563

5664
class ProtocolError(PyMongoError):
5765
"""Raised for failures related to the wire protocol."""
@@ -69,6 +77,10 @@ class WaitQueueTimeoutError(ConnectionFailure):
6977
.. versionadded:: 4.2
7078
"""
7179

80+
@property
81+
def timeout(self) -> bool:
82+
return True
83+
7284

7385
class AutoReconnect(ConnectionFailure):
7486
"""Raised when a connection to the database is lost and an attempt to
@@ -106,6 +118,10 @@ class NetworkTimeout(AutoReconnect):
106118
Subclass of :exc:`~pymongo.errors.AutoReconnect`.
107119
"""
108120

121+
@property
122+
def timeout(self) -> bool:
123+
return True
124+
109125

110126
def _format_detailed_error(message, details):
111127
if details is not None:
@@ -149,6 +165,10 @@ class ServerSelectionTimeoutError(AutoReconnect):
149165
Preference that the replica set cannot satisfy.
150166
"""
151167

168+
@property
169+
def timeout(self) -> bool:
170+
return True
171+
152172

153173
class ConfigurationError(PyMongoError):
154174
"""Raised when something is incorrectly configured."""
@@ -199,6 +219,10 @@ def details(self) -> Optional[Mapping[str, Any]]:
199219
"""
200220
return self.__details
201221

222+
@property
223+
def timeout(self) -> bool:
224+
return self.__code in (50,)
225+
202226

203227
class CursorNotFound(OperationFailure):
204228
"""Raised while iterating query results if the cursor is
@@ -217,6 +241,10 @@ class ExecutionTimeout(OperationFailure):
217241
.. versionadded:: 2.7
218242
"""
219243

244+
@property
245+
def timeout(self) -> bool:
246+
return True
247+
220248

221249
class WriteConcernError(OperationFailure):
222250
"""Base exception type for errors raised due to write concern.
@@ -242,11 +270,20 @@ class WTimeoutError(WriteConcernError):
242270
.. versionadded:: 2.7
243271
"""
244272

273+
@property
274+
def timeout(self) -> bool:
275+
return True
276+
245277

246278
class DuplicateKeyError(WriteError):
247279
"""Raised when an insert or update fails due to a duplicate key error."""
248280

249281

282+
def _wtimeout_error(error: Any) -> bool:
283+
"""Return True if this writeConcernError doc is a caused by a timeout."""
284+
return error.get("code") == 50 or ("errInfo" in error and error["errInfo"].get("wtimeout"))
285+
286+
250287
class BulkWriteError(OperationFailure):
251288
"""Exception class for bulk write errors.
252289
@@ -261,6 +298,19 @@ def __init__(self, results: Mapping[str, Any]) -> None:
261298
def __reduce__(self) -> Tuple[Any, Any]:
262299
return self.__class__, (self.details,)
263300

301+
@property
302+
def timeout(self) -> bool:
303+
# Check the last writeConcernError and last writeError to determine if this
304+
# BulkWriteError was caused by a timeout.
305+
wces = self.details.get("writeConcernErrors", [])
306+
if wces and _wtimeout_error(wces[-1]):
307+
return True
308+
309+
werrs = self.details.get("writeErrors", [])
310+
if werrs and werrs[-1].get("code") == 50:
311+
return True
312+
return False
313+
264314

265315
class InvalidOperation(PyMongoError):
266316
"""Raised when a client attempts to perform an invalid operation."""
@@ -302,6 +352,12 @@ def cause(self) -> Exception:
302352
"""The exception that caused this encryption or decryption error."""
303353
return self.__cause
304354

355+
@property
356+
def timeout(self) -> bool:
357+
if isinstance(self.__cause, PyMongoError):
358+
return self.__cause.timeout
359+
return False
360+
305361

306362
class _OperationCancelled(AutoReconnect):
307363
"""Internal error raised when a socket operation is cancelled."""

pymongo/helpers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
WriteConcernError,
3131
WriteError,
3232
WTimeoutError,
33+
_wtimeout_error,
3334
)
3435
from pymongo.hello import HelloCompat
3536

@@ -190,7 +191,7 @@ def _raise_last_write_error(write_errors: List[Any]) -> NoReturn:
190191

191192

192193
def _raise_write_concern_error(error: Any) -> NoReturn:
193-
if "errInfo" in error and error["errInfo"].get("wtimeout"):
194+
if _wtimeout_error(error):
194195
# Make sure we raise WTimeoutError
195196
raise WTimeoutError(error.get("errmsg"), error.get("code"), error)
196197
raise WriteConcernError(error.get("errmsg"), error.get("code"), error)

test/unified_format.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,9 @@
7272
ConfigurationError,
7373
ConnectionFailure,
7474
EncryptionError,
75-
ExecutionTimeout,
7675
InvalidOperation,
77-
NetworkTimeout,
7876
NotPrimaryError,
7977
PyMongoError,
80-
ServerSelectionTimeoutError,
81-
WriteConcernError,
8278
)
8379
from pymongo.monitoring import (
8480
_SENSITIVE_COMMANDS,
@@ -948,13 +944,8 @@ def process_error(self, exception, spec):
948944
self.assertNotIsInstance(exception, PyMongoError)
949945

950946
if is_timeout_error:
951-
# TODO: PYTHON-3291 Implement error transformation.
952-
if isinstance(exception, WriteConcernError):
953-
self.assertEqual(exception.code, 50)
954-
else:
955-
self.assertIsInstance(
956-
exception, (NetworkTimeout, ExecutionTimeout, ServerSelectionTimeoutError)
957-
)
947+
self.assertIsInstance(exception, PyMongoError)
948+
self.assertTrue(exception.timeout, msg=exception)
958949

959950
if error_contains:
960951
if isinstance(exception, BulkWriteError):

0 commit comments

Comments
 (0)