mirror of
https://github.com/doctrine/orm.git
synced 2026-03-23 22:42:18 +01:00
UnitOfWork should run UPDATES and DELETES in specific order to avoid ShareLocks on concurrent db transactions during commit() #7340
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @d-ph on GitHub (Mar 8, 2024).
Feature Request
Summary
I would like to propose that the
UnitOfWork::commit()should run UPDATEs (and ideally DELETEs too) in a certain order, so that the risk of running into deadlocks on "waiting for ShareLock on transaction X" (in even moderately concurrent situations) is vastly reduced. I would like to propose that the UPDATE statements should be ordered alphabetically by the FQCNs of the entities being updated, and then ascending by the ids of the entities.In other words, I would like to propose that the UnitOfWork should run the following updates in the following order:
By ordering the statements as described above, an attempt is made to avoid the following error from happening (error log from postgres, but there is a rumour that it would happen in mysql/mariadb also):
Reproduction of the problem using a real-world scenario
The problem can be reproduced by simulating a "long flush" using the
sleep()instruction in the following way:Running the code in two browser tabs, one with the
casequery-parameter set to 'a', and one with the same parameter set to 'b', leads to a situation where both tabs run for 4 seconds ("tab a" sleeps for 4 seconds, and "tab b" is sync-blocked by postgres when the tab attempts to run the "UPDATE users SET 200" statement), and then one of the tabs crashes, and the other one succeeds. The crash happens because "tab a" wakes up and attempts to run "UPDATE order SET 100" for a row which is already "locked for write" in a db transaction owned by "tab b". Postgres detects that deadlock and immediately terminates one of the conflicting db transaction.This is also explained in postgres documentation (however again: please note that this is not only postgres-specific): https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-DEADLOCKS
The severity of this problem
This problem is bound to happen sooner than later, when a dev starts using cron scripts or implements a webhook events retriever from a 3rd party api. This leads to a situation where the same rows in the database (especially of the "user" or "order" kind) may be attempted to be UPDATEd concurrently (because e.g. the website user makes changes in some sort of "My Account" panel, but at the same time a webhook event is received from a 3rd party api that attempts to UPDATE the same user record).
Note how this is not only about updating fields that are indexed or that are primary/foreign keys. A simultaneous update of a "plain & boring" text field in a record will lock that record for writing for as long a db transaction lasts (and the UnitOfWork runs commit() in a db transaction (for a very good reason)).
Also, note how currently the order of UPDATEs issued by the
UnitOfWork::commit()is determined by the order of when entities are hydrated in the app. This makes it quite difficult to reproduce reliably in complex codebases, but the db error logs don't lie, and the "ShareLock deadlocks" do happen.Further notes
As mentioned in the summary, the DELETE statements are also likely to be subject to the same problem, and therefore would also benefit from a similar ordering. However, I do see how DELETE statements would be harder to order than UPDATEs because DELETEs are already being ordered. I wouldn't say that it's impossible to "order them even further", but just ordering UPDATES would alone reduce the problem significantly.
I would love to hear your comments to this proposition.
Thank you.
@greg0ire commented on GitHub (Mar 9, 2024):
I don't see an issue with this proposal except for DELETE statements, where this cannot be enforced in all situations. Cc @mpdude
@Shaddix666 commented on GitHub (Dec 24, 2024):
@d-ph
Although I fear that this is a bit late after 9 months, I have a little Christmas present.
We had the same problem and came to the conclusion that we simply have to do the locks ourselves in the correct order. This way it doesn't matter whether Doctrine takes care of a specific lock order or not.
Pseudocode:
The fix has not yet been implemented in production for us, but we are confident that this is a good way to go.
I hope these tips will be helpful.
Merry Christmas and good luck!