diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 88c2579bdfff040a23cffee364704e33a1d3adf6..15e50e238b8b20b0321dbd65448e290e031341b9 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -4784,6 +4784,16 @@ :idempotent: true :tags: [] :queue_namespace: +- :name: pages_delete_group_pages_deployments + :worker_name: Pages::DeleteGroupPagesDeploymentsWorker + :feature_category: :pages + :has_external_dependencies: false + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] + :queue_namespace: - :name: pages_delete_pages_deployment :worker_name: Pages::DeletePagesDeploymentWorker :feature_category: :pages diff --git a/app/workers/pages/delete_group_pages_deployments_worker.rb b/app/workers/pages/delete_group_pages_deployments_worker.rb new file mode 100644 index 0000000000000000000000000000000000000000..d29b9000ba38270bcccacb15d9cbf8c386296f79 --- /dev/null +++ b/app/workers/pages/delete_group_pages_deployments_worker.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Pages + class DeleteGroupPagesDeploymentsWorker + include Gitlab::EventStore::Subscriber + + data_consistency :always + feature_category :pages + idempotent! + + def handle_event(event) + group_id = event.data[:group_id] + return unless group_id + + group = Group.find_by_id(group_id) + return unless group + + cursor = { current_id: group_id, depth: [group_id] } + iterator = Gitlab::Database::NamespaceEachBatch.new(namespace_class: Namespace, cursor: cursor) + + iterator.each_batch(of: 100) do |namespace_ids, _new_cursor| + project_namespaces = Namespaces::ProjectNamespace.id_in(namespace_ids) + + projects_with_pages(project_namespaces).each do |project| + user = project.owner + next unless user + + ::Pages::DeleteService.new(project, user).execute + end + end + end + + private + + def projects_with_pages(project_namespaces) + Project.by_project_namespace(project_namespaces).with_pages_deployed + end + end +end diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 0ddc86629373dc0a91970a39338bf6d47b0ed50a..c69dba1c47747bb53c92d54f5d816e028037143d 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -755,6 +755,8 @@ - 1 - - pages_deactivate_mr_deployments - 1 +- - pages_delete_group_pages_deployments + - 1 - - pages_delete_pages_deployment - 1 - - pages_domain_ssl_renewal diff --git a/lib/gitlab/event_store.rb b/lib/gitlab/event_store.rb index dfc569d398f4dfee74b433c3809fb00bfbc0f460..59a51f19675da336c294693e3c7b5c72be32c785 100644 --- a/lib/gitlab/event_store.rb +++ b/lib/gitlab/event_store.rb @@ -63,6 +63,7 @@ def configure!(store) if: ->(event) { ::Ml::ExperimentTracking::AssociateMlCandidateToPackageWorker.handles_event?(event) } store.subscribe ::Ci::InitializePipelinesIidSequenceWorker, to: ::Projects::ProjectCreatedEvent store.subscribe ::Pages::DeletePagesDeploymentWorker, to: ::Projects::ProjectArchivedEvent + store.subscribe ::Pages::DeleteGroupPagesDeploymentsWorker, to: ::Namespaces::Groups::GroupArchivedEvent store.subscribe ::Pages::ResetPagesDefaultDomainRedirectWorker, to: ::Pages::Domains::PagesDomainDeletedEvent store.subscribe ::MergeRequests::ProcessDraftNotePublishedWorker, to: ::MergeRequests::DraftNotePublishedEvent diff --git a/spec/workers/pages/delete_group_pages_deployments_worker_spec.rb b/spec/workers/pages/delete_group_pages_deployments_worker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..4f544d73a6b382567acb4f86d714638f76d193f8 --- /dev/null +++ b/spec/workers/pages/delete_group_pages_deployments_worker_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Pages::DeleteGroupPagesDeploymentsWorker, feature_category: :pages do + let_it_be(:group) { create(:group) } + let_it_be(:subgroup) { create(:group, parent: group) } + let_it_be(:project_1) { create(:project, group: group) } + let_it_be(:project_2) { create(:project, group: group) } + let_it_be(:project_3) { create(:project, group: subgroup) } + let_it_be(:project_without_pages) { create(:project, group: group) } + + let!(:pages_deployment_1) { create(:pages_deployment, project: project_1) } + let!(:pages_deployment_2) { create(:pages_deployment, project: project_2) } + let!(:pages_deployment_3) { create(:pages_deployment, project: project_3) } + + let(:event) do + ::Namespaces::Groups::GroupArchivedEvent.new(data: { + group_id: group.id, + root_namespace_id: group.id + }) + end + + it_behaves_like 'worker with data consistency', described_class, data_consistency: :always + it_behaves_like 'subscribes to event' + + subject(:use_event) { consume_event(subscriber: described_class, event: event) } + + describe '#handle_event' do + context 'when group has multiple projects with pages' do + it 'calls Pages::DeleteService for each project with pages', :aggregate_failures do + expect(Pages::DeleteService).to receive(:new).with(project_1, project_1.owner).and_call_original + expect(Pages::DeleteService).to receive(:new).with(project_2, project_2.owner).and_call_original + expect(Pages::DeleteService).to receive(:new).with(project_3, project_3.owner).and_call_original + expect(Pages::DeleteService).not_to receive(:new).with(project_without_pages, anything) + + use_event + end + + it 'marks pages as not deployed for all projects', :sidekiq_inline do + project_1.update!(archived: true) + project_2.update!(archived: true) + project_3.update!(archived: true) + + expect { use_event } + .to change { project_1.reload.pages_deployed? }.from(true).to(false) + .and change { project_2.reload.pages_deployed? }.from(true).to(false) + .and change { project_3.reload.pages_deployed? }.from(true).to(false) + end + + it 'removes pages deployments for all projects in the group and subgroups', :sidekiq_inline do + project_1.update!(archived: true) + project_2.update!(archived: true) + project_3.update!(archived: true) + + expect { use_event } + .to change { PagesDeployment.count }.by(-3) + end + end + + context 'when group does not exist' do + let(:event) do + ::Namespaces::Groups::GroupArchivedEvent.new(data: { + group_id: non_existing_record_id, + root_namespace_id: non_existing_record_id + }) + end + + it 'does not raise an error' do + expect { use_event }.not_to raise_error + end + + it 'does not delete any pages deployments' do + expect { use_event }.not_to change { PagesDeployment.count } + end + end + + context 'when a project does not have an owner' do + before do + allow_next_found_instance_of(Project) do |project| + allow(project).to receive(:owner).and_return(nil) + end + end + + it 'skips that project' do + expect(Pages::DeleteService).not_to receive(:new) + + use_event + end + end + + context 'when group_id is missing from event data' do + it 'returns early without processing' do + event = instance_double(Namespaces::Groups::GroupArchivedEvent, data: {}) + + expect(Group).not_to receive(:find_by_id) + expect(Pages::DeleteService).not_to receive(:new) + + described_class.new.handle_event(event) + end + end + end +end