debian-mirror-gitlab/spec/services/ci/retry_job_service_spec.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

541 lines
17 KiB
Ruby
Raw Normal View History

2019-07-31 22:56:46 +05:30
# frozen_string_literal: true
2017-08-17 22:00:37 +05:30
require 'spec_helper'
2023-04-23 21:23:45 +05:30
RSpec.describe Ci::RetryJobService, feature_category: :continuous_integration do
2023-01-13 00:05:48 +05:30
using RSpec::Parameterized::TableSyntax
2021-01-03 14:25:43 +05:30
let_it_be(:reporter) { create(:user) }
let_it_be(:developer) { create(:user) }
2020-03-13 15:44:24 +05:30
let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline) do
2022-10-11 01:57:18 +05:30
create(:ci_pipeline, project: project, sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0')
2020-03-13 15:44:24 +05:30
end
2018-03-17 18:26:18 +05:30
2021-01-03 14:25:43 +05:30
let_it_be(:stage) do
2023-01-13 00:05:48 +05:30
create(:ci_stage, pipeline: pipeline, name: 'test')
2018-03-17 18:26:18 +05:30
end
2023-01-13 00:05:48 +05:30
let_it_be(:deploy_stage) { create(:ci_stage, pipeline: pipeline, name: 'deploy', position: stage.position + 1) }
2022-08-27 11:52:29 +05:30
let(:job_variables_attributes) { [{ key: 'MANUAL_VAR', value: 'manual test var' }] }
2021-01-03 14:25:43 +05:30
let(:user) { developer }
2017-08-17 22:00:37 +05:30
2022-07-16 23:28:13 +05:30
let(:service) { described_class.new(project, user) }
2017-08-17 22:00:37 +05:30
2021-01-03 14:25:43 +05:30
before_all do
project.add_developer(developer)
project.add_reporter(reporter)
end
2023-04-23 21:23:45 +05:30
shared_context 'retryable bridge' do
let_it_be(:downstream_project) { create(:project, :repository) }
let_it_be_with_refind(:job) do
create(:ci_bridge, :success,
pipeline: pipeline, downstream: downstream_project, description: 'a trigger job', ci_stage: stage
)
end
let_it_be(:job_to_clone) { job }
before do
job.update!(retried: false)
end
end
2023-01-13 00:05:48 +05:30
shared_context 'retryable build' do
let_it_be_with_reload(:job) do
create(:ci_build, :success, pipeline: pipeline, ci_stage: stage)
2022-07-16 23:28:13 +05:30
end
let_it_be(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
2018-12-05 23:21:45 +05:30
2022-07-16 23:28:13 +05:30
let_it_be(:job_to_clone) do
create(:ci_build, :failed, :picked, :expired, :erased, :queued, :coverage, :tags,
:allowed_to_fail, :on_tag, :triggered, :teardown_environment, :resource_group,
2022-10-11 01:57:18 +05:30
description: 'my-job', ci_stage: stage,
2022-07-16 23:28:13 +05:30
pipeline: pipeline, auto_canceled_by: another_pipeline,
scheduled_at: 10.seconds.since)
2017-08-17 22:00:37 +05:30
end
2022-06-21 17:19:12 +05:30
before do
2022-07-16 23:28:13 +05:30
job.update!(retried: false, status: :success)
job_to_clone.update!(retried: false, status: :success)
2022-06-21 17:19:12 +05:30
end
2022-07-16 23:28:13 +05:30
end
2022-06-21 17:19:12 +05:30
2022-07-16 23:28:13 +05:30
shared_examples_for 'clones the job' do
let(:job) { job_to_clone }
2020-04-08 14:13:33 +05:30
2022-07-16 23:28:13 +05:30
before_all do
2022-10-11 01:57:18 +05:30
job_to_clone.update!(ci_stage: stage)
2022-07-16 23:28:13 +05:30
create(:ci_build_need, build: job_to_clone)
end
context 'when the user has ability to execute job' do
before do
stub_not_protect_default_branch
2017-08-17 22:00:37 +05:30
end
2018-03-17 18:26:18 +05:30
2022-07-16 23:28:13 +05:30
context 'when there is a failed job ToDo for the MR' do
let!(:merge_request) { create(:merge_request, source_project: project, author: user, head_pipeline: pipeline) }
let!(:todo) { create(:todo, :build_failed, user: user, project: project, author: user, target: merge_request) }
2018-03-17 18:26:18 +05:30
2022-07-16 23:28:13 +05:30
it 'resolves the ToDo for the failed job' do
expect do
service.execute(job)
end.to change { todo.reload.state }.from('pending').to('done')
2018-03-17 18:26:18 +05:30
end
end
2020-04-08 14:13:33 +05:30
2022-07-16 23:28:13 +05:30
context 'when the job has needs' do
before do
2023-01-13 00:05:48 +05:30
create_list(:ci_build_need, 2, build: job)
2022-07-16 23:28:13 +05:30
end
2020-04-08 14:13:33 +05:30
2022-07-16 23:28:13 +05:30
it 'bulk inserts all the needs' do
expect(Ci::BuildNeed).to receive(:bulk_insert!).and_call_original
2022-01-26 12:08:38 +05:30
2022-07-16 23:28:13 +05:30
new_job
end
2022-01-26 12:08:38 +05:30
end
2017-08-17 22:00:37 +05:30
2022-07-16 23:28:13 +05:30
it 'marks the old job as retried' do
expect(new_job).to be_latest
expect(job).to be_retried
expect(job).to be_processed
2017-08-17 22:00:37 +05:30
end
end
2022-07-16 23:28:13 +05:30
context 'when the user does not have permission to execute the job' do
let(:user) { reporter }
2017-08-17 22:00:37 +05:30
2022-07-16 23:28:13 +05:30
it 'raises an error' do
expect { service.execute(job) }
.to raise_error Gitlab::Access::AccessDeniedError
2018-11-18 11:00:15 +05:30
end
end
2022-07-16 23:28:13 +05:30
end
2017-08-17 22:00:37 +05:30
2023-04-23 21:23:45 +05:30
shared_examples_for 'does not retry the job' do
it 'returns :not_retryable and :unprocessable_entity' do
expect(subject.message).to be('Job cannot be retried')
expect(subject.payload[:reason]).to eq(:not_retryable)
expect(subject.payload[:job]).to eq(job)
end
end
2022-07-16 23:28:13 +05:30
shared_examples_for 'retries the job' do
it_behaves_like 'clones the job'
2017-08-17 22:00:37 +05:30
2022-07-16 23:28:13 +05:30
it 'enqueues the new job' do
expect(new_job).to be_pending
end
2017-08-17 22:00:37 +05:30
2022-07-16 23:28:13 +05:30
context 'when there are subsequent processables that are skipped' do
let!(:subsequent_build) do
2023-01-13 00:05:48 +05:30
create(:ci_build, :skipped, pipeline: pipeline, ci_stage: deploy_stage)
2017-08-17 22:00:37 +05:30
end
2022-07-16 23:28:13 +05:30
let!(:subsequent_bridge) do
2023-01-13 00:05:48 +05:30
create(:ci_bridge, :skipped, pipeline: pipeline, ci_stage: deploy_stage)
2022-07-16 23:28:13 +05:30
end
2021-03-08 18:12:59 +05:30
2022-07-16 23:28:13 +05:30
it 'resumes pipeline processing in the subsequent stage' do
service.execute(job)
2021-03-08 18:12:59 +05:30
2022-07-16 23:28:13 +05:30
expect(subsequent_build.reload).to be_created
expect(subsequent_bridge.reload).to be_created
2017-08-17 22:00:37 +05:30
end
2020-05-24 23:13:21 +05:30
2022-07-16 23:28:13 +05:30
it 'updates ownership for subsequent builds' do
expect { service.execute(job) }.to change { subsequent_build.reload.user }.to(user)
end
2020-05-24 23:13:21 +05:30
2022-07-16 23:28:13 +05:30
it 'updates ownership for subsequent bridges' do
expect { service.execute(job) }.to change { subsequent_bridge.reload.user }.to(user)
2020-05-24 23:13:21 +05:30
end
2022-07-16 23:28:13 +05:30
end
2020-11-24 15:15:51 +05:30
2022-07-16 23:28:13 +05:30
context 'when the pipeline has other jobs' do
2023-01-13 00:05:48 +05:30
let!(:other_test_build) { create(:ci_build, pipeline: pipeline, ci_stage: stage) }
let!(:deploy) { create(:ci_build, pipeline: pipeline, ci_stage: deploy_stage) }
let!(:deploy_needs_build2) { create(:ci_build_need, build: deploy, name: other_test_build.name) }
2020-11-24 15:15:51 +05:30
2022-07-16 23:28:13 +05:30
context 'when job has a nil scheduling_type' do
before do
job.pipeline.processables.update_all(scheduling_type: nil)
job.reload
end
2020-11-24 15:15:51 +05:30
2022-07-16 23:28:13 +05:30
it 'populates scheduling_type of processables' do
expect(new_job.scheduling_type).to eq('stage')
expect(job.reload.scheduling_type).to eq('stage')
2023-01-13 00:05:48 +05:30
expect(other_test_build.reload.scheduling_type).to eq('stage')
2022-07-16 23:28:13 +05:30
expect(deploy.reload.scheduling_type).to eq('dag')
2020-11-24 15:15:51 +05:30
end
end
2022-03-02 08:16:31 +05:30
2022-07-16 23:28:13 +05:30
context 'when job has scheduling_type' do
it 'does not call populate_scheduling_type!' do
expect(job.pipeline).not_to receive(:ensure_scheduling_type!)
2022-03-02 08:16:31 +05:30
2022-07-16 23:28:13 +05:30
expect(new_job.scheduling_type).to eq('stage')
2022-03-02 08:16:31 +05:30
end
end
2017-08-17 22:00:37 +05:30
end
2022-07-16 23:28:13 +05:30
context 'when the pipeline is a child pipeline and the bridge uses strategy:depend' do
let!(:parent_pipeline) { create(:ci_pipeline, project: project) }
let!(:bridge) { create(:ci_bridge, :strategy_depend, pipeline: parent_pipeline, status: 'success') }
let!(:source_pipeline) { create(:ci_sources_pipeline, pipeline: pipeline, source_job: bridge) }
2021-01-03 14:25:43 +05:30
2022-07-16 23:28:13 +05:30
it 'marks the source bridge as pending' do
service.execute(job)
2022-06-21 17:19:12 +05:30
2022-07-16 23:28:13 +05:30
expect(bridge.reload).to be_pending
2022-06-21 17:19:12 +05:30
end
2017-08-17 22:00:37 +05:30
end
end
2023-01-13 00:05:48 +05:30
shared_examples_for 'checks enqueue_immediately?' do
it "returns enqueue_immediately" do
subject
expect(new_job.enqueue_immediately?).to eq enqueue_immediately
end
end
2021-11-18 22:05:49 +05:30
describe '#clone!' do
2022-07-16 23:28:13 +05:30
let(:new_job) { service.clone!(job) }
2017-08-17 22:00:37 +05:30
2021-11-18 22:05:49 +05:30
it 'raises an error when an unexpected class is passed' do
expect { service.clone!(create(:ci_build).present) }.to raise_error(TypeError)
end
2023-04-23 21:23:45 +05:30
context 'when the job to be cloned is a bridge' do
include_context 'retryable bridge'
it_behaves_like 'clones the job'
context 'when given variables' do
let(:new_job) { service.clone!(job, variables: job_variables_attributes) }
it 'does not give variables to the new bridge' do
expect { new_job }.not_to raise_error
end
end
end
2022-07-16 23:28:13 +05:30
context 'when the job to be cloned is a build' do
include_context 'retryable build'
2017-08-17 22:00:37 +05:30
2022-07-16 23:28:13 +05:30
let(:job) { job_to_clone }
2017-08-17 22:00:37 +05:30
2022-07-16 23:28:13 +05:30
it_behaves_like 'clones the job'
2019-12-21 20:55:43 +05:30
2022-07-16 23:28:13 +05:30
context 'when a build with a deployment is retried' do
let!(:job) do
2020-03-13 15:44:24 +05:30
create(:ci_build, :with_deployment, :deploy_to_production,
2023-01-13 00:05:48 +05:30
pipeline: pipeline, ci_stage: stage)
2019-12-21 20:55:43 +05:30
end
it 'creates a new deployment' do
2022-07-16 23:28:13 +05:30
expect { new_job }.to change { Deployment.count }.by(1)
2020-04-08 14:13:33 +05:30
end
2021-12-11 22:18:48 +05:30
it 'does not create a new environment' do
2022-07-16 23:28:13 +05:30
expect { new_job }.not_to change { Environment.count }
2021-12-11 22:18:48 +05:30
end
end
2022-07-16 23:28:13 +05:30
context 'when a build with a dynamic environment is retried' do
2022-06-21 17:19:12 +05:30
let_it_be(:other_developer) { create(:user).tap { |u| project.add_developer(u) } }
2021-12-11 22:18:48 +05:30
let(:environment_name) { 'review/$CI_COMMIT_REF_SLUG-$GITLAB_USER_ID' }
2022-07-16 23:28:13 +05:30
let!(:job) do
2022-10-11 01:57:18 +05:30
create(:ci_build, :with_deployment,
environment: environment_name,
options: { environment: { name: environment_name } },
pipeline: pipeline,
ci_stage: stage,
user: other_developer)
2021-12-11 22:18:48 +05:30
end
it 'creates a new deployment' do
2022-07-16 23:28:13 +05:30
expect { new_job }.to change { Deployment.count }.by(1)
2021-12-11 22:18:48 +05:30
end
it 'does not create a new environment' do
2022-07-16 23:28:13 +05:30
expect { new_job }.not_to change { Environment.count }
2021-12-11 22:18:48 +05:30
end
2019-12-21 20:55:43 +05:30
end
2022-08-27 11:52:29 +05:30
context 'when given variables' do
let(:new_job) { service.clone!(job, variables: job_variables_attributes) }
context 'when the build is actionable' do
let_it_be_with_refind(:job) { create(:ci_build, :actionable, pipeline: pipeline) }
it 'gives variables to the new build' do
expect(new_job.job_variables.count).to be(1)
expect(new_job.job_variables.first.key).to eq('MANUAL_VAR')
expect(new_job.job_variables.first.value).to eq('manual test var')
end
end
context 'when the build is not actionable' do
let_it_be_with_refind(:job) { create(:ci_build, pipeline: pipeline) }
it 'does not give variables to the new build' do
expect(new_job.job_variables.count).to be_zero
end
end
end
2022-07-16 23:28:13 +05:30
end
2023-01-13 00:05:48 +05:30
context 'when enqueue_if_actionable is provided' do
let!(:job) do
create(:ci_build, *[trait].compact, :failed, pipeline: pipeline, ci_stage: stage)
end
2020-03-13 15:44:24 +05:30
2023-01-13 00:05:48 +05:30
let(:new_job) { subject }
2022-01-26 12:08:38 +05:30
2023-01-13 00:05:48 +05:30
subject { service.clone!(job, enqueue_if_actionable: enqueue_if_actionable) }
2022-08-27 11:52:29 +05:30
2023-01-13 00:05:48 +05:30
where(:enqueue_if_actionable, :trait, :enqueue_immediately) do
true | nil | false
true | :manual | true
true | :expired_scheduled | true
false | nil | false
false | :manual | false
false | :expired_scheduled | false
end
with_them do
it_behaves_like 'checks enqueue_immediately?'
2022-08-27 11:52:29 +05:30
end
2022-07-16 23:28:13 +05:30
end
2023-01-13 00:05:48 +05:30
end
describe '#execute' do
let(:new_job) { subject[:job] }
subject { service.execute(job) }
2020-03-13 15:44:24 +05:30
2023-04-23 21:23:45 +05:30
context 'when the job to be retried is a bridge' do
context 'and it is not retryable' do
let_it_be(:job) { create(:ci_bridge, :failed, :reached_max_descendant_pipelines_depth) }
it_behaves_like 'does not retry the job'
end
include_context 'retryable bridge'
it_behaves_like 'retries the job'
context 'when given variables' do
let(:new_job) { service.clone!(job, variables: job_variables_attributes) }
it 'does not give variables to the new bridge' do
expect { new_job }.not_to raise_error
end
end
end
2022-07-16 23:28:13 +05:30
context 'when the job to be retried is a build' do
2023-04-23 21:23:45 +05:30
context 'and it is not retryable' do
let_it_be(:job) { create(:ci_build, :deployment_rejected, pipeline: pipeline) }
it_behaves_like 'does not retry the job'
end
2022-07-16 23:28:13 +05:30
include_context 'retryable build'
it_behaves_like 'retries the job'
2020-03-13 15:44:24 +05:30
2023-05-08 21:46:49 +05:30
context 'automatic retryable build' do
let!(:auto_retryable_build) do
create(:ci_build, pipeline: pipeline, ci_stage: stage, user: user, options: { retry: 1 })
end
def drop_build!
auto_retryable_build.drop_with_exit_code!('test failure', 1)
end
it 'creates a new build and enqueues BuildQueueWorker' do
expect { drop_build! }.to change { Ci::Build.count }.by(1)
.and change { BuildQueueWorker.jobs.count }.by(1)
end
end
2022-07-16 23:28:13 +05:30
context 'when there are subsequent jobs that are skipped' do
let!(:subsequent_build) do
2023-01-13 00:05:48 +05:30
create(:ci_build, :skipped, pipeline: pipeline, ci_stage: deploy_stage)
2020-03-13 15:44:24 +05:30
end
2017-08-17 22:00:37 +05:30
2022-07-16 23:28:13 +05:30
let!(:subsequent_bridge) do
2023-01-13 00:05:48 +05:30
create(:ci_bridge, :skipped, pipeline: pipeline, ci_stage: deploy_stage)
2022-07-16 23:28:13 +05:30
end
2021-01-03 14:25:43 +05:30
2022-07-16 23:28:13 +05:30
it 'does not cause an N+1 when updating the job ownership' do
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { service.execute(job) }.count
2023-01-13 00:05:48 +05:30
create_list(:ci_build, 2, :skipped, pipeline: pipeline, ci_stage: deploy_stage)
2022-07-16 23:28:13 +05:30
expect { service.execute(job) }.not_to exceed_all_query_limit(control_count)
end
2017-08-17 22:00:37 +05:30
end
2022-08-27 11:52:29 +05:30
context 'when given variables' do
let(:new_job) { service.clone!(job, variables: job_variables_attributes) }
context 'when the build is actionable' do
let_it_be_with_refind(:job) { create(:ci_build, :actionable, pipeline: pipeline) }
it 'gives variables to the new build' do
expect(new_job.job_variables.count).to be(1)
expect(new_job.job_variables.first.key).to eq('MANUAL_VAR')
expect(new_job.job_variables.first.value).to eq('manual test var')
end
end
context 'when the build is not actionable' do
let_it_be_with_refind(:job) { create(:ci_build, pipeline: pipeline) }
it 'does not give variables to the new build' do
expect(new_job.job_variables.count).to be_zero
end
end
end
2017-08-17 22:00:37 +05:30
end
2023-01-13 00:05:48 +05:30
context 'when job being retried has jobs in previous stages' do
let!(:job) do
create(
:ci_build,
:failed,
name: 'deploy_a',
pipeline: pipeline,
ci_stage: deploy_stage
)
end
before do
create(
:ci_build,
previous_stage_job_status,
name: 'test_a',
pipeline: pipeline,
ci_stage: stage
)
end
where(:previous_stage_job_status, :after_status) do
:created | 'created'
:pending | 'created'
:running | 'created'
:manual | 'created'
:scheduled | 'created'
:success | 'pending'
:failed | 'skipped'
:skipped | 'pending'
end
with_them do
it 'updates the new job status to after_status' do
expect(subject).to be_success
expect(new_job.status).to eq after_status
end
end
end
context 'when job being retried has DAG dependencies' do
let!(:job) do
create(
:ci_build,
:failed,
:dependent,
name: 'deploy_a',
pipeline: pipeline,
ci_stage: deploy_stage,
needed: dependency
)
end
let(:dependency) do
create(
:ci_build,
dag_dependency_status,
name: 'test_a',
pipeline: pipeline,
ci_stage: stage
)
end
where(:dag_dependency_status, :after_status) do
:created | 'created'
:pending | 'created'
:running | 'created'
:manual | 'created'
:scheduled | 'created'
:success | 'pending'
:failed | 'skipped'
:skipped | 'skipped'
end
with_them do
it 'updates the new job status to after_status' do
expect(subject).to be_success
expect(new_job.status).to eq after_status
end
end
end
context 'when there are other manual/scheduled jobs' do
let_it_be(:test_manual_build) do
create(:ci_build, :manual, pipeline: pipeline, ci_stage: stage)
end
let_it_be(:subsequent_manual_build) do
create(:ci_build, :manual, pipeline: pipeline, ci_stage: deploy_stage)
end
let_it_be(:test_scheduled_build) do
create(:ci_build, :scheduled, pipeline: pipeline, ci_stage: stage)
end
let_it_be(:subsequent_scheduled_build) do
create(:ci_build, :scheduled, pipeline: pipeline, ci_stage: deploy_stage)
end
let!(:job) do
create(:ci_build, *[trait].compact, :failed, pipeline: pipeline, ci_stage: stage)
end
where(:trait, :enqueue_immediately) do
nil | false
:manual | true
:expired_scheduled | true
end
with_them do
it 'retries the given job but not the other manual/scheduled jobs' do
expect { subject }
.to change { Ci::Build.count }.by(1)
.and not_change { test_manual_build.reload.status }
.and not_change { subsequent_manual_build.reload.status }
.and not_change { test_scheduled_build.reload.status }
.and not_change { subsequent_scheduled_build.reload.status }
expect(new_job).to be_pending
end
it_behaves_like 'checks enqueue_immediately?'
end
end
2017-08-17 22:00:37 +05:30
end
end