Skip to content

Commit f458473

Browse files
PYTHON-2452 Ensure command-responses with RetryableWriteError label are retried on MongoDB 4.4+ (#530)
1 parent 61232b7 commit f458473

File tree

2 files changed

+67
-2
lines changed

2 files changed

+67
-2
lines changed

pymongo/helpers.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,11 @@ def _check_command_response(response, max_wire_version,
115115
max_wire_version)
116116

117117
if parse_write_concern_error and 'writeConcernError' in response:
118-
_raise_write_concern_error(response['writeConcernError'])
118+
_error = response["writeConcernError"]
119+
_labels = response.get("errorLabels")
120+
if _labels:
121+
_error.update({'errorLabels': _labels})
122+
_raise_write_concern_error(_error)
119123

120124
if response["ok"]:
121125
return
@@ -223,6 +227,9 @@ def _check_write_command_response(result):
223227

224228
error = result.get("writeConcernError")
225229
if error:
230+
error_labels = result.get("errorLabels")
231+
if error_labels:
232+
error.update({'errorLabels': error_labels})
226233
_raise_write_concern_error(error)
227234

228235

test/test_retryable_writes.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,17 @@
2020

2121
sys.path[0:0] = [""]
2222

23+
from bson.codec_options import DEFAULT_CODEC_OPTIONS
2324
from bson.int64 import Int64
2425
from bson.objectid import ObjectId
26+
from bson.raw_bson import RawBSONDocument
2527
from bson.son import SON
2628

2729

2830
from pymongo.errors import (ConnectionFailure,
2931
OperationFailure,
30-
ServerSelectionTimeoutError)
32+
ServerSelectionTimeoutError,
33+
WriteConcernError)
3134
from pymongo.mongo_client import MongoClient
3235
from pymongo.operations import (InsertOne,
3336
DeleteMany,
@@ -43,6 +46,7 @@
4346
OvertCommandListener,
4447
TestCreator)
4548
from test.utils_spec_runner import SpecRunner
49+
from test.version import Version
4650

4751
# Location of JSON test specifications.
4852
_TEST_PATH = os.path.join(
@@ -454,6 +458,60 @@ def test_batch_splitting_retry_fails(self):
454458
self.assertEqual(coll.find_one(projection={'_id': True}), {'_id': 1})
455459

456460

461+
class TestWriteConcernError(IntegrationTest):
462+
@classmethod
463+
@client_context.require_replica_set
464+
@client_context.require_no_mmap
465+
@client_context.require_failCommand_fail_point
466+
def setUpClass(cls):
467+
super(TestWriteConcernError, cls).setUpClass()
468+
cls.fail_insert = {
469+
'configureFailPoint': 'failCommand',
470+
'mode': {'times': 2},
471+
'data': {
472+
'failCommands': ['insert'],
473+
'writeConcernError': {
474+
'code': 91,
475+
'errmsg': 'Replication is being shut down'},
476+
}}
477+
478+
@client_context.require_version_min(4, 0)
479+
def test_RetryableWriteError_error_label(self):
480+
listener = OvertCommandListener()
481+
client = rs_or_single_client(
482+
retryWrites=True, event_listeners=[listener])
483+
484+
# Ensure collection exists.
485+
client.pymongo_test.testcoll.insert_one({})
486+
487+
with self.fail_point(self.fail_insert):
488+
with self.assertRaises(WriteConcernError) as cm:
489+
client.pymongo_test.testcoll.insert_one({})
490+
self.assertTrue(cm.exception.has_error_label(
491+
'RetryableWriteError'))
492+
493+
if client_context.version >= Version(4, 4):
494+
# In MongoDB 4.4+ we rely on the server returning the error label.
495+
self.assertIn(
496+
'RetryableWriteError',
497+
listener.results['succeeded'][-1].reply['errorLabels'])
498+
499+
@client_context.require_version_min(4, 4)
500+
def test_RetryableWriteError_error_label_RawBSONDocument(self):
501+
# using RawBSONDocument should not cause errorLabel parsing to fail
502+
with self.fail_point(self.fail_insert):
503+
with self.client.start_session() as s:
504+
s._start_retryable_write()
505+
result = self.client.pymongo_test.command(
506+
'insert', 'testcoll', documents=[{'_id': 1}],
507+
txnNumber=s._server_session.transaction_id, session=s,
508+
codec_options=DEFAULT_CODEC_OPTIONS.with_options(
509+
document_class=RawBSONDocument))
510+
511+
self.assertIn('writeConcernError', result)
512+
self.assertIn('RetryableWriteError', result['errorLabels'])
513+
514+
457515
# TODO: Make this a real integration test where we stepdown the primary.
458516
class TestRetryableWritesTxnNumber(IgnoreDeprecationsTest):
459517
@client_context.require_version_min(3, 6)

0 commit comments

Comments
 (0)