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 #16646 - Add ability to plugins to modify index scope #3887

Merged
merged 1 commit into from
Nov 3, 2016

Conversation

ShimShtein
Copy link
Member

Added the ability for plugins to modify the scope used to get hosts for #index action. This is needed if a plugin wants to add more includes or eager_load statements, so Active Record won't try to lazy load additional relations and create N+1 queries.

@@ -0,0 +1,37 @@
# This module will hold a regstry of scope directives that should be
Copy link
Member

Choose a reason for hiding this comment

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

nitpick: regstry´should beregistry´

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

def action_scope_for(action, base_scope)
scope = base_scope
self.class.scopes_for(action).each do |scope_func|
scope = scope_func.call(scope) || scope
Copy link
Member

Choose a reason for hiding this comment

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

instead of looping them all, you can return the first match? or do you rather want to match the last one? the code is a bit hard to read.

Copy link
Member Author

Choose a reason for hiding this comment

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

I am chaining the scopes, here is an example:
In puppet plugin:

HostsController.add_scope_for(:index) { |base_scope| base_scope.eager_load(:environment) }

In provisioning plugin:

HostsController.add_scope_for(:index) { |base_scope| base_scope.eager_load(:operatingsystem) }

I want to create a scope that will contain both operations:

base_scope = Hosts.authorized
actual_scope = base_scope.eager_load(environment).eager_load(operatingsystem)

I achieve this by calling each block, and passing it the result of the previous block.

@@ -351,6 +351,12 @@ def parameter_filters(klass)
@parameter_filters.fetch(klass.is_a?(Class) ? klass.name : klass, [])
end

def hosts_controller_action_scope(action, &block)
# Add scope for both API and UI controller.
HostsController.add_scope_for(action, block)
Copy link
Member

Choose a reason for hiding this comment

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

does it make sense to always assume you want to extend them both?

Copy link
Member Author

Choose a reason for hiding this comment

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

It sounds reasonable to me that the info that is returned is similar between the API and UI. In the original bug in Katello I saw that the problem exists in both UI and API.
I won't fight over it though. If you think it's unlikely, I can separate it to different methods.

Copy link
Member

@timogoebel timogoebel left a comment

Choose a reason for hiding this comment

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

Looks good to me all in all. I like the DSL and this feature is long past due. The Plugin method needs test coverage (and fixing), though.

@@ -351,6 +351,12 @@ def parameter_filters(klass)
@parameter_filters.fetch(klass.is_a?(Class) ? klass.name : klass, [])
end

def hosts_controller_action_scope(action, &block)
Copy link
Member

Choose a reason for hiding this comment

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

Could you please add a test for this? Extra points for inline usage example.

@@ -351,6 +351,12 @@ def parameter_filters(klass)
@parameter_filters.fetch(klass.is_a?(Class) ? klass.name : klass, [])
end

def hosts_controller_action_scope(action, &block)
# Add scope for both API and UI controller.
HostsController.add_scope_for(action, block)
Copy link
Member

Choose a reason for hiding this comment

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

I think, you need to pass the block like so (note the extra ampersand):

HostsController.add_scope_for(action, &block)

Same below.

@ShimShtein
Copy link
Member Author

Rebased, fixed plugin registration and added tests.

@timogoebel did I get the extra points?
Is it clear how to use it?

@ShimShtein
Copy link
Member Author

Rebased again

Copy link
Member

@timogoebel timogoebel left a comment

Choose a reason for hiding this comment

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

The code looks good, thanks for the tests. It is also clear how to use this.
My only concern is, that this won't work in develop mode when classes can be reloaded. My suggestion is to store the scope codeblocks in the plugin model and when the concern is included have it get the scopes from the plugin model. You can check c676093 for an example.

@ShimShtein
Copy link
Member Author

My only concern is, that this won't work in develop mode when classes can be reloaded.

