Skip to content

PYTHON-2452 Ensure command-responses with RetryableWriteError label are retried on MongoDB 4.4+ #530

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

11 changes: 9 additions & 2 deletions pymongo/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,11 @@ def _check_command_response(response, max_wire_version,
max_wire_version)

if parse_write_concern_error and 'writeConcernError' in response:
_raise_write_concern_error(response['writeConcernError'])
_error = response["writeConcernError"]
_labels = response.get("errorLabels", [])
if _labels:
_error.update({'errorLabels': _labels})
_raise_write_concern_error(_error)

if response["ok"]:
return
Expand Down Expand Up @@ -221,8 +225,11 @@ def _check_write_command_response(result):
if write_errors:
_raise_last_write_error(write_errors)

error = result.get("writeConcernError")
error = result.get("writeConcernError", {})
if error:
error_labels = result.get("errorLabels", [])
if error_labels:
error.update({'errorLabels': error_labels})
_raise_write_concern_error(error)


Expand Down
46 changes: 45 additions & 1 deletion test/test_retryable_writes.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@

from pymongo.errors import (ConnectionFailure,
OperationFailure,
ServerSelectionTimeoutError)
ServerSelectionTimeoutError,
WriteConcernError)
from pymongo.mongo_client import MongoClient
from pymongo.operations import (InsertOne,
DeleteMany,
Expand All @@ -43,6 +44,7 @@
OvertCommandListener,
TestCreator)
from test.utils_spec_runner import SpecRunner
from test.version import Version

# Location of JSON test specifications.
_TEST_PATH = os.path.join(
Expand Down Expand Up @@ -454,6 +456,48 @@ def test_batch_splitting_retry_fails(self):
self.assertEqual(coll.find_one(projection={'_id': True}), {'_id': 1})


class TestWriteConcernError(IntegrationTest):
@client_context.require_version_min(4, 0)
@client_context.require_replica_set
@client_context.require_no_mmap
@client_context.require_failCommand_fail_point
def test_RetryableWriteError_error_label(self):
listener = OvertCommandListener()
client = rs_or_single_client(
retryWrites=True, event_listeners=[listener])

fail_insert = {
'configureFailPoint': 'failCommand',
'mode': {'times': 2},
'data': {
'failCommands': ['insert'],
'closeConnection': False,
'writeConcernError': {
'code': 91,
'errmsg': 'Replication is being shut down'},
}}

if client_context.version < Version(4, 2):
# SERVER-39292: specifying closeConnection on MongoDB 4.0+,<4.2
# causes the failPoint to fire twice so we remove it.
fail_insert['data'].pop('closeConnection')

# Ensure collection exists.
client.pymongo_test.testcoll.insert_one({})

with self.fail_point(fail_insert):
with self.assertRaises(WriteConcernError) as cm:
client.pymongo_test.testcoll.insert_one({})
self.assertTrue(cm.exception.has_error_label(
'RetryableWriteError'))

if client_context.version >= Version(4, 4):
# In MongoDB 4.4+ we rely on the server returning the error label.
self.assertIn(
'RetryableWriteError',
listener.results['succeeded'][-1].reply['errorLabels'])


# TODO: Make this a real integration test where we stepdown the primary.
class TestRetryableWritesTxnNumber(IgnoreDeprecationsTest):
@client_context.require_version_min(3, 6)
Expand Down