1# frozen_string_literal: true
2
3module UpdateRepositoryStorageMethods
4  include Gitlab::Utils::StrongMemoize
5
6  Error = Class.new(StandardError)
7
8  attr_reader :repository_storage_move
9  delegate :container, :source_storage_name, :destination_storage_name, to: :repository_storage_move
10
11  def initialize(repository_storage_move)
12    @repository_storage_move = repository_storage_move
13  end
14
15  def execute
16    repository_storage_move.with_lock do
17      return ServiceResponse.success unless repository_storage_move.scheduled? # rubocop:disable Cop/AvoidReturnFromBlocks
18
19      repository_storage_move.start!
20    end
21
22    mirror_repositories unless same_filesystem?
23
24    repository_storage_move.transaction do
25      repository_storage_move.finish_replication!
26
27      track_repository(destination_storage_name)
28    end
29
30    unless same_filesystem?
31      remove_old_paths
32      enqueue_housekeeping
33    end
34
35    repository_storage_move.finish_cleanup!
36
37    ServiceResponse.success
38  rescue StandardError => e
39    repository_storage_move.do_fail!
40
41    Gitlab::ErrorTracking.track_and_raise_exception(e, container_klass: container.class.to_s, container_path: container.full_path)
42  end
43
44  private
45
46  def track_repository(destination_shard)
47    raise NotImplementedError
48  end
49
50  def mirror_repositories
51    raise NotImplementedError
52  end
53
54  def mirror_repository(type:)
55    unless wait_for_pushes(type)
56      raise Error, s_('UpdateRepositoryStorage|Timeout waiting for %{type} repository pushes') % { type: type.name }
57    end
58
59    repository = type.repository_for(container)
60    full_path = repository.full_path
61    raw_repository = repository.raw
62    checksum = repository.checksum
63
64    # Initialize a git repository on the target path
65    new_repository = Gitlab::Git::Repository.new(
66      destination_storage_name,
67      raw_repository.relative_path,
68      raw_repository.gl_repository,
69      full_path
70    )
71
72    new_repository.replicate(raw_repository)
73    new_checksum = new_repository.checksum
74
75    if checksum != new_checksum
76      raise Error, s_('UpdateRepositoryStorage|Failed to verify %{type} repository checksum from %{old} to %{new}') % { type: type.name, old: checksum, new: new_checksum }
77    end
78  end
79
80  def same_filesystem?
81    strong_memoize(:same_filesystem) do
82      Gitlab::GitalyClient.filesystem_id(source_storage_name) == Gitlab::GitalyClient.filesystem_id(destination_storage_name)
83    end
84  end
85
86  def remove_old_paths
87    if container.repository_exists?
88      Gitlab::Git::Repository.new(
89        source_storage_name,
90        "#{container.disk_path}.git",
91        nil,
92        nil
93      ).remove
94    end
95  end
96
97  def enqueue_housekeeping
98    # no-op
99  end
100
101  def wait_for_pushes(type)
102    reference_counter = container.reference_counter(type: type)
103
104    # Try for 30 seconds, polling every 10
105    3.times do
106      return true if reference_counter.value == 0
107
108      sleep 10
109    end
110
111    false
112  end
113end
114