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

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

module Ci
  module PipelineCreation
    class CancelRedundantPipelinesService
      include Gitlab::Utils::StrongMemoize

      BATCH_SIZE = 25
      PAGE_SIZE = 500

      def initialize(pipeline)
        @pipeline = pipeline
        @project = @pipeline.project
      end

      # rubocop: disable CodeReuse/ActiveRecord
      def execute
        return if service_disabled?
        return if pipeline.parent_pipeline? # skip if child pipeline
        return unless project.auto_cancel_pending_pipelines?

        if Feature.enabled?(:use_offset_pagination_for_canceling_redundant_pipelines, project)
          paginator.each do |ids|
            pipelines = parent_and_child_pipelines(ids)

            Gitlab::OptimisticLocking.retry_lock(pipelines, name: 'cancel_pending_pipelines') do |cancelables|
              auto_cancel_interruptible_pipelines(cancelables.ids)
            end
          end
        else
          Gitlab::OptimisticLocking
            .retry_lock(parent_and_child_pipelines, name: 'cancel_pending_pipelines') do |cancelables|
            cancelables.select(:id).each_batch(of: BATCH_SIZE) do |cancelables_batch|
              auto_cancel_interruptible_pipelines(cancelables_batch.ids)
            end
          end
        end
      end

      private

      attr_reader :pipeline, :project

      def paginator
        page = 1
        Enumerator.new do |yielder|
          loop do
            # leverage the index_ci_pipelines_on_project_id_and_status_and_created_at index
            records = project.all_pipelines
              .created_after(pipelines_created_after)
              .order(:status, :created_at)
              .page(page) # use offset pagination because there is no other way to loop over the data
              .per(PAGE_SIZE)
              .pluck(:id)

            raise StopIteration if records.empty?

            yielder << records
            page += 1
          end
        end
      end

      def parent_auto_cancelable_pipelines(ids = nil)
        scope = project.all_pipelines
          .created_after(pipelines_created_after)
          .for_ref(pipeline.ref)
          .where_not_sha(project.commit(pipeline.ref).try(:id))
          .where("created_at < ?", pipeline.created_at)
          .for_status(CommitStatus::AVAILABLE_STATUSES) # Force usage of project_id_and_status_and_created_at_index
          .ci_sources

        scope = scope.id_in(ids) if ids.present?
        scope
      end

      def parent_and_child_pipelines(ids = nil)
        Ci::Pipeline.object_hierarchy(parent_auto_cancelable_pipelines(ids), project_condition: :same)
          .base_and_descendants
          .alive_or_scheduled
      end
      # rubocop: enable CodeReuse/ActiveRecord

      def auto_cancel_interruptible_pipelines(pipeline_ids)
        ::Ci::Pipeline
          .id_in(pipeline_ids)
          .with_only_interruptible_builds
          .each do |cancelable_pipeline|
            Gitlab::AppLogger.info(
              class: self.class.name,
              message: "Pipeline #{pipeline.id} auto-canceling pipeline #{cancelable_pipeline.id}",
              canceled_pipeline_id: cancelable_pipeline.id,
              canceled_by_pipeline_id: pipeline.id,
              canceled_by_pipeline_source: pipeline.source
            )

            # cascade_to_children not needed because we iterate through descendants here
            ::Ci::CancelPipelineService.new(
              pipeline: cancelable_pipeline,
              current_user: nil,
              auto_canceled_by_pipeline_id: pipeline.id,
              cascade_to_children: false
            ).force_execute
          end
      end

      def pipelines_created_after
        3.days.ago
      end

      # Finding the pipelines to cancel is an expensive task that is not well
      # covered by indexes for all project use-cases and sometimes it might
      # harm other services. See https://gitlab.com/gitlab-com/gl-infra/production/-/issues/14758
      # This feature flag is in place to disable this feature for rogue projects.
      #
      def service_disabled?
        Feature.enabled?(:disable_cancel_redundant_pipelines_service, project, type: :ops)
      end
    end
  end
end