-
Notifications
You must be signed in to change notification settings - Fork 74
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
Cinstance N+1 issues clean-up #3889
base: master
Are you sure you want to change the base?
Conversation
``` Bullet::Notification::UnoptimizedQueryError: user: localuser GET http://admin.foo.3scale.localhost:39073/p/admin/applications/16 AVOID eager loading detected ApplicationPlan => [:plan_metrics] Remove from your query: .includes([:plan_metrics]) ```
Fixes issue with keys.feature:Regenerate provider key Bullet::Notification::UnoptimizedQueryError: user: avalon PUT http://master-account.3scale.localhost:42285/p/admin/applications/4/change_user_key AVOID eager loading detected ApplicationPlan => [:original] Remove from your query: .includes([:original])
``` Bullet::Notification::UnoptimizedQueryError: user: default GET /admin/api/applications.json?provider_key=***&inactive_since=2014-05-05 AVOID eager loading detected Service => [:default_application_plan] Remove from your query: .includes([:default_application_plan]) ```
The `#where_values_hash` method does not support joins and sub-queries. Originally the `account.provided_cinstances` part was ignored because it was JOINs. With the update to sub-queries, it turned into `plan_id: nil` which is incorrect and broke the logic. So now we keep logic as previously by resorting only to the `Cinstance.permitted_for` part of the query. This relies on the fact that `Cinstance.plan.issuer` is set as `Cinstance.service` when that issuer is a service. Also relies on the fact that `User.member_permission_service_ids` will not set to ids of services that don't belong to the account. Which may not be ideal but allows for permission checking without extra database queries.
Historically it was a conscious decision to optimize access to cintance -> service by denormalizing the database. So we can access service directly and not through the plan. We also keep these two in sync with model callbacks. So this commit further simplifies the queries to fully rely on this fact.
@@ -27,7 +27,7 @@ def buyers | |||
end | |||
|
|||
def products | |||
paginated_products.map { |p| ServicePresenter.new(p).new_application_data.as_json } | |||
paginated_products.includes(:default_application_plan).map { |p| ServicePresenter.new(p).new_application_data.as_json } |
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.
How do you identify the N+1 scenarios? From Bullet logs?
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.
Bullet errors are enabled in testing but many are whitelisted to avoid breaking test suite. See the environment file.
@@ -110,6 +115,12 @@ def buyer_account | |||
@buyer_account ||= buyer_accounts.find(params[:id]) | |||
end | |||
|
|||
def to_present(accounts) | |||
# ActiveRecord::Associations::Preloader.new(records: Array(accounts), associations: [:annotations, {bought_plans: %i[original]}]).call # Rails 7.x | |||
ActiveRecord::Associations::Preloader.new.preload(Array(accounts), [:annotations, {bought_plans: %i[original]}]) |
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.
Not sure how this Preloader
class works. Is the data kept in the memory forever? or only for this controller instance life?
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.
the associated objects are preloaded into the array of objects. Similar to how #includes
on associations works. So you can say it is in memory until normal garbage collection takes place.
@@ -110,6 +115,12 @@ def buyer_account | |||
@buyer_account ||= buyer_accounts.find(params[:id]) | |||
end | |||
|
|||
def to_present(accounts) |
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.
Maybe the method should be called preload!
? Calling it to_present
makes me think of a conversion that doesn't modify the original given object.
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.
this has changed in the merged PR.
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.
Which PR?
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.
same one as in the other comments #3845
@@ -7,7 +7,7 @@ class Admin::Api::ApplicationsController < Admin::Api::BaseController | |||
# GET /admin/api/applications.xml | |||
def index | |||
apps = applications.scope_search(search) | |||
.serialization_preloading.paginate(:page => current_page, :per_page => per_page) | |||
.serialization_preloading(request.format).paginate(:page => current_page, :per_page => per_page) |
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.
What's this for?
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.
different preloading rules for different requested format (xml vs json). So a parameter had to be added. But this is already upstream, need to rebase.
@@ -18,7 +18,7 @@ def policy_chain | |||
end | |||
|
|||
def with_subpaths? | |||
backend_api_configs.with_subpath.any? | |||
backend_api_configs.any?(&:with_subpath?) |
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 not the original better? It scopes the results. Your modified version always returns all configs and then calls :with_subpath?
for each one.
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.
This was already merged with #3845
If you look from the perspective of taking a service and calling this method on it, what you say makes sense. I fixed a number of N+1 though with the aforementioned PR and I don't remember which test it was related to. Also I looked at a couple of requests and optimized them to a maximum least number of queries.
I think in this case, this change is based on the premises that we have the backend_api_configs already preloaded for the service(s) we deal with. So calling with_subpath?
on each is more effective than performing a new database query. Applying a scope results in a new query.
In the other PR I have added active_record_query_trace
gem and I assume it showed to me that a query came from this line where I didn't expect a query at this point.
I think even if backend_api_configs is not preloaded, it wouldn't be a huge deal because I don't expect too many backends in services. And it will still be one query, although with more data returned than the original code. It will also load the backend_api_configs into the respective service in case they are further needed.
If you have spotted a particular call that is less efficient this way, we may think about it. But I think it might likely be an edge case when a single service is involved. But with original code I don't see how we can avoid N+1 when many services are loaded at once and we want to preload everything needed for presentation. As the original code will still try to perform a new query for each service.
Hope explanation makes sense. But better comment further on the original (merged) PR because I will be rebasing this one to avoid all the extra commits already in master.
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, so data is preloaded and you save a query. Fine.
sifter :of_account do |account| | ||
account_id == account.id | ||
end | ||
|
||
scope :of_account, ->(account) { where.has { sift(:of_account, account) } } |
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.
Why is sifter
better than a regular scope
?
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.
sifters can be applied to other models, scopes only to associations of same model
Bullet.add_safelist class_name: "Cinstance", type: :unused_eager_loading, association: :plan | ||
Bullet.add_safelist class_name: "Cinstance", type: :unused_eager_loading, association: :user_account | ||
Bullet.add_safelist class_name: "Cinstance", type: :unused_eager_loading, association: :user_account | ||
Bullet.add_safelist class_name: "Cinstance", type: :unused_eager_loading, association: :service |
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 didn't know about this list. Nice to remove items from 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.
No description provided.