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
|
# 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
def initialize(
pipeline:,
current_user:,
cascade_to_children: true,
auto_canceled_by_pipeline: nil,
execute_async: true)
@pipeline = pipeline
@current_user = current_user
@cascade_to_children = cascade_to_children
@auto_canceled_by_pipeline = auto_canceled_by_pipeline
@execute_async = execute_async
end
def execute
unless can?(current_user, :cancel_pipeline, pipeline)
return ServiceResponse.error(
message: 'Insufficient permissions to cancel the pipeline',
reason: :insufficient_permissions)
end
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
cancel_jobs(pipeline.cancelable_statuses)
return ServiceResponse.success unless cascade_to_children?
# cancel any bridges that could spin up new child pipelines
cancel_jobs(pipeline.bridges_in_self_and_project_descendants.cancelable)
cancel_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
# For parent child-pipelines only (not multi-project)
def cancel_children
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
|