Skip to content

Commit

Permalink
PYTHON-3291 Add PyMongoError.timeout to identify timeout related erro…
Browse files Browse the repository at this point in the history
…rs (#1008)
  • Loading branch information
ShaneHarvey authored Jul 18, 2022
1 parent 484374e commit c434861
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 15 deletions.
2 changes: 2 additions & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ PyMongo 4.2 brings a number of improvements including:
changes may be made before the final release. See :ref:`automatic-queryable-client-side-encryption` for example usage.
- Provisional (beta) support for :func:`pymongo.timeout` to apply a single timeout
to an entire block of pymongo operations.
- Added the :attr:`pymongo.errors.PyMongoError.timeout` property which is ``True`` when
the error was caused by a timeout.
- Added ``check_exists`` option to :meth:`~pymongo.database.Database.create_collection`
that when True (the default) runs an additional ``listCollections`` command to verify that the
collection does not exist already.
Expand Down
8 changes: 5 additions & 3 deletions pymongo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,11 @@ def timeout(seconds: Optional[float]) -> ContextManager:
# The deadline has now expired, the next operation will raise
# a timeout exception.
client.db.coll2.insert_one({})
except (ServerSelectionTimeoutError, ExecutionTimeout, WTimeoutError,
NetworkTimeout) as exc:
print(f"block timed out: {exc!r}")
except PyMongoError as exc:
if exc.timeout:
print(f"block timed out: {exc!r}")
else:
print(f"failed with non-timeout error: {exc!r}")
When nesting :func:`~pymongo.timeout`, the newly computed deadline is capped to at most
the existing deadline. The deadline can only be shortened, not extended.
Expand Down
56 changes: 56 additions & 0 deletions pymongo/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ def _remove_error_label(self, label):
"""Remove the given label from this error."""
self._error_labels.discard(label)

@property
def timeout(self) -> bool:
"""True if this error was caused by a timeout.
.. versionadded:: 4.2
"""
return False


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

@property
def timeout(self) -> bool:
return True


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

@property
def timeout(self) -> bool:
return True


def _format_detailed_error(message, details):
if details is not None:
Expand Down Expand Up @@ -149,6 +165,10 @@ class ServerSelectionTimeoutError(AutoReconnect):
Preference that the replica set cannot satisfy.
"""

@property
def timeout(self) -> bool:
return True


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

@property
def timeout(self) -> bool:
return self.__code in (50,)


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

@property
def timeout(self) -> bool:
return True


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

@property
def timeout(self) -> bool:
return True


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


def _wtimeout_error(error: Any) -> bool:
"""Return True if this writeConcernError doc is a caused by a timeout."""
return error.get("code") == 50 or ("errInfo" in error and error["errInfo"].get("wtimeout"))


class BulkWriteError(OperationFailure):
"""Exception class for bulk write errors.
Expand All @@ -261,6 +298,19 @@ def __init__(self, results: Mapping[str, Any]) -> None:
def __reduce__(self) -> Tuple[Any, Any]:
return self.__class__, (self.details,)

@property
def timeout(self) -> bool:
# Check the last writeConcernError and last writeError to determine if this
# BulkWriteError was caused by a timeout.
wces = self.details.get("writeConcernErrors", [])
if wces and _wtimeout_error(wces[-1]):
return True

werrs = self.details.get("writeErrors", [])
if werrs and werrs[-1].get("code") == 50:
return True
return False


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

@property
def timeout(self) -> bool:
if isinstance(self.__cause, PyMongoError):
return self.__cause.timeout
return False


class _OperationCancelled(AutoReconnect):
"""Internal error raised when a socket operation is cancelled."""
Expand Down
3 changes: 2 additions & 1 deletion pymongo/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
WriteConcernError,
WriteError,
WTimeoutError,
_wtimeout_error,
)
from pymongo.hello import HelloCompat

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


def _raise_write_concern_error(error: Any) -> NoReturn:
if "errInfo" in error and error["errInfo"].get("wtimeout"):
if _wtimeout_error(error):
# Make sure we raise WTimeoutError
raise WTimeoutError(error.get("errmsg"), error.get("code"), error)
raise WriteConcernError(error.get("errmsg"), error.get("code"), error)
Expand Down
13 changes: 2 additions & 11 deletions test/unified_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,9 @@
ConfigurationError,
ConnectionFailure,
EncryptionError,
ExecutionTimeout,
InvalidOperation,
NetworkTimeout,
NotPrimaryError,
PyMongoError,
ServerSelectionTimeoutError,
WriteConcernError,
)
from pymongo.monitoring import (
_SENSITIVE_COMMANDS,
Expand Down Expand Up @@ -948,13 +944,8 @@ def process_error(self, exception, spec):
self.assertNotIsInstance(exception, PyMongoError)

if is_timeout_error:
# TODO: PYTHON-3291 Implement error transformation.
if isinstance(exception, WriteConcernError):
self.assertEqual(exception.code, 50)
else:
self.assertIsInstance(
exception, (NetworkTimeout, ExecutionTimeout, ServerSelectionTimeoutError)
)
self.assertIsInstance(exception, PyMongoError)
self.assertTrue(exception.timeout, msg=exception)

if error_contains:
if isinstance(exception, BulkWriteError):
Expand Down

0 comments on commit c434861

Please sign in to comment.