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
Concurrent nested transactions are not compatible with connection pool of finite size #10976
Comments
Can you create a MCVE that does not rely on CLS, please? |
You mean to remove that comment about CLS? Ok done. Result is exactly same whether I setup sequelize with or without CLS. I just copied part of example from sequelize documentation with headings "Concurrent/Partial transactions" and "Without CLS enabled". |
Oh, sorry about that. My mistake. Thanks!! |
FWIW, native "nested" transactions for mysql, mariadb and other dbs are approximated within the same connection by using SAVEPOINT foo ... ROLLBACK TO foo, support for those would be wonderful. |
This issue has been automatically marked as stale because it has been open for 14 days without activity. It will be closed if no further activity occurs within the next 14 days. If this is still an issue, just leave a comment or remove the "stale" label. 🙂 |
Should we now ping every one of our issues every 14 days? (This comment did not remove the "stale" label.) |
The bot will remove the stale label when it runs again tonight. There is work being done that you don't have to ping every one of your issues every 14 days. That is part of #13648 |
I believe the updated documentation & added transaction features covers the issues raised in this issue: https://sequelize.org/docs/v7/querying/transactions/#nested-transactions If not, please let us know |
@ephys It is same as before, nothing changed, there is still deadlock probably due to exactly same reasons as before, sequelize version 6.37.1:
|
The documentation in question is Sequelize 7, this can't be changed in Sequelize 6 In the new version, nesting transactions re-uses the same transaction by default. Creating a new transaction requires opting into that nest mode, as shown here. In this instance, if you actively opt into creating separate but dependent transactions until you run out of connections, you will have a deadlock. It's not something we can stop you from doing, but it's using transactions incorrectly. We can only provide safer defaults and document the risks of the alternative solutions, which is what we did for v7 |
@ephys How is that usage incorrect? It results in deadlock only because of internal implementation detail of sequelize that is not documented. I do not see this in that documentation (something like warning we use a pool of 4 connections by default so do not use more than 4 nested transactions). Actually sequelize could also detect this and just throw some error instead of letting users to fumble what might be wrong. What if the user is not under direct control like they need to call some external package function which already uses 4 nested transactions with |
All transactions run on their own database connection. That's not a Sequelize implementation detail, that's a pretty hard rule for most if not all SQL databases All databases also have a limit on the number of concurrent connections, that's also not a Sequelize implementation detail. This is why not limiting the pool size if not a possible solution, nor is having a different pool per nest level, as you can have an infinite number of nest levels. You're running transactions that are separate from each-other but dependent on each-other. If you have too many of them that are nested, you are going to reach that limit. If you search for
If you can see a way to reliably detect this issue, please feel free to open a pull request. The only actual solution to this problem would be to reserve multiple connections before calling the transactions. Something like // this could actually detect that the number of connections being requested at the same time is bigger than the pool size
sequelize.withConnections(2, connections => {
// will only run once 2 connections become available at the same time
return sequelize.transaction({ connection: connections[0] }, () => {
return sequelize.transaction({ connection: connections[1] }, () => {});
});
}); |
@ephys Even if there was not any limit enforced by the database there would always be some limit, it is not possible to have infinite many connections on any computer. I meant that having pool in a way sequelize has is implementation detail. Some more naive implementation could for example create a new connection for each new transaction and when its limit is reached you would get some clear error message instead of deadlock. Some more overthought implementation could track connections and detect deadlock (each connection can have assigned a transaction which is actually doing and there can be a graph of nested transactions) then just report error or to try to create a new connection and if it fails (due to database limit for example, or maybe better some additional sequelize limit for extra connection count) then report error. So this definitely is implementation detail of sequelize that it just deadlocks in such cases. And |
That's already what happens. The code you provided does not hang forever. It errors with a message saying that it could not acquire a connection in the specified time.
I expect that the code required to track this would be very complex, very costly, and it wouldn't even solve the underlying issue, only change the error message. Something more general that is able to map the inter-dependencies between connections would be great, but the runtime cost would be huge. Feel free to prove me wrong with a technical solution on how this would work
We're not going to go over the limit specified by the user, that's simply not on the table. It's there for a reason and must be reliable.
Agree to disagree, it would deadlock (or lead to an error, as we do) when you reach the connection limit in any implementation even if you did not use the ORM. Has nothing to do with our implementation details.
If you have way too few connections for your database usage, sure. It would safely solve the actual issue though, not just change the error message. |
User needs to wait a few minutes for that.
Sequelize functions need to detect that they are executed inside a transaction anyway through CLS or by manually passing the transaction. So why not utilize this? Nested transaction then knows that it is executed inside another transaction. This can be used to build the mentioned graph. Now when you have such a graph and also each connection holds info about which transaction it currently executes you can check whether there is a deadlock. For example like in this pseudocode (very rough and simplified, just for illustration): function sequelize.transaction(callback)
{
let connection = null;
for(connection in connections)
{
if(connection.free())break;
if(!cls.transactions_waiting_on_this.contains(connection.transaction))break;
}
if(connection == null)throw SomeError;
await connection.ready();
current_transaction = ...
connection.transaction = current_transaction;
cls.transactions_waiting_on_this.push(current_transaction);
await callback(current_transaction);
cls.transactions_waiting_on_this.pop();
}
That is why there could be 2 limits actually, soft limit like number of connections in pool and hard limit which is never going to be gone over, they can be set to the same value by default.
I meant some other implementation could report an error immediately, not after a few minutes while waiting in a deadlock.
Yes, maybe. Just notice that default in sequelize are just 4 connections. And the user needs to manually count how many nested transactions are needed, if that counted number is less then there is deadlock again. |
What are you doing?
When using connection pool with maximum number of connections then there should not be "dependencies" between connections or else deadlock can happen. I suppose concurrent nested transactions is feature of sequelize as they are mentioned in documentation: http://docs.sequelizejs.com/manual/transactions.html#concurrent-partial-transactions
I borrowed example from documentation, deleted some lines and created function
nested
from it:Output can be similar to this:
Naive solution would be to increase maximum number of connections but none finite value will work well in theory when number of concurrent requests/calls to function
nested
is also not limited so the best solution would be to not limit number of connections in pool or to not use concurrent nested transactions. But then is lost benefit of having limited number of db connections. Sequelize should see these types of dependencies and maybe is possible to avoid such deadlocks. Maybe multiple pools could help one for each nesting and option in seuqelize config for (maximum) number of pools or create new pools automatically to avoid deadlocks (or resize pool temporarily above max number of connections in case of deadlock).To Reproduce
Steps to reproduce the behavior:
What do you expect to happen?
To not see deadlock and this error.
What is actually happening?
Deadlock and sequelize error.
Environment
Dialect:
Sequelize version: 5.8.6
The text was updated successfully, but these errors were encountered: