diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-16 18:25:58 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-16 18:25:58 +0000 |
commit | a5f4bba440d7f9ea47046a0a561d49adf0a1e6d4 (patch) | |
tree | fb69158581673816a8cd895f9d352dcb3c678b1e /spec/services/ci/update_build_queue_service_spec.rb | |
parent | d16b2e8639e99961de6ddc93909f3bb5c1445ba1 (diff) | |
download | gitlab-ce-a5f4bba440d7f9ea47046a0a561d49adf0a1e6d4.tar.gz |
Add latest changes from gitlab-org/gitlab@14-0-stable-eev14.0.0-rc42
Diffstat (limited to 'spec/services/ci/update_build_queue_service_spec.rb')
-rw-r--r-- | spec/services/ci/update_build_queue_service_spec.rb | 368 |
1 files changed, 279 insertions, 89 deletions
diff --git a/spec/services/ci/update_build_queue_service_spec.rb b/spec/services/ci/update_build_queue_service_spec.rb index 2d9f80a249d..44d7809b85f 100644 --- a/spec/services/ci/update_build_queue_service_spec.rb +++ b/spec/services/ci/update_build_queue_service_spec.rb @@ -4,154 +4,344 @@ require 'spec_helper' RSpec.describe Ci::UpdateBuildQueueService do let(:project) { create(:project, :repository) } - let(:build) { create(:ci_build, pipeline: pipeline) } let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, pipeline: pipeline) } + + describe 'pending builds queue push / pop' do + describe '#push' do + let(:transition) { double('transition') } + + before do + allow(transition).to receive(:to).and_return('pending') + allow(transition).to receive(:within_transaction).and_yield + end + + context 'when pending build can be created' do + it 'creates a new pending build in transaction' do + queued = subject.push(build, transition) + + expect(queued).to eq build.id + end + + it 'increments queue push metric' do + metrics = spy('metrics') + + described_class.new(metrics).push(build, transition) + + expect(metrics) + .to have_received(:increment_queue_operation) + .with(:build_queue_push) + end + end - shared_examples 'refreshes runner' do - it 'ticks runner queue value' do - expect { subject.execute(build) }.to change { runner.ensure_runner_queue_value } + context 'when invalid transition is detected' do + it 'raises an error' do + allow(transition).to receive(:to).and_return('created') + + expect { subject.push(build, transition) } + .to raise_error(described_class::InvalidQueueTransition) + end + end + + context 'when duplicate entry exists' do + before do + ::Ci::PendingBuild.create!(build: build, project: project) + end + + it 'does nothing and returns build id' do + queued = subject.push(build, transition) + + expect(queued).to eq build.id + end + end end - end - shared_examples 'does not refresh runner' do - it 'ticks runner queue value' do - expect { subject.execute(build) }.not_to change { runner.ensure_runner_queue_value } + describe '#pop' do + let(:transition) { double('transition') } + + before do + allow(transition).to receive(:from).and_return('pending') + allow(transition).to receive(:within_transaction).and_yield + end + + context 'when pending build exists' do + before do + Ci::PendingBuild.create!(build: build, project: project) + end + + it 'removes pending build in a transaction' do + dequeued = subject.pop(build, transition) + + expect(dequeued).to eq build.id + end + + it 'increments queue pop metric' do + metrics = spy('metrics') + + described_class.new(metrics).pop(build, transition) + + expect(metrics) + .to have_received(:increment_queue_operation) + .with(:build_queue_pop) + end + end + + context 'when pending build does not exist' do + it 'does nothing if there is no pending build to remove' do + dequeued = subject.pop(build, transition) + + expect(dequeued).to be_nil + end + end + + context 'when invalid transition is detected' do + it 'raises an error' do + allow(transition).to receive(:from).and_return('created') + + expect { subject.pop(build, transition) } + .to raise_error(described_class::InvalidQueueTransition) + end + end end end - shared_examples 'matching build' do - context 'when there is a online runner that can pick build' do + describe 'shared runner builds tracking' do + let(:runner) { create(:ci_runner, :instance_type) } + let(:build) { create(:ci_build, runner: runner, pipeline: pipeline) } + + describe '#track' do + let(:transition) { double('transition') } + before do - runner.update!(contacted_at: 30.minutes.ago) + allow(transition).to receive(:to).and_return('running') + allow(transition).to receive(:within_transaction).and_yield end - it_behaves_like 'refreshes runner' + context 'when a shared runner build can be tracked' do + it 'creates a new shared runner build tracking entry' do + build_id = subject.track(build, transition) + + expect(build_id).to eq build.id + end + + it 'increments new shared runner build metric' do + metrics = spy('metrics') - it 'avoids running redundant queries' do - expect(Ci::Runner).not_to receive(:owned_or_instance_wide) + described_class.new(metrics).track(build, transition) - subject.execute(build) + expect(metrics) + .to have_received(:increment_queue_operation) + .with(:shared_runner_build_new) + end end - context 'when feature flag ci_reduce_queries_when_ticking_runner_queue is disabled' do + context 'when invalid transition is detected' do + it 'raises an error' do + allow(transition).to receive(:to).and_return('pending') + + expect { subject.track(build, transition) } + .to raise_error(described_class::InvalidQueueTransition) + end + end + + context 'when duplicate entry exists' do before do - stub_feature_flags(ci_reduce_queries_when_ticking_runner_queue: false) - stub_feature_flags(ci_runners_short_circuit_assignable_for: false) + ::Ci::RunningBuild.create!( + build: build, project: project, runner: runner, runner_type: runner.runner_type + ) end - it 'runs redundant queries using `owned_or_instance_wide` scope' do - expect(Ci::Runner).to receive(:owned_or_instance_wide).and_call_original + it 'does nothing and returns build id' do + build_id = subject.track(build, transition) - subject.execute(build) + expect(build_id).to eq build.id end end end - end - shared_examples 'mismatching tags' do - context 'when there is no runner that can pick build due to tag mismatch' do + describe '#untrack' do + let(:transition) { double('transition') } + before do - build.tag_list = [:docker] + allow(transition).to receive(:from).and_return('running') + allow(transition).to receive(:within_transaction).and_yield end - it_behaves_like 'does not refresh runner' + context 'when shared runner build tracking entry exists' do + before do + Ci::RunningBuild.create!( + build: build, project: project, runner: runner, runner_type: runner.runner_type + ) + end + + it 'removes shared runner build' do + build_id = subject.untrack(build, transition) + + expect(build_id).to eq build.id + end + + it 'increments shared runner build done metric' do + metrics = spy('metrics') + + described_class.new(metrics).untrack(build, transition) + + expect(metrics) + .to have_received(:increment_queue_operation) + .with(:shared_runner_build_done) + end + end + + context 'when tracking entry does not exist' do + it 'does nothing if there is no tracking entry to remove' do + build_id = subject.untrack(build, transition) + + expect(build_id).to be_nil + end + end + + context 'when invalid transition is detected' do + it 'raises an error' do + allow(transition).to receive(:from).and_return('pending') + + expect { subject.untrack(build, transition) } + .to raise_error(described_class::InvalidQueueTransition) + end + end end end - shared_examples 'recent runner queue' do - context 'when there is runner with expired cache' do - before do - runner.update!(contacted_at: Ci::Runner.recent_queue_deadline) + describe '#tick' do + shared_examples 'refreshes runner' do + it 'ticks runner queue value' do + expect { subject.tick(build) }.to change { runner.ensure_runner_queue_value } end + end - it_behaves_like 'does not refresh runner' + shared_examples 'does not refresh runner' do + it 'ticks runner queue value' do + expect { subject.tick(build) }.not_to change { runner.ensure_runner_queue_value } + end end - end - context 'when updating specific runners' do - let(:runner) { create(:ci_runner, :project, projects: [project]) } + shared_examples 'matching build' do + context 'when there is a online runner that can pick build' do + before do + runner.update!(contacted_at: 30.minutes.ago) + end - it_behaves_like 'matching build' - it_behaves_like 'mismatching tags' - it_behaves_like 'recent runner queue' + it_behaves_like 'refreshes runner' - context 'when the runner is assigned to another project' do - let(:another_project) { create(:project) } - let(:runner) { create(:ci_runner, :project, projects: [another_project]) } + it 'avoids running redundant queries' do + expect(Ci::Runner).not_to receive(:owned_or_instance_wide) - it_behaves_like 'does not refresh runner' + subject.tick(build) + end + end end - end - context 'when updating shared runners' do - let(:runner) { create(:ci_runner, :instance) } - - it_behaves_like 'matching build' - it_behaves_like 'mismatching tags' - it_behaves_like 'recent runner queue' + shared_examples 'mismatching tags' do + context 'when there is no runner that can pick build due to tag mismatch' do + before do + build.tag_list = [:docker] + end - context 'when there is no runner that can pick build due to being disabled on project' do - before do - build.project.shared_runners_enabled = false + it_behaves_like 'does not refresh runner' end + end - it_behaves_like 'does not refresh runner' + shared_examples 'recent runner queue' do + context 'when there is runner with expired cache' do + before do + runner.update!(contacted_at: Ci::Runner.recent_queue_deadline) + end + + it_behaves_like 'does not refresh runner' + end end - end - context 'when updating group runners' do - let(:group) { create(:group) } - let(:project) { create(:project, group: group) } - let(:runner) { create(:ci_runner, :group, groups: [group]) } + context 'when updating specific runners' do + let(:runner) { create(:ci_runner, :project, projects: [project]) } - it_behaves_like 'matching build' - it_behaves_like 'mismatching tags' - it_behaves_like 'recent runner queue' + it_behaves_like 'matching build' + it_behaves_like 'mismatching tags' + it_behaves_like 'recent runner queue' - context 'when there is no runner that can pick build due to being disabled on project' do - before do - build.project.group_runners_enabled = false - end + context 'when the runner is assigned to another project' do + let(:another_project) { create(:project) } + let(:runner) { create(:ci_runner, :project, projects: [another_project]) } - it_behaves_like 'does not refresh runner' + it_behaves_like 'does not refresh runner' + end end - end - context 'avoids N+1 queries', :request_store do - let!(:build) { create(:ci_build, pipeline: pipeline, tag_list: %w[a b]) } - let!(:project_runner) { create(:ci_runner, :project, :online, projects: [project], tag_list: %w[a b c]) } + context 'when updating shared runners' do + let(:runner) { create(:ci_runner, :instance) } - context 'when ci_preload_runner_tags and ci_reduce_queries_when_ticking_runner_queue are enabled' do - before do - stub_feature_flags( - ci_reduce_queries_when_ticking_runner_queue: true, - ci_preload_runner_tags: true - ) + it_behaves_like 'matching build' + it_behaves_like 'mismatching tags' + it_behaves_like 'recent runner queue' + + context 'when there is no runner that can pick build due to being disabled on project' do + before do + build.project.shared_runners_enabled = false + end + + it_behaves_like 'does not refresh runner' end + end - it 'does execute the same amount of queries regardless of number of runners' do - control_count = ActiveRecord::QueryRecorder.new { subject.execute(build) }.count + context 'when updating group runners' do + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } + let(:runner) { create(:ci_runner, :group, groups: [group]) } - create_list(:ci_runner, 10, :project, :online, projects: [project], tag_list: %w[b c d]) + it_behaves_like 'matching build' + it_behaves_like 'mismatching tags' + it_behaves_like 'recent runner queue' - expect { subject.execute(build) }.not_to exceed_all_query_limit(control_count) + context 'when there is no runner that can pick build due to being disabled on project' do + before do + build.project.group_runners_enabled = false + end + + it_behaves_like 'does not refresh runner' end end - context 'when ci_preload_runner_tags and ci_reduce_queries_when_ticking_runner_queue are disabled' do - before do - stub_feature_flags( - ci_reduce_queries_when_ticking_runner_queue: false, - ci_preload_runner_tags: false - ) + context 'avoids N+1 queries', :request_store do + let!(:build) { create(:ci_build, pipeline: pipeline, tag_list: %w[a b]) } + let!(:project_runner) { create(:ci_runner, :project, :online, projects: [project], tag_list: %w[a b c]) } + + context 'when ci_preload_runner_tags is enabled' do + before do + stub_feature_flags( + ci_preload_runner_tags: true + ) + end + + it 'does execute the same amount of queries regardless of number of runners' do + control_count = ActiveRecord::QueryRecorder.new { subject.tick(build) }.count + + create_list(:ci_runner, 10, :project, :online, projects: [project], tag_list: %w[b c d]) + + expect { subject.tick(build) }.not_to exceed_all_query_limit(control_count) + end end - it 'does execute more queries for more runners' do - control_count = ActiveRecord::QueryRecorder.new { subject.execute(build) }.count + context 'when ci_preload_runner_tags are disabled' do + before do + stub_feature_flags( + ci_preload_runner_tags: false + ) + end + + it 'does execute more queries for more runners' do + control_count = ActiveRecord::QueryRecorder.new { subject.tick(build) }.count - create_list(:ci_runner, 10, :project, :online, projects: [project], tag_list: %w[b c d]) + create_list(:ci_runner, 10, :project, :online, projects: [project], tag_list: %w[b c d]) - expect { subject.execute(build) }.to exceed_all_query_limit(control_count) + expect { subject.tick(build) }.to exceed_all_query_limit(control_count) + end end end end |