summaryrefslogtreecommitdiff
path: root/app/models/ci/processable.rb
blob: fae65ed0632b65d0a41a19eb15e49cd09b7e2939 (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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# frozen_string_literal: true

module Ci
  class Processable < ::CommitStatus
    include Gitlab::Utils::StrongMemoize
    extend ::Gitlab::Utils::Override

    has_one :resource, class_name: 'Ci::Resource', foreign_key: 'build_id', inverse_of: :processable

    belongs_to :resource_group, class_name: 'Ci::ResourceGroup', inverse_of: :processables

    accepts_nested_attributes_for :needs

    scope :preload_needs, -> { preload(:needs) }

    scope :with_needs, -> (names = nil) do
      needs = Ci::BuildNeed.scoped_build.select(1)
      needs = needs.where(name: names) if names
      where('EXISTS (?)', needs).preload(:needs)
    end

    scope :without_needs, -> (names = nil) do
      needs = Ci::BuildNeed.scoped_build.select(1)
      needs = needs.where(name: names) if names
      where('NOT EXISTS (?)', needs)
    end

    state_machine :status do
      event :enqueue do
        transition [:created, :skipped, :manual, :scheduled] => :waiting_for_resource, if: :with_resource_group?
      end

      event :enqueue_scheduled do
        transition scheduled: :waiting_for_resource, if: :with_resource_group?
      end

      event :enqueue_waiting_for_resource do
        transition waiting_for_resource: :preparing, if: :any_unmet_prerequisites?
        transition waiting_for_resource: :pending
      end

      before_transition any => :waiting_for_resource do |processable|
        processable.waiting_for_resource_at = Time.current
      end

      before_transition on: :enqueue_waiting_for_resource do |processable|
        next unless processable.with_resource_group?

        processable.resource_group.assign_resource_to(processable)
      end

      after_transition any => :waiting_for_resource do |processable|
        processable.run_after_commit do
          Ci::ResourceGroups::AssignResourceFromResourceGroupWorker
            .perform_async(processable.resource_group_id)
        end
      end

      after_transition any => ::Ci::Processable.completed_statuses do |processable|
        next unless processable.with_resource_group?
        next unless processable.resource_group.release_resource_from(processable)

        processable.run_after_commit do
          Ci::ResourceGroups::AssignResourceFromResourceGroupWorker
            .perform_async(processable.resource_group_id)
        end
      end
    end

    def self.select_with_aggregated_needs(project)
      aggregated_needs_names = Ci::BuildNeed
        .scoped_build
        .select("ARRAY_AGG(name)")
        .to_sql

      all.select(
        '*',
        "(#{aggregated_needs_names}) as aggregated_needs_names"
      )
    end

    # Old processables may have scheduling_type as nil,
    # so we need to ensure the data exists before using it.
    def self.populate_scheduling_type!
      needs = Ci::BuildNeed.scoped_build.select(1)
      where(scheduling_type: nil).update_all(
        "scheduling_type = CASE WHEN (EXISTS (#{needs.to_sql}))
         THEN #{scheduling_types[:dag]}
         ELSE #{scheduling_types[:stage]}
         END"
      )
    end

    validates :type, presence: true
    validates :scheduling_type, presence: true, on: :create, unless: :importing?

    delegate :merge_request?,
      :merge_request_ref?,
      :legacy_detached_merge_request_pipeline?,
      :merge_train_pipeline?,
      to: :pipeline

    def aggregated_needs_names
      read_attribute(:aggregated_needs_names)
    end

    def schedulable?
      raise NotImplementedError
    end

    def action?
      raise NotImplementedError
    end

    def when
      read_attribute(:when) || 'on_success'
    end

    def expanded_environment_name
      raise NotImplementedError
    end

    def scoped_variables_hash
      raise NotImplementedError
    end

    override :all_met_to_become_pending?
    def all_met_to_become_pending?
      super && !with_resource_group?
    end

    def with_resource_group?
      self.resource_group_id.present?
    end

    # Overriding scheduling_type enum's method for nil `scheduling_type`s
    def scheduling_type_dag?
      scheduling_type.nil? ? find_legacy_scheduling_type == :dag : super
    end

    # scheduling_type column of previous builds/bridges have not been populated,
    # so we calculate this value on runtime when we need it.
    def find_legacy_scheduling_type
      strong_memoize(:find_legacy_scheduling_type) do
        needs.exists? ? :dag : :stage
      end
    end

    def needs_attributes
      strong_memoize(:needs_attributes) do
        needs.map { |need| need.attributes.except('id', 'build_id') }
      end
    end

    def ensure_scheduling_type!
      # If this has a scheduling_type, it means all processables in the pipeline already have.
      return if scheduling_type

      pipeline.ensure_scheduling_type!
      reset
    end

    def dependency_variables
      return [] if all_dependencies.empty?

      Gitlab::Ci::Variables::Collection.new.concat(
        Ci::JobVariable.where(job: all_dependencies).dotenv_source
      )
    end

    def all_dependencies
      dependencies.all
    end

    private

    def dependencies
      strong_memoize(:dependencies) do
        Ci::BuildDependencies.new(self)
      end
    end
  end
end