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

Cinstance N+1 issues clean-up #3889

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

akostadinov
Copy link
Contributor

No description provided.

@github-actions github-actions bot added the Stale label Oct 14, 2024
@akostadinov akostadinov removed the Stale label Oct 14, 2024
@3scale 3scale deleted a comment from github-actions bot Oct 14, 2024
@@ -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 }
Copy link
Contributor

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?

Copy link
Contributor Author

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]}])
Copy link
Contributor

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?

Copy link
Contributor Author

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

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.

Copy link
Contributor Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

Which PR?

Copy link
Contributor Author

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

Choose a reason for hiding this comment

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

What's this for?

Copy link
Contributor Author

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

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Comment on lines 63 to 67
sifter :of_account do |account|
account_id == account.id
end

scope :of_account, ->(account) { where.has { sift(:of_account, account) } }
Copy link
Contributor

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?

Copy link
Contributor Author

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

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

As for now, this is the only change in this PR, why did you remove the others? are they already merged or something?

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, merged already. This needs more work, I'm not sure why I kept open, maybe not to forget.

@github-actions github-actions bot added the Stale label Nov 30, 2024
@3scale 3scale deleted a comment from github-actions bot Nov 30, 2024
@akostadinov akostadinov removed the Stale label Nov 30, 2024
Copy link

This PR is stale because it has not received activity for more than 30 days. Remove stale label or comment or this will be closed in 15 days.

@github-actions github-actions bot added the Stale label Dec 31, 2024
@github-actions github-actions bot closed this Jan 16, 2025
@akostadinov akostadinov reopened this Feb 4, 2025
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.

2 participants