Skip to content

Commit b3948aa

Browse files
kvwalkerdaprahamian
authored andcommitted
feat(transaction): allow applications to set maxTimeMS for commitTransaction
Fixes NODE-1978
1 parent b0a4a0c commit b3948aa

File tree

10 files changed

+1025
-42
lines changed

10 files changed

+1025
-42
lines changed

lib/core/sessions.js

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ class ClientSession extends EventEmitter {
287287
const MAX_WITH_TRANSACTION_TIMEOUT = 120000;
288288
const UNSATISFIABLE_WRITE_CONCERN_CODE = 100;
289289
const UNKNOWN_REPL_WRITE_CONCERN_CODE = 79;
290+
const MAX_TIME_MS_EXPIRED_CODE = 50;
290291
const NON_DETERMINISTIC_WRITE_CONCERN_ERRORS = new Set([
291292
'CannotSatisfyWriteConcern',
292293
'UnknownReplWriteConcern',
@@ -299,15 +300,27 @@ function hasNotTimedOut(startTime, max) {
299300

300301
function isUnknownTransactionCommitResult(err) {
301302
return (
302-
!NON_DETERMINISTIC_WRITE_CONCERN_ERRORS.has(err.codeName) &&
303-
err.code !== UNSATISFIABLE_WRITE_CONCERN_CODE &&
304-
err.code !== UNKNOWN_REPL_WRITE_CONCERN_CODE
303+
isMaxTimeMSExpiredError(err) ||
304+
(!NON_DETERMINISTIC_WRITE_CONCERN_ERRORS.has(err.codeName) &&
305+
err.code !== UNSATISFIABLE_WRITE_CONCERN_CODE &&
306+
err.code !== UNKNOWN_REPL_WRITE_CONCERN_CODE)
307+
);
308+
}
309+
310+
function isMaxTimeMSExpiredError(err) {
311+
return (
312+
err.code === MAX_TIME_MS_EXPIRED_CODE ||
313+
(err.writeConcernError && err.writeConcernError.code === MAX_TIME_MS_EXPIRED_CODE)
305314
);
306315
}
307316

308317
function attemptTransactionCommit(session, startTime, fn, options) {
309318
return session.commitTransaction().catch(err => {
310-
if (err instanceof MongoError && hasNotTimedOut(startTime, MAX_WITH_TRANSACTION_TIMEOUT)) {
319+
if (
320+
err instanceof MongoError &&
321+
hasNotTimedOut(startTime, MAX_WITH_TRANSACTION_TIMEOUT) &&
322+
!isMaxTimeMSExpiredError(err)
323+
) {
311324
if (err.hasErrorLabel('UnknownTransactionCommitResult')) {
312325
return attemptTransactionCommit(session, startTime, fn, options);
313326
}
@@ -364,6 +377,13 @@ function attemptTransaction(session, startTime, fn, options) {
364377
return attemptTransaction(session, startTime, fn, options);
365378
}
366379

380+
if (isMaxTimeMSExpiredError(err)) {
381+
if (err.errorLabels == null) {
382+
err.errorLabels = [];
383+
}
384+
err.errorLabels.push('UnknownTransactionCommitResult');
385+
}
386+
367387
throw err;
368388
}
369389

@@ -445,6 +465,10 @@ function endTransaction(session, commandName, callback) {
445465
Object.assign(command, { writeConcern });
446466
}
447467

468+
if (commandName === 'commitTransaction' && session.transaction.options.maxTimeMS) {
469+
Object.assign(command, { maxTimeMS: session.transaction.options.maxTimeMS });
470+
}
471+
448472
function commandHandler(e, r) {
449473
if (commandName === 'commitTransaction') {
450474
session.transaction.transition(TxnState.TRANSACTION_COMMITTED);
@@ -453,7 +477,8 @@ function endTransaction(session, commandName, callback) {
453477
e &&
454478
(e instanceof MongoNetworkError ||
455479
e instanceof MongoWriteConcernError ||
456-
isRetryableError(e))
480+
isRetryableError(e) ||
481+
isMaxTimeMSExpiredError(e))
457482
) {
458483
if (e.errorLabels) {
459484
const idx = e.errorLabels.indexOf('TransientTransactionError');

lib/core/transactions.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ class Transaction {
101101

102102
if (options.readConcern) this.options.readConcern = options.readConcern;
103103
if (options.readPreference) this.options.readPreference = options.readPreference;
104+
if (options.maxCommitTimeMS) this.options.maxTimeMS = options.maxCommitTimeMS;
104105

105106
// TODO: This isn't technically necessary
106107
this._pinnedServer = undefined;

test/functional/spec/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
}

test/functional/spec/transactions/convenient-api/commit-retry.yml

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ tests:
2121
failCommands: ["commitTransaction"]
2222
closeConnection: true
2323
operations:
24-
-
24+
- &withTransaction
2525
name: withTransaction
2626
object: session0
2727
arguments:
@@ -194,20 +194,7 @@ tests:
194194
errorCode: 10107 # NotMaster
195195
closeConnection: false
196196
operations:
197-
-
198-
name: withTransaction
199-
object: session0
200-
arguments:
201-
callback:
202-
operations:
203-
-
204-
name: insertOne
205-
object: collection
206-
arguments:
207-
session: session0
208-
document: { _id: 1 }
209-
result:
210-
insertedId: 1
197+
- *withTransaction
211198
expectations:
212199
-
213200
command_started_event:
@@ -270,3 +257,68 @@ tests:
270257
collection:
271258
data:
272259
- { _id: 1 }
260+
-
261+
description: commit is not retried after MaxTimeMSExpired error
262+
failPoint:
263+
configureFailPoint: failCommand
264+
mode: { times: 1 }
265+
data:
266+
failCommands: ["commitTransaction"]
267+
errorCode: 50 # MaxTimeMSExpired
268+
operations:
269+
- name: withTransaction
270+
object: session0
271+
arguments:
272+
callback:
273+
operations:
274+
-
275+
name: insertOne
276+
object: collection
277+
arguments:
278+
session: session0
279+
document: { _id: 1 }
280+
result:
281+
insertedId: 1
282+
options:
283+
maxCommitTimeMS: 60000
284+
result:
285+
errorCodeName: MaxTimeMSExpired
286+
errorLabelsContain: ["UnknownTransactionCommitResult"]
287+
errorLabelsOmit: ["TransientTransactionError"]
288+
expectations:
289+
-
290+
command_started_event:
291+
command:
292+
insert: *collection_name
293+
documents:
294+
- { _id: 1 }
295+
ordered: true
296+
lsid: session0
297+
txnNumber: { $numberLong: "1" }
298+
startTransaction: true
299+
autocommit: false
300+
# omitted fields
301+
readConcern: ~
302+
writeConcern: ~
303+
command_name: insert
304+
database_name: *database_name
305+
-
306+
command_started_event:
307+
command:
308+
commitTransaction: 1
309+
lsid: session0
310+
txnNumber: { $numberLong: "1" }
311+
autocommit: false
312+
maxTimeMS: 60000
313+
# omitted fields
314+
readConcern: ~
315+
startTransaction: ~
316+
writeConcern: ~
317+
command_name: commitTransaction
318+
database_name: admin
319+
outcome:
320+
collection:
321+
# In reality, the outcome of the commit is unknown but we fabricate
322+
# the error with failCommand.errorCode which does not apply the commit
323+
# operation.
324+
data: []

0 commit comments

Comments
 (0)