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

Fixes #27185 - dirty association after save #6881

Merged
merged 1 commit into from
Aug 8, 2019

Conversation

sseelam2
Copy link
Contributor

@sseelam2 sseelam2 commented Jul 8, 2019

Created this PR to obtain consistent results for _changed? Dirty Associations was returning true even after saving the changes to the database.

@theforeman-bot
Copy link
Member

@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 git commit --amend).


This message was auto-generated by Foreman's prprocessor

@theforeman-bot
Copy link
Member

Issues: #27185

@theforeman-bot
Copy link
Member

@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 git commit --amend).


This message was auto-generated by Foreman's prprocessor

Copy link
Member

@ares ares left a 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
Copy link
Member

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?

Copy link
Contributor Author

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|
Copy link
Member

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.

Copy link
Contributor Author

@sseelam2 sseelam2 Jul 9, 2019

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

Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for clarifying.

@theforeman-bot
Copy link
Member

@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 git commit --amend).


This message was auto-generated by Foreman's prprocessor

1 similar comment
@theforeman-bot
Copy link
Member

@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 git commit --amend).


This message was auto-generated by Foreman's prprocessor

@ares
Copy link
Member

ares commented Jul 9, 2019

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.

end
end

if !ActiveRecord::Base.connection.table_exists? 'dummy_dirty_association'

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.

end
end

if !ActiveRecord::Base.connection.table_exists? 'third_mock'

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.

end
end

if !ActiveRecord::Base.connection.table_exists? 'second_mock'

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.

class DirtyAssociationsTest < ActiveSupport::TestCase
def setup
if !ActiveRecord::Base.connection.table_exists? 'first_mock'

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

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
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Member

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

Copy link
Contributor

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

Copy link
Member

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?

Copy link
Contributor Author

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
Copy link
Member

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|
Copy link
Member

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.

Copy link
Member

@ares ares left a 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|
Copy link
Member

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

Copy link
Contributor Author

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'

Copy link
Member

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 ||= []
Copy link
Member

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.

Copy link
Contributor Author

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
Copy link
Member

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

@ares
Copy link
Member

ares commented Jul 16, 2019

also you need to rebase this for rubocop to pass and tests to run

@sseelam2 sseelam2 changed the title Fixes #27185 - dirty association change Fixes #27185 - dirty association after save Jul 16, 2019
@jturel
Copy link
Contributor

jturel commented Jul 24, 2019

[test foreman]

@jturel
Copy link
Contributor

jturel commented Jul 24, 2019

Looks like there's lint in the user model test:

[vagrant@centos7-devel foreman]$ bundle exec rubocop test/models/user_test.rb 
Inspecting 1 file
C

Offenses:

test/models/user_test.rb:379:21: C: Layout/SpaceAroundOperators: Operator = should be surrounded by a single space.
    extra_role      = Role.where(:name => "foobar").first_or_create

@sseelam2 sseelam2 force-pushed the act_key_bug branch 2 times, most recently from 9ff27c1 to 363e25c Compare August 1, 2019 13:36
@jturel
Copy link
Contributor

jturel commented Aug 1, 2019

[test foreman]
[test katello]

@sseelam2 sseelam2 force-pushed the act_key_bug branch 2 times, most recently from 6ae4503 to 75e4066 Compare August 1, 2019 19:06
@jturel
Copy link
Contributor

jturel commented Aug 2, 2019

[test katello]


@first_mock.second_mock_ids = []
@first_mock.third_mock_ids = []
@first_mock.save!
Copy link
Contributor

@jturel jturel Aug 2, 2019

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.

Copy link
Contributor Author

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

@ianballou
Copy link
Contributor

[test katello]

@sseelam2
Copy link
Contributor Author

sseelam2 commented Aug 5, 2019

[test katello]

Copy link
Member

@ares ares left a 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.

@ares
Copy link
Member

ares commented Aug 6, 2019

I tested the change and it works fine! Please keep the update as a separate commit, I'll squash on merge.

jturel
jturel previously approved these changes Aug 7, 2019
Copy link
Contributor

@jturel jturel left a 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
Copy link
Member

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.

@sseelam2
Copy link
Contributor Author

sseelam2 commented Aug 8, 2019

Since the model is always included in active record model, I have removed my last commit.

Copy link
Member

@ares ares left a 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!

@ares ares merged commit 02a24ab into theforeman:develop Aug 8, 2019
@sseelam2
Copy link
Contributor Author

sseelam2 commented Aug 8, 2019

Thanks @sseelam2, great work in complicated area, merging!

Thanks for all your feedback @ares and @jturel. Couldn't have completed it without you guys.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
6 participants