Commit 53811074 authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'backstage/gb/use-persisted-stages-to-improve-pipelines-table' into 'master'

Improve pipeline index action performance by using persisted stages

Closes #43132

See merge request gitlab-org/gitlab-ce!19063
parents 245f60f2 ced0b445
...@@ -23,8 +23,6 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -23,8 +23,6 @@ class Projects::PipelinesController < Projects::ApplicationController
@finished_count = limited_pipelines_count(project, 'finished') @finished_count = limited_pipelines_count(project, 'finished')
@pipelines_count = limited_pipelines_count(project) @pipelines_count = limited_pipelines_count(project)
Gitlab::Ci::Pipeline::Preloader.preload(@pipelines)
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
...@@ -34,7 +32,7 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -34,7 +32,7 @@ class Projects::PipelinesController < Projects::ApplicationController
pipelines: PipelineSerializer pipelines: PipelineSerializer
.new(project: @project, current_user: @current_user) .new(project: @project, current_user: @current_user)
.with_pagination(request, response) .with_pagination(request, response)
.represent(@pipelines, disable_coverage: true), .represent(@pipelines, disable_coverage: true, preload: true),
count: { count: {
all: @pipelines_count, all: @pipelines_count,
running: @running_count, running: @running_count,
......
...@@ -31,6 +31,14 @@ module Ci ...@@ -31,6 +31,14 @@ module Ci
end end
end end
def self.fabricate(stage)
stage.statuses.ordered.latest
.sort_by(&:sortable_name).group_by(&:group_name)
.map do |group_name, grouped_statuses|
self.new(stage, name: group_name, jobs: grouped_statuses)
end
end
private private
def commit_statuses def commit_statuses
......
...@@ -16,11 +16,7 @@ module Ci ...@@ -16,11 +16,7 @@ module Ci
end end
def groups def groups
@groups ||= statuses.ordered.latest @groups ||= Ci::Group.fabricate(self)
.sort_by(&:sortable_name).group_by(&:group_name)
.map do |group_name, grouped_statuses|
Ci::Group.new(self, name: group_name, jobs: grouped_statuses)
end
end end
def to_param def to_param
......
...@@ -18,7 +18,7 @@ module Ci ...@@ -18,7 +18,7 @@ module Ci
s&.project&.pipelines&.maximum(:iid) || s&.project&.pipelines&.count s&.project&.pipelines&.maximum(:iid) || s&.project&.pipelines&.count
end end
has_many :stages has_many :stages, -> { order(position: :asc) }, inverse_of: :pipeline
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
...@@ -254,6 +254,20 @@ module Ci ...@@ -254,6 +254,20 @@ module Ci
stage unless stage.statuses_count.zero? stage unless stage.statuses_count.zero?
end end
##
# TODO We do not completely switch to persisted stages because of
# race conditions with setting statuses gitlab-ce#23257.
#
def ordered_stages
return legacy_stages unless complete?
if Feature.enabled?('ci_pipeline_persisted_stages')
stages
else
legacy_stages
end
end
def legacy_stages def legacy_stages
# TODO, this needs refactoring, see gitlab-ce#26481. # TODO, this needs refactoring, see gitlab-ce#26481.
...@@ -416,7 +430,7 @@ module Ci ...@@ -416,7 +430,7 @@ module Ci
def number_of_warnings def number_of_warnings
BatchLoader.for(id).batch(default_value: 0) do |pipeline_ids, loader| BatchLoader.for(id).batch(default_value: 0) do |pipeline_ids, loader|
Build.where(commit_id: pipeline_ids) ::Ci::Build.where(commit_id: pipeline_ids)
.latest .latest
.failed_but_allowed .failed_but_allowed
.group(:commit_id) .group(:commit_id)
...@@ -508,7 +522,8 @@ module Ci ...@@ -508,7 +522,8 @@ module Ci
def update_status def update_status
retry_optimistic_lock(self) do retry_optimistic_lock(self) do
case latest_builds_status case latest_builds_status.to_s
when 'created' then nil
when 'pending' then enqueue when 'pending' then enqueue
when 'running' then run when 'running' then run
when 'success' then succeed when 'success' then succeed
...@@ -516,6 +531,9 @@ module Ci ...@@ -516,6 +531,9 @@ module Ci
when 'canceled' then cancel when 'canceled' then cancel
when 'skipped' then skip when 'skipped' then skip
when 'manual' then block when 'manual' then block
else
raise HasStatus::UnknownStatusError,
"Unknown status `#{latest_builds_status}`"
end end
end end
end end
......
...@@ -68,16 +68,44 @@ module Ci ...@@ -68,16 +68,44 @@ module Ci
def update_status def update_status
retry_optimistic_lock(self) do retry_optimistic_lock(self) do
case statuses.latest.status case statuses.latest.status
when 'created' then nil
when 'pending' then enqueue when 'pending' then enqueue
when 'running' then run when 'running' then run
when 'success' then succeed when 'success' then succeed
when 'failed' then drop when 'failed' then drop
when 'canceled' then cancel when 'canceled' then cancel
when 'manual' then block when 'manual' then block
when 'skipped' then skip when 'skipped', nil then skip
else skip else
raise HasStatus::UnknownStatusError,
"Unknown status `#{statuses.latest.status}`"
end end
end end
end end
def groups
@groups ||= Ci::Group.fabricate(self)
end
def has_warnings?
number_of_warnings.positive?
end
def number_of_warnings
BatchLoader.for(id).batch(default_value: 0) do |stage_ids, loader|
::Ci::Build.where(stage_id: stage_ids)
.latest
.failed_but_allowed
.group(:stage_id)
.count
.each { |id, amount| loader.call(id, amount) }
end
end
def detailed_status(current_user)
Gitlab::Ci::Status::Stage::Factory
.new(self, current_user)
.fabricate!
end
end end
end end
...@@ -11,6 +11,8 @@ module HasStatus ...@@ -11,6 +11,8 @@ module HasStatus
STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3, STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3,
failed: 4, canceled: 5, skipped: 6, manual: 7 }.freeze failed: 4, canceled: 5, skipped: 6, manual: 7 }.freeze
UnknownStatusError = Class.new(StandardError)
class_methods do class_methods do
def status_sql def status_sql
scope_relevant = respond_to?(:exclude_ignored) ? exclude_ignored : all scope_relevant = respond_to?(:exclude_ignored) ? exclude_ignored : all
......
...@@ -228,6 +228,7 @@ class Project < ActiveRecord::Base ...@@ -228,6 +228,7 @@ class Project < ActiveRecord::Base
has_many :commit_statuses has_many :commit_statuses
has_many :pipelines, class_name: 'Ci::Pipeline', inverse_of: :project has_many :pipelines, class_name: 'Ci::Pipeline', inverse_of: :project
has_many :stages, class_name: 'Ci::Stage', inverse_of: :project
# Ci::Build objects store data on the file system such as artifact files and # Ci::Build objects store data on the file system such as artifact files and
# build traces. Currently there's no efficient way of removing this data in # build traces. Currently there's no efficient way of removing this data in
...@@ -1425,8 +1426,14 @@ class Project < ActiveRecord::Base ...@@ -1425,8 +1426,14 @@ class Project < ActiveRecord::Base
Ci::Runner.from("(#{union.to_sql}) ci_runners") Ci::Runner.from("(#{union.to_sql}) ci_runners")
end end
def active_runners
strong_memoize(:active_runners) do
all_runners.active
end
end
def any_runners?(&block) def any_runners?(&block)
all_runners.active.any?(&block) active_runners.any?(&block)
end end
def valid_runners_token?(token) def valid_runners_token?(token)
......
class PipelineDetailsEntity < PipelineEntity class PipelineDetailsEntity < PipelineEntity
expose :details do expose :details do
expose :legacy_stages, as: :stages, using: StageEntity expose :ordered_stages, as: :stages, using: StageEntity
expose :artifacts, using: BuildArtifactEntity expose :artifacts, using: BuildArtifactEntity
expose :manual_actions, using: BuildActionEntity expose :manual_actions, using: BuildActionEntity
end end
......
class PipelineSerializer < BaseSerializer class PipelineSerializer < BaseSerializer
include WithPagination include WithPagination
InvalidResourceError = Class.new(StandardError)
entity PipelineDetailsEntity entity PipelineDetailsEntity
def represent(resource, opts = {}) def represent(resource, opts = {})
if resource.is_a?(ActiveRecord::Relation) if resource.is_a?(ActiveRecord::Relation)
resource = resource.preload([ resource = resource.preload([
:stages,
:retryable_builds, :retryable_builds,
:cancelable_statuses, :cancelable_statuses,
:trigger_requests, :trigger_requests,
...@@ -20,10 +17,14 @@ class PipelineSerializer < BaseSerializer ...@@ -20,10 +17,14 @@ class PipelineSerializer < BaseSerializer
end end
if paginated? if paginated?
super(@paginator.paginate(resource), opts) resource = paginator.paginate(resource)
else
super(resource, opts)
end end
if opts.delete(:preload)
resource = Gitlab::Ci::Pipeline::Preloader.preload!(resource)
end
super(resource, opts)
end end
def represent_status(resource) def represent_status(resource)
...@@ -36,7 +37,7 @@ class PipelineSerializer < BaseSerializer ...@@ -36,7 +37,7 @@ class PipelineSerializer < BaseSerializer
def represent_stages(resource) def represent_stages(resource)
return {} unless resource.present? return {} unless resource.present?
data = represent(resource, { only: [{ details: [:stages] }] }) data = represent(resource, { only: [{ details: [:stages] }], preload: true })
data.dig(:details, :stages) || [] data.dig(:details, :stages) || []
end end
end end
class AddIndexToStagesPosition < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :ci_stages, [:pipeline_id, :position]
end
def down
remove_concurrent_index :ci_stages, [:pipeline_id, :position]
end
end
...@@ -520,6 +520,7 @@ ActiveRecord::Schema.define(version: 20180531220618) do ...@@ -520,6 +520,7 @@ ActiveRecord::Schema.define(version: 20180531220618) do
end end
add_index "ci_stages", ["pipeline_id", "name"], name: "index_ci_stages_on_pipeline_id_and_name", unique: true, using: :btree add_index "ci_stages", ["pipeline_id", "name"], name: "index_ci_stages_on_pipeline_id_and_name", unique: true, using: :btree
add_index "ci_stages", ["pipeline_id", "position"], name: "index_ci_stages_on_pipeline_id_and_position", using: :btree
add_index "ci_stages", ["pipeline_id"], name: "index_ci_stages_on_pipeline_id", using: :btree add_index "ci_stages", ["pipeline_id"], name: "index_ci_stages_on_pipeline_id", using: :btree
add_index "ci_stages", ["project_id"], name: "index_ci_stages_on_project_id", using: :btree add_index "ci_stages", ["project_id"], name: "index_ci_stages_on_project_id", using: :btree
......
...@@ -5,23 +5,47 @@ module Gitlab ...@@ -5,23 +5,47 @@ module Gitlab
module Pipeline module Pipeline
# Class for preloading data associated with pipelines such as commit # Class for preloading data associated with pipelines such as commit
# authors. # authors.
module Preloader class Preloader
def self.preload(pipelines) def self.preload!(pipelines)
# This ensures that all the pipeline commits are eager loaded before we ##
# start using them. # This preloads all commits at once, because `Ci::Pipeline#commit` is
# using a lazy batch loading, what results in only one batched Gitaly
# call.
#
pipelines.each(&:commit) pipelines.each(&:commit)
pipelines.each do |pipeline| pipelines.each do |pipeline|
# This preloads the author of every commit. We're using "lazy_author" self.new(pipeline).tap do |preloader|
# here since "author" immediately loads the data on the first call. preloader.preload_commit_authors
pipeline.commit.try(:lazy_author) preloader.preload_pipeline_warnings
preloader.preload_stages_warnings
# This preloads the number of warnings for every pipeline, ensuring end
# that Ci::Pipeline#has_warnings? doesn't execute any additional
# queries.
pipeline.number_of_warnings
end end
end end
def initialize(pipeline)
@pipeline = pipeline
end
def preload_commit_authors
# This also preloads the author of every commit. We're using "lazy_author"
# here since "author" immediately loads the data on the first call.
@pipeline.commit.try(:lazy_author)
end
def preload_pipeline_warnings
# This preloads the number of warnings for every pipeline, ensuring
# that Ci::Pipeline#has_warnings? doesn't execute any additional
# queries.
@pipeline.number_of_warnings
end
def preload_stages_warnings
# This preloads the number of warnings for every stage, ensuring
# that Ci::Stage#has_warnings? doesn't execute any additional
# queries.
@pipeline.stages.each { |stage| stage.number_of_warnings }
end
end end
end end
end end
......
...@@ -8,7 +8,9 @@ module Gitlab ...@@ -8,7 +8,9 @@ module Gitlab
end end
def details_path def details_path
project_pipeline_path(subject.project, subject.pipeline, anchor: subject.name) project_pipeline_path(subject.pipeline.project,
subject.pipeline,
anchor: subject.name)
end end
def has_action? def has_action?
......
...@@ -17,44 +17,103 @@ describe Projects::PipelinesController do ...@@ -17,44 +17,103 @@ describe Projects::PipelinesController do
describe 'GET index.json' do describe 'GET index.json' do
before do before do
%w(pending running created success).each_with_index do |status, index| %w(pending running success failed canceled).each_with_index do |status, index|
sha = project.commit("HEAD~#{index}") create_pipeline(status, project.commit("HEAD~#{index}"))
create(:ci_empty_pipeline, status: status, project: project, sha: sha)
end end
end end
subject do context 'when using persisted stages', :request_store do
get :index, namespace_id: project.namespace, project_id: project, format: :json before do
stub_feature_flags(ci_pipeline_persisted_stages: true)
end
it 'returns serialized pipelines', :request_store do
queries = ActiveRecord::QueryRecorder.new do
get_pipelines_index_json
end
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('pipeline')
expect(json_response).to include('pipelines')
expect(json_response['pipelines'].count).to eq 5
expect(json_response['count']['all']).to eq '5'
expect(json_response['count']['running']).to eq '1'
expect(json_response['count']['pending']).to eq '1'
expect(json_response['count']['finished']).to eq '3'
json_response.dig('pipelines', 0, 'details', 'stages').tap do |stages|
expect(stages.count).to eq 3
end
expect(queries.count).to be
end
end end
it 'returns JSON with serialized pipelines' do context 'when using legacy stages', :request_store do
subject before do
stub_feature_flags(ci_pipeline_persisted_stages: false)
end
expect(response).to have_gitlab_http_status(:ok) it 'returns JSON with serialized pipelines', :request_store do
expect(response).to match_response_schema('pipeline') queries = ActiveRecord::QueryRecorder.new do
get_pipelines_index_json
end
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('pipeline')
expect(json_response).to include('pipelines') expect(json_response).to include('pipelines')
expect(json_response['pipelines'].count).to eq 4 expect(json_response['pipelines'].count).to eq 5
expect(json_response['count']['all']).to eq '4' expect(json_response['count']['all']).to eq '5'
expect(json_response['count']['running']).to eq '1' expect(json_response['count']['running']).to eq '1'
expect(json_response['count']['pending']).to eq '1' expect(json_response['count']['pending']).to eq '1'
expect(json_response['count']['finished']).to eq '1' expect(json_response['count']['finished']).to eq '3'
json_response.dig('pipelines', 0, 'details', 'stages').tap do |stages|
expect(stages.count).to eq 3
end
expect(queries.count).to be_within(3).of(30)
end
end end
it 'does not include coverage data for the pipelines' do it 'does not include coverage data for the pipelines' do
subject get_pipelines_index_json
expect(json_response['pipelines'][0]).not_to include('coverage') expect(json_response['pipelines'][0]).not_to include('coverage')
end end
context 'when performing gitaly calls', :request_store do context 'when performing gitaly calls', :request_store do
it 'limits the Gitaly requests' do it 'limits the Gitaly requests' do
expect { subject }.to change { Gitlab::GitalyClient.get_request_count }.by(3) expect { get_pipelines_index_json }
.to change { Gitlab::GitalyClient.get_request_count }.by(2)
end end
end end
def get_pipelines_index_json
get :index, namespace_id: project.namespace,
project_id: project,
format: :json
end
def create_pipeline(status, sha)
pipeline = create(:ci_empty_pipeline, status: status,
project: project,
sha: sha)
create_build(pipeline, 'build', 1, 'build')
create_build(pipeline, 'test', 2, 'test')
create_build(pipeline, 'deploy', 3, 'deploy')
end
def create_build(pipeline, stage, stage_idx, name)
status = %w[created running pending success failed canceled].sample
create(:ci_build, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name, status: status)
end
end end
describe 'GET show JSON' do describe 'GET show.json' do
let(:pipeline) { create(:ci_pipeline_with_one_job, project: project) } let(:pipeline) { create(:ci_pipeline_with_one_job, project: project) }
it 'returns the pipeline' do it 'returns the pipeline' do
...@@ -67,6 +126,14 @@ describe Projects::PipelinesController do ...@@ -67,6 +126,14 @@ describe Projects::PipelinesController do
end end
context 'when the pipeline has multiple stages and groups', :request_store do context 'when the pipeline has multiple stages and groups', :request_store do
let(:project) { create(:project, :repository) }
let(:pipeline) do
create(:ci_empty_pipeline, project: project,
user: user,
sha: project.commit.id)
end
before do before do
create_build('build', 0, 'build') create_build('build', 0, 'build')
create_build('test', 1, 'rspec 0') create_build('test', 1, 'rspec 0')
...@@ -74,11 +141,6 @@ describe Projects::PipelinesController do ...@@ -74,11 +141,6 @@ describe Projects::PipelinesController do
create_build('post deploy', 3, 'pages 0') create_build('post deploy', 3, 'pages 0')
end end
let(:project) { create(:project, :repository) }
let(:pipeline) do
create(:ci_empty_pipeline, project: project, user: user, sha: project.commit.id)
end
it 'does not perform N + 1 queries' do it 'does not perform N + 1 queries' do
control_count = ActiveRecord::QueryRecorder.new { get_pipeline_json }.count control_count = ActiveRecord::QueryRecorder.new { get_pipeline_json }.count
...@@ -90,6 +152,7 @@ describe Projects::PipelinesController do ...@@ -90,6 +152,7 @@ describe Projects::PipelinesController do
create_build('post deploy', 3, 'pages 2') create_build('post deploy', 3, 'pages 2')
new_count = ActiveRecord::QueryRecorder.new { get_pipeline_json }.count new_count = ActiveRecord::QueryRecorder.new { get_pipeline_json }.count
expect(new_count).to be_within(12).of(control_count) expect(new_count).to be_within(12).of(control_count)
end end
end end
......
...@@ -3,18 +3,47 @@ ...@@ -3,18 +3,47 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::Ci::Pipeline::Preloader do describe Gitlab::Ci::Pipeline::Preloader do
describe '.preload' do let(:stage) { double(:stage) }
it 'preloads the author of every pipeline commit' do let(:commit) { double(:commit) }
commit = double(:commit)
pipeline = double(:pipeline, commit: commit)
expect(commit) let(:pipeline) do
.to receive(:lazy_author) double(:pipeline, commit: commit, stages: [stage])
end
describe '.preload!' do
context 'when preloading multiple commits' do
let(:project) { create(:project, :repository) }
it 'preloads all commits once' do
expect(Commit).to receive(:decorate).once.and_call_original
pipelines = [build_pipeline(ref: 'HEAD'),
build_pipeline(ref: 'HEAD~1')]
described_class.preload!(pipelines)
end
def build_pipeline(ref:)
build_stubbed(:ci_pipeline, project: project, sha: project.commit(ref).id)
end
end
it 'preloads commit authors and number of warnings' do
expect(commit).to receive(:lazy_author)
expect(pipeline).to receive(:number_of_warnings)
expect(stage).to receive(:number_of_warnings)
described_class.preload!([pipeline])
end
it 'returns original collection' do
allow(commit).to receive(:lazy_author)
allow(pipeline).to receive(:number_of_warnings)
allow(stage).to receive(:number_of_warnings)
expect(pipeline) pipelines = [pipeline, pipeline]
.to receive(:number_of_warnings)
described_class.preload([pipeline]) expect(described_class.preload!(pipelines)).to eq pipelines
end end
end end
end end
...@@ -257,6 +257,7 @@ project: ...@@ -257,6 +257,7 @@ project:
- import_data - import_data
- commit_statuses - commit_statuses
- pipelines - pipelines
- stages
- builds - builds
- runner_projects - runner_projects
- runners - runners
......
...@@ -41,4 +41,55 @@ describe Ci::Group do ...@@ -41,4 +41,55 @@ describe Ci::Group do
end end
end end
end end
describe '.fabricate' do
let(:pipeline) { create(:ci_empty_pipeline) }
let(:stage) { create(:ci_stage_entity, pipeline: pipeline) }
before do
create_build(:ci_build, name: 'rspec 0 2')
create_build(:ci_build, name: 'rspec 0 1')
create_build(:ci_build, name: 'spinach 0 1')
create_build(:commit_status, name: 'aaaaa')
end
it 'returns an array of three groups' do
expect(stage.groups).to be_a Array
expect(stage.groups).to all(be_a described_class)
expect(stage.groups.size).to eq 3
end
it 'returns groups with correctly ordered statuses' do
expect(stage.groups.first.jobs.map(&:name))
.to eq ['aaaaa']
expect(stage.groups.second.jobs.map(&:name))
.to eq ['rspec 0 1', 'rspec 0 2']
expect(stage.groups.third.jobs.map(&:name))
.to eq ['spinach 0 1']
end
it 'returns groups with correct names' do
expect(stage.groups.map(&:name))
.to eq %w[aaaaa rspec spinach]
end
context 'when a name is nil on legacy pipelines' do
before do
pipeline.builds.first.update_attribute(:name, nil)
end
it 'returns an array of three groups' do
expect(stage.groups.map(&:name))
.to eq ['', 'aaaaa', 'rspec', 'spinach']
end
end
def create_build(type, status: 'success', **opts)
create(type, pipeline: pipeline,
stage: stage.name,
status: status,
stage_id: stage.id,
**opts)
end
end
end end
...@@ -537,6 +537,87 @@ describe Ci::Pipeline, :mailer do ...@@ -537,6 +537,87 @@ describe Ci::Pipeline, :mailer do
end end
end end
end end
describe '#stages' do
before do
create(:ci_stage_entity, project: project,
pipeline: pipeline,
name: 'build')
end
it 'returns persisted stages' do
expect(pipeline.stages).not_to be_empty
expect(pipeline.stages).to all(be_persisted)
end
end
describe '#ordered_stages' do
before do
create(:ci_stage_entity, project: project,
pipeline: pipeline,
position: 4,
name: 'deploy')
create(:ci_build, project: project,
pipeline: pipeline,
stage: 'test',
stage_idx: 3,
name: 'test')
create(:ci_build, project: project,
pipeline: pipeline,
stage: 'build',
stage_idx: 2,
name: 'build')
create(:ci_stage_entity, project: project,
pipeline: pipeline,
position: 1,
name: 'sanity')
create(:ci_stage_entity, project: project,
pipeline: pipeline,
position: 5,
name: 'cleanup')
end
subject { pipeline.ordered_stages }
context 'when using legacy stages' do
before do
stub_feature_flags(ci_pipeline_persisted_stages: false)
end
it 'returns legacy stages in valid order' do
expect(subject.map(&:name)).to eq %w[build test]
end
end
context 'when using persisted stages' do
before do
stub_feature_flags(ci_pipeline_persisted_stages: true)
end
context 'when pipelines is not complete' do
it 'still returns legacy stages' do
expect(subject).to all(be_a Ci::LegacyStage)
expect(subject.map(&:name)).to eq %w[build test]
end
end
context 'when pipeline is complete' do
before do
pipeline.succeed!
end
it 'returns stages in valid order' do
expect(subject).to all(be_a Ci::Stage)
expect(subject.map(&:name))
.to eq %w[sanity build test deploy cleanup]
end
end
end
end
end end
describe 'state machine' do describe 'state machine' do
...@@ -1181,6 +1262,43 @@ describe Ci::Pipeline, :mailer do ...@@ -1181,6 +1262,43 @@ describe Ci::Pipeline, :mailer do
end end
end end
describe '#update_status' do
context 'when pipeline is empty' do
it 'updates does not change pipeline status' do
expect(pipeline.statuses.latest.status).to be_nil
expect { pipeline.update_status }
.to change { pipeline.reload.status }.to 'skipped'
end
end
context 'when updating status to pending' do
before do
allow(pipeline)
.to receive_message_chain(:statuses, :latest, :status)
.and_return(:running)
end
it 'updates pipeline status to running' do
expect { pipeline.update_status }
.to change { pipeline.reload.status }.to 'running'
end
end
context 'when statuses status was not recognized' do
before do
allow(pipeline)
.to receive(:latest_builds_status)
.and_return(:unknown)
end
it 'raises an exception' do
expect { pipeline.update_status }
.to raise_error(HasStatus::UnknownStatusError)
end
end
end
describe '#detailed_status' do describe '#detailed_status' do
subject { pipeline.detailed_status(user) } subject { pipeline.detailed_status(user) }
......
...@@ -65,7 +65,31 @@ describe Ci::Stage, :models do ...@@ -65,7 +65,31 @@ describe Ci::Stage, :models do
end end
end end
context 'when stage is skipped' do context 'when stage has only created builds' do
let(:stage) { create(:ci_stage_entity, status: :created) }
before do
create(:ci_build, :created, stage_id: stage.id)
end
it 'updates status to skipped' do
expect(stage.reload.status).to eq 'created'
end
end
context 'when stage is skipped because of skipped builds' do
before do
create(:ci_build, :skipped, stage_id: stage.id)
end
it 'updates status to skipped' do
expect { stage.update_status }
.to change { stage.reload.status }
.to 'skipped'
end
end
context 'when stage is skipped because is empty' do
it 'updates status to skipped' do it 'updates status to skipped' do
expect { stage.update_status } expect { stage.update_status }
.to change { stage.reload.status } .to change { stage.reload.status }
...@@ -86,9 +110,85 @@ describe Ci::Stage, :models do ...@@ -86,9 +110,85 @@ describe Ci::Stage, :models do
expect(stage.reload).to be_failed expect(stage.reload).to be_failed
end end
end end
context 'when statuses status was not recognized' do
before do
allow(stage)
.to receive_message_chain(:statuses, :latest, :status)
.and_return(:unknown)
end
it 'raises an exception' do
expect { stage.update_status }
.to raise_error(HasStatus::UnknownStatusError)
end
end
end
describe '#detailed_status' do
using RSpec::Parameterized::TableSyntax
let(:user) { create(:user) }
let(:stage) { create(:ci_stage_entity, status: :created) }
subject { stage.detailed_status(user) }
where(:statuses, :label) do
%w[created] | :created
%w[success] | :passed
%w[pending] | :pending
%w[skipped] | :skipped
%w[canceled] | :canceled
%w[success failed] | :failed
%w[running pending] | :running
end
with_them do
before do
statuses.each do |status|
create(:commit_status, project: stage.project,
pipeline: stage.pipeline,
stage_id: stage.id,
status: status)
stage.update_status
end
end
it 'has a correct label' do
expect(subject.label).to eq label.to_s
end
end
context 'when stage has warnings' do
before do
create(:ci_build, project: stage.project,
pipeline: stage.pipeline,
stage_id: stage.id,
status: :failed,
allow_failure: true)
stage.update_status
end
it 'is passed with warnings' do
expect(subject.label).to eq 'passed with warnings'
end
end
end end
describe '#index' do describe '#groups' do
before do
create(:ci_build, stage_id: stage.id, name: 'rspec 0 1')
create(:ci_build, stage_id: stage.id, name: 'rspec 0 2')
end
it 'groups stage builds by name' do
expect(stage.groups).to be_one
expect(stage.groups.first.name).to eq 'rspec'
end
end
describe '#position' do
context 'when stage has been imported and does not have position index set' do context 'when stage has been imported and does not have position index set' do
before do before do
stage.update_column(:position, nil) stage.update_column(:position, nil)
...@@ -119,4 +219,42 @@ describe Ci::Stage, :models do ...@@ -119,4 +219,42 @@ describe Ci::Stage, :models do
end end
end end
end end
context 'when stage has warnings' do
before do
create(:ci_build, :failed, :allowed_to_fail, stage_id: stage.id)
end
describe '#has_warnings?' do
it 'returns true' do
expect(stage).to have_warnings
end
end
describe '#number_of_warnings' do
it 'returns a lazy stage warnings counter' do
lazy_queries = ActiveRecord::QueryRecorder.new do
stage.number_of_warnings
end
synced_queries = ActiveRecord::QueryRecorder.new do
stage.number_of_warnings.to_i
end
expect(lazy_queries.count).to eq 0
expect(synced_queries.count).to eq 1
expect(stage.number_of_warnings.inspect).to include 'BatchLoader'
expect(stage.number_of_warnings).to eq 1
end
end
end
context 'when stage does not have warnings' do
describe '#has_warnings?' do
it 'returns false' do
expect(stage).not_to have_warnings
end
end
end
end end
...@@ -8,6 +8,10 @@ describe PipelineSerializer do ...@@ -8,6 +8,10 @@ describe PipelineSerializer do
described_class.new(current_user: user) described_class.new(current_user: user)
end end
before do
stub_feature_flags(ci_pipeline_persisted_stages: true)
end
subject { serializer.represent(resource) } subject { serializer.represent(resource) }
describe '#represent' do describe '#represent' do
...@@ -99,7 +103,8 @@ describe PipelineSerializer do ...@@ -99,7 +103,8 @@ describe PipelineSerializer do
end end
end end
context 'number of queries' do describe 'number of queries when preloaded' do
subject { serializer.represent(resource, preload: true) }
let(:resource) { Ci::Pipeline.all } let(:resource) { Ci::Pipeline.all }
before do before do
...@@ -107,7 +112,7 @@ describe PipelineSerializer do ...@@ -107,7 +112,7 @@ describe PipelineSerializer do
# gitaly calls in this block # gitaly calls in this block
# Issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/37772 # Issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/37772
Gitlab::GitalyClient.allow_n_plus_1_calls do Gitlab::GitalyClient.allow_n_plus_1_calls do
Ci::Pipeline::AVAILABLE_STATUSES.each do |status| Ci::Pipeline::COMPLETED_STATUSES.each do |status|
create_pipeline(status) create_pipeline(status)
end end
end end
...@@ -120,7 +125,7 @@ describe PipelineSerializer do ...@@ -120,7 +125,7 @@ describe PipelineSerializer do
it 'verifies number of queries', :request_store do it 'verifies number of queries', :request_store do
recorded = ActiveRecord::QueryRecorder.new { subject } recorded = ActiveRecord::QueryRecorder.new { subject }
expect(recorded.count).to be_within(1).of(44) expect(recorded.count).to be_within(2).of(27)
expect(recorded.cached_count).to eq(0) expect(recorded.cached_count).to eq(0)
end end
end end
...@@ -139,7 +144,7 @@ describe PipelineSerializer do ...@@ -139,7 +144,7 @@ describe PipelineSerializer do
# pipeline. With the same ref this check is cached but if refs are # pipeline. With the same ref this check is cached but if refs are
# different then there is an extra query per ref # different then there is an extra query per ref
# https://gitlab.com/gitlab-org/gitlab-ce/issues/46368 # https://gitlab.com/gitlab-org/gitlab-ce/issues/46368
expect(recorded.count).to be_within(1).of(51) expect(recorded.count).to be_within(2).of(30)
expect(recorded.cached_count).to eq(0) expect(recorded.cached_count).to eq(0)
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