1# frozen_string_literal: true
2
3module Ci
4  class Processable < ::CommitStatus
5    include Gitlab::Utils::StrongMemoize
6    extend ::Gitlab::Utils::Override
7
8    has_one :resource, class_name: 'Ci::Resource', foreign_key: 'build_id', inverse_of: :processable
9
10    belongs_to :resource_group, class_name: 'Ci::ResourceGroup', inverse_of: :processables
11
12    accepts_nested_attributes_for :needs
13
14    scope :preload_needs, -> { preload(:needs) }
15
16    scope :with_needs, -> (names = nil) do
17      needs = Ci::BuildNeed.scoped_build.select(1)
18      needs = needs.where(name: names) if names
19      where('EXISTS (?)', needs).preload(:needs)
20    end
21
22    scope :without_needs, -> (names = nil) do
23      needs = Ci::BuildNeed.scoped_build.select(1)
24      needs = needs.where(name: names) if names
25      where('NOT EXISTS (?)', needs)
26    end
27
28    state_machine :status do
29      event :enqueue do
30        transition [:created, :skipped, :manual, :scheduled] => :waiting_for_resource, if: :with_resource_group?
31      end
32
33      event :enqueue_scheduled do
34        transition scheduled: :waiting_for_resource, if: :with_resource_group?
35      end
36
37      event :enqueue_waiting_for_resource do
38        transition waiting_for_resource: :preparing, if: :any_unmet_prerequisites?
39        transition waiting_for_resource: :pending
40      end
41
42      before_transition any => :waiting_for_resource do |processable|
43        processable.waiting_for_resource_at = Time.current
44      end
45
46      before_transition on: :enqueue_waiting_for_resource do |processable|
47        next unless processable.with_resource_group?
48
49        processable.resource_group.assign_resource_to(processable)
50      end
51
52      after_transition any => :waiting_for_resource do |processable|
53        processable.run_after_commit do
54          Ci::ResourceGroups::AssignResourceFromResourceGroupWorker
55            .perform_async(processable.resource_group_id)
56        end
57      end
58
59      after_transition any => ::Ci::Processable.completed_statuses do |processable|
60        next unless processable.with_resource_group?
61
62        processable.resource_group.release_resource_from(processable)
63
64        processable.run_after_commit do
65          Ci::ResourceGroups::AssignResourceFromResourceGroupWorker
66            .perform_async(processable.resource_group_id)
67        end
68      end
69    end
70
71    def self.select_with_aggregated_needs(project)
72      aggregated_needs_names = Ci::BuildNeed
73        .scoped_build
74        .select("ARRAY_AGG(name)")
75        .to_sql
76
77      all.select(
78        '*',
79        "(#{aggregated_needs_names}) as aggregated_needs_names"
80      )
81    end
82
83    # Old processables may have scheduling_type as nil,
84    # so we need to ensure the data exists before using it.
85    def self.populate_scheduling_type!
86      needs = Ci::BuildNeed.scoped_build.select(1)
87      where(scheduling_type: nil).update_all(
88        "scheduling_type = CASE WHEN (EXISTS (#{needs.to_sql}))
89         THEN #{scheduling_types[:dag]}
90         ELSE #{scheduling_types[:stage]}
91         END"
92      )
93    end
94
95    validates :type, presence: true
96    validates :scheduling_type, presence: true, on: :create, unless: :importing?
97
98    delegate :merge_request?,
99      :merge_request_ref?,
100      :legacy_detached_merge_request_pipeline?,
101      :merge_train_pipeline?,
102      to: :pipeline
103
104    def aggregated_needs_names
105      read_attribute(:aggregated_needs_names)
106    end
107
108    def schedulable?
109      raise NotImplementedError
110    end
111
112    def action?
113      raise NotImplementedError
114    end
115
116    def when
117      read_attribute(:when) || 'on_success'
118    end
119
120    def expanded_environment_name
121      raise NotImplementedError
122    end
123
124    def persisted_environment
125      raise NotImplementedError
126    end
127
128    override :all_met_to_become_pending?
129    def all_met_to_become_pending?
130      super && !with_resource_group?
131    end
132
133    def with_resource_group?
134      self.resource_group_id.present?
135    end
136
137    # Overriding scheduling_type enum's method for nil `scheduling_type`s
138    def scheduling_type_dag?
139      scheduling_type.nil? ? find_legacy_scheduling_type == :dag : super
140    end
141
142    # scheduling_type column of previous builds/bridges have not been populated,
143    # so we calculate this value on runtime when we need it.
144    def find_legacy_scheduling_type
145      strong_memoize(:find_legacy_scheduling_type) do
146        needs.exists? ? :dag : :stage
147      end
148    end
149
150    def needs_attributes
151      strong_memoize(:needs_attributes) do
152        needs.map { |need| need.attributes.except('id', 'build_id') }
153      end
154    end
155
156    def ensure_scheduling_type!
157      # If this has a scheduling_type, it means all processables in the pipeline already have.
158      return if scheduling_type
159
160      pipeline.ensure_scheduling_type!
161      reset
162    end
163
164    def dependency_variables
165      return [] if all_dependencies.empty?
166
167      Gitlab::Ci::Variables::Collection.new.concat(
168        Ci::JobVariable.where(job: all_dependencies).dotenv_source
169      )
170    end
171
172    def all_dependencies
173      strong_memoize(:all_dependencies) do
174        dependencies.all
175      end
176    end
177
178    private
179
180    def dependencies
181      strong_memoize(:dependencies) do
182        Ci::BuildDependencies.new(self)
183      end
184    end
185  end
186end
187