Skip to content

Conversation

zbarbuto
Copy link
Contributor

@zbarbuto zbarbuto commented Mar 20, 2017

Description

Issue

This fixes a pretty critical issue where operations can occur on non-transactional connections when the user expects them to land in a transaction. Tests added which fail on master but pass here.

Example use case:

  • Begin a transaction.
  • Perform some creates on the transaction
  • An external event (e.g. timeout) triggers a rollback
  • Additional creates are allowed to continue despite the transaction no longer being active
  • These creates still hit the database when the programmer expects them not to as the transaction has rolled back (finished)

Solution:

I've added a uuid to each transaction which exists on the transaction itself and is tagged onto the current connection. Whenever the transaction is committed or rolled back ('finished') the id is removed from the connection but remains on the transaction. Any attempts to execute sql on the transaction checks the transaction id against the specified connection and will fail if the ids do not match.

Related issues

Checklist

  • New tests added or existing tests modified to cover all changes
  • Code conforms with the style
    guide
@slnode
Copy link

slnode commented Mar 20, 2017

Can one of the admins verify this patch?

2 similar comments
@slnode
Copy link

slnode commented Mar 20, 2017

Can one of the admins verify this patch?

@slnode
Copy link

slnode commented Mar 20, 2017

Can one of the admins verify this patch?

@zbarbuto
Copy link
Contributor Author

@zbarbuto
Copy link
Contributor Author

I suppose the id need not be a uuid. Could easily be an incrementing integer. Open to opinions.

@zbarbuto
Copy link
Contributor Author

Not sure what was happening with the failed tests on node 4.x and 6.x - all passing here on my end 7.x but have fixed the eslint errors in the test files.

@jannyHou jannyHou self-assigned this Mar 21, 2017
@zbarbuto
Copy link
Contributor Author

@jannyHou Any idea what might be causing the failures? Is it something I can fix up?

@zbarbuto
Copy link
Contributor Author

Seems to be unrelated to this PR:

transactions bulk.should work with bulk transactions
sorry, too many clients already

What is the connection limit on the build server's postgres?

var tx = new Transaction(connector, connection);
tx.txId = uuid.v1();
connection.txId = tx.txId;
cb(null, tx);
Copy link
Contributor

@jannyHou jannyHou Mar 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think here we should callback connection instead of tx?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal here is to make the new connection and transaction and say "this connection is currently working on this transaction" allowing for the error if you pass in a transaction with a connection id that doesn't match.

If you callback with connection, loop back connector will make a transaction out of it and pas it up the chain. This means you how no tie between the id of the connection and the current transaction.

@jannyHou
Copy link
Contributor

@slnode test please

@jannyHou
Copy link
Contributor

@zbarbuto In my opinion the expected behaviour is:
If we do tx.commit(), then use that expired tx in other operations like create, postgresql db should return us an error.
NOT
We add a uuid to check the transaction still active on our end

