Skip to content
Snippets Groups Projects
Verified Commit ab16a6fb authored by Yorick Peterse's avatar Yorick Peterse
Browse files

Optimise getting the pipeline status of commits

This adds an optimised way of getting the latest pipeline status for a
list of Commit objects (or just a single one).
parent 81e94ce1
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -57,6 +57,7 @@ class Projects::CommitsController < Projects::ApplicationController
@repository.commits(@ref, path: @path, limit: @limit, offset: @offset)
end
 
@commits = @commits.with_pipeline_status
@commits = prepare_commits_for_rendering(@commits)
end
end
Loading
Loading
@@ -80,7 +80,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
def commits
# Get commits from repository
# or from cache if already merged
@commits = prepare_commits_for_rendering(@merge_request.commits)
@commits =
prepare_commits_for_rendering(@merge_request.commits.with_pipeline_status)
 
render json: { html: view_to_html_string('projects/merge_requests/_commits') }
end
Loading
Loading
Loading
Loading
@@ -149,34 +149,70 @@ module Ci
end
end
 
# ref can't be HEAD or SHA, can only be branch/tag name
scope :latest, ->(ref = nil) do
max_id = unscope(:select)
.select("max(#{quoted_table_name}.id)")
.group(:ref, :sha)
if ref
where(ref: ref, id: max_id.where(ref: ref))
else
where(id: max_id)
end
end
scope :internal, -> { where(source: internal_sources) }
 
# Returns the pipelines in descending order (= newest first), optionally
# limited to a number of references.
#
# ref - The name (or names) of the branch(es)/tag(s) to limit the list of
# pipelines to.
def self.newest_first(ref = nil)
relation = order(id: :desc)
ref ? relation.where(ref: ref) : relation
end
def self.latest_status(ref = nil)
latest(ref).status
newest_first(ref).pluck(:status).first
end
 
def self.latest_successful_for(ref)
success.latest(ref).order(id: :desc).first
newest_first(ref).success.take
end
 
def self.latest_successful_for_refs(refs)
success.latest(refs).order(id: :desc).each_with_object({}) do |pipeline, hash|
relation = newest_first(refs).success
relation.each_with_object({}) do |pipeline, hash|
hash[pipeline.ref] ||= pipeline
end
end
 
# Returns a Hash containing the latest pipeline status for every given
# commit.
#
# The keys of this Hash are the commit SHAs, the values the statuses.
#
# commits - The list of commit SHAs to get the status for.
# ref - The ref to scope the data to (e.g. "master"). If the ref is not
# given we simply get the latest status for the commits, regardless
# of what refs their pipelines belong to.
def self.latest_status_per_commit(commits, ref = nil)
p1 = arel_table
p2 = arel_table.alias
# This LEFT JOIN will filter out all but the newest row for every
# combination of (project_id, sha) or (project_id, sha, ref) if a ref is
# given.
cond = p1[:sha].eq(p2[:sha])
.and(p1[:project_id].eq(p2[:project_id]))
.and(p1[:id].lt(p2[:id]))
cond = cond.and(p1[:ref].eq(p2[:ref])) if ref
join = p1.join(p2, Arel::Nodes::OuterJoin).on(cond)
relation = select(:sha, :status)
.where(sha: commits)
.where(p2[:id].eq(nil))
.joins(join.join_sources)
relation = relation.where(ref: ref) if ref
relation.each_with_object({}) do |row, hash|
hash[row[:sha]] = row[:status]
end
end
def self.truncate_sha(sha)
sha[0...8]
end
Loading
Loading
Loading
Loading
@@ -80,6 +80,7 @@ class Commit
 
@raw = raw_commit
@project = project
@statuses = {}
end
 
def id
Loading
Loading
@@ -236,11 +237,13 @@ class Commit
end
 
def status(ref = nil)
@statuses ||= {}
return @statuses[ref] if @statuses.key?(ref)
 
@statuses[ref] = pipelines.latest_status(ref)
@statuses[ref] = project.pipelines.latest_status_per_commit(id, ref)[id]
end
def set_status_for_ref(ref, status)
@statuses[ref] = status
end
 
def signature
Loading
Loading
# frozen_string_literal: true
# A collection of Commit instances for a specific project and Git reference.
class CommitCollection
include Enumerable
attr_reader :project, :ref, :commits
# project - The project the commits belong to.
# commits - The Commit instances to store.
# ref - The name of the ref (e.g. "master").
def initialize(project, commits, ref = nil)
@project = project
@commits = commits
@ref = ref
end
def each(&block)
commits.each(&block)
end
# Sets the pipeline status for every commit.
#
# Setting this status ahead of time removes the need for running a query for
# every commit we're displaying.
def with_pipeline_status
statuses = project.pipelines.latest_status_per_commit(map(&:id), ref)
each do |commit|
commit.set_status_for_ref(ref, statuses[commit.id])
end
self
end
def respond_to_missing?(message, inc_private = false)
commits.respond_to?(message, inc_private)
end
# rubocop:disable GitlabSecurity/PublicSend
def method_missing(message, *args, &block)
commits.public_send(message, *args, &block)
end
end
Loading
Loading
@@ -284,8 +284,10 @@ class MergeRequestDiff < ActiveRecord::Base
 
def load_commits
commits = st_commits.presence || merge_request_diff_commits
commits = commits.map { |commit| Commit.from_hash(commit.to_hash, project) }
 
commits.map { |commit| Commit.from_hash(commit.to_hash, project) }
CommitCollection
.new(merge_request.source_project, commits, merge_request.source_branch)
end
 
def save_diffs
Loading
Loading
Loading
Loading
@@ -132,7 +132,8 @@ class Repository
 
commits = Gitlab::Git::Commit.where(options)
commits = Commit.decorate(commits, @project) if commits.present?
commits
CommitCollection.new(project, commits, ref)
end
 
def commits_between(from, to)
Loading
Loading
@@ -148,11 +149,14 @@ class Repository
end
 
raw_repository.gitaly_migrate(:commits_by_message) do |is_enabled|
if is_enabled
find_commits_by_message_by_gitaly(query, ref, path, limit, offset)
else
find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
end
commits =
if is_enabled
find_commits_by_message_by_gitaly(query, ref, path, limit, offset)
else
find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
end
CommitCollection.new(project, commits, ref)
end
end
 
Loading
Loading
---
title: Optimise getting the pipeline status of commits
merge_request:
author:
type: performance
Loading
Loading
@@ -625,38 +625,29 @@ describe Ci::Pipeline, :mailer do
 
shared_context 'with some outdated pipelines' do
before do
create_pipeline(:canceled, 'ref', 'A')
create_pipeline(:success, 'ref', 'A')
create_pipeline(:failed, 'ref', 'B')
create_pipeline(:skipped, 'feature', 'C')
create_pipeline(:canceled, 'ref', 'A', project)
create_pipeline(:success, 'ref', 'A', project)
create_pipeline(:failed, 'ref', 'B', project)
create_pipeline(:skipped, 'feature', 'C', project)
end
 
def create_pipeline(status, ref, sha)
create(:ci_empty_pipeline, status: status, ref: ref, sha: sha)
def create_pipeline(status, ref, sha, project)
create(
:ci_empty_pipeline,
status: status,
ref: ref,
sha: sha,
project: project
)
end
end
 
describe '.latest' do
describe '.newest_first' do
include_context 'with some outdated pipelines'
 
context 'when no ref is specified' do
let(:pipelines) { described_class.latest.all }
it 'returns the latest pipeline for the same ref and different sha' do
expect(pipelines.map(&:sha)).to contain_exactly('A', 'B', 'C')
expect(pipelines.map(&:status))
.to contain_exactly('success', 'failed', 'skipped')
end
end
context 'when ref is specified' do
let(:pipelines) { described_class.latest('ref').all }
it 'returns the latest pipeline for ref and different sha' do
expect(pipelines.map(&:sha)).to contain_exactly('A', 'B')
expect(pipelines.map(&:status))
.to contain_exactly('success', 'failed')
end
it 'returns the pipelines from new to old' do
expect(described_class.newest_first.pluck(:status))
.to eq(%w[skipped failed success canceled])
end
end
 
Loading
Loading
@@ -664,20 +655,14 @@ describe Ci::Pipeline, :mailer do
include_context 'with some outdated pipelines'
 
context 'when no ref is specified' do
let(:latest_status) { described_class.latest_status }
it 'returns the latest status for the same ref and different sha' do
expect(latest_status).to eq(described_class.latest.status)
expect(latest_status).to eq('failed')
it 'returns the status of the latest pipeline' do
expect(described_class.latest_status).to eq('skipped')
end
end
 
context 'when ref is specified' do
let(:latest_status) { described_class.latest_status('ref') }
it 'returns the latest status for ref and different sha' do
expect(latest_status).to eq(described_class.latest_status('ref'))
expect(latest_status).to eq('failed')
it 'returns the status of the latest pipeline for the given ref' do
expect(described_class.latest_status('ref')).to eq('failed')
end
end
end
Loading
Loading
@@ -686,7 +671,7 @@ describe Ci::Pipeline, :mailer do
include_context 'with some outdated pipelines'
 
let!(:latest_successful_pipeline) do
create_pipeline(:success, 'ref', 'D')
create_pipeline(:success, 'ref', 'D', project)
end
 
it 'returns the latest successful pipeline' do
Loading
Loading
@@ -698,8 +683,13 @@ describe Ci::Pipeline, :mailer do
describe '.latest_successful_for_refs' do
include_context 'with some outdated pipelines'
 
let!(:latest_successful_pipeline1) { create_pipeline(:success, 'ref1', 'D') }
let!(:latest_successful_pipeline2) { create_pipeline(:success, 'ref2', 'D') }
let!(:latest_successful_pipeline1) do
create_pipeline(:success, 'ref1', 'D', project)
end
let!(:latest_successful_pipeline2) do
create_pipeline(:success, 'ref2', 'D', project)
end
 
it 'returns the latest successful pipeline for both refs' do
refs = %w(ref1 ref2 ref3)
Loading
Loading
@@ -708,6 +698,62 @@ describe Ci::Pipeline, :mailer do
end
end
 
describe '.latest_status_per_commit' do
let(:project) { create(:project) }
before do
pairs = [
%w[success ref1 123],
%w[manual master 123],
%w[failed ref 456]
]
pairs.each do |(status, ref, sha)|
create(
:ci_empty_pipeline,
status: status,
ref: ref,
sha: sha,
project: project
)
end
end
context 'without a ref' do
it 'returns a Hash containing the latest status per commit for all refs' do
expect(described_class.latest_status_per_commit(%w[123 456]))
.to eq({ '123' => 'manual', '456' => 'failed' })
end
it 'only includes the status of the given commit SHAs' do
expect(described_class.latest_status_per_commit(%w[123]))
.to eq({ '123' => 'manual' })
end
context 'when there are two pipelines for a ref and SHA' do
it 'returns the status of the latest pipeline' do
create(
:ci_empty_pipeline,
status: 'failed',
ref: 'master',
sha: '123',
project: project
)
expect(described_class.latest_status_per_commit(%w[123]))
.to eq({ '123' => 'failed' })
end
end
end
context 'with a ref' do
it 'only includes the pipelines for the given ref' do
expect(described_class.latest_status_per_commit(%w[123 456], 'master'))
.to eq({ '123' => 'manual' })
end
end
end
describe '.internal_sources' do
subject { described_class.internal_sources }
 
Loading
Loading
require 'spec_helper'
describe CommitCollection do
let(:project) { create(:project, :repository) }
let(:commit) { project.commit }
describe '#each' do
it 'yields every commit' do
collection = described_class.new(project, [commit])
expect { |b| collection.each(&b) }.to yield_with_args(commit)
end
end
describe '#with_pipeline_status' do
it 'sets the pipeline status for every commit so no additional queries are necessary' do
create(
:ci_empty_pipeline,
ref: 'master',
sha: commit.id,
status: 'success',
project: project
)
collection = described_class.new(project, [commit])
collection.with_pipeline_status
recorder = ActiveRecord::QueryRecorder.new do
expect(commit.status).to eq('success')
end
expect(recorder.count).to be_zero
end
end
describe '#respond_to_missing?' do
it 'returns true when the underlying Array responds to the message' do
collection = described_class.new(project, [])
expect(collection.respond_to?(:last)).to eq(true)
end
it 'returns false when the underlying Array does not respond to the message' do
collection = described_class.new(project, [])
expect(collection.respond_to?(:foo)).to eq(false)
end
end
describe '#method_missing' do
it 'delegates undefined methods to the underlying Array' do
collection = described_class.new(project, [commit])
expect(collection.length).to eq(1)
expect(collection.last).to eq(commit)
expect(collection).not_to be_empty
end
end
end
Loading
Loading
@@ -351,12 +351,19 @@ eos
end
 
it 'gives compound status from latest pipelines if ref is nil' do
expect(commit.status(nil)).to eq(Ci::Pipeline.latest_status)
expect(commit.status(nil)).to eq('failed')
expect(commit.status(nil)).to eq(pipeline_from_fix.status)
end
end
end
 
describe '#set_status_for_ref' do
it 'sets the status for a given reference' do
commit.set_status_for_ref('master', 'failed')
expect(commit.status('master')).to eq('failed')
end
end
describe '#participants' do
let(:user1) { build(:user) }
let(:user2) { build(:user) }
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment