1# frozen_string_literal: true
2
3module RepositoryStorageMovable
4  extend ActiveSupport::Concern
5  include AfterCommitQueue
6
7  included do
8    scope :order_created_at_desc, -> { order(created_at: :desc) }
9
10    validates :container, presence: true
11    validates :state, presence: true
12    validates :source_storage_name,
13      on: :create,
14      presence: true,
15      inclusion: { in: ->(_) { Gitlab.config.repositories.storages.keys } }
16    validates :destination_storage_name,
17      on: :create,
18      presence: true,
19      inclusion: { in: ->(_) { Gitlab.config.repositories.storages.keys } }
20    validate :container_repository_writable, on: :create
21
22    default_value_for(:destination_storage_name, allows_nil: false) do
23      Repository.pick_storage_shard
24    end
25
26    state_machine initial: :initial do
27      event :schedule do
28        transition initial: :scheduled
29      end
30
31      event :start do
32        transition scheduled: :started
33      end
34
35      event :finish_replication do
36        transition started: :replicated
37      end
38
39      event :finish_cleanup do
40        transition replicated: :finished
41      end
42
43      event :do_fail do
44        transition [:initial, :scheduled, :started] => :failed
45        transition replicated: :cleanup_failed
46      end
47
48      around_transition initial: :scheduled do |storage_move, block|
49        block.call
50
51        begin
52          storage_move.container.set_repository_read_only!(skip_git_transfer_check: true)
53        rescue StandardError => err
54          storage_move.add_error(err.message)
55          next false
56        end
57
58        storage_move.run_after_commit do
59          storage_move.schedule_repository_storage_update_worker
60        end
61
62        true
63      end
64
65      before_transition started: :replicated do |storage_move|
66        storage_move.container.set_repository_writable!
67
68        storage_move.update_repository_storage(storage_move.destination_storage_name)
69      end
70
71      after_transition started: :replicated do |storage_move|
72        # We have several scripts in place that replicate some statistics information
73        # to other databases. Some of them depend on the updated_at column
74        # to identify the models they need to extract.
75        #
76        # If we don't update the `updated_at` of the container after a repository storage move,
77        # the scripts won't know that they need to sync them.
78        #
79        # See https://gitlab.com/gitlab-data/analytics/-/issues/7868
80        storage_move.container.touch
81      end
82
83      before_transition started: :failed do |storage_move|
84        storage_move.container.set_repository_writable!
85      end
86
87      state :initial, value: 1
88      state :scheduled, value: 2
89      state :started, value: 3
90      state :finished, value: 4
91      state :failed, value: 5
92      state :replicated, value: 6
93      state :cleanup_failed, value: 7
94    end
95  end
96
97  # Projects, snippets, and group wikis has different db structure. In projects,
98  # we need to update some columns in this step, but we don't with the other resources.
99  #
100  # Therefore, we create this No-op method for snippets and wikis and let project
101  # overwrite it in their implementation.
102  def update_repository_storage(new_storage)
103    # No-op
104  end
105
106  def schedule_repository_storage_update_worker
107    raise NotImplementedError
108  end
109
110  def add_error(message)
111    errors.add(error_key, message)
112  end
113
114  private
115
116  def container_repository_writable
117    add_error(_('is read-only')) if container&.repository_read_only?
118  end
119
120  def error_key
121    raise NotImplementedError
122  end
123end
124