Commit 9254fa57 authored by Fabio Pitino's avatar Fabio Pitino

Loosen rule to detect cyclical pipelines

The rule to detect whether upstream/downstream
pipelines chain had an infinite loop was too
strict.
To soften this rule we distinguish
pipelines also by their source and add another
level of depth to avoid false positives.

Changelog: changed
parent 88848ff8
...@@ -125,7 +125,9 @@ module Ci ...@@ -125,7 +125,9 @@ module Ci
config_checksum(pipeline) unless pipeline.child? config_checksum(pipeline) unless pipeline.child?
end end
pipeline_checksums.uniq.length != pipeline_checksums.length # To avoid false positives we allow 1 cycle in the ancestry and
# fail when 2 cycles are detected: A -> B -> A -> B -> A
pipeline_checksums.tally.any? { |_checksum, occurrences| occurrences > 2 }
end end
end end
...@@ -137,7 +139,7 @@ module Ci ...@@ -137,7 +139,7 @@ module Ci
end end
def config_checksum(pipeline) def config_checksum(pipeline)
[pipeline.project_id, pipeline.ref].hash [pipeline.project_id, pipeline.ref, pipeline.source].hash
end end
end end
end end
...@@ -441,44 +441,99 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do ...@@ -441,44 +441,99 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
end end
end end
context 'when relationship between pipelines is cyclical' do describe 'cyclical dependency detection' do
before do shared_examples 'detects cyclical pipelines' do
pipeline_a = create(:ci_pipeline, project: upstream_project) it 'does not create a new pipeline' do
pipeline_b = create(:ci_pipeline, project: downstream_project) expect { service.execute(bridge) }
pipeline_c = create(:ci_pipeline, project: upstream_project) .not_to change { Ci::Pipeline.count }
end
it 'changes status of the bridge build' do
service.execute(bridge)
create_source_pipeline(pipeline_a, pipeline_b) expect(bridge.reload).to be_failed
create_source_pipeline(pipeline_b, pipeline_c) expect(bridge.failure_reason).to eq 'pipeline_loop_detected'
create_source_pipeline(pipeline_c, upstream_pipeline) end
end end
it 'does not create a new pipeline' do shared_examples 'passes cyclical pipeline precondition' do
expect { service.execute(bridge) } it 'creates a new pipeline' do
.not_to change { Ci::Pipeline.count } expect { service.execute(bridge) }
.to change { Ci::Pipeline.count }
end
it 'expect bridge build not to be failed' do
service.execute(bridge)
expect(bridge.reload).not_to be_failed
end
end end
it 'changes status of the bridge build' do context 'when pipeline ancestry contains 2 cycles of dependencies' do
service.execute(bridge) before do
# A(push on master) -> B(pipeline on master) -> A(push on master) ->
# B(pipeline on master) -> A(push on master)
pipeline_1 = create(:ci_pipeline, project: upstream_project, source: :push)
pipeline_2 = create(:ci_pipeline, project: downstream_project, source: :pipeline)
pipeline_3 = create(:ci_pipeline, project: upstream_project, source: :push)
pipeline_4 = create(:ci_pipeline, project: downstream_project, source: :pipeline)
create_source_pipeline(pipeline_1, pipeline_2)
create_source_pipeline(pipeline_2, pipeline_3)
create_source_pipeline(pipeline_3, pipeline_4)
create_source_pipeline(pipeline_4, upstream_pipeline)
end
expect(bridge.reload).to be_failed it_behaves_like 'detects cyclical pipelines'
expect(bridge.failure_reason).to eq 'pipeline_loop_detected'
context 'when ci_drop_cyclical_triggered_pipelines is not enabled' do
before do
stub_feature_flags(ci_drop_cyclical_triggered_pipelines: false)
end
it_behaves_like 'passes cyclical pipeline precondition'
end
end end
context 'when ci_drop_cyclical_triggered_pipelines is not enabled' do context 'when source in the ancestry differ' do
before do before do
stub_feature_flags(ci_drop_cyclical_triggered_pipelines: false) # A(push on master) -> B(pipeline on master) -> A(pipeline on master)
pipeline_1 = create(:ci_pipeline, project: upstream_project, source: :push)
pipeline_2 = create(:ci_pipeline, project: downstream_project, source: :pipeline)
upstream_pipeline.update!(source: :pipeline)
create_source_pipeline(pipeline_1, pipeline_2)
create_source_pipeline(pipeline_2, upstream_pipeline)
end end
it 'creates a new pipeline' do it_behaves_like 'passes cyclical pipeline precondition'
expect { service.execute(bridge) } end
.to change { Ci::Pipeline.count }
context 'when ref in the ancestry differ' do
before do
# A(push on master) -> B(pipeline on master) -> A(push on feature-1)
pipeline_1 = create(:ci_pipeline, ref: 'master', project: upstream_project, source: :push)
pipeline_2 = create(:ci_pipeline, ref: 'master', project: downstream_project, source: :pipeline)
upstream_pipeline.update!(ref: 'feature-1')
create_source_pipeline(pipeline_1, pipeline_2)
create_source_pipeline(pipeline_2, upstream_pipeline)
end end
it 'expect bridge build not to be failed' do it_behaves_like 'passes cyclical pipeline precondition'
service.execute(bridge) end
expect(bridge.reload).not_to be_failed context 'when only 1 cycle is detected' do
before do
# A(push on master) -> B(pipeline on master) -> A(push on master)
pipeline_1 = create(:ci_pipeline, ref: 'master', project: upstream_project, source: :push)
pipeline_2 = create(:ci_pipeline, ref: 'master', project: downstream_project, source: :pipeline)
create_source_pipeline(pipeline_1, pipeline_2)
create_source_pipeline(pipeline_2, upstream_pipeline)
end end
it_behaves_like 'passes cyclical pipeline precondition'
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