-
Notifications
You must be signed in to change notification settings - Fork 209
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
Support Updating Counter Cache After Commit (to avoid deadlocks) #263
Comments
Cross-posting this from #120 to explain why this was removed: Interestingly, calling the counter update after commit was first added because of deadlock issues with Postgres. It's somewhat ironic that it's now MySQL causing a similar issue. In any case, the history of removing this comes from a bugfix to allow multiple saves in one transaction, see 81dfbf5. Plus, in the absence of issues like this with the database layer, I don't see why you'd ever want to push the counter cache update outside of the transaction. But then again, you are seeing database issues. If you can figure out how to write a test for this behavior, I think I'd probably be down to re-add this functionality, given that the original code for this was quite straightforward. This is the first I'm hearing of this issue, so not sure how widespread it is, but of course still would be good to figure out how to address this for you. |
I'm trying to think how we could add a test for this—probably with a test model with different callbacks? Have one |
I'm seeing similar deadlocks (Postgres) in my app. They're infrequent and I'm still trying to figure out how to reproduce in a test. |
Just for the record—if we can come up with a sensible PR that adds this back as an option, I'm down to merge that. But I won't have time to work on it myself. |
I don't think this problem is unique to CounterCulture, or even caused by it. Still, this could be made an option I suppose, or deferred to before commit like @jbritten, if you can make your association updates happen in a consistent order (generally, starting from the lowest model in your belonging hierarchy and going up) then you can eliminate those deadlocks. (I wrote a blog post about Transaction deadlocks on ActiveRecord associations in case it helps anyone else debug these.) In your example transaction you have: BEGIN
UPDATE `subscribers` ... WHERE `subscribers`.`id` = 2001 -- lock A
UPDATE `campaign_metrics` ... WHERE `campaign_metrics`.`id` = 1001 -- lock B
UPDATE `campaign_metrics` ... WHERE `campaign_metrics`.`id` = 1001
COMMIT This is the order of the row locks, from when rows are first updated in the transaction (the third update doesn't matter, as it reuses an existing lock). You must have another transaction elsewhere that is updating these rows in the opposite order. e.g. BEGIN
UPDATE `campaign_metrics` ... WHERE `campaign_metrics`.`id` = 1001 -- lock B
UPDATE `subscribers` ... WHERE `subscribers`.`id` = 2001 -- lock A
COMMIT I'm calling this second one "out of order", since if I'm assuming correctly, your belonging hierarchy starts from subscriber and BEGIN
SELECT * FROM `subscribers` WHERE `subscribers`.`id` = 2001 FOR UPDATE -- lock A
UPDATE `campaign_metrics` ... WHERE `campaign_metrics`.`id` = 1001 -- lock B
UPDATE `subscribers` ... WHERE `subscribers`.`id` = 2001
COMMIT ActiveRecord has a bug related to this when using |
Very interesting blog post and rails PR. |
I would also like to see the after_commit option being added back in, since this was for me the original reason to consider this library. In our case we have the following deadlock scenario (based on my current understanding):
|
Hey guys—I took a quick stab at adding this back, see #309. Let's see if the tests go green on that, and I'd love some extra eyes on that PR, too. |
Hey @magnusvk, I really appreciate you taking the time to add this back! I'm testing your 'execute-after-commit' branch and getting the following error:
Looks like a missing dependency. |
@jbritten so the thing is that you only need that gem if you set the |
@magnusvk got it; thanks for the more helpful error message. I've been running this branch in staging for 2 days and seems to be working as expected. |
@jbritten any problems with this branch? I’m thinking I should merge and release as it seems to be working. |
@magnusvk I've had the branch running in our production app for over a week and haven't encountered issues yet. I'd say go ahead and merge and release. |
Awesome, thanks for the update. Just released this as gem version 2.8.0. See documentation here. |
Support updating the counter cache after commit (outside the primary transaction) where the SQL UPDATE calls would be less susceptible to deadlocks.
Let's say we've got a Campaign model which can have many Subscribers. We store lots of campaign metrics in a separate CampaignMetrics summary table. When a subscriber confirms their email address we may see a MySQL transaction such as the following, which illustrates counter_culture updating various counts as well as additional proprietary work being performed:
There are other actions a Subscriber could do which will also update data in the CampaignMetrics table. For example, opening an email received would update the email_opens_count on the CampaignMetrics table with a transaction such as the following:
Now, at scale when many concurrent activities are occurring, such as many subscribers confirming their email address, opening emails, clicking emails, etc. deadlocks such as the following can occur when updating the CampaignMetrics summary table:
Yes, SQL calls to update the counter cache are atomic; however, transactions must be short (lock fewer rows for the smallest amount of time as much as possible). Supporting an optional configuration flag to execute updating the counter cache after commit would yield the following SQL, which would be much less susceptible to deadlocks:
The text was updated successfully, but these errors were encountered: