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

This commit is contained in:
Prashant Mital 2021-06-22 13:24:07 -07:00 committed by GitHub
parent 640fee9d5d
commit ff6ca53328
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 92 additions and 70 deletions

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

@ -94,7 +94,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
@ -105,10 +120,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

@ -24,7 +24,7 @@ from pymongo import ASCENDING
from pymongo.errors import (CursorNotFound,
DuplicateKeyError,
ExecutionTimeout,
NotMasterError,
NotPrimaryError,
OperationFailure,
WriteError,
WriteConcernError,
@ -35,15 +35,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.
@ -148,12 +148,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
@ -185,7 +185,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
@ -995,7 +995,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)
@ -1020,7 +1020,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)
@ -1514,7 +1514,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:
@ -1537,7 +1537,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"),
@ -1558,7 +1558,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:
@ -1715,7 +1715,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

@ -56,7 +56,7 @@ from pymongo.errors import (AutoReconnect,
ConfigurationError,
ConnectionFailure,
InvalidOperation,
NotMasterError,
NotPrimaryError,
OperationFailure,
PyMongoError,
ServerSelectionTimeoutError)
@ -1938,7 +1938,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
@ -1963,10 +1963,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

@ -20,7 +20,7 @@ import time
import weakref
from pymongo import common, periodic_executor
from pymongo.errors import (NotMasterError,
from pymongo.errors import (NotPrimaryError,
OperationFailure,
_OperationCancelled)
from pymongo.ismaster import IsMaster
@ -212,7 +212,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

@ -26,7 +26,7 @@ from bson import _decode_all_selective
from pymongo import helpers, message
from pymongo.common import MAX_MESSAGE_SIZE
from pymongo.compression_support import decompress, _NO_COMPRESSION
from pymongo.errors import (NotMasterError,
from pymongo.errors import (NotPrimaryError,
OperationFailure,
ProtocolError,
_OperationCancelled)
@ -160,7 +160,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

@ -47,7 +47,7 @@ from pymongo.errors import (AutoReconnect,
InvalidOperation,
DocumentTooLarge,
NetworkTimeout,
NotMasterError,
NotPrimaryError,
OperationFailure,
PyMongoError)
from pymongo.hello import HelloCompat
@ -736,7 +736,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:
@ -770,13 +770,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.
@ -811,7 +811,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

@ -29,7 +29,7 @@ from pymongo.client_session import _ServerSessionPool
from pymongo.errors import (ConnectionFailure,
ConfigurationError,
NetworkTimeout,
NotMasterError,
NotPrimaryError,
OperationFailure,
PyMongoError,
ServerSelectionTimeoutError,
@ -611,9 +611,9 @@ class Topology(object):
elif issubclass(exc_type, WriteError):
# Ignore writeErrors.
return
elif issubclass(exc_type, (NotMasterError, OperationFailure)):
elif issubclass(exc_type, (NotPrimaryError, OperationFailure)):
# 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)
@ -104,7 +104,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):
@ -63,8 +64,8 @@ class TestErrors(PyMongoTestCase):
self._test_unicode_strs(exc)
def test_unicode_strs_not_master_error(self):
exc = NotMasterError('unicode \U0001f40d',
{"errmsg": 'unicode \U0001f40d'})
exc = NotPrimaryError('unicode \U0001f40d',
{"errmsg": 'unicode \U0001f40d'})
self._test_unicode_strs(exc)
def assertPyMongoErrorEqual(self, exc1, exc2):
@ -79,8 +80,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):
@ -100,6 +101,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

@ -29,7 +29,7 @@ sys.path[0:0] = [""]
from bson.binary import Binary
from pymongo.mongo_client import MongoClient
from pymongo.errors import (ConfigurationError,
NotMasterError,
NotPrimaryError,
ServerSelectionTimeoutError)
from pymongo.read_preferences import ReadPreference
import gridfs
@ -539,7 +539,7 @@ class TestGridfsReplicaSet(IntegrationTest):
fs = gridfs.GridFS(secondary_connection.gfsreplica, 'gfssecondarytest')
# This won't detect secondary, raises error
self.assertRaises(NotMasterError, fs.put, b'foo')
self.assertRaises(NotPrimaryError, fs.put, b'foo')
def test_gridfs_secondary_lazy(self):
# Should detect it's connected to secondary and not attempt to
@ -556,7 +556,7 @@ class TestGridfsReplicaSet(IntegrationTest):
# Connects, doesn't create index.
self.assertRaises(NoFile, fs.get_last_version)
self.assertRaises(NotMasterError, fs.put, 'data')
self.assertRaises(NotPrimaryError, fs.put, 'data')
if __name__ == "__main__":

View File

@ -30,7 +30,7 @@ from bson.son import SON
import gridfs
from gridfs.errors import NoFile, CorruptGridFile
from pymongo.errors import (ConfigurationError,
NotMasterError,
NotPrimaryError,
ServerSelectionTimeoutError)
from pymongo.mongo_client import MongoClient
from pymongo.read_preferences import ReadPreference
@ -536,7 +536,7 @@ class TestGridfsBucketReplicaSet(IntegrationTest):
secondary_connection.gfsbucketreplica, 'gfsbucketsecondarytest')
# This won't detect secondary, raises error
self.assertRaises(NotMasterError, gfs.upload_from_stream,
self.assertRaises(NotPrimaryError, gfs.upload_from_stream,
"test_filename", b'foo')
def test_gridfs_secondary_lazy(self):
@ -556,7 +556,7 @@ class TestGridfsBucketReplicaSet(IntegrationTest):
# Connects, doesn't create index.
self.assertRaises(NoFile, gfs.open_download_stream_by_name,
"test_filename")
self.assertRaises(NotMasterError, gfs.upload_from_stream,
self.assertRaises(NotPrimaryError, gfs.upload_from_stream,
"test_filename", b'data')

View File

@ -26,7 +26,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
@ -438,7 +438,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]
@ -983,7 +983,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

@ -40,7 +40,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,
@ -720,7 +720,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)