diff --git a/app/events/projects/project_deleted_event.rb b/app/events/projects/project_deleted_event.rb new file mode 100644 index 0000000000000000000000000000000000000000..ac58c5c6755d53231d9685b943d545912ad73075 --- /dev/null +++ b/app/events/projects/project_deleted_event.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Projects + class ProjectDeletedEvent < ::Gitlab::EventStore::Event + def schema + { + 'type' => 'object', + 'properties' => { + 'project_id' => { 'type' => 'integer' }, + 'namespace_id' => { 'type' => 'integer' } + }, + 'required' => %w[project_id namespace_id] + } + end + end +end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 733a4b45cb2d9c06d4313bf5a48b590311e7ece6..95af5a6863ffe5f880621d0136e7f4628eda62ba 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -37,6 +37,8 @@ def execute system_hook_service.execute_hooks_for(project, :destroy) log_info("Project \"#{project.full_path}\" was deleted") + publish_project_deleted_event_for(project) if Feature.enabled?(:publish_project_deleted_event, default_enabled: :yaml) + current_user.invalidate_personal_projects_count true @@ -260,6 +262,12 @@ def raise_error(message) def flush_caches(project) Projects::ForksCountService.new(project).delete_cache end + + def publish_project_deleted_event_for(project) + data = { project_id: project.id, namespace_id: project.namespace_id } + event = Projects::ProjectDeletedEvent.new(data: data) + Gitlab::EventStore.publish(event) + end end end diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 18646e5320b582c16128ebfe485f19f31c7744a6..f39aee14a69a4c0c4fb311e2c83798430edd0fdd 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -2569,6 +2569,15 @@ :weight: 1 :idempotent: true :tags: [] +- :name: namespaces_update_root_statistics + :worker_name: Namespaces::UpdateRootStatisticsWorker + :feature_category: :source_code_management + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: new_issue :worker_name: NewIssueWorker :feature_category: :team_planning diff --git a/app/workers/namespaces/update_root_statistics_worker.rb b/app/workers/namespaces/update_root_statistics_worker.rb new file mode 100644 index 0000000000000000000000000000000000000000..9fdf8e2506b42e8c72bc70be16208d9b4c3b5fc8 --- /dev/null +++ b/app/workers/namespaces/update_root_statistics_worker.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Namespaces + class UpdateRootStatisticsWorker + include Gitlab::EventStore::Subscriber + + data_consistency :always + + idempotent! + + feature_category :source_code_management + + def handle_event(event) + ScheduleAggregationWorker.perform_async(event.data[:namespace_id]) + end + end +end diff --git a/config/feature_flags/development/publish_project_deleted_event.yml b/config/feature_flags/development/publish_project_deleted_event.yml new file mode 100644 index 0000000000000000000000000000000000000000..1287ebe9f662a7719921465d41f94928c5bea474 --- /dev/null +++ b/config/feature_flags/development/publish_project_deleted_event.yml @@ -0,0 +1,8 @@ +--- +name: publish_project_deleted_event +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78862 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/351073 +milestone: '14.8' +type: development +group: group::pipeline insights +default_enabled: false diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index d52659d80e76636d7d4fc3c62a419db94624d69e..9c5294b6b2b80c69a6e32dd5c30110cacf33b5bb 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -295,6 +295,8 @@ - 1 - - namespaces_sync_namespace_name - 1 +- - namespaces_update_root_statistics + - 1 - - new_epic - 2 - - new_issue diff --git a/lib/gitlab/event_store.rb b/lib/gitlab/event_store.rb index 7dbbcdbb1a78d95b94e5b1f8bf6f1320ebd54500..e20ea1c736500177edc1e660b1a4717ed96f7d67 100644 --- a/lib/gitlab/event_store.rb +++ b/lib/gitlab/event_store.rb @@ -34,6 +34,7 @@ def self.configure!(store) # Add subscriptions here: store.subscribe ::MergeRequests::UpdateHeadPipelineWorker, to: ::Ci::PipelineCreatedEvent + store.subscribe ::Namespaces::UpdateRootStatisticsWorker, to: ::Projects::ProjectDeletedEvent end private_class_method :configure! end diff --git a/spec/events/projects/project_deleted_event_spec.rb b/spec/events/projects/project_deleted_event_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..fd8cec7271b5e0be916ba94d4fc14886785a324a --- /dev/null +++ b/spec/events/projects/project_deleted_event_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::ProjectDeletedEvent do + where(:data, :valid) do + [ + [{ project_id: 1, namespace_id: 2 }, true], + [{ project_id: 1 }, false], + [{ namespace_id: 1 }, false], + [{ project_id: 'foo', namespace_id: 2 }, false], + [{ project_id: 1, namespace_id: 'foo' }, false], + [{ project_id: [], namespace_id: 2 }, false], + [{ project_id: 1, namespace_id: [] }, false], + [{ project_id: {}, namespace_id: 2 }, false], + [{ project_id: 1, namespace_id: {} }, false], + ['foo', false], + [123, false], + [[], false] + ] + end + + with_them do + it 'validates data' do + constructor = -> { described_class.new(data: data) } + + if valid + expect { constructor.call }.not_to raise_error + else + expect { constructor.call }.to raise_error(Gitlab::EventStore::InvalidEvent) + end + end + end +end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index a6aa76c7e6ceab830d5dd4c5c2f8263d749f4e02..0beb5157ed6b6127006302a001b168abf575e5ab 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -18,17 +18,35 @@ end shared_examples 'deleting the project' do - before do - # Run sidekiq immediately to check that renamed repository will be removed + it 'deletes the project', :sidekiq_inline do destroy_project(project, user, {}) - end - it 'deletes the project', :sidekiq_inline do expect(Project.unscoped.all).not_to include(project) expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_falsey expect(project.gitlab_shell.repository_exists?(project.repository_storage, remove_path + '.git')).to be_falsey end + + it 'publishes a ProjectDeleted event with project id and namespace id' do + expected_data = { project_id: project.id, namespace_id: project.namespace_id } + expect(Gitlab::EventStore) + .to receive(:publish) + .with(event_type(Projects::ProjectDeletedEvent).containing(expected_data)) + + destroy_project(project, user, {}) + end + + context 'when feature flag publish_project_deleted_event is disabled' do + before do + stub_feature_flags(publish_project_deleted_event: false) + end + + it 'does not publish an event' do + expect(Gitlab::EventStore).not_to receive(:publish) + + destroy_project(project, user, {}) + end + end end shared_examples 'deleting the project with pipeline and build' do diff --git a/spec/support/matchers/event_store.rb b/spec/support/matchers/event_store.rb new file mode 100644 index 0000000000000000000000000000000000000000..96a71ae3c22fc3e7bbfad4e148aea66b4536cde3 --- /dev/null +++ b/spec/support/matchers/event_store.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +RSpec::Matchers.define :event_type do |event_class| + match do |actual| + actual.instance_of?(event_class) && + actual.data == @expected_data + end + + chain :containing do |expected_data| + @expected_data = expected_data + end +end diff --git a/spec/tooling/quality/test_level_spec.rb b/spec/tooling/quality/test_level_spec.rb index 8a944a473d73545d0457f2b424b050107005f2c7..33d3a5b49b319d1b7c038ba36a10d366abf5d968 100644 --- a/spec/tooling/quality/test_level_spec.rb +++ b/spec/tooling/quality/test_level_spec.rb @@ -28,7 +28,7 @@ context 'when level is unit' do it 'returns a pattern' do expect(subject.pattern(:unit)) - .to eq("spec/{bin,channels,config,db,dependencies,elastic,elastic_integration,experiments,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,metrics_server,models,policies,presenters,rack_servers,replicators,routing,rubocop,scripts,serializers,services,sidekiq,sidekiq_cluster,spam,support_specs,tasks,uploaders,validators,views,workers,tooling}{,/**/}*_spec.rb") + .to eq("spec/{bin,channels,config,db,dependencies,elastic,elastic_integration,experiments,events,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,metrics_server,models,policies,presenters,rack_servers,replicators,routing,rubocop,scripts,serializers,services,sidekiq,sidekiq_cluster,spam,support_specs,tasks,uploaders,validators,views,workers,tooling}{,/**/}*_spec.rb") end end @@ -110,7 +110,7 @@ context 'when level is unit' do it 'returns a regexp' do expect(subject.regexp(:unit)) - .to eq(%r{spec/(bin|channels|config|db|dependencies|elastic|elastic_integration|experiments|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|metrics_server|models|policies|presenters|rack_servers|replicators|routing|rubocop|scripts|serializers|services|sidekiq|sidekiq_cluster|spam|support_specs|tasks|uploaders|validators|views|workers|tooling)}) + .to eq(%r{spec/(bin|channels|config|db|dependencies|elastic|elastic_integration|experiments|events|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|metrics_server|models|policies|presenters|rack_servers|replicators|routing|rubocop|scripts|serializers|services|sidekiq|sidekiq_cluster|spam|support_specs|tasks|uploaders|validators|views|workers|tooling)}) end end diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb index a21cf3ed5f65072467850f62cf3b49180042db17..1cd5d23d8fc90f239a10bf3e57ad09c56a666e1a 100644 --- a/spec/workers/every_sidekiq_worker_spec.rb +++ b/spec/workers/every_sidekiq_worker_spec.rb @@ -349,6 +349,7 @@ 'Namespaces::OnboardingPipelineCreatedWorker' => 3, 'Namespaces::OnboardingProgressWorker' => 3, 'Namespaces::OnboardingUserAddedWorker' => 3, + 'Namespaces::RefreshRootStatisticsWorker' => 3, 'Namespaces::RootStatisticsWorker' => 3, 'Namespaces::ScheduleAggregationWorker' => 3, 'NetworkPolicyMetricsWorker' => 3, diff --git a/spec/workers/namespaces/update_root_statistics_worker_spec.rb b/spec/workers/namespaces/update_root_statistics_worker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a525904b7578748e4c3027aa13e4297bfe0f44af --- /dev/null +++ b/spec/workers/namespaces/update_root_statistics_worker_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Namespaces::UpdateRootStatisticsWorker do + let(:namespace_id) { 123 } + + let(:event) do + Projects::ProjectDeletedEvent.new(data: { project_id: 1, namespace_id: namespace_id }) + end + + subject { consume_event(event) } + + def consume_event(event) + described_class.new.perform(event.class.name, event.data) + end + + it 'enqueues ScheduleAggregationWorker' do + expect(Namespaces::ScheduleAggregationWorker).to receive(:perform_async).with(namespace_id) + + subject + end +end diff --git a/tooling/quality/test_level.rb b/tooling/quality/test_level.rb index 50cbc69beb214b37de9da76c36869794bcdee3aa..624564ecd059967cb0ae22e67c36f5d942ffa323 100644 --- a/tooling/quality/test_level.rb +++ b/tooling/quality/test_level.rb @@ -24,6 +24,7 @@ class TestLevel elastic elastic_integration experiments + events factories finders frontend