# frozen_string_literal: true module Projects class UpdateRepositoryStorageService < BaseService include Gitlab::ShellAdapter Error = Class.new(StandardError) def initialize(project) @project = project end def execute(new_repository_storage_key) mirror_repositories(new_repository_storage_key) mark_old_paths_for_archive project.update(repository_storage: new_repository_storage_key, repository_read_only: false) project.leave_pool_repository project.track_project_repository enqueue_housekeeping success rescue Error, ArgumentError, Gitlab::Git::BaseError => e project.update(repository_read_only: false) Gitlab::ErrorTracking.track_exception(e, project_path: project.full_path) error(s_("UpdateRepositoryStorage|Error moving repository storage for %{project_full_path} - %{message}") % { project_full_path: project.full_path, message: e.message }) end private def mirror_repositories(new_repository_storage_key) mirror_repository(new_repository_storage_key) if project.wiki.repository_exists? mirror_repository(new_repository_storage_key, type: Gitlab::GlRepository::WIKI) end end def mirror_repository(new_storage_key, type: Gitlab::GlRepository::PROJECT) unless wait_for_pushes(type) raise Error, s_('UpdateRepositoryStorage|Timeout waiting for %{type} repository pushes') % { type: type.name } end repository = type.repository_for(project) 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( new_storage_key, raw_repository.relative_path, raw_repository.gl_repository, full_path ) new_repository.create_repository 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 mark_old_paths_for_archive old_repository_storage = project.repository_storage new_project_path = moved_path(project.disk_path) # Notice that the block passed to `run_after_commit` will run with `project` # as its context project.run_after_commit do GitlabShellWorker.perform_async(:mv_repository, old_repository_storage, disk_path, new_project_path) if wiki.repository_exists? GitlabShellWorker.perform_async(:mv_repository, old_repository_storage, wiki.disk_path, "#{new_project_path}.wiki") end end end def moved_path(path) "#{path}+#{project.id}+moved+#{Time.now.to_i}" 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 Projects::UpdateRepositoryStorageService.prepend_if_ee('EE::Projects::UpdateRepositoryStorageService')