summaryrefslogtreecommitdiff
path: root/app/services/projects/update_repository_storage_service.rb
blob: 30b99e85304b66aeeb70893a2eb968db6a065154 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# frozen_string_literal: true

module Projects
  class UpdateRepositoryStorageService < BaseService
    include Gitlab::ShellAdapter

    RepositoryAlreadyMoved = Class.new(StandardError)

    def initialize(project)
      @project = project
    end

    def execute(new_repository_storage_key)
      # Raising an exception is a little heavy handed but this behavior (doing
      # nothing if the repo is already on the right storage) prevents data
      # loss, so it is valuable for us to be able to observe it via the
      # exception.
      raise RepositoryAlreadyMoved if project.repository_storage == new_repository_storage_key

      if 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
      else
        project.update(repository_read_only: false)
      end
    end

    private

    def mirror_repositories(new_repository_storage_key)
      result = mirror_repository(new_repository_storage_key)

      if project.wiki.repository_exists?
        result &&= mirror_repository(new_repository_storage_key, type: Gitlab::GlRepository::WIKI)
      end

      result
    end

    def mirror_repository(new_storage_key, type: Gitlab::GlRepository::PROJECT)
      return false unless wait_for_pushes(type)

      repository = type.repository_for(project)
      full_path = repository.full_path
      raw_repository = repository.raw

      # Initialize a git repository on the target path
      gitlab_shell.create_repository(new_storage_key, raw_repository.relative_path, full_path)
      new_repository = Gitlab::Git::Repository.new(new_storage_key,
                                                   raw_repository.relative_path,
                                                   raw_repository.gl_repository,
                                                   full_path)

      new_repository.fetch_repository_as_mirror(raw_repository)
    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')