Welcome to mirror list, hosted at ThFree Co, Russian Federation.

cancel_pipeline_service.rb « ci « services « app - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 92eead3fdd14c46dfa576abc4406e4621b7af3be (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
# frozen_string_literal: true

module Ci
  # Cancel a pipelines cancelable jobs and optionally it's child pipelines cancelable jobs
  class CancelPipelineService
    include Gitlab::OptimisticLocking
    include Gitlab::Allowable

    ##
    # @cascade_to_children - if true cancels all related child pipelines for parent child pipelines
    # @auto_canceled_by_pipeline - store the pipeline_id of the pipeline that triggered cancellation
    # @execute_async - if true cancel the children asyncronously
    # @safe_cancellation - if true only cancel interruptible:true jobs
    def initialize(
      pipeline:,
      current_user:,
      cascade_to_children: true,
      auto_canceled_by_pipeline: nil,
      execute_async: true,
      safe_cancellation: false)
      @pipeline = pipeline
      @current_user = current_user
      @cascade_to_children = cascade_to_children
      @auto_canceled_by_pipeline = auto_canceled_by_pipeline
      @execute_async = execute_async
      @safe_cancellation = safe_cancellation
    end

    def execute
      return permission_error_response unless can?(current_user, :cancel_pipeline, pipeline)

      force_execute
    end

    # This method should be used only when we want to always cancel the pipeline without
    # checking whether the current_user has permissions to do so, or when we don't have
    # a current_user available in the context.
    def force_execute
      return ServiceResponse.error(message: 'No pipeline provided', reason: :no_pipeline) unless pipeline

      unless pipeline.cancelable?
        return ServiceResponse.error(message: 'Pipeline is not cancelable', reason: :pipeline_not_cancelable)
      end

      log_pipeline_being_canceled

      pipeline.update_column(:auto_canceled_by_id, @auto_canceled_by_pipeline.id) if @auto_canceled_by_pipeline

      if @safe_cancellation
        # Only build and bridge (trigger) jobs can be interruptible.
        # We do not cancel GenericCommitStatuses because they can't have the `interruptible` attribute.
        cancel_jobs(pipeline.processables.cancelable.interruptible)
      else
        cancel_jobs(pipeline.cancelable_statuses)
      end

      cancel_children if cascade_to_children?

      ServiceResponse.success
    end

    private

    attr_reader :pipeline, :current_user

    def log_pipeline_being_canceled
      Gitlab::AppJsonLogger.info(
        event: 'pipeline_cancel_running',
        pipeline_id: pipeline.id,
        auto_canceled_by_pipeline_id: @auto_canceled_by_pipeline&.id,
        cascade_to_children: cascade_to_children?,
        execute_async: execute_async?,
        **Gitlab::ApplicationContext.current
      )
    end

    def cascade_to_children?
      @cascade_to_children
    end

    def execute_async?
      @execute_async
    end

    def cancel_jobs(jobs)
      retries = 3
      retry_lock(jobs, retries, name: 'ci_pipeline_cancel_running') do |jobs_to_cancel|
        preloaded_relations = [:project, :pipeline, :deployment, :taggings]

        jobs_to_cancel.find_in_batches do |batch|
          relation = CommitStatus.id_in(batch)
          Preloaders::CommitStatusPreloader.new(relation).execute(preloaded_relations)

          relation.each { |job| cancel_job(job) }
        end
      end
    end

    def cancel_job(job)
      if @auto_canceled_by_pipeline
        job.auto_canceled_by_id = @auto_canceled_by_pipeline.id
        job.auto_canceled_by_partition_id = @auto_canceled_by_pipeline.partition_id
      end

      job.cancel
    end

    def permission_error_response
      ServiceResponse.error(
        message: 'Insufficient permissions to cancel the pipeline',
        reason: :insufficient_permissions
      )
    end

    # We don't handle the case when `cascade_to_children` is `true` and `safe_cancellation` is `true`
    # because `safe_cancellation` is passed as `true` only when `cascade_to_children` is `false`
    # from `CancelRedundantPipelinesService`.
    # In the future, when "safe cancellation" is implemented as a regular cancellation feature,
    # we need to handle this case.
    def cancel_children
      cancel_jobs(pipeline.bridges_in_self_and_project_descendants.cancelable)

      # For parent child-pipelines only (not multi-project)
      pipeline.all_child_pipelines.each do |child_pipeline|
        if execute_async?
          ::Ci::CancelPipelineWorker.perform_async(
            child_pipeline.id,
            @auto_canceled_by_pipeline&.id
          )
        else
          # cascade_to_children is false because we iterate through children
          # we also cancel bridges prior to prevent more children
          self.class.new(
            pipeline: child_pipeline.reset,
            current_user: nil,
            cascade_to_children: false,
            execute_async: execute_async?,
            auto_canceled_by_pipeline: @auto_canceled_by_pipeline
          ).force_execute
        end
      end
    end
  end
end