So if the example use case you described in the issue happens, it looks like a bug of nodejs postgresql driver to me :(

@raymondfeng any thought?

@jannyHou jannyHou requested a review from raymondfeng March 23, 2017 16:17
@zbarbuto
Copy link
Contributor Author

zbarbuto commented Mar 23, 2017

I don't think it's a problem with the driver since the driver doesn't actually handle transactions natively. You're just executing normal transaction sql on a single connection:

https://github.com/brianc/node-postgres/wiki/Transactions

This assumes you're managing the connection in such a way that no other operations can execute on it until the transaction has resolved. Their example does this by not passing the connection outside the callback chain.

In the case of the loop back connector, the connection gets released from the transaction but nothing notifies an ongoing promise chain that the connection being used is no longer part of a transaction. If it doesn't error out creates, they just end up in the database which is very dangerous and can result in orphaned records:

ModelA.create(tx) .then(() => ModelB.create(tx)) // Tx times out here .then(() => JoinTable.create({ model a, model b, tx})

If the transaction rolls back, a and b don't his the database (they were part of the transaction) but the join between them does (it was on a connection that no longer has an active transaction). Surely this can't be expected behaviour.

Copy link
Contributor

@jannyHou jannyHou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM looking into the CI failure reason

@jannyHou
Copy link
Contributor

@slnode test please

@jannyHou jannyHou force-pushed the create-on-ended-tx branch from 2fd53d9 to 0bf6311 Compare March 24, 2017 21:14
@jannyHou
Copy link
Contributor

jannyHou commented Mar 24, 2017

@zbarbuto I think the default poolSize is 10, see https://github.com/brianc/node-postgres/blob/master/lib/defaults.js#L40, while increasing the limit doesn't solve the issue but make it worse...

And my commit is testing the limit increase, sorry if that confuses you, feel free to remove it if you don't need.

@zbarbuto
Copy link
Contributor Author

Thanks for trying. The test that is failing is one that I added a little while ago so strange that it starts failing now.

Just waiting on approval from @raymondfeng?

Did you want me to switch from uuid to an incrementing value to avoid unnecessary dependencies?

@siddhipai siddhipai added review and removed review labels Mar 27, 2017
transaction.connector === this) {
if (transaction && transaction.connector === this) {
if (!transaction.connection) {
return callback(new Error(g.f('Connection does not exist')));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please wrap it into process.nextTick or setImmediate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

return callback(new Error(g.f('Connection does not exist')));
}
if (transaction.txId !== transaction.connection.txId) {
return callback(new Error(g.f('Transaction is not active')));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please wrap it into process.nextTick or setImmediate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@zbarbuto zbarbuto mentioned this pull request Mar 28, 2017
2 tasks
@zbarbuto
Copy link
Contributor Author

No further ideas on why the tests might be failing?

@jannyHou
Copy link
Contributor

@zbarbuto I am making time to debug why done() is called multiple times, and why it complains too many connections....

@jannyHou jannyHou force-pushed the create-on-ended-tx branch from 385c9c8 to 2876a95 Compare March 30, 2017 18:54
@jannyHou
Copy link
Contributor

jannyHou commented Apr 6, 2017

The same failure also happens on master branch, it fails on only one machine and I set up a windows vm using same environment, but could not reproduce the error.

I will create another PR just debug why it fails. Sorry for the delay of checking this pr in, it looks good but we have to fix the test :)

@zbarbuto
Copy link
Contributor Author

zbarbuto commented Apr 6, 2017

No problems. Thanks for your help @jannyHou

@jannyHou jannyHou mentioned this pull request Apr 9, 2017
@ssh24 ssh24 force-pushed the create-on-ended-tx branch from b139e70 to 224240b Compare April 10, 2017 21:53
@jannyHou jannyHou force-pushed the create-on-ended-tx branch from 224240b to 1361460 Compare April 13, 2017 21:22
@jannyHou jannyHou merged commit 039b206 into loopbackio:master Apr 13, 2017
@jannyHou jannyHou removed the review label Apr 13, 2017
@jannyHou jannyHou modified the milestones: Sprint 33, Sprint 33 - Apex Apr 13, 2017
@jannyHou jannyHou added the apex label Apr 13, 2017
@dhr00v
Copy link

dhr00v commented Apr 23, 2018

@zbarbuto I have one question.

https://github.com/zbarbuto/loopback-connector-postgresql/blob/1361460cea86fadd14a68aa8fe9c55f14ecb9a6d/lib/transaction.js#L58-L86

What if 'ROLLBACK' query is executed, but the callback hasn't been called. So basically releaseConnection hasn't been called and connection.txId is not null. So at this time, any other query in the same transaction can still go through, right? Shouldn't we move connection.txId in rollback function itself?

@zbarbuto
Copy link
Contributor Author

@dhr00v Wh would the callback not be executed if ROLLBACK was? Could you put together an example repo for an instance where this might be the case?

@sahilarora946
Copy link

@zbarbuto After the ROLLBACK was executed, callback will be sent to the event loop to be executed. Event loop can pick any of the available callbacks to execute. It can pick another function that can do database calls in the same transaction and because releaseConnection has not been called, the statement will execute.

@zbarbuto
Copy link
Contributor Author

Hmm - I see what you're saying. Do you have a proposed solution for this? Would you be willing to submit a PR?

@sahilarora946
Copy link

Ok. I will submit a PR for this @zbarbuto

@sahilarora946
Copy link

sahilarora946 commented Apr 24, 2018

@zbarbuto kindly refer to #330

@dhmlau
Copy link
Member

dhmlau commented May 25, 2018

Fixed in #330, which is released as loopback-connector-postgresql@3.3.2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

10 participants