Commit d016aa6a authored by Stan Hu's avatar Stan Hu

Reduce CommitIsAncestor RPCs with environments

When there are many environments, loading diffs, blobs, and comparisons
can be slow because we load many environments and call CommitIsAncestor
on each of them to determine whether each environment is relevant to a
given commit SHA. This is a bit of a waste because in most cases, the
controller just needs the latest environment for the given commit.

This commit adds a `find_latest` flag to `EnvironmentsFinder` to look
for only one environment that matches the criteria. In the common case,
we will only need to look for one or two environments instead of
iterating through all of them.

Closes https://gitlab.com/gitlab-org/gitlab/issues/29562
parent 4ebca08d
...@@ -17,6 +17,7 @@ class Projects::BlameController < Projects::ApplicationController ...@@ -17,6 +17,7 @@ class Projects::BlameController < Projects::ApplicationController
end end
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit } environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
environment_params[:find_latest] = true
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
@blame_groups = Gitlab::Blame.new(@blob, @commit).groups @blame_groups = Gitlab::Blame.new(@blob, @commit).groups
......
...@@ -205,6 +205,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -205,6 +205,7 @@ class Projects::BlobController < Projects::ApplicationController
def show_html def show_html
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit } environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
environment_params[:find_latest] = true
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
@last_commit = @repository.last_commit_for_path(@commit.id, @blob.path) @last_commit = @repository.last_commit_for_path(@commit.id, @blob.path)
......
...@@ -151,7 +151,7 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -151,7 +151,7 @@ class Projects::CommitController < Projects::ApplicationController
@diffs = commit.diffs(opts) @diffs = commit.diffs(opts)
@notes_count = commit.notes.count @notes_count = commit.notes.count
@environment = EnvironmentsFinder.new(@project, current_user, commit: @commit).execute.last @environment = EnvironmentsFinder.new(@project, current_user, commit: @commit, find_latest: true).execute.last
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
......
...@@ -101,6 +101,7 @@ class Projects::CompareController < Projects::ApplicationController ...@@ -101,6 +101,7 @@ class Projects::CompareController < Projects::ApplicationController
def define_environment def define_environment
if compare if compare
environment_params = @repository.branch_exists?(head_ref) ? { ref: head_ref } : { commit: compare.commit } environment_params = @repository.branch_exists?(head_ref) ? { ref: head_ref } : { commit: compare.commit }
environment_params[:find_latest] = true
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
end end
end end
......
...@@ -52,7 +52,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap ...@@ -52,7 +52,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
@diff_notes_disabled = true @diff_notes_disabled = true
@environment = @merge_request.environments_for(current_user).last @environment = @merge_request.environments_for(current_user, latest: true).last
render json: { html: view_to_html_string('projects/merge_requests/creations/_diffs', diffs: @diffs, environment: @environment) } render json: { html: view_to_html_string('projects/merge_requests/creations/_diffs', diffs: @diffs, environment: @environment) }
end end
......
...@@ -51,7 +51,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic ...@@ -51,7 +51,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
# Deprecated: https://gitlab.com/gitlab-org/gitlab/issues/37735 # Deprecated: https://gitlab.com/gitlab-org/gitlab/issues/37735
def render_diffs def render_diffs
diffs = @compare.diffs(diff_options) diffs = @compare.diffs(diff_options)
@environment = @merge_request.environments_for(current_user).last @environment = @merge_request.environments_for(current_user, latest: true).last
diffs.unfold_diff_files(note_positions.unfoldable) diffs.unfold_diff_files(note_positions.unfoldable)
diffs.write_cache diffs.write_cache
......
...@@ -25,25 +25,13 @@ class EnvironmentsFinder ...@@ -25,25 +25,13 @@ class EnvironmentsFinder
.select(:environment_id) .select(:environment_id)
environments = project.environments.available environments = project.environments.available
.where(id: environment_ids).order_by_last_deployed_at.to_a .where(id: environment_ids)
environments.select! do |environment| if params[:find_latest]
Ability.allowed?(current_user, :read_environment, environment) find_one(environments.order_by_last_deployed_at_desc)
end else
find_all(environments.order_by_last_deployed_at.to_a)
if ref && commit
environments.select! do |environment|
environment.includes_commit?(commit)
end
end
if ref && params[:recently_updated]
environments.select! do |environment|
environment.recently_updated_on_branch?(ref)
end
end end
environments
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
...@@ -62,6 +50,24 @@ class EnvironmentsFinder ...@@ -62,6 +50,24 @@ class EnvironmentsFinder
private private
def find_one(environments)
[environments.find { |environment| valid_environment?(environment) }].compact
end
def find_all(environments)
environments.select { |environment| valid_environment?(environment) }
end
def valid_environment?(environment)
# Go in order of cost: SQL calls are cheaper than Gitaly calls
return false unless Ability.allowed?(current_user, :read_environment, environment)
return false if ref && params[:recently_updated] && !environment.recently_updated_on_branch?(ref)
return false if ref && commit && !environment.includes_commit?(commit)
true
end
def ref def ref
params[:ref].try(:to_s) params[:ref].try(:to_s)
end end
......
...@@ -48,13 +48,14 @@ class Environment < ApplicationRecord ...@@ -48,13 +48,14 @@ class Environment < ApplicationRecord
scope :available, -> { with_state(:available) } scope :available, -> { with_state(:available) }
scope :stopped, -> { with_state(:stopped) } scope :stopped, -> { with_state(:stopped) }
scope :order_by_last_deployed_at, -> do scope :order_by_last_deployed_at, -> do
max_deployment_id_sql =
Deployment.select(Deployment.arel_table[:id].maximum)
.where(Deployment.arel_table[:environment_id].eq(arel_table[:id]))
.to_sql
order(Gitlab::Database.nulls_first_order("(#{max_deployment_id_sql})", 'ASC')) order(Gitlab::Database.nulls_first_order("(#{max_deployment_id_sql})", 'ASC'))
end end
scope :order_by_last_deployed_at_desc, -> do
order(Gitlab::Database.nulls_last_order("(#{max_deployment_id_sql})", 'DESC'))
end
scope :in_review_folder, -> { where(environment_type: "review") } scope :in_review_folder, -> { where(environment_type: "review") }
scope :for_name, -> (name) { where(name: name) } scope :for_name, -> (name) { where(name: name) }
scope :preload_cluster, -> { preload(last_deployment: :cluster) } scope :preload_cluster, -> { preload(last_deployment: :cluster) }
...@@ -90,6 +91,12 @@ class Environment < ApplicationRecord ...@@ -90,6 +91,12 @@ class Environment < ApplicationRecord
end end
end end
def self.max_deployment_id_sql
Deployment.select(Deployment.arel_table[:id].maximum)
.where(Deployment.arel_table[:environment_id].eq(arel_table[:id]))
.to_sql
end
def self.pluck_names def self.pluck_names
pluck(:name) pluck(:name)
end end
......
...@@ -1122,22 +1122,18 @@ class MergeRequest < ApplicationRecord ...@@ -1122,22 +1122,18 @@ class MergeRequest < ApplicationRecord
actual_head_pipeline.success? actual_head_pipeline.success?
end end
def environments_for(current_user) def environments_for(current_user, latest: false)
return [] unless diff_head_commit return [] unless diff_head_commit
@environments ||= Hash.new do |h, current_user|
envs = EnvironmentsFinder.new(target_project, current_user, envs = EnvironmentsFinder.new(target_project, current_user,
ref: target_branch, commit: diff_head_commit, with_tags: true).execute ref: target_branch, commit: diff_head_commit, with_tags: true, find_latest: latest).execute
if source_project if source_project
envs.concat EnvironmentsFinder.new(source_project, current_user, envs.concat EnvironmentsFinder.new(source_project, current_user,
ref: source_branch, commit: diff_head_commit).execute ref: source_branch, commit: diff_head_commit, find_latest: latest).execute
end end
h[current_user] = envs.uniq envs.uniq
end
@environments[current_user]
end end
## ##
......
---
title: Reduce CommitIsAncestor RPCs with environments
merge_request: 21778
author:
type: performance
...@@ -13,17 +13,22 @@ describe EnvironmentsFinder do ...@@ -13,17 +13,22 @@ describe EnvironmentsFinder do
end end
context 'tagged deployment' do context 'tagged deployment' do
let(:environment_two) { create(:environment, project: project) }
# Environments need to include commits, so rewind two commits to fit
let(:commit) { project.commit('HEAD~2') }
before do before do
create(:deployment, :success, environment: environment, ref: 'v1.1.0', tag: true, sha: project.commit.id) create(:deployment, :success, environment: environment, ref: 'v1.0.0', tag: true, sha: project.commit.id)
create(:deployment, :success, environment: environment_two, ref: 'v1.1.0', tag: true, sha: project.commit('HEAD~1').id)
end end
it 'returns environment when with_tags is set' do it 'returns environment when with_tags is set' do
expect(described_class.new(project, user, ref: 'master', commit: project.commit, with_tags: true).execute) expect(described_class.new(project, user, ref: 'master', commit: commit, with_tags: true).execute)
.to contain_exactly(environment) .to contain_exactly(environment, environment_two)
end end
it 'does not return environment when no with_tags is set' do it 'does not return environment when no with_tags is set' do
expect(described_class.new(project, user, ref: 'master', commit: project.commit).execute) expect(described_class.new(project, user, ref: 'master', commit: commit).execute)
.to be_empty .to be_empty
end end
...@@ -31,6 +36,21 @@ describe EnvironmentsFinder do ...@@ -31,6 +36,21 @@ describe EnvironmentsFinder do
expect(described_class.new(project, user, ref: 'master', commit: project.commit('feature')).execute) expect(described_class.new(project, user, ref: 'master', commit: project.commit('feature')).execute)
.to be_empty .to be_empty
end end
it 'returns environment when with_tags is set' do
expect(described_class.new(project, user, ref: 'master', commit: commit, with_tags: true).execute)
.to contain_exactly(environment, environment_two)
end
# We expect two Gitaly calls: FindCommit, CommitIsAncestor
# This tests to ensure we don't call one CommitIsAncestor per environment
it 'only calls Gitaly twice when multiple environments are present', :request_store do
expect do
result = described_class.new(project, user, ref: 'master', commit: commit, with_tags: true, find_latest: true).execute
expect(result).to contain_exactly(environment_two)
end.to change { Gitlab::GitalyClient.get_request_count }.by(2)
end
end end
context 'branch deployment' do context 'branch deployment' do
......
...@@ -36,9 +36,13 @@ describe Environment, :use_clean_rails_memory_store_caching do ...@@ -36,9 +36,13 @@ describe Environment, :use_clean_rails_memory_store_caching do
let!(:deployment2) { create(:deployment, environment: environment2) } let!(:deployment2) { create(:deployment, environment: environment2) }
let!(:deployment3) { create(:deployment, environment: environment1) } let!(:deployment3) { create(:deployment, environment: environment1) }
it 'returns the environments in order of having been last deployed' do it 'returns the environments in ascending order of having been last deployed' do
expect(project.environments.order_by_last_deployed_at.to_a).to eq([environment3, environment2, environment1]) expect(project.environments.order_by_last_deployed_at.to_a).to eq([environment3, environment2, environment1])
end end
it 'returns the environments in descending order of having been last deployed' do
expect(project.environments.order_by_last_deployed_at_desc.to_a).to eq([environment1, environment2, environment3])
end
end end
describe 'state machine' do describe 'state machine' do
......
...@@ -2322,6 +2322,10 @@ describe MergeRequest do ...@@ -2322,6 +2322,10 @@ describe MergeRequest do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:user) { project.creator } let(:user) { project.creator }
let(:merge_request) { create(:merge_request, source_project: project) } let(:merge_request) { create(:merge_request, source_project: project) }
let(:source_branch) { merge_request.source_branch }
let(:target_branch) { merge_request.target_branch }
let(:source_oid) { project.commit(source_branch).id }
let(:target_oid) { project.commit(target_branch).id }
before do before do
merge_request.source_project.add_maintainer(user) merge_request.source_project.add_maintainer(user)
...@@ -2332,13 +2336,21 @@ describe MergeRequest do ...@@ -2332,13 +2336,21 @@ describe MergeRequest do
let(:environments) { create_list(:environment, 3, project: project) } let(:environments) { create_list(:environment, 3, project: project) }
before do before do
create(:deployment, :success, environment: environments.first, ref: 'master', sha: project.commit('master').id) create(:deployment, :success, environment: environments.first, ref: source_branch, sha: source_oid)
create(:deployment, :success, environment: environments.second, ref: 'feature', sha: project.commit('feature').id) create(:deployment, :success, environment: environments.second, ref: target_branch, sha: target_oid)
end end
it 'selects deployed environments' do it 'selects deployed environments' do
expect(merge_request.environments_for(user)).to contain_exactly(environments.first) expect(merge_request.environments_for(user)).to contain_exactly(environments.first)
end end
it 'selects latest deployed environment' do
latest_environment = create(:environment, project: project)
create(:deployment, :success, environment: latest_environment, ref: source_branch, sha: source_oid)
expect(merge_request.environments_for(user)).to eq([environments.first, latest_environment])
expect(merge_request.environments_for(user, latest: true)).to contain_exactly(latest_environment)
end
end end
context 'with environments on source project' do context 'with environments on source project' 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