Cascade pipeline cancellation in multi-project pipeline
The problem
We are using Multi-Project Pipeline, with strategy:depend
.
Both the triggering/upstream pipeline and the downstream pipeline jobs are interruptible
.
When the Upstream pipeline is canceled while the downstream pipeline is still running, we want that the downstream pipeline to be canceled as well.
A use case is when the pipeline is set to "Auto-cancel redundant pipelines". If a new pipeline begun then we want that the entire stream will be canceled.
There is no point of keeping the downstream pipeline running.
Issue #273378 (closed) did this change for "parent/child pipeline" but not for "multi-project pipeline". See this comment.
The same should be done for "multi-project pipeline".
Proposal
When both the "upstream" pipeline and the "downstream" pipeline in a multi-project pipeline are interruptible
and the upstream is canceled, the downstream will be canceled as well.
This should be applied to both:
- if the downstream pipeline was triggered by the
trigger
keyword - if the downstream pipeline was triggered using the API
Note: we currently consider same-project child pipeline cancellations to be 'system' cancellations. We will need to authorize against the user who triggered the cancellation for each pipeline for multi-project downstreams.
Rough Implementation guide
diff --git a/app/services/ci/cancel_pipeline_service.rb b/app/services/ci/cancel_pipeline_service.rb
index 92eead3fdd14..dc847d9a6951 100644
--- a/app/services/ci/cancel_pipeline_service.rb
+++ b/app/services/ci/cancel_pipeline_service.rb
@@ -29,13 +29,22 @@ def initialize(
def execute
return permission_error_response unless can?(current_user, :cancel_pipeline, pipeline)
- force_execute
+ cancel(current_user)
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
+ # We pass a nil user to child cancellations. We don't cancel children on forces at this time.
+ # since that is handled as part of the CancelRedundantPipelinesService.
+ cancel(nil)
+ end
+
+ private
+
+ attr_reader :pipeline, :current_user
+
+ # The user for the child cancellation check
+ def cancel(user)
return ServiceResponse.error(message: 'No pipeline provided', reason: :no_pipeline) unless pipeline
unless pipeline.cancelable?
@@ -54,15 +63,11 @@ def force_execute
cancel_jobs(pipeline.cancelable_statuses)
end
- cancel_children if cascade_to_children?
+ cancel_children(user) 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',
@@ -117,22 +122,23 @@ def permission_error_response
# from `CancelRedundantPipelinesService`.
# In the future, when "safe cancellation" is implemented as a regular cancellation feature,
# we need to handle this case.
- def cancel_children
+ def cancel_children(user)
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(
+ ::Ci::UserCancelPipelineWorker.perform_async(
child_pipeline.id,
- @auto_canceled_by_pipeline&.id
+ @auto_canceled_by_pipeline&.id,
+ user
)
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,
+ current_user: user,
cascade_to_children: false,
execute_async: execute_async?,
auto_canceled_by_pipeline: @auto_canceled_by_pipeline
This should allow use to remove Ci::CancelPipelineWorker
#452215.