-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
Fixed nested transactions #1226
Conversation
Before, if we had parent transaction A, and two nested sibling transactions inside it: B1 & B2, knex would hang when you asked it to execute any kind of a database operation in the second child transaction. Also, it had a bad Promise.settle() call that would break in case the previous child transaction promise not resolve to an array, which typically (e.g. when calling trx.commit() on the passed transaction object or resolving the transaction promise with no value) it would not.
- removed an unnecessary `||` with an always `undefined` value - avoided waiting for an always resolved promise
The only thing each transaction needs to track is its last direct child transaction. That is then used to prevent each sibling transaction from starting (i.e. returning its connection) before its predecessor transaction completed.
- improved related comments - renamed `trx._queue` to `trx._previousSibling` - made code waiting for `trx._previousSibling`, instead of code initializing that promise, more explicit about waiting for the promise to be either resolved or rejected, i.e. `settled`/`completed` - made the code a bit more compact
Note that this pull request does not attack the issue of why we'd want to try to synchronize sibling nested transactions in the first place, but only makes sure that the logic already implemented no longer breaks. The current code, due to the way transactions perform their |
@jurko-gospodnetic I'm not too familiar with the code for transactions, but the changes here makes sense to me and it seems you thoroughly tested this with several new tests and also updated the inline documentation so I am perfectly fine with merging this. Thanks! |
@@ -238,7 +222,7 @@ function makeTxClient(trx, client, connection) { | |||
}) | |||
} | |||
trxClient.acquireConnection = function() { | |||
return trx._queue.then(function() { | |||
return Promise.settle([trx._previousSibling]).then(function () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it ok here to continue if _previousSibling
did fail? Edit: looks like previous implementation did use settle too...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This behaviour is unchanged. And from how I read the original code, the idea is that if we have a set of transactions like this:
TP (parent)
- TS1 (first sibling)
- TS2 (second sibling)
created using code like this:
knex.transaction(function (tp) {
tp.transaction(function (ts1) {
... // returns a promise indicating when ts1 completes
});
tp.transaction(function (ts2) {
... // returns a promise indicating when ts2 completes
});
});
TS2
should be allowed to proceed (i.e. acquire its connection so it can perform actual database operations) only after TS1
has completed its work. Whether TS1
got committed or rolled back.
If we want TS2
to be run only if TS1
gets committed, then we should create TS2
in a .then()
block chained to the TS1
resolution promise, and not just create it as a completely unrelated sibling transaction. i.e. they should created using code something like this:
knex.transaction(function (tp) {
tp.transaction(function (ts1) {
... // returns a promise indicating when ts1 completes
})
.then(function () {
return tp.transaction(function (ts2) {
... // returns a promise indicating when ts2 completes
});
}
});
The original code for handling nested transactions did not work correctly when you had sibling nested transactions, i.e. two or more transactions all living inside the same parent transaction and latter ones getting started after their predecessor sibling completes (rolls back or commits). In such cases, knex would hang on any DB operation attempted using a non-initial sibling transaction.
Basic cause was in knex accidentally waiting for that same transaction to complete before allowing someone else to access the transaction's database connection. 😈
Basic bug fixed in d0836b8.
Detailed tests added for handling sibling tested transactions in 675c684. You can run those tests to reproduce the hang if you run them without the fix applied.
The three later commits (e0e77df, f38f901, 675c684) then just clean up the related code. And in the end the code is simplified enough that the original fix becomes unnecessary to begin with. 😄
Hope this helps.
Best regards,
Jurko Gospodnetić