Commit e418dcb7 authored by Doug Stull's avatar Doug Stull Committed by Igor Drozdov

Resolve group_member policy n+1

parent a7c591ee
...@@ -346,6 +346,10 @@ class Group < Namespace ...@@ -346,6 +346,10 @@ class Group < Namespace
members_with_parents.owners.exists?(user_id: user) members_with_parents.owners.exists?(user_id: user)
end end
def blocked_owners
members.blocked.where(access_level: Gitlab::Access::OWNER)
end
def has_maintainer?(user) def has_maintainer?(user)
return false unless user return false unless user
...@@ -358,14 +362,29 @@ class Group < Namespace ...@@ -358,14 +362,29 @@ class Group < Namespace
# Check if user is a last owner of the group. # Check if user is a last owner of the group.
def last_owner?(user) def last_owner?(user)
has_owner?(user) && members_with_parents.owners.size == 1 has_owner?(user) && single_owner?
end
def member_last_owner?(member)
return member.last_owner unless member.last_owner.nil?
last_owner?(member.user)
end end
def last_blocked_owner?(user) def single_owner?
members_with_parents.owners.size == 1
end
def single_blocked_owner?
blocked_owners.size == 1
end
def member_last_blocked_owner?(member)
return member.last_blocked_owner unless member.last_blocked_owner.nil?
return false if members_with_parents.owners.any? return false if members_with_parents.owners.any?
blocked_owners = members.blocked.where(access_level: Gitlab::Access::OWNER) single_blocked_owner? && blocked_owners.exists?(user_id: member.user)
blocked_owners.size == 1 && blocked_owners.exists?(user_id: user)
end end
def ldap_synced? def ldap_synced?
......
...@@ -26,6 +26,8 @@ class GroupMember < Member ...@@ -26,6 +26,8 @@ class GroupMember < Member
after_create :update_two_factor_requirement, unless: :invite? after_create :update_two_factor_requirement, unless: :invite?
after_destroy :update_two_factor_requirement, unless: :invite? after_destroy :update_two_factor_requirement, unless: :invite?
attr_accessor :last_owner, :last_blocked_owner
def self.access_level_roles def self.access_level_roles
Gitlab::Access.options_with_owner Gitlab::Access.options_with_owner
end end
......
# frozen_string_literal: true
module Members
class LastGroupOwnerAssigner
def initialize(group, members)
@group = group
@members = members
end
def execute
@last_blocked_owner = no_owners_in_heirarchy? && group.single_blocked_owner?
@group_single_owner = owners.size == 1
members.each { |member| set_last_owner(member) }
end
private
attr_reader :group, :members, :last_blocked_owner, :group_single_owner
def no_owners_in_heirarchy?
owners.empty?
end
def set_last_owner(member)
member.last_owner = member.id.in?(owner_ids) && group_single_owner
member.last_blocked_owner = member.id.in?(blocked_owner_ids) && last_blocked_owner
end
def owner_ids
@owner_ids ||= owners.where(id: member_ids).ids
end
def blocked_owner_ids
@blocked_owner_ids ||= group.blocked_owners.where(id: member_ids).ids
end
def member_ids
@members_ids ||= members.pluck(:id)
end
def owners
@owners ||= group.members_with_parents.owners.load
end
end
end
...@@ -4,7 +4,7 @@ class GroupMemberPolicy < BasePolicy ...@@ -4,7 +4,7 @@ class GroupMemberPolicy < BasePolicy
delegate :group delegate :group
with_scope :subject with_scope :subject
condition(:last_owner) { @subject.group.last_owner?(@subject.user) || @subject.group.last_blocked_owner?(@subject.user) } condition(:last_owner) { @subject.group.member_last_owner?(@subject) || @subject.group.member_last_blocked_owner?(@subject) }
desc "Membership is users' own" desc "Membership is users' own"
with_score 0 with_score 0
......
...@@ -2,4 +2,10 @@ ...@@ -2,4 +2,10 @@
class MemberSerializer < BaseSerializer class MemberSerializer < BaseSerializer
entity MemberEntity entity MemberEntity
def represent(members, opts = {})
Members::LastGroupOwnerAssigner.new(opts[:group], members).execute unless opts[:source].is_a?(Project)
super(members, opts)
end
end end
---
title: Resolve group_member policy n+1
merge_request: 58668
author:
type: performance
...@@ -681,21 +681,42 @@ RSpec.describe Group do ...@@ -681,21 +681,42 @@ RSpec.describe Group do
end end
end end
describe '#last_blocked_owner?' do describe '#member_last_blocked_owner?' do
let(:blocked_user) { create(:user, :blocked) } let_it_be(:blocked_user) { create(:user, :blocked) }
let(:member) { blocked_user.group_members.last }
before do before do
group.add_user(blocked_user, GroupMember::OWNER) group.add_user(blocked_user, GroupMember::OWNER)
end end
it { expect(group.last_blocked_owner?(blocked_user)).to be_truthy } context 'when last_blocked_owner is set' do
before do
expect(group).not_to receive(:members_with_parents)
end
it 'returns true' do
member.last_blocked_owner = true
expect(group.member_last_blocked_owner?(member)).to be(true)
end
it 'returns false' do
member.last_blocked_owner = false
expect(group.member_last_blocked_owner?(member)).to be(false)
end
end
context 'when last_blocked_owner is not set' do
it { expect(group.member_last_blocked_owner?(member)).to be(true) }
context 'with another active owner' do context 'with another active owner' do
before do before do
group.add_user(create(:user), GroupMember::OWNER) group.add_user(create(:user), GroupMember::OWNER)
end end
it { expect(group.last_blocked_owner?(blocked_user)).to be_falsy } it { expect(group.member_last_blocked_owner?(member)).to be(false) }
end end
context 'with 2 blocked owners' do context 'with 2 blocked owners' do
...@@ -703,7 +724,7 @@ RSpec.describe Group do ...@@ -703,7 +724,7 @@ RSpec.describe Group do
group.add_user(create(:user, :blocked), GroupMember::OWNER) group.add_user(create(:user, :blocked), GroupMember::OWNER)
end end
it { expect(group.last_blocked_owner?(blocked_user)).to be_falsy } it { expect(group.member_last_blocked_owner?(member)).to be(false) }
end end
context 'with owners from a parent' do context 'with owners from a parent' do
...@@ -713,7 +734,125 @@ RSpec.describe Group do ...@@ -713,7 +734,125 @@ RSpec.describe Group do
group.update(parent: parent_group) group.update(parent: parent_group)
end end
it { expect(group.last_blocked_owner?(blocked_user)).to be_falsy } it { expect(group.member_last_blocked_owner?(member)).to be(false) }
end
end
end
context 'when analyzing blocked owners' do
let_it_be(:blocked_user) { create(:user, :blocked) }
describe '#single_blocked_owner?' do
context 'when there is only one blocked owner' do
before do
group.add_user(blocked_user, GroupMember::OWNER)
end
it 'returns true' do
expect(group.single_blocked_owner?).to eq(true)
end
end
context 'when there are multiple blocked owners' do
let_it_be(:blocked_user_2) { create(:user, :blocked) }
before do
group.add_user(blocked_user, GroupMember::OWNER)
group.add_user(blocked_user_2, GroupMember::OWNER)
end
it 'returns true' do
expect(group.single_blocked_owner?).to eq(false)
end
end
context 'when there are no blocked owners' do
it 'returns false' do
expect(group.single_blocked_owner?).to eq(false)
end
end
end
describe '#blocked_owners' do
let_it_be(:user) { create(:user) }
before do
group.add_user(blocked_user, GroupMember::OWNER)
group.add_user(user, GroupMember::OWNER)
end
it 'has only blocked owners' do
expect(group.blocked_owners.map(&:user)).to match([blocked_user])
end
end
end
describe '#single_owner?' do
let_it_be(:user) { create(:user) }
context 'when there is only one owner' do
before do
group.add_user(user, GroupMember::OWNER)
end
it 'returns true' do
expect(group.single_owner?).to eq(true)
end
end
context 'when there are multiple owners' do
let_it_be(:user_2) { create(:user) }
before do
group.add_user(user, GroupMember::OWNER)
group.add_user(user_2, GroupMember::OWNER)
end
it 'returns true' do
expect(group.single_owner?).to eq(false)
end
end
context 'when there are no owners' do
it 'returns false' do
expect(group.single_owner?).to eq(false)
end
end
end
describe '#member_last_owner?' do
let_it_be(:user) { create(:user) }
let(:member) { group.members.last }
before do
group.add_user(user, GroupMember::OWNER)
end
context 'when last_owner is set' do
before do
expect(group).not_to receive(:last_owner?)
end
it 'returns true' do
member.last_owner = true
expect(group.member_last_owner?(member)).to be(true)
end
it 'returns false' do
member.last_owner = false
expect(group.member_last_owner?(member)).to be(false)
end
end
context 'when last_owner is not set' do
it 'returns true' do
expect(group).to receive(:last_owner?).and_call_original
expect(group.member_last_owner?(member)).to be(true)
end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Members::LastGroupOwnerAssigner do
describe "#execute" do
let_it_be(:user, reload: true) { create(:user) }
let_it_be(:group) { create(:group) }
let(:group_member) { user.members.last }
subject(:assigner) { described_class.new(group, [group_member]) }
before do
group.add_owner(user)
end
it "avoids extra database queries utilizing memoization", :aggregate_failures do
control = ActiveRecord::QueryRecorder.new { assigner.execute }
count_queries = control.occurrences_by_line_method.first[1][:occurrences].find_all { |i| i.include?('SELECT COUNT') }
expect(control.count).to be <= 5
expect(count_queries.count).to eq(0)
end
context "when there are unblocked owners" do
context "with one unblocked owner" do
specify do
expect { assigner.execute }.to change(group_member, :last_owner)
.from(nil).to(true)
.and change(group_member, :last_blocked_owner)
.from(nil).to(false)
end
end
context "with multiple unblocked owners" do
let_it_be(:unblocked_owner_member) { create(:group_member, :owner, source: group) }
specify do
expect { assigner.execute }.to change(group_member, :last_owner)
.from(nil).to(false)
.and change(group_member, :last_blocked_owner)
.from(nil).to(false)
end
it "has many members passed" do
assigner = described_class.new(group, [unblocked_owner_member, group_member])
expect { assigner.execute }.to change(group_member, :last_owner)
.from(nil).to(false)
.and change(group_member, :last_blocked_owner)
.from(nil).to(false)
.and change(unblocked_owner_member, :last_owner)
.from(nil).to(false)
.and change(unblocked_owner_member, :last_blocked_owner)
.from(nil).to(false)
end
end
end
context "when there are blocked owners" do
before do
user.block!
end
context "with one blocked owner" do
specify do
expect { assigner.execute }.to change(group_member, :last_owner)
.from(nil).to(false)
.and change(group_member, :last_blocked_owner)
.from(nil).to(true)
end
end
context "with multiple unblocked owners" do
specify do
create_list(:group_member, 2, :owner, source: group)
expect { assigner.execute }.to change(group_member, :last_owner)
.from(nil).to(false)
.and change(group_member, :last_blocked_owner)
.from(nil).to(false)
end
end
context "with multiple blocked owners" do
specify do
create(:group_member, :owner, :blocked, source: group)
expect { assigner.execute }.to change(group_member, :last_owner)
.from(nil).to(false)
.and change(group_member, :last_blocked_owner)
.from(nil).to(false)
end
end
end
end
end
...@@ -7,28 +7,77 @@ RSpec.describe MemberSerializer do ...@@ -7,28 +7,77 @@ RSpec.describe MemberSerializer do
let_it_be(:current_user) { create(:user) } let_it_be(:current_user) { create(:user) }
subject { described_class.new.represent(members, { current_user: current_user, group: group, source: source }) } subject(:representation) do
described_class.new.represent(members, { current_user: current_user, group: group, source: source }).to_json
end
shared_examples 'members.json' do shared_examples 'members.json' do
it 'matches json schema' do it { is_expected.to match_schema('members') }
expect(subject.to_json).to match_schema('members')
end
end end
context 'group member' do context 'group member' do
let(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:members) { present_members(create_list(:group_member, 1, group: group)) }
let(:source) { group } let(:source) { group }
let(:members) { present_members(create_list(:group_member, 1, group: group)) }
it_behaves_like 'members.json' it_behaves_like 'members.json'
it 'handles last group owner assignment' do
group_member = members.last
expect { representation }.to change(group_member, :last_owner)
.from(nil).to(true)
.and change(group_member, :last_blocked_owner).from(nil).to(false)
end
context "with LastGroupOwnerAssigner query improvements" do
it "avoids N+1 database queries for last group owner assignment in MembersPresenter" do
group_member = create(:group_member, group: group)
control_count = ActiveRecord::QueryRecorder.new { member_last_owner_with_preload([group_member]) }.count
group_members = create_list(:group_member, 3, group: group)
expect { member_last_owner_with_preload(group_members) }.not_to exceed_query_limit(control_count)
end
it "avoids N+1 database queries for last blocked owner assignment in MembersPresenter" do
group_member = create(:group_member, group: group)
control_count = ActiveRecord::QueryRecorder.new { member_last_blocked_owner_with_preload([group_member]) }.count
group_members = create_list(:group_member, 3, group: group)
expect { member_last_blocked_owner_with_preload(group_members) }.not_to exceed_query_limit(control_count)
end
def member_last_owner_with_preload(members)
assigner_with_preload(members)
members.map { |m| group.member_last_owner?(m) }
end
def member_last_blocked_owner_with_preload(members)
assigner_with_preload(members)
members.map { |m| group.member_last_blocked_owner?(m) }
end
def assigner_with_preload(members)
MembersPreloader.new(members).preload_all
Members::LastGroupOwnerAssigner.new(group, members).execute
end
end
end end
context 'project member' do context 'project member' do
let(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:members) { present_members(create_list(:project_member, 1, project: project)) }
let(:source) { project } let(:source) { project }
let(:group) { project.group } let(:group) { project.group }
let(:members) { present_members(create_list(:project_member, 1, project: project)) }
it_behaves_like 'members.json' it_behaves_like 'members.json'
it 'does not invoke group owner assignment' do
expect(Members::LastGroupOwnerAssigner).not_to receive(:new)
representation
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