In the past we had config.to_prepare block that was run after reload. I can see that in Rails 5, this block has moved to ActiveSupport::Reloader.to_prepare just put those definitions in the to_prepare block and the callback will be registered again after reload.

@timogoebel
Copy link
Member

@ShimShtein : That would be possible, however it's not the Straight-A student's solution. The code in "to_prepare" runs on every development request. I think, it's better to implement a solution that just runs when a class is actually reloaded.

@ShimShtein
Copy link
Member Author

Now the list is global - no reloads should affect it.

@lzap
Copy link
Member

lzap commented Oct 25, 2016

FYI there are conflicting files.

@ShimShtein
Copy link
Member Author

Rebased

Copy link
Member

@timogoebel timogoebel left a comment

Choose a reason for hiding this comment

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

@ShimShtein : Thanks for the latest updates. Class reloading still beaks the scopes from Plugins.

Open hosts index page -> everyting is fine.

To reproduce:

touch app/controllers/concerns/scopes_per_action.rb

Open host index page again -> it detects a n+1 query.

@@ -383,6 +383,19 @@ def test_add_smart_proxy_for
assert_equal({:foo => {:feature => 'Foo'}}, Foreman::Plugin.find(:test_smart_proxy).smart_proxies(Awesome))
end

def test_hosts_controller_action_scope
mock_scope = ->(scope) { scope }
Foreman::Plugin.register :test_parameter_filter do
Copy link
Member

Choose a reason for hiding this comment

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

should be test_controller_action_scope

@ShimShtein
Copy link
Member Author

touch app/controllers/concerns/scopes_per_action.rb

Indeed, reloading of this class will break the scopes. But that class'es sole responsibility is to maintain the registry, so if it changes - the old registry is probably not valid.
If I store this registry in the plugin class and it will get reloaded, it will be lost too.

@timogoebel
Copy link
Member

@ShimShtein : True. But if you store the data in

  1. the host controller class
  2. the plugin class

When these classes get reloaded, the code is parsed as well and the information is restored.

When you access the data, you can get it from both classes (and merge it) and you shouldn't have a problem. What do you think?

@timogoebel
Copy link
Member

@domcleal: May @ShimShtein and I ask for your opinion regarding where to store the data so that it survives a class reload in development? Is the current implementation fine, where the data from plugins gets lost when the ScopesPerAction concern is reloaded?

@domcleal
Copy link
Contributor

@domcleal: May @ShimShtein and I ask for your opinion regarding where to store the data so that it survives a class reload in development?

It really should be stored in Foreman::Plugin (which is immune from reloads) when the API is called. Either access it from there when called, or something akin to c676093 where it's retrieved when the concern is included.

I'd also suggest not adding an API specifically for hosts, else it'd spawn a new API for every single model/controller collection, it ought to be more generic.

Is the current implementation fine, where the data from plugins gets lost when the ScopesPerAction concern is reloaded?

Not really, IMHO. This is normally treated as a bug, and it ought to be fixed.

@ShimShtein
Copy link
Member Author

@domcleal First, thanks for the review. My only question is what to do with scopes that are not part of a particular plugin. For example API::V2::HostsController adds a default scope for index here. Of course I can add this scope manually in the method, but it sounds like I should really use the same framework here too.

@ShimShtein
Copy link
Member Author

@domcleal, Sorry, ignore my previous comment, I did not read the code thoroughly enough.

I have change the implementation to store scopes in both the plugin and in the controller (locally). The local storage is there for cases like the API::V2::HostsController.
I didn't cache the result of querying the plugins intentionally. It is done to avoid issues with construction of the controller class before all the plugins are loaded.

@ShimShtein
Copy link
Member Author

test failure is not related.

@timogoebel timogoebel merged commit 87f8f03 into theforeman:develop Nov 3, 2016
@timogoebel
Copy link
Member

Merged, thanks @ShimShtein. Please add a usage example to http://projects.theforeman.org/projects/foreman/wiki/How_to_Create_a_Plugin.

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