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) }
let(:group) { create(:group) }
let(:second_group) { create(:group) }
let(:master) { create(:user) }
let(:reporter) { create(:user) }
let(:guest) { create(:user) }
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
let(:expected) do
{
master.id => Gitlab::Access::MASTER,
reporter.id => Gitlab::Access::REPORTER,
promoted_guest.id => Gitlab::Access::DEVELOPER,
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_reporter(reporter)
project.add_guest(promoted_guest)
project.add_guest(guest)
project.project_group_links.create(
group: group,
group_access: Gitlab::Access::DEVELOPER
)
group.add_master(promoted_guest)
group.add_developer(group_developer)
group.add_developer(second_developer)
project.project_group_links.create(
group: second_group,
group_access: Gitlab::Access::MASTER
)
second_group.add_master(second_developer)
end
it 'returns correct roles for different users' do
expect(project.team.max_member_access_for_user_ids(users)).to eq(expected)
end
end
describe '#max_member_access_for_user_ids' do
context 'with RequestStore enabled' do
before do before do
RequestStore.begin! if enable_request_store RequestStore.begin!
end end
after do after do
if enable_request_store RequestStore.end!
RequestStore.end! RequestStore.clear!
RequestStore.clear!
end
end end
it 'returns correct roles for different users' do include_examples 'max member access for users'
master = create(:user)
reporter = create(:user)
promoted_guest = create(:user)
guest = create(:user)
project = create(:empty_project)
project.add_master(master) def access_levels(users)
project.add_reporter(reporter) project.team.max_member_access_for_user_ids(users)
project.add_guest(promoted_guest) end
project.add_guest(guest)
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)
group = create(:group) expect(access_levels(users)).to eq(expected)
group_developer = create(:user)
second_developer = create(:user)
project.project_group_links.create(
group: group,
group_access: Gitlab::Access::DEVELOPER)
group.add_master(promoted_guest)
group.add_developer(group_developer)
group.add_developer(second_developer)
second_group = create(:group)
project.project_group_links.create(
group: second_group,
group_access: Gitlab::Access::MASTER)
second_group.add_master(second_developer)
users = [master, reporter, promoted_guest, guest, group_developer, second_developer].map(&:id)
expected = {
master.id => Gitlab::Access::MASTER,
reporter.id => Gitlab::Access::REPORTER,
promoted_guest.id => Gitlab::Access::DEVELOPER,
guest.id => Gitlab::Access::GUEST,
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)
end end
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)
end 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)
describe '#max_member_access_for_users without RequestStore' do queries = ActiveRecord::QueryRecorder.new { access_levels(all_users) }
it_behaves_like "#max_member_access_for_users", false
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
context 'with RequestStore disabled' do
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