Commit 7f73f440 authored by Sean McGivern's avatar Sean McGivern

Fix N+1 queries for non-members in comment threads

When getting the max member access for a group of users, we stored the results
in RequestStore. However, this will only return results for project members, so
anyone who wasn't a member of the project would be checked once at the start,
and then once for each comment they made. These queries are generally quite
fast, but no query is faster!
parent 228926da
...@@ -167,7 +167,7 @@ class ProjectTeam ...@@ -167,7 +167,7 @@ class ProjectTeam
access = RequestStore.store[key] access = RequestStore.store[key]
end end
# Lookup only the IDs we need # Look up only the IDs we need
user_ids = user_ids - access.keys user_ids = user_ids - access.keys
return access if user_ids.empty? return access if user_ids.empty?
...@@ -178,6 +178,13 @@ class ProjectTeam ...@@ -178,6 +178,13 @@ class ProjectTeam
maximum(:access_level) maximum(:access_level)
access.merge!(users_access) access.merge!(users_access)
missing_user_ids = user_ids - users_access.keys
missing_user_ids.each do |user_id|
access[user_id] = Gitlab::Access::NO_ACCESS
end
access access
end end
......
---
title: Fix N+1 queries for non-members in comment threads
merge_request:
author:
...@@ -327,69 +327,114 @@ describe ProjectTeam, models: true do ...@@ -327,69 +327,114 @@ describe ProjectTeam, models: true do
end end
end end
shared_examples_for "#max_member_access_for_users" do |enable_request_store| shared_examples 'max member access for users' do
describe "#max_member_access_for_users" do let(:project) { create(:project) }
before do let(:group) { create(:group) }
RequestStore.begin! if enable_request_store let(:second_group) { create(:group) }
end
after do let(:master) { create(:user) }
if enable_request_store let(:reporter) { create(:user) }
RequestStore.end! let(:guest) { create(:user) }
RequestStore.clear!
end let(:promoted_guest) { create(:user) }
let(:group_developer) { create(:user) }
let(:second_developer) { create(:user) }
let(:user_without_access) { create(:user) }
let(:second_user_without_access) { create(:user) }
let(:users) do
[master, reporter, promoted_guest, guest, group_developer, second_developer, user_without_access].map(&:id)
end end
it 'returns correct roles for different users' do let(:expected) do
master = create(:user) {
reporter = create(:user) master.id => Gitlab::Access::MASTER,
promoted_guest = create(:user) reporter.id => Gitlab::Access::REPORTER,
guest = create(:user) promoted_guest.id => Gitlab::Access::DEVELOPER,
project = create(:empty_project) guest.id => Gitlab::Access::GUEST,
group_developer.id => Gitlab::Access::DEVELOPER,
second_developer.id => Gitlab::Access::MASTER,
user_without_access.id => Gitlab::Access::NO_ACCESS
}
end
before do
project.add_master(master) project.add_master(master)
project.add_reporter(reporter) project.add_reporter(reporter)
project.add_guest(promoted_guest) project.add_guest(promoted_guest)
project.add_guest(guest) project.add_guest(guest)
group = create(:group)
group_developer = create(:user)
second_developer = create(:user)
project.project_group_links.create( project.project_group_links.create(
group: group, group: group,
group_access: Gitlab::Access::DEVELOPER) group_access: Gitlab::Access::DEVELOPER
)
group.add_master(promoted_guest) group.add_master(promoted_guest)
group.add_developer(group_developer) group.add_developer(group_developer)
group.add_developer(second_developer) group.add_developer(second_developer)
second_group = create(:group)
project.project_group_links.create( project.project_group_links.create(
group: second_group, group: second_group,
group_access: Gitlab::Access::MASTER) group_access: Gitlab::Access::MASTER
)
second_group.add_master(second_developer) second_group.add_master(second_developer)
end
users = [master, reporter, promoted_guest, guest, group_developer, second_developer].map(&:id) it 'returns correct roles for different users' do
expect(project.team.max_member_access_for_user_ids(users)).to eq(expected)
end
end
expected = { describe '#max_member_access_for_user_ids' do
master.id => Gitlab::Access::MASTER, context 'with RequestStore enabled' do
reporter.id => Gitlab::Access::REPORTER, before do
promoted_guest.id => Gitlab::Access::DEVELOPER, RequestStore.begin!
guest.id => Gitlab::Access::GUEST, end
group_developer.id => Gitlab::Access::DEVELOPER,
second_developer.id => Gitlab::Access::MASTER
}
expect(project.team.max_member_access_for_user_ids(users)).to eq(expected) after do
RequestStore.end!
RequestStore.clear!
end end
include_examples 'max member access for users'
def access_levels(users)
project.team.max_member_access_for_user_ids(users)
end end
it 'does not perform extra queries when asked for users who have already been found' do
access_levels(users)
expect { access_levels(users) }.not_to exceed_query_limit(0)
expect(access_levels(users)).to eq(expected)
end end
describe '#max_member_access_for_users with RequestStore' do it 'only requests the extra users when uncached users are passed' do
it_behaves_like "#max_member_access_for_users", true new_user = create(:user)
second_new_user = create(:user)
all_users = users + [new_user.id, second_new_user.id]
expected_all = expected.merge(new_user.id => Gitlab::Access::NO_ACCESS,
second_new_user.id => Gitlab::Access::NO_ACCESS)
access_levels(users)
queries = ActiveRecord::QueryRecorder.new { access_levels(all_users) }
expect(queries.count).to eq(1)
expect(queries.log_message).to match(/\W#{new_user.id}\W/)
expect(queries.log_message).to match(/\W#{second_new_user.id}\W/)
expect(queries.log_message).not_to match(/\W#{promoted_guest.id}\W/)
expect(access_levels(all_users)).to eq(expected_all)
end
end end
describe '#max_member_access_for_users without RequestStore' do context 'with RequestStore disabled' do
it_behaves_like "#max_member_access_for_users", false include_examples 'max member access for users'
end
end end
end end
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