-
Notifications
You must be signed in to change notification settings - Fork 162
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
Lock the parent object to prevent deadlocks updating most_recent #228
base: master
Are you sure you want to change the base?
Conversation
Oh, note that even with this commit, running that script twice simultaneously will still give you a
And that exception is a lot better than a deadlock! |
@pjungwir sorry for the late response! This looks pretty legit to me - I can reproduce the deadlock and your fix seems to do the trick. Will do a bit more testing on this and hopefully merge it soon. |
@@ -70,7 +70,7 @@ def create_transition(from, to, metadata) | |||
|
|||
transition = transitions_for_parent.build(transition_attributes) | |||
|
|||
::ActiveRecord::Base.transaction do | |||
@parent_model.with_lock do |
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 there a reason to lock here (around the whole transaction) rather than just around unset_most_recent
?
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.
Actually this is probably the best option. Any locks acquired will be held until the end of the transaction anyway (I think).
This prevents deadlocks in some cases. See #228 for more info. This branch is only here to test the change.
Note that this is a breaking change due to the fact that This will no longer do what you’d expect: model.foo = "bar"
model.transition_to!(:some_state)
model.save!
|
@pjungwir I haven't tested this yet but you could try acquiring the locks in the same order in L98 and L100 and that way this wouldn't be a breaking change: if transition_class.columns_hash['most_recent'].null == false
update_fields = { most_recent: false }
else
update_fields = { most_recent: nil }
end
transitions_for_parent.order("`#{@association_name}`.`id` ASC").update_all(update_fields) In my case, this is changing the query from UPDATE `rental_photo_set_transitions` SET `rental_photo_set_transitions`.`most_recent` = 0 WHERE `rental_photo_set_transitions`.`rental_photo_set_id` = 6 to UPDATE `rental_photo_set_transitions` SET `rental_photo_set_transitions`.`most_recent` = 0 WHERE `rental_photo_set_transitions`.`rental_photo_set_id` = 6 ORDER BY id ASC
So I'm assuming it also tries to obtain the locks in the order that is specified. |
Postgres doesn't support
It has come up on the mailing list before though: https://www.postgresql.org/message-id/426DB1A0.2000604%40carvalhaes.net https://www.postgresql.org/message-id/freemail.20070030161126.43285%40fm10.freemail.hu |
Here is a thread about it I started myself a few years ago: :-) |
:( |
This prevents deadlocks in some cases. See #228 for more info. This branch is only here to test the change.
In
unset_old_most_recent
it is easy to get deadlocks from this code:The problem is that it updates several rows, and concurrent updates may not update the rows in the same order. This causes deadlocks in Postgres.
This commit fixes the problem by locking the parent object before altering its status transitions.
I wasn't really sure how to write a test for this, although in my own project I could reproduce the problem right away by running this in two shells at once: