Commit c96409cf authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch '267546-limited-worker-nonloopless-inner-class' into 'master'

Add an inner class in the cleanup container repository worker [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!56989
parents 083f3824 2c3e5971
...@@ -21,82 +21,34 @@ module ContainerExpirationPolicies ...@@ -21,82 +21,34 @@ module ContainerExpirationPolicies
cleanup_tags_service_deleted_size cleanup_tags_service_deleted_size
].freeze ].freeze
def perform_work delegate :perform_work, :remaining_work_count, to: :inner_instance
return unless throttling_enabled?
return unless container_repository def inner_instance
strong_memoize(:inner_instance) do
log_extra_metadata_on_done(:container_repository_id, container_repository.id) if loopless_enabled?
log_extra_metadata_on_done(:project_id, project.id) Loopless.new(self)
else
unless allowed_to_run?(container_repository) Looping.new(self)
container_repository.cleanup_unscheduled! end
log_extra_metadata_on_done(:cleanup_status, :skipped)
return
end end
result = ContainerExpirationPolicies::CleanupService.new(container_repository)
.execute
log_on_done(result)
end
def remaining_work_count
cleanup_scheduled_count = ContainerRepository.cleanup_scheduled.count
cleanup_unfinished_count = ContainerRepository.cleanup_unfinished.count
total_count = cleanup_scheduled_count + cleanup_unfinished_count
log_info(
cleanup_scheduled_count: cleanup_scheduled_count,
cleanup_unfinished_count: cleanup_unfinished_count,
cleanup_total_count: total_count
)
total_count
end end
def max_running_jobs def max_running_jobs
return 0 unless throttling_enabled? return 0 unless throttling_enabled?
::Gitlab::CurrentSettings.current_application_settings.container_registry_expiration_policies_worker_capacity ::Gitlab::CurrentSettings.container_registry_expiration_policies_worker_capacity
end
private
def allowed_to_run?(container_repository)
return false unless policy&.enabled && policy&.next_run_at
Time.zone.now + max_cleanup_execution_time.seconds < policy.next_run_at
end end
def throttling_enabled? def throttling_enabled?
Feature.enabled?(:container_registry_expiration_policies_throttling) Feature.enabled?(:container_registry_expiration_policies_throttling)
end end
def max_cleanup_execution_time def loopless_enabled?
::Gitlab::CurrentSettings.current_application_settings.container_registry_delete_tags_service_timeout Feature.enabled?(:container_registry_expiration_policies_loopless)
end
def policy
project.container_expiration_policy
end end
def project def max_cleanup_execution_time
container_repository.project ::Gitlab::CurrentSettings.container_registry_delete_tags_service_timeout
end
def container_repository
strong_memoize(:container_repository) do
ContainerRepository.transaction do
# rubocop: disable CodeReuse/ActiveRecord
# We need a lock to prevent two workers from picking up the same row
container_repository = ContainerRepository.waiting_for_cleanup
.order(:expiration_policy_cleanup_status, :expiration_policy_started_at)
.limit(1)
.lock('FOR UPDATE SKIP LOCKED')
.first
# rubocop: enable CodeReuse/ActiveRecord
container_repository&.tap(&:cleanup_ongoing!)
end
end
end end
def log_info(extra_structure) def log_info(extra_structure)
...@@ -120,5 +72,100 @@ module ContainerExpirationPolicies ...@@ -120,5 +72,100 @@ module ContainerExpirationPolicies
log_extra_metadata_on_done(:cleanup_tags_service_truncated, !!truncated) log_extra_metadata_on_done(:cleanup_tags_service_truncated, !!truncated)
log_extra_metadata_on_done(:running_jobs_count, running_jobs_count) log_extra_metadata_on_done(:running_jobs_count, running_jobs_count)
end end
# rubocop: disable Scalability/IdempotentWorker
# TODO: move the logic from this class to the parent one when container_registry_expiration_policies_loopless is removed
# Tracking issue: https://gitlab.com/gitlab-org/gitlab/-/issues/325273
class Loopless
# TODO fill the logic here with the approach documented in
# https://gitlab.com/gitlab-org/gitlab/-/issues/267546#limited-worker
def initialize(parent)
@parent = parent
end
end
# rubocop: enable Scalability/IdempotentWorker
# rubocop: disable Scalability/IdempotentWorker
# TODO remove this class when `container_registry_expiration_policies_loopless` is removed
# Tracking issue: https://gitlab.com/gitlab-org/gitlab/-/issues/325273
class Looping
include Gitlab::Utils::StrongMemoize
delegate :throttling_enabled?,
:log_extra_metadata_on_done,
:log_info,
:log_on_done,
:max_cleanup_execution_time,
to: :@parent
def initialize(parent)
@parent = parent
end
def perform_work
return unless throttling_enabled?
return unless container_repository
log_extra_metadata_on_done(:container_repository_id, container_repository.id)
log_extra_metadata_on_done(:project_id, project.id)
unless allowed_to_run?(container_repository)
container_repository.cleanup_unscheduled!
log_extra_metadata_on_done(:cleanup_status, :skipped)
return
end
result = ContainerExpirationPolicies::CleanupService.new(container_repository)
.execute
log_on_done(result)
end
def remaining_work_count
cleanup_scheduled_count = ContainerRepository.cleanup_scheduled.count
cleanup_unfinished_count = ContainerRepository.cleanup_unfinished.count
total_count = cleanup_scheduled_count + cleanup_unfinished_count
log_info(
cleanup_scheduled_count: cleanup_scheduled_count,
cleanup_unfinished_count: cleanup_unfinished_count,
cleanup_total_count: total_count
)
total_count
end
private
def allowed_to_run?(container_repository)
return false unless policy&.enabled && policy&.next_run_at
Time.zone.now + max_cleanup_execution_time.seconds < policy.next_run_at
end
def policy
project.container_expiration_policy
end
def project
container_repository.project
end
def container_repository
strong_memoize(:container_repository) do
ContainerRepository.transaction do
# rubocop: disable CodeReuse/ActiveRecord
# We need a lock to prevent two workers from picking up the same row
container_repository = ContainerRepository.waiting_for_cleanup
.order(:expiration_policy_cleanup_status, :expiration_policy_started_at)
.limit(1)
.lock('FOR UPDATE SKIP LOCKED')
.first
# rubocop: enable CodeReuse/ActiveRecord
container_repository&.tap(&:cleanup_ongoing!)
end
end
end
end
# rubocop: enable Scalability/IdempotentWorker
end end
end end
...@@ -106,96 +106,102 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do ...@@ -106,96 +106,102 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
end end
end end
context 'with repository in cleanup scheduled state' do context 'with loopless disabled' do
it_behaves_like 'handling all repository conditions'
end
context 'with repository in cleanup unfinished state' do
before do before do
repository.cleanup_unfinished! stub_feature_flags(container_registry_expiration_policies_loopless: false)
end end
it_behaves_like 'handling all repository conditions' context 'with repository in cleanup scheduled state' do
end it_behaves_like 'handling all repository conditions'
end
context 'with another repository in cleanup unfinished state' do
let_it_be(:another_repository) { create(:container_repository, :cleanup_unfinished) }
it 'process the cleanup scheduled repository first' do context 'with repository in cleanup unfinished state' do
service_response = cleanup_service_response(repository: repository) before do
expect(ContainerExpirationPolicies::CleanupService) repository.cleanup_unfinished!
.to receive(:new).with(repository).and_return(double(execute: service_response)) end
expect_log_extra_metadata(service_response: service_response)
subject it_behaves_like 'handling all repository conditions'
end end
end
context 'with multiple repositories in cleanup unfinished state' do context 'with another repository in cleanup unfinished state' do
let_it_be(:repository2) { create(:container_repository, :cleanup_unfinished, expiration_policy_started_at: 20.minutes.ago) } let_it_be(:another_repository) { create(:container_repository, :cleanup_unfinished) }
let_it_be(:repository3) { create(:container_repository, :cleanup_unfinished, expiration_policy_started_at: 10.minutes.ago) }
before do it 'process the cleanup scheduled repository first' do
repository.update!(expiration_policy_cleanup_status: :cleanup_unfinished, expiration_policy_started_at: 30.minutes.ago) service_response = cleanup_service_response(repository: repository)
expect(ContainerExpirationPolicies::CleanupService)
.to receive(:new).with(repository).and_return(double(execute: service_response))
expect_log_extra_metadata(service_response: service_response)
subject
end
end end
it 'process the repository with the oldest expiration_policy_started_at' do context 'with multiple repositories in cleanup unfinished state' do
service_response = cleanup_service_response(repository: repository) let_it_be(:repository2) { create(:container_repository, :cleanup_unfinished, expiration_policy_started_at: 20.minutes.ago) }
expect(ContainerExpirationPolicies::CleanupService) let_it_be(:repository3) { create(:container_repository, :cleanup_unfinished, expiration_policy_started_at: 10.minutes.ago) }
.to receive(:new).with(repository).and_return(double(execute: service_response))
expect_log_extra_metadata(service_response: service_response)
subject before do
end repository.update!(expiration_policy_cleanup_status: :cleanup_unfinished, expiration_policy_started_at: 30.minutes.ago)
end end
context 'with repository in cleanup ongoing state' do it 'process the repository with the oldest expiration_policy_started_at' do
before do service_response = cleanup_service_response(repository: repository)
repository.cleanup_ongoing! expect(ContainerExpirationPolicies::CleanupService)
.to receive(:new).with(repository).and_return(double(execute: service_response))
expect_log_extra_metadata(service_response: service_response)
subject
end
end end
it 'does not process it' do context 'with repository in cleanup ongoing state' do
expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new) before do
repository.cleanup_ongoing!
end
expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count } it 'does not process it' do
expect(repository.cleanup_ongoing?).to be_truthy expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new)
end
end
context 'with no repository in any cleanup state' do expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count }
before do expect(repository.cleanup_ongoing?).to be_truthy
repository.cleanup_unscheduled! end
end end
it 'does not process it' do context 'with no repository in any cleanup state' do
expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new) before do
repository.cleanup_unscheduled!
end
expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count } it 'does not process it' do
expect(repository.cleanup_unscheduled?).to be_truthy expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new)
end
end
context 'with no container repository waiting' do expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count }
before do expect(repository.cleanup_unscheduled?).to be_truthy
repository.destroy! end
end end
it 'does not execute the cleanup tags service' do context 'with no container repository waiting' do
expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new) before do
repository.destroy!
end
expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count } it 'does not execute the cleanup tags service' do
end expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new)
end
context 'with feature flag disabled' do expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count }
before do end
stub_feature_flags(container_registry_expiration_policies_throttling: false)
end end
it 'is a no-op' do context 'with feature flag disabled' do
expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new) before do
stub_feature_flags(container_registry_expiration_policies_throttling: false)
end
it 'is a no-op' do
expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new)
expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count } expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count }
end
end end
end end
...@@ -230,37 +236,42 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do ...@@ -230,37 +236,42 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
describe '#remaining_work_count' do describe '#remaining_work_count' do
subject { worker.remaining_work_count } subject { worker.remaining_work_count }
context 'with container repositoires waiting for cleanup' do context 'with loopless disabled' do
let_it_be(:unfinished_repositories) { create_list(:container_repository, 2, :cleanup_unfinished) } before do
stub_feature_flags(container_registry_expiration_policies_loopless: false)
end
context 'with container repositoires waiting for cleanup' do
let_it_be(:unfinished_repositories) { create_list(:container_repository, 2, :cleanup_unfinished) }
it { is_expected.to eq(3) } it { is_expected.to eq(3) }
it 'logs the work count' do it 'logs the work count' do
expect_log_info( expect_log_info(
cleanup_scheduled_count: 1, cleanup_scheduled_count: 1,
cleanup_unfinished_count: 2, cleanup_unfinished_count: 2,
cleanup_total_count: 3 cleanup_total_count: 3
) )
subject subject
end
end end
end
context 'with no container repositories waiting for cleanup' do context 'with no container repositories waiting for cleanup' do
before do before do
repository.cleanup_ongoing! repository.cleanup_ongoing!
end end
it { is_expected.to eq(0) } it { is_expected.to eq(0) }
it 'logs 0 work count' do it 'logs 0 work count' do
expect_log_info( expect_log_info(
cleanup_scheduled_count: 0, cleanup_scheduled_count: 0,
cleanup_unfinished_count: 0, cleanup_unfinished_count: 0,
cleanup_total_count: 0 cleanup_total_count: 0
) )
subject subject
end
end end
end end
end end
...@@ -274,14 +285,20 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do ...@@ -274,14 +285,20 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
stub_application_setting(container_registry_expiration_policies_worker_capacity: capacity) stub_application_setting(container_registry_expiration_policies_worker_capacity: capacity)
end end
it { is_expected.to eq(capacity) } context 'with loopless disabled' do
context 'with feature flag disabled' do
before do before do
stub_feature_flags(container_registry_expiration_policies_throttling: false) stub_feature_flags(container_registry_expiration_policies_loopless: false)
end end
it { is_expected.to eq(0) } it { is_expected.to eq(capacity) }
context 'with feature flag disabled' do
before do
stub_feature_flags(container_registry_expiration_policies_throttling: false)
end
it { is_expected.to eq(0) }
end
end end
end end
......
...@@ -35,10 +35,16 @@ RSpec.describe ContainerExpirationPolicyWorker do ...@@ -35,10 +35,16 @@ RSpec.describe ContainerExpirationPolicyWorker do
end end
context 'With no container expiration policies' do context 'With no container expiration policies' do
it 'does not execute any policies' do context 'with loopless disabled' do
expect(ContainerRepository).not_to receive(:for_project_id) before do
stub_feature_flags(container_registry_expiration_policies_loopless: false)
end
expect { subject }.not_to change { ContainerRepository.cleanup_scheduled.count } it 'does not execute any policies' do
expect(ContainerRepository).not_to receive(:for_project_id)
expect { subject }.not_to change { ContainerRepository.cleanup_scheduled.count }
end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment