-
Notifications
You must be signed in to change notification settings - Fork 982
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
Fixes #27185 - dirty association after save #6881
Conversation
@sseelam2, the Redmine ticket used is for a different project than the one associated with this GitHub repository. Please either:
If changing the ticket number used, remember to update the PR title and the commit message (using This message was auto-generated by Foreman's prprocessor |
Issues: #27185 |
@sseelam2, the Redmine ticket used is for a different project than the one associated with this GitHub repository. Please either:
If changing the ticket number used, remember to update the PR title and the commit message (using This message was auto-generated by Foreman's prprocessor |
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.
👍 on the direction, 2 small concerns
module ClassMethods | ||
# usage: | ||
# class Model | ||
# dirty_has_many_associations :organizations, :locations | ||
def dirty_has_many_associations(*args) | ||
extension = Module.new do | ||
define_method "reset_state" 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.
reset_state is quite generic name, given this module is included in many models, how about reset_dirty_cache_state
?
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.
Yes. That sounds better. Thanks. 👍
module ClassMethods | ||
# usage: | ||
# class Model | ||
# dirty_has_many_associations :organizations, :locations | ||
def dirty_has_many_associations(*args) | ||
extension = Module.new do | ||
define_method "reset_state" do | ||
args.each do |assoc| |
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.
Given we iterate over args inside the method, this won't work if someone callse dirty_has_many_associations
in one model several times. If you move it to this block, you can define reset method per each arg. An example of a model that calls dirty_has_many_associations
twice - ProvisioningTemplate. Once in the model definition itself, second time thanks to including Taxonomix concern.
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.
By the time after save is called args get replaced with the args of the last dirty association even if I move the reset_state code to the block. Could please clarify if my understanding is right? @ares
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.
I don't think that's what happens. When dirty_has_many_associations
is called, it only defines new methods, so you can call it many times. The method you're adding should follow the same pattern I think. I'll comment more in the code.
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.
Thanks for clarifying.
@sseelam2, the Redmine ticket used is for a different project than the one associated with this GitHub repository. Please either:
If changing the ticket number used, remember to update the PR title and the commit message (using This message was auto-generated by Foreman's prprocessor |
1 similar comment
@sseelam2, the Redmine ticket used is for a different project than the one associated with this GitHub repository. Please either:
If changing the ticket number used, remember to update the PR title and the commit message (using This message was auto-generated by Foreman's prprocessor |
I moved the redmine issue for you to Foreman. Note that the patch break tests though. It seems to change some behavior we rely on. |
test/unit/dirty_associations_test.rb
Outdated
end | ||
end | ||
|
||
if !ActiveRecord::Base.connection.table_exists? 'dummy_dirty_association' |
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.
Style/NegatedIf: Favor unless over if for negative conditions.
test/unit/dirty_associations_test.rb
Outdated
end | ||
end | ||
|
||
if !ActiveRecord::Base.connection.table_exists? 'third_mock' |
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.
Style/NegatedIf: Favor unless over if for negative conditions.
test/unit/dirty_associations_test.rb
Outdated
end | ||
end | ||
|
||
if !ActiveRecord::Base.connection.table_exists? 'second_mock' |
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.
Style/NegatedIf: Favor unless over if for negative conditions.
test/unit/dirty_associations_test.rb
Outdated
class DirtyAssociationsTest < ActiveSupport::TestCase | ||
def setup | ||
if !ActiveRecord::Base.connection.table_exists? 'first_mock' |
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.
Style/NegatedIf: Favor unless over if for negative conditions.
def dirty_has_many_associations(*args) | ||
@associations = @associations + args |
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.
Style/SelfAssignment: Use self-assignment shorthand +=.
@@ -1,6 +1,7 @@ | |||
require 'test_helper' | |||
|
|||
class DummyDirtyAssociationsModel | |||
class DummyDirtyAssociationsModel < ApplicationRecord |
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.
I think it's good to have an example tested that doesn't extend ApplicationRecord to show that the existing behavior doesn't actually require a real database model. Is it necessary to change this?
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.
If ApplicationRecord is removed, DummyDirtyAssociationsModel becomes a class and after_save will not work on it.
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.
you can implement/mock after_save on plain class too
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.
I think we should preserve the original behavior which can be done like Marek mentioned or conditionally defining the after_save callback if the class actually responds to such a method
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.
@sseelam2 this concern does not seem incorporated, do you plan to change it?
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.
@ares Yes. I'm on it.
extension = Module.new do | ||
args.each do |association| | ||
association_ids = association.to_s | ||
association_ids = association_ids.singularize + '_ids' unless association.to_s.end_with?('_ids') | ||
|
||
define_method "reset_dirty_cache_state" 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.
since you now define this in args.each block, the name should be dynamic, something like define_method "reset_#{association}_dirty_cache_state"
module ClassMethods | ||
# usage: | ||
# class Model | ||
# dirty_has_many_associations :organizations, :locations | ||
def dirty_has_many_associations(*args) | ||
extension = Module.new do | ||
define_method "reset_state" do | ||
args.each do |assoc| |
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.
I don't think that's what happens. When dirty_has_many_associations
is called, it only defines new methods, so you can call it many times. The method you're adding should follow the same pattern I think. I'll comment more in the code.
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.
I added more comments, hope it describes requested changes thoroughly enough
extension = Module.new do | ||
args.each do |association| | ||
association_ids = association.to_s | ||
association_ids = association_ids.singularize + '_ids' unless association.to_s.end_with?('_ids') | ||
|
||
define_method "reset_dirty_cache_state" do | ||
dirty_associations.each do |assoc| |
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.
since you now moved this into a block where association represent the association we define the cache for, you shouldn't be iterating over all dirty associations here. Just define reset_#{association}_dirty_cache_state
method that only cleans up one attribute, so IMHO it should look like this
define_method "reset_#{association}_dirty_cache_state" do
instance_variable_set("@#{association_ids}_changed", false)
instance_variable_set("@#{association_ids}_was", nil)
end
then the (new) reset_dirty_cache_state
method should only iterate over associations stored in @@dirt_associations array and call respective reset_#{association}_dirty_cache_state
methods
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.
I have made the change and the tests pass, but I get the below error in the rails log since Taxonomix is a module. @ares
18:49:35 rails.1 | 2019-07-16T18:49:35 [F|app|f48f8ad3] NoMethodError (undefined method `+' for nil:NilClass):
18:49:35 rails.1 | 2019-07-16T18:49:35 [F|app|f48f8ad3]
18:49:35 rails.1 | 2019-07-16T18:49:35 [F|app|f48f8ad3] app/models/concerns/dirty_associations.rb:38:in `dirty_has_many_associations'
18:49:35 rails.1 | | app/models/concerns/taxonomix.rb:21:in `block in <module:Taxonomix>'
18:49:35 rails.1 | | app/models/auth_sources/auth_source_ldap.rb:30:in `include'
18:49:35 rails.1 | | app/models/auth_sources/auth_source_ldap.rb:30:in `<class:AuthSourceLdap>'
18:49:35 rails.1 | | app/models/auth_sources/auth_source_ldap.rb:21:in `<top (required)>'
18:49:35 rails.1 | | app/controllers/api/v2/auth_source_ldaps_controller.rb:6:in `<class:AuthSourceLdapsController>'
18:49:35 rails.1 | | app/controllers/api/v2/auth_source_ldaps_controller.rb:3:in `<module:V2>'
18:49:35 rails.1 | | app/controllers/api/v2/auth_source_ldaps_controller.rb:2:in `<module:Api>'
18:49:35 rails.1 | | app/controllers/api/v2/auth_source_ldaps_controller.rb:1:in `<top (required)>'
18:49:35 rails.1 | | lib/foreman/middleware/telemetry.rb:10:in `call'
18:49:35 rails.1 | | lib/foreman/middleware/catch_json_parse_errors.rb:9:in `call'
18:49:35 rails.1 | | lib/foreman/middleware/logging_context_session.rb:22:in `call'
18:49:35 rails.1 | | lib/foreman/middleware/logging_context_request.rb:11:in `call'
18:49:35 rails.1 | | /home/vagrant/katello/lib/katello/prevent_json_parsing.rb:12:in `call'
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 this still the issue? the trace points to line 38 but I guess in current version it's line 35 that would be failing. How do you reproduce it?
In modules, we may need mattr_accessor
instead but that would work with classes again (same issue as with @@
). Perhaps we could use
class << self
attr_accessor :dirty_associations
end
that should work both in class and module and keep separate storage even if module is included in the class.
def dirty_has_many_associations(*args) | ||
@@dirty_associations ||= [] |
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.
don't use class variables like this, this would cause problems with class inheritance, note that @@vars are shared between parents and children classes. It's bettter to use class_attribute or simply @var
since this is a class scope already (note we're in module ClassMethods), simply replacing @@
with @
should 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.
Yes. That makes sense. Made the change you suggested.
def dirty_has_many_associations(*args) | ||
@@dirty_associations ||= [] | ||
@@dirty_associations += args | ||
dirty_associations = @@dirty_associations |
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.
we don't need this I believe
also you need to rebase this for rubocop to pass and tests to run |
[test foreman] |
Looks like there's lint in the user model test:
|
9ff27c1
to
363e25c
Compare
[test foreman] |
6ae4503
to
75e4066
Compare
[test katello] |
|
||
@first_mock.second_mock_ids = [] | ||
@first_mock.third_mock_ids = [] | ||
@first_mock.save! |
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.
One request for this test, since it is very much concerned with the state of _changed?, is to assert that _changed? is true before calling save!
This defines the behavior we expect more precisely and may help catch regressions in the future.
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.
Yes. Thanks for pointing that out. @jturel
[test katello] |
[test katello] |
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.
Code wise it looks good now, not sure what's with [test katello] that got sigtermed. There's still one concern with the test that I think hasn't been addressed, so I'm keeping "request changes" status. I'll proceed with testing now, since I don't expect changes to app code.
I tested the change and it works fine! Please keep the update as a separate commit, I'll squash on merge. |
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.
Nice work @sseelam2 - no further suggestions from me :)
@@ -14,16 +14,35 @@ | |||
module DirtyAssociations | |||
extend ActiveSupport::Concern | |||
|
|||
included do | |||
after_save :reset_dirty_cache_state |
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.
I don't like this change tbh, this module is always expected to be included in active record model, which always has after_save defined. It's just the test that we wanted to keep simple and therefore we don't use the model there.
I was hoping to actually modify the test model so that it would define after_save
even though it does not inherit from ActiveRecord. But if that does not feel right, I'd prefer the original version. Hence, please drop this commit from the PR and I'll merge. Sorry for going back and forth.
Since the model is always included in active record model, I have removed my last commit. |
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.
Thanks @sseelam2, great work in complicated area, merging!
Created this PR to obtain consistent results for _changed? Dirty Associations was returning true even after saving the changes to the database.