Commit c394109a authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'handle-object-storage-errors' into 'master'

Return 503 to Runner when object storage is unavailable

See merge request gitlab-org/gitlab!25822
parents c9a9e29c b245e198
# frozen_string_literal: true # frozen_string_literal: true
module Ci module Ci
class CreateJobArtifactsService class CreateJobArtifactsService < ::BaseService
ArtifactsExistError = Class.new(StandardError) ArtifactsExistError = Class.new(StandardError)
OBJECT_STORAGE_ERRORS = [
Errno::EIO,
Google::Apis::ServerError,
Signet::RemoteServerError
].freeze
def execute(job, artifacts_file, params, metadata_file: nil) def execute(job, artifacts_file, params, metadata_file: nil)
expire_in = params['expire_in'] || expire_in = params['expire_in'] ||
...@@ -26,18 +31,20 @@ module Ci ...@@ -26,18 +31,20 @@ module Ci
expire_in: expire_in) expire_in: expire_in)
end end
job.update(artifacts_expire_in: expire_in) if job.update(artifacts_expire_in: expire_in)
rescue ActiveRecord::RecordNotUnique => error success
return true if sha256_matches_existing_artifact?(job, params['artifact_type'], artifacts_file) else
error(job.errors.messages, :bad_request)
end
Gitlab::ErrorTracking.track_exception(error, rescue ActiveRecord::RecordNotUnique => error
job_id: job.id, return success if sha256_matches_existing_artifact?(job, params['artifact_type'], artifacts_file)
project_id: job.project_id,
uploading_type: params['artifact_type']
)
job.errors.add(:base, 'another artifact of the same type already exists') track_exception(error, job, params)
false error('another artifact of the same type already exists', :bad_request)
rescue *OBJECT_STORAGE_ERRORS => error
track_exception(error, job, params)
error(error.message, :service_unavailable)
end end
private private
...@@ -48,5 +55,13 @@ module Ci ...@@ -48,5 +55,13 @@ module Ci
existing_artifact.file_sha256 == artifacts_file.sha256 existing_artifact.file_sha256 == artifacts_file.sha256
end end
def track_exception(error, job, params)
Gitlab::ErrorTracking.track_exception(error,
job_id: job.id,
project_id: job.project_id,
uploading_type: params['artifact_type']
)
end
end end
end end
---
title: Return 503 to the Runner when the object storage is unavailable
merge_request: 25822
author:
type: fixed
...@@ -283,10 +283,12 @@ module API ...@@ -283,10 +283,12 @@ module API
bad_request!('Missing artifacts file!') unless artifacts bad_request!('Missing artifacts file!') unless artifacts
file_too_large! unless artifacts.size < max_artifacts_size(job) file_too_large! unless artifacts.size < max_artifacts_size(job)
if Ci::CreateJobArtifactsService.new.execute(job, artifacts, params, metadata_file: metadata) result = Ci::CreateJobArtifactsService.new(job.project).execute(job, artifacts, params, metadata_file: metadata)
if result[:status] == :success
status :created status :created
else else
render_validation_error!(job) render_api_error!(result[:message], result[:http_status])
end end
end end
......
...@@ -1979,6 +1979,21 @@ describe API::Runner, :clean_gitlab_redis_shared_state do ...@@ -1979,6 +1979,21 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end end
end end
context 'when object storage throws errors' do
let(:params) { { artifact_type: :archive, artifact_format: :zip } }
it 'does not store artifacts' do
allow_next_instance_of(JobArtifactUploader) do |uploader|
allow(uploader).to receive(:store!).and_raise(Errno::EIO)
end
upload_artifacts(file_upload, headers_with_token, params)
expect(response).to have_gitlab_http_status(:service_unavailable)
expect(job.reload.job_artifacts_archive).to be_nil
end
end
context 'when artifacts are being stored outside of tmp path' do context 'when artifacts are being stored outside of tmp path' do
let(:new_tmpdir) { Dir.mktmpdir } let(:new_tmpdir) { Dir.mktmpdir }
......
...@@ -3,8 +3,9 @@ ...@@ -3,8 +3,9 @@
require 'spec_helper' require 'spec_helper'
describe Ci::CreateJobArtifactsService do describe Ci::CreateJobArtifactsService do
let(:service) { described_class.new } let_it_be(:project) { create(:project) }
let(:job) { create(:ci_build) } let(:service) { described_class.new(project) }
let(:job) { create(:ci_build, project: project) }
let(:artifacts_sha256) { '0' * 64 } let(:artifacts_sha256) { '0' * 64 }
let(:metadata_file) { nil } let(:metadata_file) { nil }
...@@ -64,7 +65,7 @@ describe Ci::CreateJobArtifactsService do ...@@ -64,7 +65,7 @@ describe Ci::CreateJobArtifactsService do
it 'sets expiration date according to application settings' do it 'sets expiration date according to application settings' do
expected_expire_at = 1.day.from_now expected_expire_at = 1.day.from_now
expect(subject).to be_truthy expect(subject).to match(a_hash_including(status: :success))
archive_artifact, metadata_artifact = job.job_artifacts.last(2) archive_artifact, metadata_artifact = job.job_artifacts.last(2)
expect(job.artifacts_expire_at).to be_within(1.minute).of(expected_expire_at) expect(job.artifacts_expire_at).to be_within(1.minute).of(expected_expire_at)
...@@ -80,7 +81,7 @@ describe Ci::CreateJobArtifactsService do ...@@ -80,7 +81,7 @@ describe Ci::CreateJobArtifactsService do
it 'sets expiration date according to the parameter' do it 'sets expiration date according to the parameter' do
expected_expire_at = 2.hours.from_now expected_expire_at = 2.hours.from_now
expect(subject).to be_truthy expect(subject).to match(a_hash_including(status: :success))
archive_artifact, metadata_artifact = job.job_artifacts.last(2) archive_artifact, metadata_artifact = job.job_artifacts.last(2)
expect(job.artifacts_expire_at).to be_within(1.minute).of(expected_expire_at) expect(job.artifacts_expire_at).to be_within(1.minute).of(expected_expire_at)
...@@ -101,21 +102,50 @@ describe Ci::CreateJobArtifactsService do ...@@ -101,21 +102,50 @@ describe Ci::CreateJobArtifactsService do
it 'ignores the changes' do it 'ignores the changes' do
expect { subject }.not_to change { Ci::JobArtifact.count } expect { subject }.not_to change { Ci::JobArtifact.count }
expect(subject).to be_truthy expect(subject).to match(a_hash_including(status: :success))
end end
end end
context 'when sha256 of uploading artifact is different than the existing one' do context 'when sha256 of uploading artifact is different than the existing one' do
let(:existing_sha256) { '1' * 64 } let(:existing_sha256) { '1' * 64 }
it 'returns false and logs the error' do it 'returns error status' do
expect(Gitlab::ErrorTracking).to receive(:track_exception).and_call_original expect(Gitlab::ErrorTracking).to receive(:track_exception).and_call_original
expect { subject }.not_to change { Ci::JobArtifact.count } expect { subject }.not_to change { Ci::JobArtifact.count }
expect(subject).to be_falsey expect(subject).to match(
expect(job.errors[:base]).to contain_exactly('another artifact of the same type already exists') a_hash_including(http_status: :bad_request,
message: 'another artifact of the same type already exists',
status: :error))
end end
end end
end end
shared_examples 'rescues object storage error' do |klass, message, expected_message|
it "handles #{klass}" do
allow_next_instance_of(JobArtifactUploader) do |uploader|
allow(uploader).to receive(:store!).and_raise(klass, message)
end
expect(Gitlab::ErrorTracking)
.to receive(:track_exception)
.and_call_original
expect(subject).to match(
a_hash_including(
http_status: :service_unavailable,
message: expected_message || message,
status: :error))
end
end
it_behaves_like 'rescues object storage error',
Errno::EIO, 'some/path', 'Input/output error - some/path'
it_behaves_like 'rescues object storage error',
Google::Apis::ServerError, 'Server error'
it_behaves_like 'rescues object storage error',
Signet::RemoteServerError, 'The service is currently unavailable'
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