Commit 77291e34 authored by Fabio Pitino's avatar Fabio Pitino

Merge branch 'fix/gb/pipeline-index-latest' into 'master'

Fix pipeline index json Gitaly N+1 with `latest?`

Closes #19697

See merge request gitlab-org/gitlab!34160
parents 9c7c3a56 81892b00
...@@ -553,10 +553,28 @@ module Ci ...@@ -553,10 +553,28 @@ module Ci
end end
end end
def lazy_ref_commit
return unless ::Gitlab::Ci::Features.pipeline_latest?
BatchLoader.for(ref).batch do |refs, loader|
next unless project.repository_exists?
project.repository.list_commits_by_ref_name(refs).then do |commits|
loader.call(ref, commits[ref])
end
end
end
def latest? def latest?
return false unless git_ref && commit.present? return false unless git_ref && commit.present?
project.commit(git_ref) == commit unless ::Gitlab::Ci::Features.pipeline_latest?
return project.commit(git_ref) == commit
end
return false if lazy_ref_commit.nil?
lazy_ref_commit.id == commit.id
end end
def retried def retried
......
---
title: Improve pipeline index controller performance by resolving Gitaly N+1 calls
merge_request: 34160
author:
type: performance
...@@ -38,6 +38,10 @@ module Gitlab ...@@ -38,6 +38,10 @@ module Gitlab
::Feature.enabled?(:ci_atomic_processing, project, default_enabled: true) ::Feature.enabled?(:ci_atomic_processing, project, default_enabled: true)
end end
def self.pipeline_latest?
::Feature.enabled?(:ci_pipeline_latest, default_enabled: true)
end
def self.release_generation_enabled? def self.release_generation_enabled?
::Feature.enabled?(:ci_release_generation) ::Feature.enabled?(:ci_release_generation)
end end
......
...@@ -17,6 +17,7 @@ module Gitlab ...@@ -17,6 +17,7 @@ module Gitlab
pipelines.each do |pipeline| pipelines.each do |pipeline|
self.new(pipeline).tap do |preloader| self.new(pipeline).tap do |preloader|
preloader.preload_commit_authors preloader.preload_commit_authors
preloader.preload_ref_commits
preloader.preload_pipeline_warnings preloader.preload_pipeline_warnings
preloader.preload_stages_warnings preloader.preload_stages_warnings
end end
...@@ -27,12 +28,19 @@ module Gitlab ...@@ -27,12 +28,19 @@ module Gitlab
@pipeline = pipeline @pipeline = pipeline
end end
def preload_commit_authors
# This also preloads the author of every commit. We're using "lazy_author" # This also preloads the author of every commit. We're using "lazy_author"
# here since "author" immediately loads the data on the first call. # here since "author" immediately loads the data on the first call.
def preload_commit_authors
@pipeline.commit.try(:lazy_author) @pipeline.commit.try(:lazy_author)
end end
# This preloads latest commits for given refs and therefore makes it
# much less expensive to check if a pipeline is a latest one for
# given branch.
def preload_ref_commits
@pipeline.lazy_ref_commit
end
def preload_pipeline_warnings def preload_pipeline_warnings
# This preloads the number of warnings for every pipeline, ensuring # This preloads the number of warnings for every pipeline, ensuring
# that Ci::Pipeline#has_warnings? doesn't execute any additional # that Ci::Pipeline#has_warnings? doesn't execute any additional
...@@ -40,10 +48,10 @@ module Gitlab ...@@ -40,10 +48,10 @@ module Gitlab
@pipeline.number_of_warnings @pipeline.number_of_warnings
end end
def preload_stages_warnings
# This preloads the number of warnings for every stage, ensuring # This preloads the number of warnings for every stage, ensuring
# that Ci::Stage#has_warnings? doesn't execute any additional # that Ci::Stage#has_warnings? doesn't execute any additional
# queries. # queries.
def preload_stages_warnings
@pipeline.stages.each { |stage| stage.number_of_warnings } @pipeline.stages.each { |stage| stage.number_of_warnings }
end end
end end
......
...@@ -1008,6 +1008,12 @@ module Gitlab ...@@ -1008,6 +1008,12 @@ module Gitlab
end end
end end
def list_commits_by_ref_name(refs)
wrapped_gitaly_errors do
gitaly_commit_client.list_commits_by_ref_name(refs)
end
end
def last_commit_for_path(sha, path, literal_pathspec: false) def last_commit_for_path(sha, path, literal_pathspec: false)
wrapped_gitaly_errors do wrapped_gitaly_errors do
gitaly_commit_client.last_commit_for_path(sha, path, literal_pathspec: literal_pathspec) gitaly_commit_client.last_commit_for_path(sha, path, literal_pathspec: literal_pathspec)
......
...@@ -391,6 +391,21 @@ module Gitlab ...@@ -391,6 +391,21 @@ module Gitlab
messages messages
end end
def list_commits_by_ref_name(refs)
request = Gitaly::ListCommitsByRefNameRequest
.new(repository: @gitaly_repo, ref_names: refs)
response = GitalyClient.call(@repository.storage, :commit_service, :list_commits_by_ref_name, request, timeout: GitalyClient.medium_timeout)
commit_refs = response.flat_map do |message|
message.commit_refs.map do |commit_ref|
[commit_ref.ref_name, Gitlab::Git::Commit.new(@repository, commit_ref.commit)]
end
end
Hash[commit_refs]
end
private private
def call_commit_diff(request_params, options = {}) def call_commit_diff(request_params, options = {})
......
...@@ -77,9 +77,9 @@ RSpec.describe Projects::PipelinesController do ...@@ -77,9 +77,9 @@ RSpec.describe Projects::PipelinesController do
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
# ListCommitsByOid, RepositoryExists, HasLocalBranches # ListCommitsByOid, RepositoryExists, HasLocalBranches, ListCommitsByRefNames
expect { get_pipelines_index_json } expect { get_pipelines_index_json }
.to change { Gitlab::GitalyClient.get_request_count }.by(3) .to change { Gitlab::GitalyClient.get_request_count }.by(4)
end end
end end
...@@ -111,13 +111,15 @@ RSpec.describe Projects::PipelinesController do ...@@ -111,13 +111,15 @@ RSpec.describe Projects::PipelinesController do
context 'scope is branches or tags' do context 'scope is branches or tags' do
before do before do
create(:ci_pipeline, :failed, project: project, ref: 'v1.0.0', tag: true) create(:ci_pipeline, :failed, project: project, ref: 'v1.0.0', tag: true)
create(:ci_pipeline, :failed, project: project, ref: 'master', tag: false)
create(:ci_pipeline, :failed, project: project, ref: 'feature', tag: false)
end end
context 'when scope is branches' do context 'when scope is branches' do
it 'returns matched pipelines' do it 'returns matched pipelines' do
get_pipelines_index_json(scope: 'branches') get_pipelines_index_json(scope: 'branches')
check_pipeline_response(returned: 1, all: 7, running: 2, pending: 1, finished: 4) check_pipeline_response(returned: 2, all: 9, running: 2, pending: 1, finished: 6)
end end
end end
...@@ -125,7 +127,7 @@ RSpec.describe Projects::PipelinesController do ...@@ -125,7 +127,7 @@ RSpec.describe Projects::PipelinesController do
it 'returns matched pipelines' do it 'returns matched pipelines' do
get_pipelines_index_json(scope: 'tags') get_pipelines_index_json(scope: 'tags')
check_pipeline_response(returned: 1, all: 7, running: 2, pending: 1, finished: 4) check_pipeline_response(returned: 1, all: 9, running: 2, pending: 1, finished: 6)
end end
end end
end end
...@@ -234,7 +236,8 @@ RSpec.describe Projects::PipelinesController do ...@@ -234,7 +236,8 @@ RSpec.describe Projects::PipelinesController do
user = create(:user) user = create(:user)
pipeline = create(:ci_empty_pipeline, status: status, pipeline = create(:ci_empty_pipeline, status: status,
project: project, project: project,
sha: sha, sha: sha.id,
ref: sha.id.first(8),
user: user, user: user,
merge_request: merge_request) merge_request: merge_request)
......
...@@ -28,8 +28,9 @@ describe Gitlab::Ci::Pipeline::Preloader do ...@@ -28,8 +28,9 @@ describe Gitlab::Ci::Pipeline::Preloader do
end end
end end
it 'preloads commit authors and number of warnings' do it 'preloads commit authors, number of warnings and ref commits' do
expect(commit).to receive(:lazy_author) expect(commit).to receive(:lazy_author)
expect(pipeline).to receive(:lazy_ref_commit)
expect(pipeline).to receive(:number_of_warnings) expect(pipeline).to receive(:number_of_warnings)
expect(stage).to receive(:number_of_warnings) expect(stage).to receive(:number_of_warnings)
...@@ -38,6 +39,7 @@ describe Gitlab::Ci::Pipeline::Preloader do ...@@ -38,6 +39,7 @@ describe Gitlab::Ci::Pipeline::Preloader do
it 'returns original collection' do it 'returns original collection' do
allow(commit).to receive(:lazy_author) allow(commit).to receive(:lazy_author)
allow(pipeline).to receive(:lazy_ref_commit)
allow(pipeline).to receive(:number_of_warnings) allow(pipeline).to receive(:number_of_warnings)
allow(stage).to receive(:number_of_warnings) allow(stage).to receive(:number_of_warnings)
......
...@@ -381,4 +381,15 @@ describe Gitlab::GitalyClient::CommitService do ...@@ -381,4 +381,15 @@ describe Gitlab::GitalyClient::CommitService do
commits.map { |commit| Gitlab::Git::Commit.new(repository, commit) } commits.map { |commit| Gitlab::Git::Commit.new(repository, commit) }
end end
end end
describe '#list_commits_by_ref_name' do
it 'lists latest commits grouped by a ref name' do
response = client.list_commits_by_ref_name(%w[master feature v1.0.0 nonexistent])
expect(response.fetch('master').id).to eq 'b83d6e391c22777fca1ed3012fce84f633d7fed0'
expect(response.fetch('feature').id).to eq '0b4bc9a49b562e85de7cc9e834518ea6828729b9'
expect(response.fetch('v1.0.0').id).to eq '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9'
expect(response).not_to have_key 'nonexistent'
end
end
end end
...@@ -1488,6 +1488,12 @@ describe Ci::Pipeline, :mailer do ...@@ -1488,6 +1488,12 @@ describe Ci::Pipeline, :mailer do
sha: project.commit.sha) sha: project.commit.sha)
end end
describe '#lazy_ref_commit' do
it 'returns the latest commit for a ref lazily' do
expect(pipeline.lazy_ref_commit.id).to eq project.commit(pipeline.ref).id
end
end
describe '#latest?' do describe '#latest?' do
context 'with latest sha' do context 'with latest sha' do
it 'returns true' do it 'returns true' do
...@@ -1496,17 +1502,26 @@ describe Ci::Pipeline, :mailer do ...@@ -1496,17 +1502,26 @@ describe Ci::Pipeline, :mailer do
end end
context 'with a branch name as the ref' do context 'with a branch name as the ref' do
it 'looks up commit with the full ref name' do it 'looks up a commit for a branch' do
expect(pipeline.project).to receive(:commit).with('refs/heads/master').and_call_original expect(pipeline.ref).to eq 'master'
expect(pipeline).to be_latest
end
end
context 'with a tag name as a ref' do
it 'looks up a commit for a tag' do
expect(project.repository.branch_names).not_to include 'v1.0.0'
pipeline.update(sha: project.commit('v1.0.0').sha, ref: 'v1.0.0', tag: true)
expect(pipeline).to be_tag
expect(pipeline).to be_latest expect(pipeline).to be_latest
end end
end end
context 'with not latest sha' do context 'with not latest sha' do
before do before do
pipeline.update( pipeline.update(sha: project.commit("#{project.default_branch}~1").sha)
sha: project.commit("#{project.default_branch}~1").sha)
end end
it 'returns false' do it 'returns false' 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