Commit 88371a67 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 4c098afb f71556b2
...@@ -84,13 +84,7 @@ class RemoteMirror < ApplicationRecord ...@@ -84,13 +84,7 @@ class RemoteMirror < ApplicationRecord
end end
after_transition started: :failed do |remote_mirror| after_transition started: :failed do |remote_mirror|
Gitlab::Metrics.add_event(:remote_mirrors_failed) remote_mirror.send_failure_notifications
remote_mirror.update(last_update_at: Time.current)
remote_mirror.run_after_commit do
RemoteMirrorNotificationWorker.perform_async(remote_mirror.id)
end
end end
end end
...@@ -188,6 +182,24 @@ class RemoteMirror < ApplicationRecord ...@@ -188,6 +182,24 @@ class RemoteMirror < ApplicationRecord
update_fail! update_fail!
end end
# Force the mrror into the retry state
def hard_retry!(error_message)
update_error_message(error_message)
self.update_status = :to_retry
save!(validate: false)
end
# Force the mirror into the failed state
def hard_fail!(error_message)
update_error_message(error_message)
self.update_status = :failed
save!(validate: false)
send_failure_notifications
end
def url=(value) def url=(value)
super(value) && return unless Gitlab::UrlSanitizer.valid?(value) super(value) && return unless Gitlab::UrlSanitizer.valid?(value)
...@@ -239,6 +251,17 @@ class RemoteMirror < ApplicationRecord ...@@ -239,6 +251,17 @@ class RemoteMirror < ApplicationRecord
last_update_at.present? ? MAX_INCREMENTAL_RUNTIME : MAX_FIRST_RUNTIME last_update_at.present? ? MAX_INCREMENTAL_RUNTIME : MAX_FIRST_RUNTIME
end end
def send_failure_notifications
Gitlab::Metrics.add_event(:remote_mirrors_failed)
run_after_commit do
RemoteMirrorNotificationWorker.perform_async(id)
end
self.last_update_at = Time.current
save!(validate: false)
end
private private
def store_credentials def store_credentials
......
...@@ -42,9 +42,9 @@ module Packages ...@@ -42,9 +42,9 @@ module Packages
def files def files
strong_memoize(:files) do strong_memoize(:files) do
raise ExtractionError.new("is not a changes file") unless file_type == :changes raise ExtractionError.new("is not a changes file") unless file_type == :changes
raise ExtractionError.new("Files field is missing") if fields[:Files].blank? raise ExtractionError.new("Files field is missing") if fields['Files'].blank?
raise ExtractionError.new("Checksums-Sha1 field is missing") if fields[:'Checksums-Sha1'].blank? raise ExtractionError.new("Checksums-Sha1 field is missing") if fields['Checksums-Sha1'].blank?
raise ExtractionError.new("Checksums-Sha256 field is missing") if fields[:'Checksums-Sha256'].blank? raise ExtractionError.new("Checksums-Sha256 field is missing") if fields['Checksums-Sha256'].blank?
init_entries_from_files init_entries_from_files
entries_from_checksums_sha1 entries_from_checksums_sha1
...@@ -56,7 +56,7 @@ module Packages ...@@ -56,7 +56,7 @@ module Packages
end end
def init_entries_from_files def init_entries_from_files
each_lines_for(:Files) do |line| each_lines_for('Files') do |line|
md5sum, size, section, priority, filename = line.split md5sum, size, section, priority, filename = line.split
entry = FileEntry.new( entry = FileEntry.new(
filename: filename, filename: filename,
...@@ -70,7 +70,7 @@ module Packages ...@@ -70,7 +70,7 @@ module Packages
end end
def entries_from_checksums_sha1 def entries_from_checksums_sha1
each_lines_for(:'Checksums-Sha1') do |line| each_lines_for('Checksums-Sha1') do |line|
sha1sum, size, filename = line.split sha1sum, size, filename = line.split
entry = @entries[filename] entry = @entries[filename]
raise ExtractionError.new("#{filename} is listed in Checksums-Sha1 but not in Files") unless entry raise ExtractionError.new("#{filename} is listed in Checksums-Sha1 but not in Files") unless entry
...@@ -81,7 +81,7 @@ module Packages ...@@ -81,7 +81,7 @@ module Packages
end end
def entries_from_checksums_sha256 def entries_from_checksums_sha256
each_lines_for(:'Checksums-Sha256') do |line| each_lines_for('Checksums-Sha256') do |line|
sha256sum, size, filename = line.split sha256sum, size, filename = line.split
entry = @entries[filename] entry = @entries[filename]
raise ExtractionError.new("#{filename} is listed in Checksums-Sha256 but not in Files") unless entry raise ExtractionError.new("#{filename} is listed in Checksums-Sha256 but not in Files") unless entry
......
...@@ -72,7 +72,7 @@ module Packages ...@@ -72,7 +72,7 @@ module Packages
def extract_metadata def extract_metadata
fields = extracted_fields fields = extracted_fields
architecture = fields.delete(:Architecture) if file_type_debian? architecture = fields.delete('Architecture') if file_type_debian?
{ {
file_type: file_type, file_type: file_type,
......
...@@ -26,7 +26,7 @@ module Packages ...@@ -26,7 +26,7 @@ module Packages
section[field] += line[1..] unless paragraph_separator?(line) section[field] += line[1..] unless paragraph_separator?(line)
elsif match = match_section_line(line) elsif match = match_section_line(line)
section_name = match[:name] if section_name.nil? section_name = match[:name] if section_name.nil?
field = match[:field].to_sym field = match[:field]
raise InvalidDebian822Error, "Duplicate field '#{field}' in section '#{section_name}'" if section.include?(field) raise InvalidDebian822Error, "Duplicate field '#{field}' in section '#{section_name}'" if section.include?(field)
......
...@@ -9,8 +9,10 @@ module Projects ...@@ -9,8 +9,10 @@ module Projects
def execute(remote_mirror, tries) def execute(remote_mirror, tries)
return success unless remote_mirror.enabled? return success unless remote_mirror.enabled?
# Blocked URLs are a hard failure, no need to attempt to retry
if Gitlab::UrlBlocker.blocked_url?(normalized_url(remote_mirror.url)) if Gitlab::UrlBlocker.blocked_url?(normalized_url(remote_mirror.url))
return error("The remote mirror URL is invalid.") hard_retry_or_fail(remote_mirror, _('The remote mirror URL is invalid.'), tries)
return error(remote_mirror.last_error)
end end
update_mirror(remote_mirror) update_mirror(remote_mirror)
...@@ -19,11 +21,11 @@ module Projects ...@@ -19,11 +21,11 @@ module Projects
rescue Gitlab::Git::CommandError => e rescue Gitlab::Git::CommandError => e
# This happens if one of the gitaly calls above fail, for example when # This happens if one of the gitaly calls above fail, for example when
# branches have diverged, or the pre-receive hook fails. # branches have diverged, or the pre-receive hook fails.
retry_or_fail(remote_mirror, e.message, tries) hard_retry_or_fail(remote_mirror, e.message, tries)
error(e.message) error(e.message)
rescue => e rescue => e
remote_mirror.mark_as_failed!(e.message) remote_mirror.hard_fail!(e.message)
raise e raise e
end end
...@@ -70,15 +72,15 @@ module Projects ...@@ -70,15 +72,15 @@ module Projects
).execute ).execute
end end
def retry_or_fail(mirror, message, tries) def hard_retry_or_fail(mirror, message, tries)
if tries < MAX_TRIES if tries < MAX_TRIES
mirror.mark_for_retry!(message) mirror.hard_retry!(message)
else else
# It's not likely we'll be able to recover from this ourselves, so we'll # It's not likely we'll be able to recover from this ourselves, so we'll
# notify the users of the problem, and don't trigger any sidekiq retries # notify the users of the problem, and don't trigger any sidekiq retries
# Instead, we'll wait for the next change to try the push again, or until # Instead, we'll wait for the next change to try the push again, or until
# a user manually retries. # a user manually retries.
mirror.mark_as_failed!(message) mirror.hard_fail!(message)
end end
end end
end end
......
---
title: A blocked URL for a push mirror is a hard failure
merge_request: 57392
author:
type: fixed
...@@ -9,7 +9,7 @@ module Gitlab ...@@ -9,7 +9,7 @@ module Gitlab
production: "58dc0f06-936c-43b3-93bb-71693f1b6570" production: "58dc0f06-936c-43b3-93bb-71693f1b6570"
}.freeze }.freeze
UUID_V5_PATTERN = /\h{8}-\h{4}-5\h{3}-\h{4}-\h{4}\h{8}/.freeze UUID_V5_PATTERN = /\h{8}-\h{4}-5\h{3}-\h{4}-\h{12}/.freeze
NAMESPACE_REGEX = /(\h{8})-(\h{4})-(\h{4})-(\h{4})-(\h{4})(\h{8})/.freeze NAMESPACE_REGEX = /(\h{8})-(\h{4})-(\h{4})-(\h{4})-(\h{4})(\h{8})/.freeze
PACK_PATTERN = "NnnnnN" PACK_PATTERN = "NnnnnN"
......
...@@ -30450,6 +30450,9 @@ msgstr "" ...@@ -30450,6 +30450,9 @@ msgstr ""
msgid "The regular expression used to find test coverage output in the job log. For example, use %{regex} for Simplecov (Ruby). Leave blank to disable." msgid "The regular expression used to find test coverage output in the job log. For example, use %{regex} for Simplecov (Ruby). Leave blank to disable."
msgstr "" msgstr ""
msgid "The remote mirror URL is invalid."
msgstr ""
msgid "The remote mirror took to long to complete." msgid "The remote mirror took to long to complete."
msgstr "" msgstr ""
......
...@@ -263,6 +263,30 @@ RSpec.describe RemoteMirror, :mailer do ...@@ -263,6 +263,30 @@ RSpec.describe RemoteMirror, :mailer do
end end
end end
describe '#hard_retry!' do
let(:remote_mirror) { create(:remote_mirror).tap {|mirror| mirror.update_column(:url, 'invalid') } }
it 'transitions an invalid mirror to the to_retry state' do
remote_mirror.hard_retry!('Invalid')
expect(remote_mirror.update_status).to eq('to_retry')
expect(remote_mirror.last_error).to eq('Invalid')
end
end
describe '#hard_fail!' do
let(:remote_mirror) { create(:remote_mirror).tap {|mirror| mirror.update_column(:url, 'invalid') } }
it 'transitions an invalid mirror to the failed state' do
remote_mirror.hard_fail!('Invalid')
expect(remote_mirror.update_status).to eq('failed')
expect(remote_mirror.last_error).to eq('Invalid')
expect(remote_mirror.last_update_at).not_to be_nil
expect(RemoteMirrorNotificationWorker.jobs).not_to be_empty
end
end
context 'when remote mirror gets destroyed' do context 'when remote mirror gets destroyed' do
it 'removes remote' do it 'removes remote' do
mirror = create_mirror(url: 'http://foo:bar@test.com') mirror = create_mirror(url: 'http://foo:bar@test.com')
......
...@@ -13,7 +13,7 @@ RSpec.describe Packages::Debian::ExtractChangesMetadataService do ...@@ -13,7 +13,7 @@ RSpec.describe Packages::Debian::ExtractChangesMetadataService do
context 'with valid package file' do context 'with valid package file' do
it 'extract metadata', :aggregate_failures do it 'extract metadata', :aggregate_failures do
expected_fields = { 'Architecture': 'source amd64', 'Binary': 'libsample0 sample-dev sample-udeb' } expected_fields = { 'Architecture' => 'source amd64', 'Binary' => 'libsample0 sample-dev sample-udeb' }
expect(subject[:file_type]).to eq(:changes) expect(subject[:file_type]).to eq(:changes)
expect(subject[:architecture]).to be_nil expect(subject[:architecture]).to be_nil
...@@ -40,7 +40,7 @@ RSpec.describe Packages::Debian::ExtractChangesMetadataService do ...@@ -40,7 +40,7 @@ RSpec.describe Packages::Debian::ExtractChangesMetadataService do
let(:sha256_dsc) { '844f79825b7e8aaa191e514b58a81f9ac1e58e2180134b0c9512fa66d896d7ba 671 sample_1.2.3~alpha2.dsc' } let(:sha256_dsc) { '844f79825b7e8aaa191e514b58a81f9ac1e58e2180134b0c9512fa66d896d7ba 671 sample_1.2.3~alpha2.dsc' }
let(:sha256_source) { 'b5a599e88e7cbdda3bde808160a21ba1dd1ec76b2ec8d4912aae769648d68362 864 sample_1.2.3~alpha2.tar.xz' } let(:sha256_source) { 'b5a599e88e7cbdda3bde808160a21ba1dd1ec76b2ec8d4912aae769648d68362 864 sample_1.2.3~alpha2.tar.xz' }
let(:sha256s) { "#{sha256_dsc}\n#{sha256_source}" } let(:sha256s) { "#{sha256_dsc}\n#{sha256_source}" }
let(:fields) { { Files: md5s, 'Checksums-Sha1': sha1s, 'Checksums-Sha256': sha256s } } let(:fields) { { 'Files' => md5s, 'Checksums-Sha1' => sha1s, 'Checksums-Sha256' => sha256s } }
let(:metadata) { { file_type: :changes, architecture: 'amd64', fields: fields } } let(:metadata) { { file_type: :changes, architecture: 'amd64', fields: fields } }
before do before do
......
...@@ -10,17 +10,17 @@ RSpec.describe Packages::Debian::ExtractDebMetadataService do ...@@ -10,17 +10,17 @@ RSpec.describe Packages::Debian::ExtractDebMetadataService do
context 'with correct file' do context 'with correct file' do
it 'return as expected' do it 'return as expected' do
expected = { expected = {
'Package': 'libsample0', 'Package' => 'libsample0',
'Source': 'sample', 'Source' => 'sample',
'Version': '1.2.3~alpha2', 'Version' => '1.2.3~alpha2',
'Architecture': 'amd64', 'Architecture' => 'amd64',
'Maintainer': 'John Doe <john.doe@example.com>', 'Maintainer' => 'John Doe <john.doe@example.com>',
'Installed-Size': '7', 'Installed-Size' => '7',
'Section': 'libs', 'Section' => 'libs',
'Priority': 'optional', 'Priority' => 'optional',
'Multi-Arch': 'same', 'Multi-Arch' => 'same',
'Homepage': 'https://gitlab.com/', 'Homepage' => 'https://gitlab.com/',
'Description': "Some mostly empty lib\nUsed in GitLab tests.\n\nTesting another paragraph." 'Description' => "Some mostly empty lib\nUsed in GitLab tests.\n\nTesting another paragraph."
} }
expect(subject.execute).to eq expected expect(subject.execute).to eq expected
......
...@@ -33,11 +33,11 @@ RSpec.describe Packages::Debian::ExtractMetadataService do ...@@ -33,11 +33,11 @@ RSpec.describe Packages::Debian::ExtractMetadataService do
where(:case_name, :trait, :expected_file_type, :expected_architecture, :expected_fields) do where(:case_name, :trait, :expected_file_type, :expected_architecture, :expected_fields) do
'with invalid' | :invalid | :unknown | nil | nil 'with invalid' | :invalid | :unknown | nil | nil
'with source' | :source | :source | nil | nil 'with source' | :source | :source | nil | nil
'with dsc' | :dsc | :dsc | nil | { 'Binary': 'sample-dev, libsample0, sample-udeb' } 'with dsc' | :dsc | :dsc | nil | { 'Binary' => 'sample-dev, libsample0, sample-udeb' }
'with deb' | :deb | :deb | 'amd64' | { 'Multi-Arch': 'same' } 'with deb' | :deb | :deb | 'amd64' | { 'Multi-Arch' => 'same' }
'with udeb' | :udeb | :udeb | 'amd64' | { 'Package': 'sample-udeb' } 'with udeb' | :udeb | :udeb | 'amd64' | { 'Package' => 'sample-udeb' }
'with buildinfo' | :buildinfo | :buildinfo | nil | { 'Architecture': 'amd64 source', 'Build-Architecture': 'amd64' } 'with buildinfo' | :buildinfo | :buildinfo | nil | { 'Architecture' => 'amd64 source', 'Build-Architecture' => 'amd64' }
'with changes' | :changes | :changes | nil | { 'Architecture': 'source amd64', 'Binary': 'libsample0 sample-dev sample-udeb' } 'with changes' | :changes | :changes | nil | { 'Architecture' => 'source amd64', 'Binary' => 'libsample0 sample-dev sample-udeb' }
end end
with_them do with_them do
......
...@@ -27,17 +27,17 @@ RSpec.describe Packages::Debian::ParseDebian822Service do ...@@ -27,17 +27,17 @@ RSpec.describe Packages::Debian::ParseDebian822Service do
it 'return as expected, preserving order' do it 'return as expected, preserving order' do
expected = { expected = {
'Package: libsample0' => { 'Package: libsample0' => {
'Package': 'libsample0', 'Package' => 'libsample0',
'Source': 'sample', 'Source' => 'sample',
'Version': '1.2.3~alpha2', 'Version' => '1.2.3~alpha2',
'Architecture': 'amd64', 'Architecture' => 'amd64',
'Maintainer': 'John Doe <john.doe@example.com>', 'Maintainer' => 'John Doe <john.doe@example.com>',
'Installed-Size': '9', 'Installed-Size' => '9',
'Section': 'libs', 'Section' => 'libs',
'Priority': 'optional', 'Priority' => 'optional',
'Multi-Arch': 'same', 'Multi-Arch' => 'same',
'Homepage': 'https://gitlab.com/', 'Homepage' => 'https://gitlab.com/',
'Description': "Some mostly empty lib\nUsed in GitLab tests.\n\nTesting another paragraph." 'Description' => "Some mostly empty lib\nUsed in GitLab tests.\n\nTesting another paragraph."
} }
} }
...@@ -51,38 +51,38 @@ RSpec.describe Packages::Debian::ParseDebian822Service do ...@@ -51,38 +51,38 @@ RSpec.describe Packages::Debian::ParseDebian822Service do
it 'return as expected, preserving order' do it 'return as expected, preserving order' do
expected = { expected = {
'Source: sample' => { 'Source: sample' => {
'Source': 'sample', 'Source' => 'sample',
'Priority': 'optional', 'Priority' => 'optional',
'Maintainer': 'John Doe <john.doe@example.com>', 'Maintainer' => 'John Doe <john.doe@example.com>',
'Build-Depends': 'debhelper-compat (= 13)', 'Build-Depends' => 'debhelper-compat (= 13)',
'Standards-Version': '4.5.0', 'Standards-Version' => '4.5.0',
'Section': 'libs', 'Section' => 'libs',
'Homepage': 'https://gitlab.com/', 'Homepage' => 'https://gitlab.com/',
# 'Vcs-Browser': 'https://salsa.debian.org/debian/sample-1.2.3', # 'Vcs-Browser' => 'https://salsa.debian.org/debian/sample-1.2.3',
# '#Vcs-Git': 'https://salsa.debian.org/debian/sample-1.2.3.git', # '#Vcs-Git' => 'https://salsa.debian.org/debian/sample-1.2.3.git',
'Rules-Requires-Root': 'no' 'Rules-Requires-Root' => 'no'
}, },
'Package: sample-dev' => { 'Package: sample-dev' => {
'Package': 'sample-dev', 'Package' => 'sample-dev',
'Section': 'libdevel', 'Section' => 'libdevel',
'Architecture': 'any', 'Architecture' => 'any',
'Multi-Arch': 'same', 'Multi-Arch' => 'same',
'Depends': 'libsample0 (= ${binary:Version}), ${misc:Depends}', 'Depends' => 'libsample0 (= ${binary:Version}), ${misc:Depends}',
'Description': "Some mostly empty developpement files\nUsed in GitLab tests.\n\nTesting another paragraph." 'Description' => "Some mostly empty developpement files\nUsed in GitLab tests.\n\nTesting another paragraph."
}, },
'Package: libsample0' => { 'Package: libsample0' => {
'Package': 'libsample0', 'Package' => 'libsample0',
'Architecture': 'any', 'Architecture' => 'any',
'Multi-Arch': 'same', 'Multi-Arch' => 'same',
'Depends': '${shlibs:Depends}, ${misc:Depends}', 'Depends' => '${shlibs:Depends}, ${misc:Depends}',
'Description': "Some mostly empty lib\nUsed in GitLab tests.\n\nTesting another paragraph." 'Description' => "Some mostly empty lib\nUsed in GitLab tests.\n\nTesting another paragraph."
}, },
'Package: sample-udeb' => { 'Package: sample-udeb' => {
'Package': 'sample-udeb', 'Package' => 'sample-udeb',
'Package-Type': 'udeb', 'Package-Type' => 'udeb',
'Architecture': 'any', 'Architecture' => 'any',
'Depends': 'installed-base', 'Depends' => 'installed-base',
'Description': 'Some mostly empty udeb' 'Description' => 'Some mostly empty udeb'
} }
} }
......
...@@ -12,7 +12,9 @@ RSpec.describe Projects::UpdateRemoteMirrorService do ...@@ -12,7 +12,9 @@ RSpec.describe Projects::UpdateRemoteMirrorService do
subject(:service) { described_class.new(project, project.creator) } subject(:service) { described_class.new(project, project.creator) }
describe '#execute' do describe '#execute' do
subject(:execute!) { service.execute(remote_mirror, 0) } let(:retries) { 0 }
subject(:execute!) { service.execute(remote_mirror, retries) }
before do before do
project.repository.add_branch(project.owner, 'existing-branch', 'master') project.repository.add_branch(project.owner, 'existing-branch', 'master')
...@@ -62,8 +64,18 @@ RSpec.describe Projects::UpdateRemoteMirrorService do ...@@ -62,8 +64,18 @@ RSpec.describe Projects::UpdateRemoteMirrorService do
allow(Gitlab::UrlBlocker).to receive(:blocked_url?).and_return(true) allow(Gitlab::UrlBlocker).to receive(:blocked_url?).and_return(true)
end end
it 'fails and returns error status' do it 'hard retries and returns error status' do
expect(execute!).to eq(status: :error, message: 'The remote mirror URL is invalid.') expect(execute!).to eq(status: :error, message: 'The remote mirror URL is invalid.')
expect(remote_mirror).to be_to_retry
end
context 'when retries are exceeded' do
let(:retries) { 4 }
it 'hard fails and returns error status' do
expect(execute!).to eq(status: :error, message: 'The remote mirror URL is invalid.')
expect(remote_mirror).to be_failed
end
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