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
|
# 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
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
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
def permission_error_response
ServiceResponse.error(
message: 'Insufficient permissions to cancel the pipeline',
reason: :insufficient_permissions
)
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
|