Commit 316b5507 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'bvl-optimize-mr-approvals' into 'master'

Optimize number of queries for approvals

Closes #4903

See merge request gitlab-org/gitlab-ee!4492
parents fec98d0f 61170c66
---
title: Speed up approvals calculations
merge_request: 4492
author:
type: performance
...@@ -3,6 +3,8 @@ module EE ...@@ -3,6 +3,8 @@ module EE
module MergeRequestsController module MergeRequestsController
extend ActiveSupport::Concern extend ActiveSupport::Concern
APPROVAL_RENDERING_ACTIONS = [:approve, :approvals, :unapprove].freeze
def approve def approve
unless merge_request.can_approve?(current_user) unless merge_request.can_approve?(current_user)
return render_404 return render_404
...@@ -31,6 +33,23 @@ module EE ...@@ -31,6 +33,23 @@ module EE
protected protected
# rubocop:disable Gitlab/ModuleWithInstanceVariables
# Assigning both @merge_request and @issuable like in
# `Projects::MergeRequests::ApplicationController`, and calling super if
# we don't need the extra includes requires us to disable this cop.
def merge_request
return super unless APPROVAL_RENDERING_ACTIONS.include?(action_name.to_sym)
@issuable = @merge_request ||= project.merge_requests
.includes(
:approved_by_users,
approvers: :user
)
.find_by!(iid: params[:id])
super
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def define_edit_vars def define_edit_vars
super super
......
module Approvable module Approvable
include Gitlab::Utils::StrongMemoize
def requires_approve? def requires_approve?
approvals_required.nonzero? approvals_required.nonzero?
end end
...@@ -13,7 +15,7 @@ module Approvable ...@@ -13,7 +15,7 @@ module Approvable
# #
def approvals_left def approvals_left
[ [
[approvals_required - approvals.count, number_of_potential_approvers].min, [approvals_required - approvals.size, number_of_potential_approvers].min,
0 0
].max ].max
end end
...@@ -66,7 +68,9 @@ module Approvable ...@@ -66,7 +68,9 @@ module Approvable
# Users in the list of approvers who have not already approved this MR. # Users in the list of approvers who have not already approved this MR.
# #
def approvers_left def approvers_left
User.where(id: all_approvers_including_groups.map(&:id)).where.not(id: approvals.select(:user_id)) strong_memoize(:approvers_left) do
User.where(id: all_approvers_including_groups.map(&:id)).where.not(id: approved_by_users.select(:id))
end
end end
# The list of approvers from either this MR (if they've been set on the MR) or the # The list of approvers from either this MR (if they've been set on the MR) or the
...@@ -79,7 +83,7 @@ module Approvable ...@@ -79,7 +83,7 @@ module Approvable
approvers_relation = approvers_overwritten? ? approvers : target_project.approvers approvers_relation = approvers_overwritten? ? approvers : target_project.approvers
approvers_relation = approvers_relation.where.not(user_id: author.id) if author approvers_relation = approvers_relation.where.not(user_id: author.id) if author
approvers_relation approvers_relation.includes(:user)
end end
def overall_approver_groups def overall_approver_groups
...@@ -87,14 +91,16 @@ module Approvable ...@@ -87,14 +91,16 @@ module Approvable
end end
def all_approvers_including_groups def all_approvers_including_groups
approvers = [] strong_memoize(:all_approvers_including_groups) do
approvers = []
# Approvers from direct assignment # Approvers from direct assignment
approvers << approvers_from_users approvers << approvers_from_users
approvers << approvers_from_groups approvers << approvers_from_groups
approvers.flatten approvers.flatten
end
end end
def approvers_from_users def approvers_from_users
...@@ -144,10 +150,6 @@ module Approvable ...@@ -144,10 +150,6 @@ module Approvable
remaining_approvals.zero? || remaining_approvals > approvers_left.count remaining_approvals.zero? || remaining_approvals > approvers_left.count
end end
def approved_by_users
approvals.map(&:user)
end
def approver_ids=(value) def approver_ids=(value)
value.split(",").map(&:strip).each do |user_id| value.split(",").map(&:strip).each do |user_id|
next if author && user_id == author.id next if author && user_id == author.id
...@@ -161,4 +163,12 @@ module Approvable ...@@ -161,4 +163,12 @@ module Approvable
approver_groups.find_or_initialize_by(group_id: group_id, target_id: id) approver_groups.find_or_initialize_by(group_id: group_id, target_id: id)
end end
end end
def reset_approval_cache!
approvals(true)
approved_by_users(true)
clear_memoization(:approvers_left)
clear_memoization(:all_approvers_including_groups)
end
end end
...@@ -6,6 +6,7 @@ module EE ...@@ -6,6 +6,7 @@ module EE
included do included do
has_many :approvals, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent has_many :approvals, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :approved_by_users, through: :approvals, source: :user
has_many :approvers, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent has_many :approvers, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :approver_groups, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent has_many :approver_groups, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
......
...@@ -4,6 +4,8 @@ module MergeRequests ...@@ -4,6 +4,8 @@ module MergeRequests
approval = merge_request.approvals.new(user: current_user) approval = merge_request.approvals.new(user: current_user)
if approval.save if approval.save
merge_request.reset_approval_cache!
create_approval_note(merge_request) create_approval_note(merge_request)
mark_pending_todos_as_done(merge_request) mark_pending_todos_as_done(merge_request)
......
...@@ -9,9 +9,7 @@ module MergeRequests ...@@ -9,9 +9,7 @@ module MergeRequests
currently_approved = merge_request.approved? currently_approved = merge_request.approved?
if approval.destroy_all if approval.destroy_all
# bust the cache here, otherwise will show results from merge_request.reset_approval_cache!
# before the deletion
merge_request.approvals(true)
create_note(merge_request) create_note(merge_request)
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
Approvers Approvers
.col-sm-10 .col-sm-10
- if can_update_approvers - if can_update_approvers
= users_select_tag("merge_request[approver_ids]", multiple: true, class: 'input-large', email_user: true, skip_users: issuable.all_approvers_including_groups << ineligible_approver, project: issuable.target_project) = users_select_tag("merge_request[approver_ids]", multiple: true, class: 'input-large', email_user: true, skip_users: issuable.all_approvers_including_groups + [ineligible_approver], project: issuable.target_project)
.help-block .help-block
This merge request must be approved by these users. This merge request must be approved by these users.
You can override the project settings by setting your own list of approvers. You can override the project settings by setting your own list of approvers.
......
require 'spec_helper'
describe Approvable do
let(:merge_request) { create(:merge_request, :with_approver) }
describe '#approvers_left' do
it 'only queries once' do
merge_request
expect(User).to receive(:where).and_call_original.once
3.times { merge_request.approvers_left }
end
end
describe '#reset_approval_cache!' do
it 'clears the cache of approvers left' do
user_can_approve = merge_request.approvers_left.first
merge_request.approvals.create!(user: user_can_approve)
merge_request.reset_approval_cache!
expect(merge_request.approvers_left).to be_empty
end
end
end
...@@ -11,6 +11,7 @@ describe MergeRequest do ...@@ -11,6 +11,7 @@ describe MergeRequest do
it { is_expected.to have_many(:approvals).dependent(:delete_all) } it { is_expected.to have_many(:approvals).dependent(:delete_all) }
it { is_expected.to have_many(:approvers).dependent(:delete_all) } it { is_expected.to have_many(:approvers).dependent(:delete_all) }
it { is_expected.to have_many(:approver_groups).dependent(:delete_all) } it { is_expected.to have_many(:approver_groups).dependent(:delete_all) }
it { is_expected.to have_many(:approved_by_users) }
end end
describe '#squash_in_progress?' do describe '#squash_in_progress?' do
......
...@@ -44,6 +44,12 @@ describe MergeRequests::ApprovalService do ...@@ -44,6 +44,12 @@ describe MergeRequests::ApprovalService do
expect(todo.reload).to be_done expect(todo.reload).to be_done
end end
it 'resets the cache for approvals' do
expect(merge_request).to receive(:reset_approval_cache!)
service.execute(merge_request)
end
context 'with remaining approvals' do context 'with remaining approvals' do
it 'does not fire a webhook' do it 'does not fire a webhook' do
expect(merge_request).to receive(:approvals_left).and_return(5) expect(merge_request).to receive(:approvals_left).and_return(5)
......
...@@ -34,6 +34,12 @@ describe MergeRequests::RemoveApprovalService do ...@@ -34,6 +34,12 @@ describe MergeRequests::RemoveApprovalService do
execute! execute!
end end
it 'resets the cache for approvals' do
expect(merge_request).to receive(:reset_approval_cache!)
execute!
end
end end
context 'with an approved merge request' do context 'with an approved merge request' do
......
...@@ -95,6 +95,7 @@ merge_requests: ...@@ -95,6 +95,7 @@ merge_requests:
- timelogs - timelogs
- head_pipeline - head_pipeline
- latest_merge_request_diff - latest_merge_request_diff
- approved_by_users
merge_request_diff: merge_request_diff:
- merge_request - merge_request
- merge_request_diff_commits - merge_request_diff_commits
......
...@@ -692,10 +692,17 @@ describe MergeRequest do ...@@ -692,10 +692,17 @@ describe MergeRequest do
let(:author) { create(:user) } let(:author) { create(:user) }
let(:merge_request) { create(:merge_request, source_project: project, author: author) } let(:merge_request) { create(:merge_request, source_project: project, author: author) }
def reloaded_merge_request
merge_request.reload
merge_request.reset_approval_cache!
merge_request
end
it "includes approvers set on the MR" do it "includes approvers set on the MR" do
expect do expect do
create(:approver, user: create(:user), target: merge_request) create(:approver, user: create(:user), target: merge_request)
end.to change { merge_request.reload.number_of_potential_approvers }.by(1) end.to change { reloaded_merge_request.number_of_potential_approvers }.by(1)
end end
it "includes approvers from group" do it "includes approvers from group" do
...@@ -703,7 +710,7 @@ describe MergeRequest do ...@@ -703,7 +710,7 @@ describe MergeRequest do
expect do expect do
create(:approver_group, group: group, target: merge_request) create(:approver_group, group: group, target: merge_request)
end.to change { merge_request.reload.number_of_potential_approvers }.by(1) end.to change { reloaded_merge_request.number_of_potential_approvers }.by(1)
end end
it "includes project members with developer access and up" do it "includes project members with developer access and up" do
...@@ -718,7 +725,7 @@ describe MergeRequest do ...@@ -718,7 +725,7 @@ describe MergeRequest do
# Add this user as both someone with access, and an explicit approver, # Add this user as both someone with access, and an explicit approver,
# to ensure they aren't double-counted. # to ensure they aren't double-counted.
create(:approver, user: developer, target: merge_request) create(:approver, user: developer, target: merge_request)
end.to change { merge_request.reload.number_of_potential_approvers }.by(2) end.to change { reloaded_merge_request.number_of_potential_approvers }.by(2)
end end
it "excludes users who have already approved the MR" do it "excludes users who have already approved the MR" do
...@@ -726,14 +733,14 @@ describe MergeRequest do ...@@ -726,14 +733,14 @@ describe MergeRequest do
approver = create(:user) approver = create(:user)
create(:approver, user: approver, target: merge_request) create(:approver, user: approver, target: merge_request)
create(:approval, user: approver, merge_request: merge_request) create(:approval, user: approver, merge_request: merge_request)
end.not_to change { merge_request.reload.number_of_potential_approvers } end.not_to change { reloaded_merge_request.number_of_potential_approvers }
end end
it "excludes the MR author" do it "excludes the MR author" do
expect do expect do
create(:approver, user: create(:user), target: merge_request) create(:approver, user: create(:user), target: merge_request)
create(:approver, user: author, target: merge_request) create(:approver, user: author, target: merge_request)
end.to change { merge_request.reload.number_of_potential_approvers }.by(1) end.to change { reloaded_merge_request.number_of_potential_approvers }.by(1)
end end
it "excludes blocked users" do it "excludes blocked users" do
...@@ -742,7 +749,7 @@ describe MergeRequest do ...@@ -742,7 +749,7 @@ describe MergeRequest do
project.add_developer(developer) project.add_developer(developer)
project.add_developer(blocked_developer) project.add_developer(blocked_developer)
expect(merge_request.reload.number_of_potential_approvers).to eq(2) expect(reloaded_merge_request.number_of_potential_approvers).to eq(2)
end end
context "when the project is part of a group" do context "when the project is part of a group" do
...@@ -760,7 +767,7 @@ describe MergeRequest do ...@@ -760,7 +767,7 @@ describe MergeRequest do
group.add_master(create(:user)) group.add_master(create(:user))
blocked_developer = create(:user).tap { |u| u.block! } blocked_developer = create(:user).tap { |u| u.block! }
group.add_developer(blocked_developer) group.add_developer(blocked_developer)
end.to change { merge_request.reload.number_of_potential_approvers }.by(2) end.to change { reloaded_merge_request.number_of_potential_approvers }.by(2)
end 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