Skip to content

Commit 4170d8a

Browse files
committed
PYTHON-1829 Support maxTimeMS for commitTransaction
Add max_commit_time_ms to TransactionOptions. MaxTimeMSExpired errors on commit are labelled UnknownTransactionCommitResult. with_transaction does not retry commit after MaxTimeMSExpired errors.
1 parent fc645a2 commit 4170d8a

File tree

8 files changed

+754
-82
lines changed

8 files changed

+754
-82
lines changed

doc/changelog.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ Version 3.9 adds support for MongoDB 4.2. Highlights include:
1111
- New method :meth:`pymongo.client_session.ClientSession.with_transaction` to
1212
support conveniently running a transaction in a session with automatic
1313
retries and at-most-once semantics.
14+
- Added the ``max_commit_time_ms`` parameter to
15+
:meth:`~pymongo.client_session.ClientSession.start_transaction`.
1416
- Implement the `URI options specification`_ in the
1517
:meth:`~pymongo.mongo_client.MongoClient` constructor. Consequently, there are
1618
a number of changes in connection options:

pymongo/client_session.py

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@
100100

101101
from bson.binary import Binary
102102
from bson.int64 import Int64
103-
from bson.py3compat import abc, reraise_instance
103+
from bson.py3compat import abc, integer_types, reraise_instance
104104
from bson.son import SON
105105
from bson.timestamp import Timestamp
106106

