Commit 49a372d5 authored by Marius Bobin's avatar Marius Bobin

Return 503 when object storage is unavailable

Catch object storage errors and return 503 to the
Runner for artifacts uploads.
parent 885e4d44
# 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
...@@ -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,20 @@ describe API::Runner, :clean_gitlab_redis_shared_state do ...@@ -1979,6 +1979,20 @@ 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_any_instance_of(JobArtifactUploader)
.to receive(:store!).and_raise(Errno::EIO)
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,44 @@ describe Ci::CreateJobArtifactsService do ...@@ -101,21 +102,44 @@ 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 'object storage' do |klass, message, expected_message|
it "handles #{klass}" do
allow_any_instance_of(JobArtifactUploader)
.to receive(:store!).and_raise(klass, message)
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 'object storage', Errno::EIO, 'some/path', 'Input/output error - some/path'
it_behaves_like 'object storage', Google::Apis::ServerError, 'Server error'
it_behaves_like 'object storage', 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