summaryrefslogtreecommitdiff
path: root/app/models/project_import_state.rb
blob: fabbd5b49cbd41038e5274e4cc3b36169a903f78 (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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# frozen_string_literal: true

class ProjectImportState < ApplicationRecord
  include AfterCommitQueue
  include ImportState::SidekiqJobTracker

  self.table_name = "project_mirror_data"

  after_commit :expire_etag_cache

  belongs_to :project, inverse_of: :import_state

  validates :project, presence: true

  alias_attribute :correlation_id, :correlation_id_value

  state_machine :status, initial: :none do
    event :schedule do
      transition [:none, :finished, :failed] => :scheduled
    end

    event :force_start do
      transition [:none, :finished, :failed] => :started
    end

    event :start do
      transition scheduled: :started
    end

    event :finish do
      transition started: :finished
    end

    event :fail_op do
      transition [:scheduled, :started] => :failed
    end

    state :scheduled
    state :started
    state :finished
    state :failed

    after_transition [:none, :finished, :failed] => :scheduled do |state, _|
      state.run_after_commit do
        job_id = project.add_import_job

        if job_id
          correlation_id = Labkit::Correlation::CorrelationId.current_or_new_id
          update(jid: job_id, correlation_id_value: correlation_id)
        end
      end
    end

    after_transition any => :finished do |state, _|
      if state.jid.present?
        Gitlab::SidekiqStatus.unset(state.jid)

        state.update_column(:jid, nil)
      end
    end

    after_transition any => :failed do |state, _|
      state.project.remove_import_data
    end

    after_transition started: :finished do |state, _|
      project = state.project

      project.reset_cache_and_import_attrs

      if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists?
        # rubocop: disable CodeReuse/ServiceClass
        state.run_after_commit do
          Projects::AfterImportService.new(project).execute
        end
        # rubocop: enable CodeReuse/ServiceClass
      end
    end
  end

  def expire_etag_cache
    if realtime_changes_path
      Gitlab::EtagCaching::Store.new.tap do |store|
        store.touch(realtime_changes_path)
      rescue Gitlab::EtagCaching::Store::InvalidKeyError
        # no-op: not every realtime changes endpoint is using etag caching
      end
    end
  end

  def realtime_changes_path
    Gitlab::Routing.url_helpers.polymorphic_path([:realtime_changes_import, project.import_type.to_sym], format: :json)
  rescue NoMethodError
    # polymorphic_path throws NoMethodError when no such path exists
    nil
  end

  def relation_hard_failures(limit:)
    project.import_failures.hard_failures_by_correlation_id(correlation_id).limit(limit)
  end

  def mark_as_failed(error_message)
    original_errors = errors.dup
    sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message)

    fail_op

    update_column(:last_error, sanitized_message)
  rescue ActiveRecord::ActiveRecordError => e
    Gitlab::Import::Logger.error(
      message: 'Error setting import status to failed',
      error: e.message,
      original_error: sanitized_message
    )
  ensure
    @errors = original_errors
  end

  alias_method :no_import?, :none?

  def in_progress?
    scheduled? || started?
  end

  def started?
    # import? does SQL work so only run it if it looks like there's an import running
    status == 'started' && project.import?
  end
end

ProjectImportState.prepend_mod_with('ProjectImportState')