Commit e723ac82 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'fix-pipeline-creation-race-conditions' into 'master'

Make status transition resilient for downstream pipeline creation

See merge request gitlab-org/gitlab!25706
parents ffd11d27 92932aa6
...@@ -62,6 +62,10 @@ module Ci ...@@ -62,6 +62,10 @@ module Ci
end end
end end
def has_downstream_pipeline?
sourced_pipelines.exists?
end
def downstream_pipeline_params def downstream_pipeline_params
return child_params if triggers_child_pipeline? return child_params if triggers_child_pipeline?
return cross_project_params if downstream_project.present? return cross_project_params if downstream_project.present?
......
...@@ -227,6 +227,7 @@ module Ci ...@@ -227,6 +227,7 @@ module Ci
end end
after_transition created: :pending do |pipeline| after_transition created: :pending do |pipeline|
next if Feature.enabled?(:ci_drop_bridge_on_downstream_errors, pipeline.project, default_enabled: true)
next unless pipeline.bridge_triggered? next unless pipeline.bridge_triggered?
next if pipeline.bridge_waiting? next if pipeline.bridge_waiting?
...@@ -756,6 +757,8 @@ module Ci ...@@ -756,6 +757,8 @@ module Ci
raise BridgeStatusError unless source_bridge.active? raise BridgeStatusError unless source_bridge.active?
source_bridge.success! source_bridge.success!
rescue => e
Gitlab::ErrorTracking.track_exception(e, pipeline_id: id)
end end
def bridge_triggered? def bridge_triggered?
...@@ -774,6 +777,10 @@ module Ci ...@@ -774,6 +777,10 @@ module Ci
child_pipelines.exists? child_pipelines.exists?
end end
def created_successfully?
persisted? && failure_reason.blank?
end
def detailed_status(current_user) def detailed_status(current_user)
Gitlab::Ci::Status::Pipeline::Factory Gitlab::Ci::Status::Pipeline::Factory
.new(self, current_user) .new(self, current_user)
......
...@@ -5,9 +5,19 @@ module Ci ...@@ -5,9 +5,19 @@ module Ci
class CreateCrossProjectPipelineService < ::BaseService class CreateCrossProjectPipelineService < ::BaseService
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
DuplicateDownstreamPipelineError = Class.new(StandardError)
def execute(bridge) def execute(bridge)
@bridge = bridge @bridge = bridge
if bridge.has_downstream_pipeline?
Gitlab::ErrorTracking.track_exception(
DuplicateDownstreamPipelineError.new,
bridge_id: @bridge.id, project_id: @bridge.project_id
)
return
end
pipeline_params = @bridge.downstream_pipeline_params pipeline_params = @bridge.downstream_pipeline_params
target_ref = pipeline_params.dig(:target_revision, :ref) target_ref = pipeline_params.dig(:target_revision, :ref)
...@@ -18,14 +28,32 @@ module Ci ...@@ -18,14 +28,32 @@ module Ci
current_user, current_user,
pipeline_params.fetch(:target_revision)) pipeline_params.fetch(:target_revision))
service.execute( downstream_pipeline = service.execute(
pipeline_params.fetch(:source), pipeline_params[:execute_params]) do |pipeline| pipeline_params.fetch(:source), pipeline_params[:execute_params]) do |pipeline|
pipeline.variables.build(@bridge.downstream_variables) pipeline.variables.build(@bridge.downstream_variables)
end end
downstream_pipeline.tap do |pipeline|
next if Feature.disabled?(:ci_drop_bridge_on_downstream_errors, project, default_enabled: true)
update_bridge_status!(@bridge, pipeline)
end
end end
private private
def update_bridge_status!(bridge, pipeline)
Gitlab::OptimisticLocking.retry_lock(bridge) do |subject|
if pipeline.created_successfully?
# If bridge uses `strategy:depend` we leave it running
# and update the status when the downstream pipeline completes.
subject.success! unless subject.dependent?
else
subject.drop!(:downstream_pipeline_creation_failed)
end
end
end
def ensure_preconditions!(target_ref) def ensure_preconditions!(target_ref)
unless downstream_project_accessible? unless downstream_project_accessible?
@bridge.drop!(:downstream_bridge_project_not_found) @bridge.drop!(:downstream_bridge_project_not_found)
......
---
title: Drop bridge if downstream pipeline has errors
merge_request: 25706
author:
type: fixed
...@@ -2813,6 +2813,30 @@ describe Ci::Pipeline, :mailer do ...@@ -2813,6 +2813,30 @@ describe Ci::Pipeline, :mailer do
end end
end end
describe '#created_successfully?' do
subject { pipeline.created_successfully? }
context 'when pipeline is not persisted' do
let(:pipeline) { build(:ci_pipeline) }
it { is_expected.to be_falsey }
end
context 'when pipeline is persisted' do
context 'when pipeline has failure reasons' do
let(:pipeline) { create(:ci_pipeline, failure_reason: :config_error) }
it { is_expected.to be_falsey }
end
context 'when pipeline has no failure reasons' do
let(:pipeline) { create(:ci_pipeline, failure_reason: nil) }
it { is_expected.to be_truthy }
end
end
end
describe '#parent_pipeline' do describe '#parent_pipeline' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) } let(:pipeline) { create(:ci_pipeline, project: project) }
...@@ -2960,8 +2984,7 @@ describe Ci::Pipeline, :mailer do ...@@ -2960,8 +2984,7 @@ describe Ci::Pipeline, :mailer do
it 'can not update bridge status if is not active' do it 'can not update bridge status if is not active' do
bridge.success! bridge.success!
expect { pipeline.update_bridge_status! } expect { pipeline.update_bridge_status! }.not_to change { bridge.status }
.to raise_error Ci::Pipeline::BridgeStatusError
end end
end end
end end
...@@ -2992,9 +3015,12 @@ describe Ci::Pipeline, :mailer do ...@@ -2992,9 +3015,12 @@ describe Ci::Pipeline, :mailer do
end end
describe '#update_bridge_status!' do describe '#update_bridge_status!' do
it 'can not update upstream job status' do it 'tracks an ArgumentError and does not update upstream job status' do
expect { pipeline.update_bridge_status! } expect(Gitlab::ErrorTracking)
.to raise_error ArgumentError .to receive(:track_exception)
.with(instance_of(ArgumentError), pipeline_id: pipeline.id)
pipeline.update_bridge_status!
end end
end end
end end
......
...@@ -116,6 +116,28 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do ...@@ -116,6 +116,28 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do
expect(bridge.reload).to be_success expect(bridge.reload).to be_success
end end
context 'when bridge job has already any downstream pipelines' do
before do
bridge.sourced_pipelines.create!(
source_pipeline: bridge.pipeline,
source_project: bridge.project,
project: bridge.project,
pipeline: create(:ci_pipeline, project: bridge.project)
)
end
it 'logs an error and exits' do
expect(Gitlab::ErrorTracking)
.to receive(:track_exception)
.with(
instance_of(Ci::CreateCrossProjectPipelineService::DuplicateDownstreamPipelineError),
bridge_id: bridge.id, project_id: bridge.project.id)
.and_call_original
expect(Ci::CreatePipelineService).not_to receive(:new)
expect(service.execute(bridge)).to be_nil
end
end
context 'when target ref is not specified' do context 'when target ref is not specified' do
let(:trigger) do let(:trigger) do
{ trigger: { project: downstream_project.full_path } } { trigger: { project: downstream_project.full_path } }
...@@ -149,13 +171,11 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do ...@@ -149,13 +171,11 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do
expect(pipeline.source_bridge).to be_a ::Ci::Bridge expect(pipeline.source_bridge).to be_a ::Ci::Bridge
end end
it 'does not update bridge status when downstream pipeline gets processed' do it 'updates the bridge status when downstream pipeline gets processed' do
pipeline = service.execute(bridge) pipeline = service.execute(bridge)
expect(pipeline.reload).to be_failed expect(pipeline.reload).to be_failed
# TODO: This should change to failed once #198354 gets fixed. expect(bridge.reload).to be_failed
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25706
expect(bridge.reload).to be_pending
end end
end end
...@@ -242,6 +262,22 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do ...@@ -242,6 +262,22 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do
it_behaves_like 'creates a child pipeline' it_behaves_like 'creates a child pipeline'
it 'updates the bridge job to success' do
expect { service.execute(bridge) }.to change { bridge.status }.to 'success'
end
context 'when bridge uses "depend" strategy' do
let(:trigger) do
{
trigger: { include: 'child-pipeline.yml', strategy: 'depend' }
}
end
it 'does not update the bridge job status' do
expect { service.execute(bridge) }.not_to change { bridge.status }
end
end
context 'when latest sha for the ref changed in the meantime' do context 'when latest sha for the ref changed in the meantime' do
before do before do
upstream_project.repository.create_file( upstream_project.repository.create_file(
...@@ -298,6 +334,34 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do ...@@ -298,6 +334,34 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do
end end
end end
context 'when downstream pipeline creation errors out' do
let(:stub_config) { false }
before do
stub_ci_pipeline_yaml_file(YAML.dump(invalid: { yaml: 'error' }))
end
it 'creates only one new pipeline' do
expect { service.execute(bridge) }
.to change { Ci::Pipeline.count }.by(1)
end
it 'creates a new pipeline in the downstream project' do
pipeline = service.execute(bridge)
expect(pipeline.user).to eq bridge.user
expect(pipeline.project).to eq downstream_project
end
it 'drops the bridge' do
pipeline = service.execute(bridge)
expect(pipeline.reload).to be_failed
expect(bridge.reload).to be_failed
expect(bridge.failure_reason).to eq('downstream_pipeline_creation_failed')
end
end
context 'when bridge job has YAML variables defined' do context 'when bridge job has YAML variables defined' do
before do before do
bridge.yaml_variables = [{ key: 'BRIDGE', value: 'var', public: true }] bridge.yaml_variables = [{ key: 'BRIDGE', value: 'var', public: true }]
......
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