Skip to content
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

Fix counter_cache double increment #29765

Merged
merged 1 commit into from
Jul 24, 2017
Merged

Conversation

lugray
Copy link

@lugray lugray commented Jul 12, 2017

Fixes #29762

Summary

When an after_create callback did update_attributes on a record with multiple belongs_to associations with counter caches, when doing belongs_to_counter_cache_after_update for the first association, it would set @_after_create_counter_called to false to prepare for subsequent operations, but then the following association would get re-incremented. This splits out @_after_create_counter_called as a hash keyed by the foreign key to track each separately.

The need for @_after_create_counter_called was removed altogether by 020abad, which also makes the test added in this PR pass.

Other Information

A similar fix may be required for @_after_replace_counter_called, but I couldn't figure out a failing test case and so didn't want to do it unnecessarily.
It may also be safe to remove @_after_replace_counter_called.

@lugray
Copy link
Author

lugray commented Jul 12, 2017

r? @rafaelfranca

@zhouguangming
Copy link
Contributor

👍

Copy link
Member

@Edouard-chin Edouard-chin left a comment

Choose a reason for hiding this comment

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

Checkout the branch and it works perfectly 💯

@@ -180,7 +180,8 @@ def _create_record(*)
each_counter_cached_associations do |association|
if send(association.reflection.name)
association.increment_counters
@_after_create_counter_called = true
@_after_create_counter_called ||= {}
Copy link
Member

Choose a reason for hiding this comment

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

Any reason we are adding the ||= to assign this ivar?

Copy link
Author

Choose a reason for hiding this comment

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

Because it may or may not be defined yet.

Copy link
Member

Choose a reason for hiding this comment

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

But why the previous implementation assumed it was never defined?

Copy link
Author

Choose a reason for hiding this comment

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

It doesn't assume anything. It just wanted to set the flag to true and it didn't matter whether it was defined or not. I just want to set the flag to true for a key, so I need to make sure the hash is defined.


if (@_after_create_counter_called ||= false)
@_after_create_counter_called = false
if (@_after_create_counter_called[foreign_key] ||= false)
Copy link
Member

Choose a reason for hiding this comment

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

Might just be a preference question 🤷‍♂️ , but won't it be more readable now that we have a hash to not assign in the condition and let the condition body do it?
Feels weird that the condition body is doing the same as the condition itself.

if @_after_create_counter_called.fetch(foreign_key, false)
  @_after_create_counter_called = false
end

Copy link
Author

Choose a reason for hiding this comment

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

I agree with you. I was unwisely making decisions for an easier to read diff instead of easier to read code. I've pushed up a new version.

@lugray lugray force-pushed the fix_counter_cache branch 2 times, most recently from cca8f3a to 0ddc30a Compare July 14, 2017 18:36
@sgrif
Copy link
Contributor

sgrif commented Jul 18, 2017

The test in this PR passes with or without the changes to the code (at least when I pull it into master). In fact, I can delete the ivar entirely and get no test failures. Can you confirm that the issue is still present on master? I would have expected this to be fixed on 5.1 but if not it may have been fixed by 020abad

@sgrif
Copy link
Contributor

sgrif commented Jul 18, 2017

Confirmed that this was fixed by 020abad. @lugray Can you update this to remove the ivar instead? (Keep the test)

@lugray
Copy link
Author

lugray commented Jul 18, 2017

Can confirm, broken on fix_counter_cache^, fixed by 020abad. Should we add the test?

@sgrif
Copy link
Contributor

sgrif commented Jul 18, 2017

Yes, we should keep the test provided by this PR

@lugray
Copy link
Author

lugray commented Jul 18, 2017

Updated.

Copy link
Contributor

@sgrif sgrif left a comment

Choose a reason for hiding this comment

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

You may want to update your commit message, but LGTM

When an `after_create` callback did `update_attributes` on a record with
multiple `belongs_to` associations with counter caches, even numbered
associations would have their counters double-incremented. Fixes to
`ActiveModel::Dirty` in 020abad fixed this.

This adds regression tests for this bug fixed incidentally in the other
commit, which also removed the need for the workaround using
@_after_create_counter_called.
@lugray
Copy link
Author

lugray commented Jul 19, 2017

🤦‍♀️ Commit message updated.

@rafaelfranca rafaelfranca merged commit dd58cc2 into rails:master Jul 24, 2017
@lugray lugray deleted the fix_counter_cache branch July 24, 2017 17:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

counter_cache called twice with after_create callback
5 participants