Commit 6625a6d3 authored by Sean McGivern's avatar Sean McGivern

Fix N+1 queries in Jira Development Panel API endpoint

There were three sources of N+1 queries here: the license check, the
path and namespace information, and the root namespace.

The license check was the worst. We were checking the license
information on each project individually. This meant we couldn't
paginate in SQL, but did so in Ruby, so it was not only an N+1, it was
loading too many records.

To fix this, we use the fact that this API endpoint can only return
projects in a particular namespace. License checks end up at one of two
places: for most instances, it's the instance's license itself. For
GitLab.com, where individual namespaces have their own plan, it's the
root namespace (subgroups can't have plans; they inherit their plan from
the root).

This means that we only ever need a single check. If it passes, every
project returned has the feature available. If it fails, we return a
404, like the other endpoints here. That way we can paginate in SQL, as
we should.

The path and namespace information N+1 was simple to fix: just preload
that information.

The final N+1 was on the root namespace, which we return as the `owner`
field for compatibility with GitHub. Again, this was always the same for
all items in the response, but we can't preload it easily because
different projects will be at different levels of the hierarchy.
Instead, we just calculate the root namespace once, and pass that as an
option to the entity. The entity uses that value if it's given, and
falls back to calculating it if it's not (in case this entity is used
elsewhere without that option).
parent 88972e49
---
title: Fix N+1 queries in Jira Development Panel API endpoint
merge_request: 18329
author:
type: performance
......@@ -9,7 +9,9 @@ module API
class Repository < Grape::Entity
expose :id
expose :owner do |project, options|
{ login: project.root_namespace.path }
root_namespace = options[:root_namespace] || project.root_namespace
{ login: root_namespace.path }
end
expose :name do |project, options|
::Gitlab::Jira::Dvcs.encode_project_name(project)
......
......@@ -54,7 +54,7 @@ module API
project = find_project!(
::Gitlab::Jira::Dvcs.restore_full_path(params.slice(:namespace, :project).symbolize_keys)
)
not_found! unless licensed_project?(project)
not_found! unless licensed?(project)
project
end
......@@ -62,7 +62,7 @@ module API
def find_merge_requests
merge_requests = authorized_merge_requests.reorder(updated_at: :desc).preload(:target_project)
merge_requests = paginate(merge_requests)
merge_requests.select { |mr| licensed_project?(mr.target_project) }
merge_requests.select { |mr| licensed?(mr.target_project) }
end
# rubocop: enable CodeReuse/ActiveRecord
......@@ -87,8 +87,8 @@ module API
end
# rubocop: enable CodeReuse/ActiveRecord
def licensed_project?(project)
project.feature_available?(JIRA_DEV_PANEL_FEATURE)
def licensed?(routable)
routable.feature_available?(JIRA_DEV_PANEL_FEATURE)
end
end
......@@ -111,15 +111,16 @@ module API
get ':namespace/repos' do
namespace = Namespace.find_by_full_path(params[:namespace])
not_found!('Namespace') unless namespace
not_found!('Namespace') unless namespace && licensed?(namespace)
projects = Project.public_or_visible_to_user(current_user)
.in_namespace(namespace.self_and_descendants)
.to_a
projects.select! { |project| licensed_project?(project) }
projects = ::Kaminari.paginate_array(projects)
.eager_load_namespace_and_owner
.with_route
present paginate(projects), with: ::API::Github::Entities::Repository
present paginate(projects),
with: ::API::Github::Entities::Repository,
root_namespace: namespace.root_ancestor
end
get ':username' do
......
......@@ -323,6 +323,38 @@ describe API::V3::Github do
it 'returns an array of projects belonging to group with github format' do
expect_project_under_namespace([parent_group_project, child_group_project], group.parent, user)
end
context 'when namespace license checks are enabled' do
before do
enable_namespace_license_check!
end
context 'when the root group does not have the correct license' do
it 'returns not found' do
jira_get v3_api("/users/#{group.parent.path}/repos", user)
expect(response).to have_gitlab_http_status(404)
end
end
context 'when the root group has the correct license' do
before do
create(:gitlab_subscription, :gold, namespace: group.parent)
end
it 'avoids N+1 queries' do
jira_get v3_api("/users/#{group.parent.path}/repos", user)
control = ActiveRecord::QueryRecorder.new { jira_get v3_api("/users/#{group.parent.path}/repos", user) }
new_group = create(:group, parent: group.parent)
create(:project, :repository, group: new_group, creator: user)
expect { jira_get v3_api("/users/#{group.parent.path}/repos", user) }.not_to exceed_query_limit(control)
expect(response).to have_gitlab_http_status(200)
end
end
end
end
context 'user namespace' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment