Commit 7e5fa10b authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'fix/create-pipeline-with-builds-in-transaction' into 'master'

Create pipeline along with builds in the transation

## What does this MR do?

This MR makes it possible to create pipeline along with all associated builds in the transaction, to avoid having empty pipelines when asynchronous job gets terminated.

This will simplify implementation of `PipelineUnlockWorker` in https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6988 and improve reliability of the CI as a whole.

## What are the relevant issue numbers?

Related to #24361

See merge request !7742
parents 21b90ef8 3761a0c5
...@@ -45,9 +45,15 @@ module Ci ...@@ -45,9 +45,15 @@ module Ci
return error('No builds for this pipeline.') return error('No builds for this pipeline.')
end end
pipeline.save Ci::Pipeline.transaction do
pipeline.process! pipeline.save
pipeline
Ci::CreatePipelineBuildsService
.new(project, current_user)
.execute(pipeline)
end
pipeline.tap(&:process!)
end end
private private
......
...@@ -5,10 +5,7 @@ module Ci ...@@ -5,10 +5,7 @@ module Ci
def execute(pipeline) def execute(pipeline)
@pipeline = pipeline @pipeline = pipeline
# This method will ensure that our pipeline does have all builds for all stages created ensure_created_builds! # TODO, remove me in 9.0
if created_builds.empty?
create_builds!
end
new_builds = new_builds =
stage_indexes_of_created_builds.map do |index| stage_indexes_of_created_builds.map do |index|
...@@ -22,10 +19,6 @@ module Ci ...@@ -22,10 +19,6 @@ module Ci
private private
def create_builds!
Ci::CreatePipelineBuildsService.new(project, current_user).execute(pipeline)
end
def process_stage(index) def process_stage(index)
current_status = status_for_prior_stages(index) current_status = status_for_prior_stages(index)
...@@ -76,5 +69,18 @@ module Ci ...@@ -76,5 +69,18 @@ module Ci
def created_builds def created_builds
pipeline.builds.created pipeline.builds.created
end end
# This method is DEPRECATED and should be removed in 9.0.
#
# We need it to maintain backwards compatibility with previous versions
# when builds were not created within one transaction with the pipeline.
#
def ensure_created_builds!
return if created_builds.any?
Ci::CreatePipelineBuildsService
.new(project, current_user)
.execute(pipeline)
end
end end
end end
---
title: Create builds in transaction to avoid empty pipelines
merge_request: 7742
author:
...@@ -7,26 +7,30 @@ FactoryGirl.define do ...@@ -7,26 +7,30 @@ FactoryGirl.define do
project factory: :empty_project project factory: :empty_project
factory :ci_pipeline_without_jobs do factory :ci_pipeline_without_jobs do
after(:build) do |commit| after(:build) do |pipeline|
allow(commit).to receive(:ci_yaml_file) { YAML.dump({}) } allow(pipeline).to receive(:ci_yaml_file) { YAML.dump({}) }
end end
end end
factory :ci_pipeline_with_one_job do factory :ci_pipeline_with_one_job do
after(:build) do |commit| after(:build) do |pipeline|
allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" } }) } allow(pipeline).to receive(:ci_yaml_file) do
end YAML.dump({ rspec: { script: "ls" } })
end end
factory :ci_pipeline_with_two_job do
after(:build) do |commit|
allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" }, spinach: { script: "ls" } }) }
end end
end end
factory :ci_pipeline do factory :ci_pipeline do
after(:build) do |commit| transient { config nil }
allow(commit).to receive(:ci_yaml_file) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) }
after(:build) do |pipeline, evaluator|
allow(pipeline).to receive(:ci_yaml_file) do
if evaluator.config
YAML.dump(evaluator.config)
else
File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
end
end
end end
end end
end end
......
require 'spec_helper' require 'spec_helper'
describe Ci::ProcessPipelineService, services: true do describe Ci::ProcessPipelineService, services: true do
let(:pipeline) { create(:ci_pipeline, ref: 'master') } let(:pipeline) { create(:ci_empty_pipeline, ref: 'master') }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:config) { nil }
before do
allow(pipeline).to receive(:ci_yaml_file).and_return(config)
end
describe '#execute' do describe '#execute' do
def all_builds
pipeline.builds
end
def builds
all_builds.where.not(status: [:created, :skipped])
end
def process_pipeline
described_class.new(pipeline.project, user).execute(pipeline)
end
def succeed_pending
builds.pending.update_all(status: 'success')
end
context 'start queuing next builds' do context 'start queuing next builds' do
before do before do
create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage_idx: 0) create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage_idx: 0)
...@@ -223,10 +202,6 @@ describe Ci::ProcessPipelineService, services: true do ...@@ -223,10 +202,6 @@ describe Ci::ProcessPipelineService, services: true do
pipeline.builds.running_or_pending.each(&:success) pipeline.builds.running_or_pending.each(&:success)
expect(manual_actions).to be_many # production and clear cache expect(manual_actions).to be_many # production and clear cache
end end
def manual_actions
pipeline.manual_actions
end
end end
end end
...@@ -282,15 +257,6 @@ describe Ci::ProcessPipelineService, services: true do ...@@ -282,15 +257,6 @@ describe Ci::ProcessPipelineService, services: true do
expect(builds.map(&:status)).to eq(%w[success skipped pending]) expect(builds.map(&:status)).to eq(%w[success skipped pending])
end end
end end
def create_build(name, stage_idx, when_value = nil)
create(:ci_build,
:created,
pipeline: pipeline,
name: name,
stage_idx: stage_idx,
when: when_value)
end
end end
context 'when failed build in the middle stage is retried' do context 'when failed build in the middle stage is retried' do
...@@ -327,65 +293,92 @@ describe Ci::ProcessPipelineService, services: true do ...@@ -327,65 +293,92 @@ describe Ci::ProcessPipelineService, services: true do
end end
end end
context 'creates a builds from .gitlab-ci.yml' do context 'when there are builds that are not created yet' do
let(:config) do let(:pipeline) do
YAML.dump({ create(:ci_pipeline, config: config)
rspec: {
stage: 'test',
script: 'rspec'
},
rubocop: {
stage: 'test',
script: 'rubocop'
},
deploy: {
stage: 'deploy',
script: 'deploy'
}
})
end end
# Using stubbed .gitlab-ci.yml created in commit factory let(:config) do
# { rspec: { stage: 'test', script: 'rspec' },
deploy: { stage: 'deploy', script: 'rsync' } }
end
before do before do
stub_ci_pipeline_yaml_file(config)
create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage: 'build', stage_idx: 0) create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage: 'build', stage_idx: 0)
create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage: 'build', stage_idx: 0) create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage: 'build', stage_idx: 0)
end end
it 'when processing a pipeline' do it 'processes the pipeline' do
# Currently we have two builds with state created # Currently we have five builds with state created
#
expect(builds.count).to eq(0) expect(builds.count).to eq(0)
expect(all_builds.count).to eq(2) expect(all_builds.count).to eq(2)
# Create builds will mark the created as pending # Process builds service will enqueue builds from the first stage.
expect(process_pipeline).to be_truthy #
process_pipeline
expect(builds.count).to eq(2) expect(builds.count).to eq(2)
expect(all_builds.count).to eq(2) expect(all_builds.count).to eq(2)
# When we builds succeed we will create a rest of pipeline from .gitlab-ci.yml # When builds succeed we will enqueue remaining builds.
# We will have 2 succeeded, 2 pending (from stage test), total 5 (one more build from deploy) #
# We will have 2 succeeded, 1 pending (from stage test), total 4 (two
# additional build from `.gitlab-ci.yml`).
#
succeed_pending succeed_pending
expect(process_pipeline).to be_truthy process_pipeline
expect(builds.success.count).to eq(2) expect(builds.success.count).to eq(2)
expect(builds.pending.count).to eq(2) expect(builds.pending.count).to eq(1)
expect(all_builds.count).to eq(5) expect(all_builds.count).to eq(4)
# When we succeed the 2 pending from stage test, # When pending build succeeds in stage test, we enqueue deploy stage.
# We will queue a deploy stage, no new builds will be created #
succeed_pending succeed_pending
expect(process_pipeline).to be_truthy process_pipeline
expect(builds.pending.count).to eq(1) expect(builds.pending.count).to eq(1)
expect(builds.success.count).to eq(4) expect(builds.success.count).to eq(3)
expect(all_builds.count).to eq(5) expect(all_builds.count).to eq(4)
# When we succeed last pending build, we will have a total of 5 succeeded builds, no new builds will be created # When the last one succeeds we have 4 successful builds.
#
succeed_pending succeed_pending
expect(process_pipeline).to be_falsey process_pipeline
expect(builds.success.count).to eq(5)
expect(all_builds.count).to eq(5) expect(builds.success.count).to eq(4)
expect(all_builds.count).to eq(4)
end end
end end
end end
def all_builds
pipeline.builds
end
def builds
all_builds.where.not(status: [:created, :skipped])
end
def process_pipeline
described_class.new(pipeline.project, user).execute(pipeline)
end
def succeed_pending
builds.pending.update_all(status: 'success')
end
def manual_actions
pipeline.manual_actions
end
def create_build(name, stage_idx, when_value = nil)
create(:ci_build,
:created,
pipeline: pipeline,
name: name,
stage_idx: stage_idx,
when: when_value)
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