Commit ffa2eddc authored by Francisco Javier López's avatar Francisco Javier López Committed by Rémy Coutable

Add SnippetRepositoryStorageMove class

This new class will store the repository storage
changes for snippets.
parent 7d094c6b
# frozen_string_literal: true
module UpdateRepositoryStorageMethods
Error = Class.new(StandardError)
SameFilesystemError = Class.new(Error)
attr_reader :repository_storage_move
delegate :container, :source_storage_name, :destination_storage_name, to: :repository_storage_move
def initialize(repository_storage_move)
@repository_storage_move = repository_storage_move
end
def execute
repository_storage_move.with_lock do
return ServiceResponse.success unless repository_storage_move.scheduled? # rubocop:disable Cop/AvoidReturnFromBlocks
repository_storage_move.start!
end
raise SameFilesystemError if same_filesystem?(source_storage_name, destination_storage_name)
mirror_repositories
repository_storage_move.transaction do
repository_storage_move.finish_replication!
track_repository(destination_storage_name)
end
remove_old_paths
enqueue_housekeeping
repository_storage_move.finish_cleanup!
ServiceResponse.success
rescue StandardError => e
repository_storage_move.do_fail!
Gitlab::ErrorTracking.track_exception(e, container_klass: container.class.to_s, container_path: container.full_path)
ServiceResponse.error(
message: s_("UpdateRepositoryStorage|Error moving repository storage for %{container_full_path} - %{message}") % { container_full_path: container.full_path, message: e.message }
)
end
private
def track_repository(destination_shard)
raise NotImplementedError
end
def mirror_repositories
raise NotImplementedError
end
def mirror_repository(type:)
unless wait_for_pushes(type)
raise Error, s_('UpdateRepositoryStorage|Timeout waiting for %{type} repository pushes') % { type: type.name }
end
repository = type.repository_for(container)
full_path = repository.full_path
raw_repository = repository.raw
checksum = repository.checksum
# Initialize a git repository on the target path
new_repository = Gitlab::Git::Repository.new(
destination_storage_name,
raw_repository.relative_path,
raw_repository.gl_repository,
full_path
)
new_repository.replicate(raw_repository)
new_checksum = new_repository.checksum
if checksum != new_checksum
raise Error, s_('UpdateRepositoryStorage|Failed to verify %{type} repository checksum from %{old} to %{new}') % { type: type.name, old: checksum, new: new_checksum }
end
end
def same_filesystem?(old_storage, new_storage)
Gitlab::GitalyClient.filesystem_id(old_storage) == Gitlab::GitalyClient.filesystem_id(new_storage)
end
def remove_old_paths
if container.repository_exists?
Gitlab::Git::Repository.new(
source_storage_name,
"#{container.disk_path}.git",
nil,
nil
).remove
end
end
def enqueue_housekeeping
# no-op
end
def wait_for_pushes(type)
reference_counter = container.reference_counter(type: type)
# Try for 30 seconds, polling every 10
3.times do
return true if reference_counter.value == 0
sleep 10
end
false
end
end
...@@ -2,59 +2,19 @@ ...@@ -2,59 +2,19 @@
module Projects module Projects
class UpdateRepositoryStorageService class UpdateRepositoryStorageService
Error = Class.new(StandardError) include UpdateRepositoryStorageMethods
SameFilesystemError = Class.new(Error)
attr_reader :repository_storage_move delegate :project, to: :repository_storage_move
delegate :project, :source_storage_name, :destination_storage_name, to: :repository_storage_move
def initialize(repository_storage_move)
@repository_storage_move = repository_storage_move
end
def execute
repository_storage_move.with_lock do
return ServiceResponse.success unless repository_storage_move.scheduled? # rubocop:disable Cop/AvoidReturnFromBlocks
repository_storage_move.start!
end
raise SameFilesystemError if same_filesystem?(source_storage_name, destination_storage_name)
mirror_repositories
repository_storage_move.transaction do
repository_storage_move.finish_replication!
project.leave_pool_repository
project.track_project_repository
end
remove_old_paths
enqueue_housekeeping
repository_storage_move.finish_cleanup!
ServiceResponse.success
rescue StandardError => e
repository_storage_move.do_fail!
Gitlab::ErrorTracking.track_exception(e, project_path: project.full_path)
ServiceResponse.error(
message: s_("UpdateRepositoryStorage|Error moving repository storage for %{project_full_path} - %{message}") % { project_full_path: project.full_path, message: e.message }
)
end
private private
def same_filesystem?(old_storage, new_storage) def track_repository(_destination_storage_name)
Gitlab::GitalyClient.filesystem_id(old_storage) == Gitlab::GitalyClient.filesystem_id(new_storage) project.leave_pool_repository
project.track_project_repository
end end
def mirror_repositories def mirror_repositories
mirror_repository if project.repository_exists? mirror_repository(type: Gitlab::GlRepository::PROJECT) if project.repository_exists?
if project.wiki.repository_exists? if project.wiki.repository_exists?
mirror_repository(type: Gitlab::GlRepository::WIKI) mirror_repository(type: Gitlab::GlRepository::WIKI)
...@@ -65,41 +25,21 @@ module Projects ...@@ -65,41 +25,21 @@ module Projects
end end
end end
def mirror_repository(type: Gitlab::GlRepository::PROJECT) # The underlying FetchInternalRemote call uses a `git fetch` to move data
unless wait_for_pushes(type) # to the new repository, which leaves it in a less-well-packed state,
raise Error, s_('UpdateRepositoryStorage|Timeout waiting for %{type} repository pushes') % { type: type.name } # lacking bitmaps and commit graphs. Housekeeping will boost performance
end # significantly.
def enqueue_housekeeping
repository = type.repository_for(project) return unless Gitlab::CurrentSettings.housekeeping_enabled?
full_path = repository.full_path return unless Feature.enabled?(:repack_after_shard_migration, project)
raw_repository = repository.raw
checksum = repository.checksum
# Initialize a git repository on the target path
new_repository = Gitlab::Git::Repository.new(
destination_storage_name,
raw_repository.relative_path,
raw_repository.gl_repository,
full_path
)
new_repository.replicate(raw_repository)
new_checksum = new_repository.checksum
if checksum != new_checksum Projects::HousekeepingService.new(project, :gc).execute
raise Error, s_('UpdateRepositoryStorage|Failed to verify %{type} repository checksum from %{old} to %{new}') % { type: type.name, old: checksum, new: new_checksum } rescue Projects::HousekeepingService::LeaseTaken
end # No action required
end end
def remove_old_paths def remove_old_paths
if project.repository_exists? super
Gitlab::Git::Repository.new(
source_storage_name,
"#{project.disk_path}.git",
nil,
nil
).remove
end
if project.wiki.repository_exists? if project.wiki.repository_exists?
Gitlab::Git::Repository.new( Gitlab::Git::Repository.new(
...@@ -119,31 +59,5 @@ module Projects ...@@ -119,31 +59,5 @@ module Projects
).remove ).remove
end end
end end
# The underlying FetchInternalRemote call uses a `git fetch` to move data
# to the new repository, which leaves it in a less-well-packed state,
# lacking bitmaps and commit graphs. Housekeeping will boost performance
# significantly.
def enqueue_housekeeping
return unless Gitlab::CurrentSettings.housekeeping_enabled?
return unless Feature.enabled?(:repack_after_shard_migration, project)
Projects::HousekeepingService.new(project, :gc).execute
rescue Projects::HousekeepingService::LeaseTaken
# No action required
end
def wait_for_pushes(type)
reference_counter = project.reference_counter(type: type)
# Try for 30 seconds, polling every 10
3.times do
return true if reference_counter.value == 0
sleep 10
end
false
end
end end
end end
# frozen_string_literal: true
module Snippets
class UpdateRepositoryStorageService
include UpdateRepositoryStorageMethods
delegate :snippet, to: :repository_storage_move
private
def track_repository(destination_storage_name)
snippet.track_snippet_repository(destination_storage_name)
end
def mirror_repositories
return unless snippet.repository_exists?
mirror_repository(type: Gitlab::GlRepository::SNIPPET)
end
end
end
...@@ -29929,7 +29929,7 @@ msgstr "" ...@@ -29929,7 +29929,7 @@ msgstr ""
msgid "UpdateProject|Project could not be updated!" msgid "UpdateProject|Project could not be updated!"
msgstr "" msgstr ""
msgid "UpdateRepositoryStorage|Error moving repository storage for %{project_full_path} - %{message}" msgid "UpdateRepositoryStorage|Error moving repository storage for %{container_full_path} - %{message}"
msgstr "" msgstr ""
msgid "UpdateRepositoryStorage|Failed to verify %{type} repository checksum from %{old} to %{new}" msgid "UpdateRepositoryStorage|Failed to verify %{type} repository checksum from %{old} to %{new}"
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Snippets::UpdateRepositoryStorageService do
include Gitlab::ShellAdapter
subject { described_class.new(repository_storage_move) }
describe "#execute" do
let_it_be_with_reload(:snippet) { create(:snippet, :repository) }
let_it_be(:destination) { 'test_second_storage' }
let_it_be(:checksum) { snippet.repository.checksum }
let(:repository_storage_move_state) { :scheduled }
let(:repository_storage_move) { create(:snippet_repository_storage_move, repository_storage_move_state, container: snippet, destination_storage_name: destination) }
let(:snippet_repository_double) { double(:repository) }
let(:original_snippet_repository_double) { double(:repository) }
before do
allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(%w[default test_second_storage])
allow(Gitlab::GitalyClient).to receive(:filesystem_id).with('default').and_call_original
allow(Gitlab::GitalyClient).to receive(:filesystem_id).with(destination).and_return(SecureRandom.uuid)
allow(Gitlab::Git::Repository).to receive(:new).and_call_original
allow(Gitlab::Git::Repository).to receive(:new)
.with(destination, snippet.repository.raw.relative_path, snippet.repository.gl_repository, snippet.repository.full_path)
.and_return(snippet_repository_double)
allow(Gitlab::Git::Repository).to receive(:new)
.with('default', snippet.repository.raw.relative_path, nil, nil)
.and_return(original_snippet_repository_double)
end
context 'when the move succeeds' do
it 'moves the repository to the new storage and unmarks the repository as read only' do
old_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
snippet.repository.path_to_repo
end
expect(snippet_repository_double).to receive(:replicate)
.with(snippet.repository.raw)
expect(snippet_repository_double).to receive(:checksum)
.and_return(checksum)
expect(original_snippet_repository_double).to receive(:remove)
result = subject.execute
snippet.reload
expect(result).to be_success
expect(snippet).not_to be_repository_read_only
expect(snippet.repository_storage).to eq(destination)
expect(gitlab_shell.repository_exists?('default', old_path)).to be(false)
expect(snippet.snippet_repository.shard_name).to eq(destination)
end
end
context 'when the filesystems are the same' do
let(:destination) { snippet.repository_storage }
it 'bails out and does nothing' do
result = subject.execute
expect(result).to be_error
expect(result.message).to match(/SameFilesystemError/)
end
end
context 'when the move fails' do
it 'unmarks the repository as read-only without updating the repository storage' do
expect(snippet_repository_double).to receive(:replicate)
.with(snippet.repository.raw)
.and_raise(Gitlab::Git::CommandError)
result = subject.execute
expect(result).to be_error
expect(snippet).not_to be_repository_read_only
expect(snippet.repository_storage).to eq('default')
expect(repository_storage_move).to be_failed
end
end
context 'when the cleanup fails' do
it 'sets the correct state' do
expect(snippet_repository_double).to receive(:replicate)
.with(snippet.repository.raw)
expect(snippet_repository_double).to receive(:checksum)
.and_return(checksum)
expect(original_snippet_repository_double).to receive(:remove)
.and_raise(Gitlab::Git::CommandError)
result = subject.execute
expect(result).to be_error
expect(repository_storage_move).to be_cleanup_failed
end
end
context 'when the checksum does not match' do
it 'unmarks the repository as read-only without updating the repository storage' do
expect(snippet_repository_double).to receive(:replicate)
.with(snippet.repository.raw)
expect(snippet_repository_double).to receive(:checksum)
.and_return('not matching checksum')
result = subject.execute
expect(result).to be_error
expect(snippet).not_to be_repository_read_only
expect(snippet.repository_storage).to eq('default')
end
end
context 'when the repository move is finished' do
let(:repository_storage_move_state) { :finished }
it 'is idempotent' do
expect do
result = subject.execute
expect(result).to be_success
end.not_to change(repository_storage_move, :state)
end
end
context 'when the repository move is failed' do
let(:repository_storage_move_state) { :failed }
it 'is idempotent' do
expect do
result = subject.execute
expect(result).to be_success
end.not_to change(repository_storage_move, :state)
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