PYTHON-2572 Introduce NotPrimaryError and deprecate NotMasterError (#646)

(cherry picked from commit ff6ca53328)
This commit is contained in:
Prashant Mital 2021-06-22 14:45:27 -07:00 committed by GitHub
parent ccb62d4c1b
commit 9d757265c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 89 additions and 65 deletions

View File

@ -41,6 +41,8 @@ Deprecations
and :meth:`~pymongo.database.Database.profiling_info`. Instead, users
should run the `profile command`_ with the
:meth:`~pymongo.database.Database.command` helper directly.
- Deprecated :exc:`~pymongo.errors.NotMasterError`. Users should
use :exc:`~pymongo.errors.NotPrimaryError` instead.
.. _PYTHON-2466: https://jira.mongodb.org/browse/PYTHON-2466
.. _PYTHON-1690: https://jira.mongodb.org/browse/PYTHON-1690

View File

@ -41,11 +41,11 @@ _RESUMABLE_GETMORE_ERRORS = frozenset([
189, # PrimarySteppedDown
262, # ExceededTimeLimit
9001, # SocketException
10107, # NotMaster
10107, # NotWritablePrimary
11600, # InterruptedAtShutdown
11602, # InterruptedDueToReplStateChange
13435, # NotMasterNoSlaveOk
13436, # NotMasterOrSecondary
13435, # NotPrimaryNoSecondaryOk
13436, # NotPrimaryOrSecondary
63, # StaleShardVersion
150, # StaleEpoch
13388, # StaleConfig

View File

@ -109,7 +109,22 @@ def _format_detailed_error(message, details):
class NotMasterError(AutoReconnect):
"""The server responded "not master" or "node is recovering".
"""**DEPRECATED** - The server responded "not master" or
"node is recovering".
This exception has been deprecated and will be removed in PyMongo 4.0.
Use :exc:`~pymongo.errors.NotPrimaryError` instead.
.. versionchanged:: 3.12
Deprecated. Use :exc:`~pymongo.errors.NotPrimaryError` instead.
"""
def __init__(self, message='', errors=None):
super(NotMasterError, self).__init__(
_format_detailed_error(message, errors), errors=errors)
class NotPrimaryError(NotMasterError):
"""The server responded "not primary" or "node is recovering".
These errors result from a query, write, or command. The operation failed
because the client thought it was using the primary but the primary has
@ -120,10 +135,11 @@ class NotMasterError(AutoReconnect):
its view of the server as soon as possible after throwing this exception.
Subclass of :exc:`~pymongo.errors.AutoReconnect`.
.. versionadded:: 3.12
"""
def __init__(self, message='', errors=None):
super(NotMasterError, self).__init__(
_format_detailed_error(message, errors), errors=errors)
super(NotPrimaryError, self).__init__(message, errors=errors)
class ServerSelectionTimeoutError(AutoReconnect):

View File

@ -23,7 +23,7 @@ from pymongo import ASCENDING
from pymongo.errors import (CursorNotFound,
DuplicateKeyError,
ExecutionTimeout,
NotMasterError,
NotPrimaryError,
OperationFailure,
WriteError,
WriteConcernError,
@ -34,15 +34,15 @@ _SHUTDOWN_CODES = frozenset([
11600, # InterruptedAtShutdown
91, # ShutdownInProgress
])
# From the SDAM spec, the "not master" error codes are combined with the
# From the SDAM spec, the "not primary" error codes are combined with the
# "node is recovering" error codes (of which the "node is shutting down"
# errors are a subset).
_NOT_MASTER_CODES = frozenset([
10058, # LegacyNotPrimary <=3.2 "not master" error code
10107, # NotMaster
13435, # NotMasterNoSlaveOk
10058, # LegacyNotPrimary <=3.2 "not primary" error code
10107, # NotWritablePrimary
13435, # NotPrimaryNoSecondaryOk
11602, # InterruptedDueToReplStateChange
13436, # NotMasterOrSecondary
13436, # NotPrimaryOrSecondary
189, # PrimarySteppedDown
]) | _SHUTDOWN_CODES
# From the retryable writes spec.
@ -147,12 +147,12 @@ def _check_command_response(response, max_wire_version,
elif errmsg in allowable_errors:
return
# Server is "not master" or "recovering"
# Server is "not primary" or "recovering"
if code is not None:
if code in _NOT_MASTER_CODES:
raise NotMasterError(errmsg, response)
raise NotPrimaryError(errmsg, response)
elif "not master" in errmsg or "node is recovering" in errmsg:
raise NotMasterError(errmsg, response)
raise NotPrimaryError(errmsg, response)
# Other errors
# findAndModify with upsert can raise duplicate key error
@ -184,7 +184,7 @@ def _check_gle_response(result, max_wire_version):
return result
if error_msg.startswith("not master"):
raise NotMasterError(error_msg, result)
raise NotPrimaryError(error_msg, result)
details = result

View File

@ -47,7 +47,7 @@ from pymongo.errors import (ConfigurationError,
DocumentTooLarge,
ExecutionTimeout,
InvalidOperation,
NotMasterError,
NotPrimaryError,
OperationFailure,
ProtocolError)
from pymongo.read_concern import DEFAULT_READ_CONCERN
@ -997,7 +997,7 @@ class _BulkWriteContext(object):
if isinstance(exc, OperationFailure):
failure = _convert_write_result(
self.name, cmd, exc.details)
elif isinstance(exc, NotMasterError):
elif isinstance(exc, NotPrimaryError):
failure = exc.details
else:
failure = _convert_exception(exc)
@ -1022,7 +1022,7 @@ class _BulkWriteContext(object):
except Exception as exc:
if self.publish:
duration = (datetime.datetime.now() - start) + duration
if isinstance(exc, (NotMasterError, OperationFailure)):
if isinstance(exc, (NotPrimaryError, OperationFailure)):
failure = exc.details
else:
failure = _convert_exception(exc)
@ -1516,7 +1516,7 @@ class _OpReply(object):
Check the response for errors and unpack.
Can raise CursorNotFound, NotMasterError, ExecutionTimeout, or
Can raise CursorNotFound, NotPrimaryError, ExecutionTimeout, or
OperationFailure.
:Parameters:
@ -1539,7 +1539,7 @@ class _OpReply(object):
# Fake the ok field if it doesn't exist.
error_object.setdefault("ok", 0)
if error_object["$err"].startswith("not master"):
raise NotMasterError(error_object["$err"], error_object)
raise NotPrimaryError(error_object["$err"], error_object)
elif error_object.get("code") == 50:
raise ExecutionTimeout(error_object.get("$err"),
error_object.get("code"),
@ -1560,7 +1560,7 @@ class _OpReply(object):
Check the response for errors and unpack, returning a dictionary
containing the response data.
Can raise CursorNotFound, NotMasterError, ExecutionTimeout, or
Can raise CursorNotFound, NotPrimaryError, ExecutionTimeout, or
OperationFailure.
:Parameters:
@ -1721,7 +1721,7 @@ def _first_batch(sock_info, db, coll, query, ntoreturn,
except Exception as exc:
if publish:
duration = (datetime.datetime.now() - start) + encoding_duration
if isinstance(exc, (NotMasterError, OperationFailure)):
if isinstance(exc, (NotPrimaryError, OperationFailure)):
failure = exc.details
else:
failure = _convert_exception(exc)

View File

@ -59,7 +59,7 @@ from pymongo.errors import (AutoReconnect,
ConfigurationError,
ConnectionFailure,
InvalidOperation,
NotMasterError,
NotPrimaryError,
OperationFailure,
PyMongoError,
ServerSelectionTimeoutError)
@ -2289,7 +2289,7 @@ def _retryable_error_doc(exc):
wces = exc.details['writeConcernErrors']
wce = wces[-1] if wces else None
return wce
if isinstance(exc, (NotMasterError, OperationFailure)):
if isinstance(exc, (NotPrimaryError, OperationFailure)):
return exc.details
return None
@ -2314,10 +2314,10 @@ def _add_retryable_write_error(exc, max_wire_version):
if code in helpers._RETRYABLE_ERROR_CODES:
exc._add_error_label("RetryableWriteError")
# Connection errors are always retryable except NotMasterError which is
# Connection errors are always retryable except NotPrimaryError which is
# handled above.
if (isinstance(exc, ConnectionFailure) and
not isinstance(exc, NotMasterError)):
not isinstance(exc, NotPrimaryError)):
exc._add_error_label("RetryableWriteError")

View File

@ -21,7 +21,7 @@ import weakref
from bson.py3compat import PY3
from pymongo import common, periodic_executor
from pymongo.errors import (NotMasterError,
from pymongo.errors import (NotPrimaryError,
OperationFailure,
_OperationCancelled)
from pymongo.ismaster import IsMaster
@ -215,7 +215,7 @@ class Monitor(MonitorBase):
try:
try:
return self._check_once()
except (OperationFailure, NotMasterError) as exc:
except (OperationFailure, NotPrimaryError) as exc:
# Update max cluster time even when isMaster fails.
self._topology.receive_cluster_time(
exc.details.get('$clusterTime'))

View File

@ -27,7 +27,7 @@ from pymongo import helpers, message
from pymongo.common import MAX_MESSAGE_SIZE
from pymongo.compression_support import decompress, _NO_COMPRESSION
from pymongo.errors import (AutoReconnect,
NotMasterError,
NotPrimaryError,
OperationFailure,
ProtocolError,
NetworkTimeout,
@ -163,7 +163,7 @@ def command(sock_info, dbname, spec, slave_ok, is_mongos,
except Exception as exc:
if publish:
duration = (datetime.datetime.now() - start) + encoding_duration
if isinstance(exc, (NotMasterError, OperationFailure)):
if isinstance(exc, (NotPrimaryError, OperationFailure)):
failure = exc.details
else:
failure = message._convert_exception(exc)

View File

@ -48,7 +48,7 @@ from pymongo.errors import (AutoReconnect,
InvalidOperation,
DocumentTooLarge,
NetworkTimeout,
NotMasterError,
NotPrimaryError,
OperationFailure,
PyMongoError)
from pymongo.hello import HelloCompat
@ -755,7 +755,7 @@ class SocketInfo(object):
unacknowledged=unacknowledged,
user_fields=user_fields,
exhaust_allowed=exhaust_allowed)
except (OperationFailure, NotMasterError):
except (OperationFailure, NotPrimaryError):
raise
# Catch socket.error, KeyboardInterrupt, etc. and close ourselves.
except BaseException as error:
@ -789,13 +789,13 @@ class SocketInfo(object):
self._raise_connection_failure(error)
def _raise_if_not_writable(self, unacknowledged):
"""Raise NotMasterError on unacknowledged write if this socket is not
"""Raise NotPrimaryError on unacknowledged write if this socket is not
writable.
"""
if unacknowledged and not self.is_writable:
# Write won't succeed, bail as if we'd received a not master error.
raise NotMasterError("not master", {
"ok": 0, "errmsg": "not master", "code": 10107})
# Write won't succeed, bail as if we'd received a not primary error.
raise NotPrimaryError("not primary", {
"ok": 0, "errmsg": "not primary", "code": 10107})
def legacy_write(self, request_id, msg, max_doc_size, with_last_error):
"""Send OP_INSERT, etc., optionally returning response as a dict.
@ -830,7 +830,7 @@ class SocketInfo(object):
reply = self.receive_message(request_id)
result = reply.command_response()
# Raises NotMasterError or OperationFailure.
# Raises NotPrimaryError or OperationFailure.
helpers._check_command_response(result, self.max_wire_version)
return result

View File

@ -18,7 +18,7 @@ from datetime import datetime
from bson import _decode_all_selective
from pymongo.errors import NotMasterError, OperationFailure
from pymongo.errors import NotPrimaryError, OperationFailure
from pymongo.helpers import _check_command_response
from pymongo.message import _convert_exception, _OpMsg
from pymongo.response import Response, PinnedResponse
@ -130,7 +130,7 @@ class Server(object):
except Exception as exc:
if publish:
duration = datetime.now() - start
if isinstance(exc, (NotMasterError, OperationFailure)):
if isinstance(exc, (NotPrimaryError, OperationFailure)):
failure = exc.details
else:
failure = _convert_exception(exc)

View File

@ -37,7 +37,7 @@ from pymongo.topology_description import (updated_topology_description,
from pymongo.errors import (ConnectionFailure,
ConfigurationError,
NetworkTimeout,
NotMasterError,
NotPrimaryError,
OperationFailure,
ServerSelectionTimeoutError,
WriteError)
@ -425,7 +425,7 @@ class Topology(object):
def handle_getlasterror(self, address, error_msg):
"""Clear our pool for a server, mark it Unknown, and check it soon."""
error = NotMasterError(error_msg, {'code': 10107, 'errmsg': error_msg})
error = NotPrimaryError(error_msg, {'code': 10107, 'errmsg': error_msg})
with self._lock:
server = self._servers.get(address)
if server:
@ -608,9 +608,9 @@ class Topology(object):
elif issubclass(exc_type, WriteError):
# Ignore writeErrors.
return
elif issubclass(exc_type, NotMasterError):
elif issubclass(exc_type, NotPrimaryError):
# As per the SDAM spec if:
# - the server sees a "not master" error, and
# - the server sees a "not primary" error, and
# - the server is not shutting down, and
# - the server version is >= 4.2, then
# we keep the existing connection pool, but mark the server type

View File

@ -20,7 +20,7 @@ sys.path[0:0] = [""]
from bson import SON
from pymongo import monitoring
from pymongo.errors import NotMasterError
from pymongo.errors import NotPrimaryError
from pymongo.write_concern import WriteConcern
from test import (client_context,
@ -93,10 +93,10 @@ class TestConnectionsSurvivePrimaryStepDown(IntegrationTest):
# Verify pool not cleared.
self.verify_pool_not_cleared()
# Attempt insertion to mark server description as stale and prevent a
# notMaster error on the subsequent operation.
# NotPrimaryError on the subsequent operation.
try:
self.coll.insert_one({})
except NotMasterError:
except NotPrimaryError:
pass
# Next insert should succeed on the new primary without clearing pool.
self.coll.insert_one({})
@ -109,7 +109,7 @@ class TestConnectionsSurvivePrimaryStepDown(IntegrationTest):
"errorCode": error_code}})
self.addCleanup(self.set_fail_point, {"mode": "off"})
# Insert record and verify failure.
with self.assertRaises(NotMasterError) as exc:
with self.assertRaises(NotPrimaryError) as exc:
self.coll.insert_one({"test": 1})
self.assertEqual(exc.exception.details['code'], error_code)
# Retry before CMAPListener assertion if retry_before=True.

View File

@ -27,7 +27,7 @@ from pymongo import (common,
from pymongo.errors import (AutoReconnect,
ConfigurationError,
NetworkTimeout,
NotMasterError,
NotPrimaryError,
OperationFailure)
from pymongo.helpers import (_check_command_response,
_check_write_command_response)
@ -122,7 +122,7 @@ def got_app_error(topology, app_error):
else:
raise AssertionError('unknown error type: %s' % (error_type,))
assert False
except (AutoReconnect, NotMasterError, OperationFailure) as e:
except (AutoReconnect, NotPrimaryError, OperationFailure) as e:
if when == 'beforeHandshakeCompletes':
completed_handshake = False
elif when == 'afterHandshakeCompletes':

View File

@ -20,6 +20,7 @@ sys.path[0:0] = [""]
from pymongo.errors import (BulkWriteError,
EncryptionError,
NotPrimaryError,
NotMasterError,
OperationFailure)
from test import (PyMongoTestCase,
@ -27,12 +28,12 @@ from test import (PyMongoTestCase,
class TestErrors(PyMongoTestCase):
def test_not_master_error(self):
exc = NotMasterError("not master test", {"errmsg": "error"})
def test_not_primary_error(self):
exc = NotPrimaryError("not primary test", {"errmsg": "error"})
self.assertIn("full error", str(exc))
try:
raise exc
except NotMasterError:
except NotPrimaryError:
self.assertIn("full error", traceback.format_exc())
def test_operation_failure(self):
@ -66,8 +67,8 @@ class TestErrors(PyMongoTestCase):
self._test_unicode_strs(exc)
def test_unicode_strs_not_master_error(self):
exc = NotMasterError(u'unicode \U0001f40d',
{"errmsg": u'unicode \U0001f40d'})
exc = NotPrimaryError(u'unicode \U0001f40d',
{"errmsg": u'unicode \U0001f40d'})
self._test_unicode_strs(exc)
def assertPyMongoErrorEqual(self, exc1, exc2):
@ -82,8 +83,8 @@ class TestErrors(PyMongoTestCase):
self.assertEqual(exc1.details, exc2.details)
self.assertEqual(exc1._max_wire_version, exc2._max_wire_version)
def test_pickle_NotMasterError(self):
exc = NotMasterError("not master test", {"errmsg": "error"})
def test_pickle_NotPrimaryError(self):
exc = NotPrimaryError("not primary test", {"errmsg": "error"})
self.assertPyMongoErrorEqual(exc, pickle.loads(pickle.dumps(exc)))
def test_pickle_OperationFailure(self):
@ -103,6 +104,11 @@ class TestErrors(PyMongoTestCase):
self.assertPyMongoErrorEqual(exc, exc2)
self.assertOperationFailureEqual(cause, exc2.cause)
def test_NotMasterError_catches_NotPrimaryError(self):
with self.assertRaises(NotMasterError) as exc:
raise NotPrimaryError("not primary test", {"errmsg": "error"})
self.assertIn("full error", str(exc.exception))
if __name__ == "__main__":
unittest.main()

View File

@ -27,7 +27,7 @@ from bson.son import SON
from pymongo import CursorType, monitoring, InsertOne, UpdateOne, DeleteOne
from pymongo.command_cursor import CommandCursor
from pymongo.errors import (AutoReconnect,
NotMasterError,
NotPrimaryError,
OperationFailure)
from pymongo.read_preferences import ReadPreference
from pymongo.write_concern import WriteConcern
@ -440,7 +440,7 @@ class TestCommandMonitoring(PyMongoTestCase):
error = None
try:
client.pymongo_test.test.find_one_and_delete({})
except NotMasterError as exc:
except NotPrimaryError as exc:
error = exc.errors
results = self.listener.results
started = results['started'][0]
@ -1212,7 +1212,7 @@ class TestCommandMonitoring(PyMongoTestCase):
},
}
with self.fail_point(insert_command_error):
with self.assertRaises(NotMasterError):
with self.assertRaises(NotPrimaryError):
coll.bulk_write([InsertOne({'_id': 1})])
failed = self.listener.results['failed']
self.assertEqual(1, len(failed))

View File

@ -25,7 +25,7 @@ from bson.json_util import object_hook
from pymongo import monitoring
from pymongo.common import clean_node
from pymongo.errors import (ConnectionFailure,
NotMasterError)
NotPrimaryError)
from pymongo.ismaster import IsMaster
from pymongo.monitor import Monitor
from pymongo.server_description import ServerDescription
@ -333,12 +333,12 @@ class TestSdamMonitoring(IntegrationTest):
def test_not_master_error_publishes_events(self):
self._test_app_error({'errorCode': 10107, 'closeConnection': False,
'errorLabels': ['RetryableWriteError']},
NotMasterError)
NotPrimaryError)
def test_shutdown_error_publishes_events(self):
self._test_app_error({'errorCode': 91, 'closeConnection': False,
'errorLabels': ['RetryableWriteError']},
NotMasterError)
NotPrimaryError)
if __name__ == "__main__":

View File

@ -39,7 +39,7 @@ from pymongo.change_stream import ChangeStream
from pymongo.collection import Collection
from pymongo.database import Database
from pymongo.errors import (
BulkWriteError, ConnectionFailure, InvalidOperation, NotMasterError,
BulkWriteError, ConnectionFailure, InvalidOperation, NotPrimaryError,
PyMongoError)
from pymongo.monitoring import (
CommandFailedEvent, CommandListener, CommandStartedEvent,
@ -730,7 +730,7 @@ class UnifiedSpecTestMixinV1(IntegrationTest):
if is_client_error:
# Connection errors are considered client errors.
if isinstance(exception, ConnectionFailure):
self.assertNotIsInstance(exception, NotMasterError)
self.assertNotIsInstance(exception, NotPrimaryError)
else:
self.assertNotIsInstance(exception, PyMongoError)