-
Notifications
You must be signed in to change notification settings - Fork 592
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
Cloud Spanner Client should prevent nested transactions #2361
Comments
@stephenplusplus correct me if I'm wrong, but I'm not sure this is technically feasible? We can block transactions from being started in the event that one is already running, but I think that would imply that a user would not be able to do parallel transactions which sounds like it could be an unwanted side effect. |
Specifically there's a problem with retrying a block of transaction code that contains a nested transaction. If you have two parallel transactions in different retry blocks, or don't automatically retry either of them, then I don't think there's any problem. |
Yes we do not want to prevent parallel transactions. I am not sure how this would be achieved in node.js since its all asynchronous. In other languages we can store something in thread local. In Go, there is some context object being passed around so we can track it in that. |
I'm also not able to think of a way to technically achieve this at the moment. Some thoughts that crossed my mind:
Any other thoughts are welcome. |
@swcloud Can you help @stephenplusplus and @vkedia figure out how to fix this problem for Node Spanner? It's Beta blocking. |
With a requirement to make the solution future proof, this is even harder. Adding @ofrobots to see if he has some idea. |
What we really need here is tracking of asynchronous context, similar to what we do in Stackdriver Trace through the use of The main problem is there does not exist a completely reliable mechanism in JavaScript to do async context propagation. Arbitrary userspace code can break async context propagation (examples, more details). We just ran into a real world case of context loss this earlier this week. This is not a problem for diagnostic tools like Stackdriver Trace, where occasional incorrectness is not fatal. However, for a database library like Spanner, I would suspect we need 100% reliability. We are bound by the fundamental limitations of the JavaScript execution model as it stands today. I would be very wary of the sandboxing as suggested in this comment for the reasons already mentioned. I do not think we can reliably prevent the code (or the functions called from that code) from getting its hands onto an unsandboxed I do have one potential suggestion though. Is it acceptable for us to restrict the JS API visible to the callback of What I am thinking is that we could impose that the contents of the This would be relevant to @matthewloring as well. |
I think the database.runTransaction((err, t1) => {
t1.end(() => {
database.runTransaction((err, t2) => {});
});
}); One alternative would be for database.runTransaction(() => {
transaction.queryAndWhatnot();
done();
}, () => {
// continue with application logic
}); This could probably be implemented something like: Database.prototype.runTransaction = function(executeTransaction, continuation) {
const t = constructTransaction();
const sandbox = { transaction: t, done: () => { t.end(continuation); } };
vm.runInNewContext('(' + executeTransaction.toString() + ')();', sandbox);
}; Injecting variables "magically" like Introducing a transaction builder could also help this problem. Transactions could be constructed before the |
To clarify, is the callback passed to |
I actually had
That is what I'd lean towards. Something like: var database = instance.database('my-db')
var transaction = database.transaction()
transaction.run(function(err) {
// ..valid transaction commands...
transaction.run(function(err) {})
// throws: "Transaction already in progress. Nested transactions are not allowed."
}) This way, surprises are suppressed, and the user is not alienated from the environment they've configured for their application. Would this work? The user could still do: var database = instance.database('my-db')
var transaction1 = database.transaction()
transaction1.run(function(err) {
// ..valid transaction commands...
var transaction2 = database.transaction()
transaction2.run(function(err) {})
// Would not throw, because `transaction2` doesn't know about `transaction1`
}) |
@stephenplusplus how would you prevent one from writing something like this: var database = instance.database('my-db')
var transaction = database.transaction()
transaction.run(function(err) {
// ..valid transaction commands...
setImmediate( () => {
var t = database.transaction();
t.run(...);
});
}) |
Can't, I mentioned that at the bottom. The goal was to make that API use feel disconnected to the point the user would intuitively feel wrong about it. But, if I didn't hit the mark, maybe @matthewloring could show the code that supports his suggestion. |
Ah. My browser wasn't showing the edited version of your comment. |
I am not sure if the possibility of accidental nested transactions is acceptable. In real world cases, I suspect it would happen somewhat like this: transaction.run(function(err) {
// ..valid transaction commands...
otherFile.doSomething(); // could it start a transaction?
}) I don't think we should give the user an API that requires them to prove correctness of their program at a non-local level. |
We're in a battle with an API requirement that doesn't suit the language. This means our solutions will be a battle of the lesser evil. In a new context, the user can't use their project's own dependencies or even their own functions they've defined, etc. That's a very restricting environment we are forcing on all users, and one that will inevitably be a cause of confusion, resulting in SO/GH issues that lead us back to another discussion about finding a better alternative. I don't consider "users assuming a nested transaction works, when it actually doesn't" worth such a harsh solution that affects even the users of our library who understand proper API usage (the assumed majority, right?). |
I setup a meeting for all of us to discuss this issue. The earliest time I could find available on our calendars is next Tuesday at 4:30p PST. Does that work for either of you @stephenplusplus or @callmehiphop? |
I see a spectrum of options here, in order of increasing guarantees of correctness:
Ultimately the answer depends quite a bit upon the end users, and what they might value from the Spanner API. Some folks might be willing to tradeoff convenience for strong guarantees – others might not. I don't have a good feeling for this right now. |
Actually thinking a bit more about 3, I think we may be able to detect the cases where we might be in a blind spot. Specifically, when I am not sure we can get to provable guarantees of safety however, but I think we should be able to build a pretty high confidence. /cc @matthewloring: Thoughts? |
If we stored the current transaction or some flag on the CLS/Zone context there are a few possible behaviors:
The CLS/Zones approach scares me because a misattributed context that happens to have a transaction defined will prevent valid transactions from executing. This seems like an unacceptable failure mode (an incomplete solution may be alright, an unsound one likely is not). |
You listed two scenarios in how context can be 'confused'. Let's call them 'context loss' and 'context misattribution'. I'll make a few assertions:
The question ultimately becomes whether this is acceptable risk. Perhaps we need to offer both this solution along with a safe sandboxing, albeit extremely onerous, solution and let users manage their risk. |
Great discussion. If we can settle this issue here, then I'll cancel the meeting for next week. I agree with the principle that we should err on the side of negative errors (i.e. not catching a nested transaction, rather than erroneously catching a non-nested transaction). It's probably okay if positive errors (the latter case) occur due to some really unusual design pattern, but if we can detect and warn about this pattern, then I think we'll be okay. |
+1 to what @bjwatson said. We should never erroneously prevent non nested transaction. And letting some nested transaction slip through the crack is ok. Intent here is not to strictly prohibit nested transactions but to prevent it in the common cases. |
@ofrobots @matthewloring given these points above, which solution sounds like the best path? |
I rescheduled this meeting for tomorrow morning so that @matthewloring can attend. |
@stephenplusplus @bjwatson ping. What's the status of this issue? |
We decided that the best we can do in Node is warn the user about nested transactions when we believe one has occurred. We cannot block it, because it's too hard / impossible to avoid false positives (i.e. a transaction that appears to be nested, but really isn't). Since adding warnings is not really a breaking change, we're not blocking Beta on this issue. |
Ack. TY. |
I just want to get a clear "TODO" for this issue. I think it was settled that a warning would be our only action here, but in a Node.js application, are we talking about printing to |
@stephenplusplus At a minimum, we need to ensure our documentation is clear about this. We were also talking about logging a warning if we detect a likely violation. Thanks for pointing out the language/cultural issue with |
The |
PR opened at #2517 -- please take a look! |
Cloud Spanner does not support nested transactions. But the client library does allow users to call
Database.runTransaction
inside of the callback that is provided to an outerDatabase.runTransaction
. This is misleading since users might believe that this will behave like a nested transaction but in reality these will be two independent transactions. This is confusing and a source of bugs. Specifically the inner transaction might succeed while the outer might ABORT, in which case the callback will be rerun thus rerunning the inner transaction.We should prevent this from happening by detecting and raising an error if someone calls
Database.runTransaction
inside the callback provided toDatabase.runTransaction
. Note that this needs to be done before Beta since this is a known breaking change.Also note that if we come across a use case where we do want to allow calling a nested runTransaction, we should be able to enable that in future without making a breaking change.
cc @jgeewax @bjwatson
The text was updated successfully, but these errors were encountered: