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
|