summaryrefslogtreecommitdiff
path: root/app/workers/container_expiration_policy_worker.rb
blob: 308ccfe2cb3f1d0f101a719d80d1fc2cfd243b1a (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
# frozen_string_literal: true

class ContainerExpirationPolicyWorker # rubocop:disable Scalability/IdempotentWorker
  include ApplicationWorker

  data_consistency :always

  include CronjobQueue
  include ExclusiveLeaseGuard

  feature_category :container_registry

  InvalidPolicyError = Class.new(StandardError)

  BATCH_SIZE = 1000

  def perform
    process_stale_ongoing_cleanups
    disable_policies_without_container_repositories
    throttling_enabled? ? perform_throttled : perform_unthrottled
    log_counts
  end

  private

  def disable_policies_without_container_repositories
    ContainerExpirationPolicy.active.each_batch(of: BATCH_SIZE) do |policies|
      policies.without_container_repositories
              .update_all(enabled: false)
    end
  end

  def log_counts
    use_replica_if_available do
      required_count = ContainerRepository.requiring_cleanup.count
      unfinished_count = ContainerRepository.with_unfinished_cleanup.count

      log_extra_metadata_on_done(:cleanup_required_count, required_count)
      log_extra_metadata_on_done(:cleanup_unfinished_count, unfinished_count)
      log_extra_metadata_on_done(:cleanup_total_count, required_count + unfinished_count)
    end
  end

  # data_consistency :delayed not used as this is a cron job and those jobs are
  # not perfomed with a delay
  # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63635#note_603771207
  def use_replica_if_available(&blk)
    ::Gitlab::Database::LoadBalancing::Session.current.use_replicas_for_read_queries(&blk)
  end

  def process_stale_ongoing_cleanups
    threshold = delete_tags_service_timeout.seconds + 30.minutes
    ContainerRepository.with_stale_ongoing_cleanup(threshold.ago)
                       .update_all(expiration_policy_cleanup_status: :cleanup_unfinished)
  end

  def perform_unthrottled
    with_runnable_policy(preloaded: true) do |policy|
      with_context(project: policy.project,
                   user: nil) do |project:, user:|
        ContainerExpirationPolicyService.new(project, user)
                                        .execute(policy)
      end
    end
  end

  def perform_throttled
    try_obtain_lease do
      ContainerExpirationPolicies::CleanupContainerRepositoryWorker.perform_with_capacity
    end
  end

  # TODO : remove the preload option when cleaning FF container_registry_expiration_policies_throttling
  def with_runnable_policy(preloaded: false)
    ContainerExpirationPolicy.runnable_schedules.each_batch(of: BATCH_SIZE) do |policies|
      # rubocop: disable CodeReuse/ActiveRecord
      cte = Gitlab::SQL::CTE.new(:batched_policies, policies.limit(BATCH_SIZE))
      # rubocop: enable CodeReuse/ActiveRecord
      scope = cte.apply_to(ContainerExpirationPolicy.all).with_container_repositories

      scope = scope.preloaded if preloaded

      scope.each do |policy|
        if policy.valid?
          yield policy
        else
          disable_invalid_policy!(policy)
        end
      end
    end
  end

  def disable_invalid_policy!(policy)
    policy.disable!
    Gitlab::ErrorTracking.log_exception(
      ::ContainerExpirationPolicyWorker::InvalidPolicyError.new,
      container_expiration_policy_id: policy.id
    )
  end

  def throttling_enabled?
    Feature.enabled?(:container_registry_expiration_policies_throttling, default_enabled: :yaml)
  end

  def lease_timeout
    5.hours
  end

  def delete_tags_service_timeout
    ::Gitlab::CurrentSettings.current_application_settings.container_registry_delete_tags_service_timeout || 0
  end
end