Skip to content

Commit

Permalink
feat(transaction): allow applications to set maxTimeMS for commitTran…
Browse files Browse the repository at this point in the history
…saction

Fixes NODE-1978
  • Loading branch information
kvwalker authored and daprahamian committed Aug 13, 2019
1 parent b0a4a0c commit b3948aa
Show file tree
Hide file tree
Showing 10 changed files with 1,025 additions and 42 deletions.
35 changes: 30 additions & 5 deletions lib/core/sessions.js
Expand Up @@ -287,6 +287,7 @@ class ClientSession extends EventEmitter {
const MAX_WITH_TRANSACTION_TIMEOUT = 120000;
const UNSATISFIABLE_WRITE_CONCERN_CODE = 100;
const UNKNOWN_REPL_WRITE_CONCERN_CODE = 79;
const MAX_TIME_MS_EXPIRED_CODE = 50;
const NON_DETERMINISTIC_WRITE_CONCERN_ERRORS = new Set([
'CannotSatisfyWriteConcern',
'UnknownReplWriteConcern',
Expand All @@ -299,15 +300,27 @@ function hasNotTimedOut(startTime, max) {

function isUnknownTransactionCommitResult(err) {
return (
!NON_DETERMINISTIC_WRITE_CONCERN_ERRORS.has(err.codeName) &&
err.code !== UNSATISFIABLE_WRITE_CONCERN_CODE &&
err.code !== UNKNOWN_REPL_WRITE_CONCERN_CODE
isMaxTimeMSExpiredError(err) ||
(!NON_DETERMINISTIC_WRITE_CONCERN_ERRORS.has(err.codeName) &&
err.code !== UNSATISFIABLE_WRITE_CONCERN_CODE &&
err.code !== UNKNOWN_REPL_WRITE_CONCERN_CODE)
);
}

function isMaxTimeMSExpiredError(err) {
return (
err.code === MAX_TIME_MS_EXPIRED_CODE ||
(err.writeConcernError && err.writeConcernError.code === MAX_TIME_MS_EXPIRED_CODE)
);
}

function attemptTransactionCommit(session, startTime, fn, options) {
return session.commitTransaction().catch(err => {
if (err instanceof MongoError && hasNotTimedOut(startTime, MAX_WITH_TRANSACTION_TIMEOUT)) {
if (
err instanceof MongoError &&
hasNotTimedOut(startTime, MAX_WITH_TRANSACTION_TIMEOUT) &&
!isMaxTimeMSExpiredError(err)
) {
if (err.hasErrorLabel('UnknownTransactionCommitResult')) {
return attemptTransactionCommit(session, startTime, fn, options);
}
Expand Down Expand Up @@ -364,6 +377,13 @@ function attemptTransaction(session, startTime, fn, options) {
return attemptTransaction(session, startTime, fn, options);
}

if (isMaxTimeMSExpiredError(err)) {
if (err.errorLabels == null) {
err.errorLabels = [];
}
err.errorLabels.push('UnknownTransactionCommitResult');
}

throw err;
}

Expand Down Expand Up @@ -445,6 +465,10 @@ function endTransaction(session, commandName, callback) {
Object.assign(command, { writeConcern });
}

if (commandName === 'commitTransaction' && session.transaction.options.maxTimeMS) {
Object.assign(command, { maxTimeMS: session.transaction.options.maxTimeMS });
}

function commandHandler(e, r) {
if (commandName === 'commitTransaction') {
session.transaction.transition(TxnState.TRANSACTION_COMMITTED);
Expand All @@ -453,7 +477,8 @@ function endTransaction(session, commandName, callback) {
e &&
(e instanceof MongoNetworkError ||
e instanceof MongoWriteConcernError ||
isRetryableError(e))
isRetryableError(e) ||
isMaxTimeMSExpiredError(e))
) {
if (e.errorLabels) {
const idx = e.errorLabels.indexOf('TransientTransactionError');
Expand Down
1 change: 1 addition & 0 deletions lib/core/transactions.js
Expand Up @@ -101,6 +101,7 @@ class Transaction {

if (options.readConcern) this.options.readConcern = options.readConcern;
if (options.readPreference) this.options.readPreference = options.readPreference;
if (options.maxCommitTimeMS) this.options.maxTimeMS = options.maxCommitTimeMS;

// TODO: This isn't technically necessary
this._pinnedServer = undefined;
Expand Down
100 changes: 100 additions & 0 deletions test/functional/spec/transactions/convenient-api/commit-retry.json
Expand Up @@ -423,6 +423,106 @@
]
}
}
},
{
"description": "commit is not retried after MaxTimeMSExpired error",
"failPoint": {
"configureFailPoint": "failCommand",
"mode": {
"times": 1
},
"data": {
"failCommands": [
"commitTransaction"
],
"errorCode": 50
}
},
"operations": [
{
"name": "withTransaction",
"object": "session0",
"arguments": {
"callback": {
"operations": [
{
"name": "insertOne",
"object": "collection",
"arguments": {
"session": "session0",
"document": {
"_id": 1
}
},
"result": {
"insertedId": 1
}
}
]
},
"options": {
"maxCommitTimeMS": 60000
}
},
"result": {
"errorCodeName": "MaxTimeMSExpired",
"errorLabelsContain": [
"UnknownTransactionCommitResult"
],
"errorLabelsOmit": [
"TransientTransactionError"
]
}
}
],
"expectations": [
{
"command_started_event": {
"command": {
"insert": "test",
"documents": [
{
"_id": 1
}
],
"ordered": true,
"lsid": "session0",
"txnNumber": {
"$numberLong": "1"
},
"startTransaction": true,
"autocommit": false,
"readConcern": null,
"writeConcern": null
},
"command_name": "insert",
"database_name": "withTransaction-tests"
}
},
{
"command_started_event": {
"command": {
"commitTransaction": 1,
"lsid": "session0",
"txnNumber": {
"$numberLong": "1"
},
"autocommit": false,
"maxTimeMS": 60000,
"readConcern": null,
"startTransaction": null,
"writeConcern": null
},
"command_name": "commitTransaction",
"database_name": "admin"
}
}
],
"outcome": {
"collection": {
"data": []
}
}
}
]
}
82 changes: 67 additions & 15 deletions test/functional/spec/transactions/convenient-api/commit-retry.yml
Expand Up @@ -21,7 +21,7 @@ tests:
failCommands: ["commitTransaction"]
closeConnection: true
operations:
-
- &withTransaction
name: withTransaction
object: session0
arguments:
Expand Down Expand Up @@ -194,20 +194,7 @@ tests:
errorCode: 10107 # NotMaster
closeConnection: false
operations:
-
name: withTransaction
object: session0
arguments:
callback:
operations:
-
name: insertOne
object: collection
arguments:
session: session0
document: { _id: 1 }
result:
insertedId: 1
- *withTransaction
expectations:
-
command_started_event:
Expand Down Expand Up @@ -270,3 +257,68 @@ tests:
collection:
data:
- { _id: 1 }
-
description: commit is not retried after MaxTimeMSExpired error
failPoint:
configureFailPoint: failCommand
mode: { times: 1 }
data:
failCommands: ["commitTransaction"]
errorCode: 50 # MaxTimeMSExpired
operations:
- name: withTransaction
object: session0
arguments:
callback:
operations:
-
name: insertOne
object: collection
arguments:
session: session0
document: { _id: 1 }
result:
insertedId: 1
options:
maxCommitTimeMS: 60000
result:
errorCodeName: MaxTimeMSExpired
errorLabelsContain: ["UnknownTransactionCommitResult"]
errorLabelsOmit: ["TransientTransactionError"]
expectations:
-
command_started_event:
command:
insert: *collection_name
documents:
- { _id: 1 }
ordered: true
lsid: session0
txnNumber: { $numberLong: "1" }
startTransaction: true
autocommit: false
# omitted fields
readConcern: ~
writeConcern: ~
command_name: insert
database_name: *database_name
-
command_started_event:
command:
commitTransaction: 1
lsid: session0
txnNumber: { $numberLong: "1" }
autocommit: false
maxTimeMS: 60000
# omitted fields
readConcern: ~
startTransaction: ~
writeConcern: ~
command_name: commitTransaction
database_name: admin
outcome:
collection:
# In reality, the outcome of the commit is unknown but we fabricate
# the error with failCommand.errorCode which does not apply the commit
# operation.
data: []

0 comments on commit b3948aa

Please sign in to comment.