@@ -158,18 +158,35 @@ class TransactionOptions(object):
158158
"""Options for :meth:`ClientSession.start_transaction`.
159159
160160
:Parameters:
161-
- `read_concern`: The :class:`~pymongo.read_concern.ReadConcern` to use
162-
for this transaction.
163-
- `write_concern`: The :class:`~pymongo.write_concern.WriteConcern` to
164-
use for this transaction.
161+
- `read_concern` (optional): The
162+
:class:`~pymongo.read_concern.ReadConcern` to use for this transaction.
163+
If ``None`` (the default) the :attr:`read_preference` of
164+
the :class:`MongoClient` is used.
165+
- `write_concern` (optional): The
166+
:class:`~pymongo.write_concern.WriteConcern` to use for this
167+
transaction. If ``None`` (the default) the :attr:`read_preference` of
168+
the :class:`MongoClient` is used.
169+
- `read_preference` (optional): The read preference to use. If
170+
``None`` (the default) the :attr:`read_preference` of this
171+
:class:`MongoClient` is used. See :mod:`~pymongo.read_preferences`
172+
for options. Transactions which read must use
173+
:attr:`~pymongo.read_preferences.ReadPreference.PRIMARY`.
174+
- `max_commit_time_ms` (optional): The maximum amount of time to allow a
175+
single commitTransaction command to run. This option is an alias for
176+
maxTimeMS option on the commitTransaction command. If ``None`` (the
177+
default) maxTimeMS is not used.
178+
179+
.. versionchanged:: 3.9
180+
Added the ``max_commit_time_ms`` option.
165181
166182
.. versionadded:: 3.7
167183
"""
168184
def __init__(self, read_concern=None, write_concern=None,
169-
read_preference=None):
185+
read_preference=None, max_commit_time_ms=None):
170186
self._read_concern = read_concern
171187
self._write_concern = write_concern
172188
self._read_preference = read_preference
189+
self._max_commit_time_ms = max_commit_time_ms
173190
if read_concern is not None:
174191
if not isinstance(read_concern, ReadConcern):
175192
raise TypeError("read_concern must be an instance of "
@@ -189,6 +206,10 @@ def __init__(self, read_concern=None, write_concern=None,
189206
raise TypeError("%r is not valid for read_preference. See "
190207
"pymongo.read_preferences for valid "
191208
"options." % (read_preference,))
209+
if max_commit_time_ms is not None:
210+
if not isinstance(max_commit_time_ms, integer_types):
211+
raise TypeError(
212+
"max_commit_time_ms must be an integer or None")
192213

193214
@property
194215
def read_concern(self):
@@ -206,6 +227,14 @@ def read_preference(self):
206227
"""
207228
return self._read_preference
208229

230+
@property
231+
def max_commit_time_ms(self):
232+
"""The maxTimeMS to use when running a commitTransaction command.
233+
234+
.. versionadded:: 3.9
235+
"""
236+
return self._max_commit_time_ms
237+
209238

210239
def _validate_session_write_concern(session, write_concern):
211240
"""Validate that an explicit session is not used with an unack'ed write.
@@ -279,10 +308,16 @@ def _reraise_with_unknown_commit(exc):
279308
reraise_instance(exc, trace=sys.exc_info()[2])
280309

281310

311+
def _max_time_expired_error(exc):
312+
"""Return true if exc is a MaxTimeMSExpired error."""
313+
return isinstance(exc, OperationFailure) and exc.code == 50
314+
315+
282316
# From the transactions spec, all the retryable writes errors plus
283317
# WriteConcernFailed.
284318
_UNKNOWN_COMMIT_ERROR_CODES = _RETRYABLE_ERROR_CODES | frozenset([
285319
64, # WriteConcernFailed
320+
50, # MaxTimeMSExpired
286321
])
287322

288323
# From the Convenient API for Transactions spec, with_transaction must
@@ -380,7 +415,7 @@ def _inherit_option(self, name, val):
380415
return getattr(self.client, name)
381416

382417
def with_transaction(self, callback, read_concern=None, write_concern=None,
383-
read_preference=None):
418+
read_preference=None, max_commit_time_ms=None):
384419
"""Execute a callback in a transaction.
385420
386421
This method starts a transaction on this session, executes ``callback``
@@ -465,7 +500,8 @@ def callback(session, custom_arg, custom_kwarg=None):
465500
start_time = monotonic.time()
466501
while True:
467502
self.start_transaction(
468-
read_concern, write_concern, read_preference)
503+
read_concern, write_concern, read_preference,
504+
max_commit_time_ms)
469505
try:
470506
ret = callback(self)
471507
except Exception as exc:
@@ -488,7 +524,8 @@ def callback(session, custom_arg, custom_kwarg=None):
488524
self.commit_transaction()
489525
except PyMongoError as exc:
490526
if (exc.has_error_label("UnknownTransactionCommitResult")
491-
and _within_time_limit(start_time)):
527+
and _within_time_limit(start_time)
528+
and not _max_time_expired_error(exc)):
492529
# Retry the commit.
493530
continue
494531

@@ -502,11 +539,14 @@ def callback(session, custom_arg, custom_kwarg=None):
502539
return ret
503540

504541
def start_transaction(self, read_concern=None, write_concern=None,
505-
read_preference=None):
542+
read_preference=None, max_commit_time_ms=None):
506543
"""Start a multi-statement transaction.
507544
508545
Takes the same arguments as :class:`TransactionOptions`.
509546
547+
.. versionchanged:: 3.9
548+
Added the ``max_commit_time_ms`` option.
549+
510550
.. versionadded:: 3.7
511551
"""
512552
self._check_ended()
@@ -518,9 +558,13 @@ def start_transaction(self, read_concern=None, write_concern=None,
518558
write_concern = self._inherit_option("write_concern", write_concern)
519559
read_preference = self._inherit_option(
520560
"read_preference", read_preference)
561+
if max_commit_time_ms is None:
562+
opts = self.options.default_transaction_options
563+
if opts:
564+
max_commit_time_ms = opts.max_commit_time_ms
521565

522566
self._transaction.opts = TransactionOptions(
523-
read_concern, write_concern, read_preference)
567+
read_concern, write_concern, read_preference, max_commit_time_ms)
524568
self._transaction.reset()
525569
self._transaction.state = _TxnState.STARTING
526570
self._start_retryable_write()
@@ -631,18 +675,25 @@ def _finish_transaction_with_retry(self, command_name, explict_retry):
631675
raise exc
632676

633677
def _finish_transaction(self, command_name, retrying):
634-
# Transaction spec says that after the initial commit attempt,
635-
# subsequent commitTransaction commands should be upgraded to use
636-
# w:"majority" and set a default value of 10 seconds for wtimeout.
637-
wc = self._transaction.opts.write_concern
638-
if retrying and command_name == "commitTransaction":
639-
wc_doc = wc.document
640-
wc_doc["w"] = "majority"
641-
wc_doc.setdefault("wtimeout", 10000)
642-
wc = WriteConcern(**wc_doc)
678+
opts = self._transaction.opts
679+
wc = opts.write_concern
643680
cmd = SON([(command_name, 1)])
681+
if command_name == "commitTransaction":
682+
if opts.max_commit_time_ms:
683+
cmd['maxTimeMS'] = opts.max_commit_time_ms
684+
685+
# Transaction spec says that after the initial commit attempt,
686+
# subsequent commitTransaction commands should be upgraded to use
687+
# w:"majority" and set a default value of 10 seconds for wtimeout.
688+
if retrying:
689+
wc_doc = wc.document
690+
wc_doc["w"] = "majority"
691+
wc_doc.setdefault("wtimeout", 10000)
692+
wc = WriteConcern(**wc_doc)
693+
644694
if self._transaction.recovery_token:
645695
cmd['recoveryToken'] = self._transaction.recovery_token
696+
646697
with self._client._socket_for_writes(self) as sock_info:
647698
return self._client.admin._command(
648699
sock_info,

test/test_transactions.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,12 @@ def test_transaction_options_validation(self):
7070
self.assertIsNone(default_options.read_concern)
7171
self.assertIsNone(default_options.write_concern)
7272
self.assertIsNone(default_options.read_preference)
73+
self.assertIsNone(default_options.max_commit_time_ms)
74+
# No error when valid options are provided.
7375
TransactionOptions(read_concern=ReadConcern(),
7476
write_concern=WriteConcern(),
75-
read_preference=ReadPreference.PRIMARY)
77+
read_preference=ReadPreference.PRIMARY,
78+
max_commit_time_ms=10000)
7679
with self.assertRaisesRegex(TypeError, "read_concern must be "):
7780
TransactionOptions(read_concern={})
7881
with self.assertRaisesRegex(TypeError, "write_concern must be "):
@@ -84,6 +87,10 @@ def test_transaction_options_validation(self):
8487
with self.assertRaisesRegex(
8588
TypeError, "is not valid for read_preference"):
8689
TransactionOptions(read_preference={})
90+
with self.assertRaisesRegex(
91+
TypeError, "max_commit_time_ms must be an integer or None"):
92+
TransactionOptions(max_commit_time_ms="10000")
93+
8794

8895
@client_context.require_transactions
8996
def test_transaction_write_concern_override(self):

test/transactions-convenient-api/commit-retry.json

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,106 @@
423423
]
424424
}
425425
}
426+
},
427+
{
428+
"description": "commit is not retried after MaxTimeMSExpired error",
429+
"failPoint": {
430+
"configureFailPoint": "failCommand",
431+
"mode": {
432+
"times": 1
433+
},
434+
"data": {
435+
"failCommands": [
436+
"commitTransaction"
437+
],
438+
"errorCode": 50
439+
}
440+
},
441+
"operations": [
442+
{
443+
"name": "withTransaction",
444+
"object": "session0",
445+
"arguments": {
446+
"callback": {
447+
"operations": [
448+
{
449+
"name": "insertOne",
450+
"object": "collection",
451+
"arguments": {
452+
"session": "session0",
453+
"document": {
454+
"_id": 1
455+
}
456+
},
457+
"result": {
458+
"insertedId": 1
459+
}
460+
}
461+
]
462+
},
463+
"options": {
464+
"maxCommitTimeMS": 60000
465+
}
466+
},
467+
"result": {
468+
"errorCodeName": "MaxTimeMSExpired",
469+
"errorLabelsContain": [
470+
"UnknownTransactionCommitResult"
471+
],
472+
"errorLabelsOmit": [
473+
"TransientTransactionError"
474+
]
475+
}
476+
}
477+
],
478+
"expectations": [
479+
{
480+
"command_started_event": {
481+
"command": {
482+
"insert": "test",
483+
"documents": [
484+
{
485+
"_id": 1
486+
}
487+
],
488+
"ordered": true,
489+
"lsid": "session0",
490+
"txnNumber": {
491+
"$numberLong": "1"
492+
},
493+
"startTransaction": true,
494+
"autocommit": false,
495+
"readConcern": null,
496+
"writeConcern": null
497+
},
498+
"command_name": "insert",
499+
"database_name": "withTransaction-tests"
500+
}
501+
},
502+
{
503+
"command_started_event": {
504+
"command": {
505+
"commitTransaction": 1,
506+
"lsid": "session0",
507+
"txnNumber": {
508+
"$numberLong": "1"
509+
},
510+
"autocommit": false,
511+
"maxTimeMS": 60000,
512+
"readConcern": null,
513+
"startTransaction": null,
514+
"writeConcern": null
515+
},
516+
"command_name": "commitTransaction",
517+
"database_name": "admin"
518+
}
519+
}
520+
],
521+
"outcome": {
522+
"collection": {
523+
"data": []
524+
}
525+
}
426526
}
427527
]
428528
}

0 commit comments

Comments
 (0)