From 2617231e65b84dda5da6bb8e4519deb71daa9d5f Mon Sep 17 00:00:00 2001 From: Terri Chu <tchu@gitlab.com> Date: Tue, 28 Nov 2023 15:45:44 +0000 Subject: [PATCH] Allow all projects to be indexed --- .../development/search_index_all_projects.yml | 8 + .../concerns/elastic/projects_search.rb | 11 ++ ee/app/models/ee/issue_assignee.rb | 2 +- ee/app/models/ee/project_feature.rb | 54 ++--- .../process_initial_bookkeeping_service.rb | 2 + .../elastic/project_transfer_worker.rb | 2 +- .../models/concerns/elastic/project_spec.rb | 184 +++++++++++------- ee/spec/models/project_feature_spec.rb | 134 ++++++++++--- ...rocess_initial_bookkeeping_service_spec.rb | 35 +++- 9 files changed, 306 insertions(+), 126 deletions(-) create mode 100644 config/feature_flags/development/search_index_all_projects.yml diff --git a/config/feature_flags/development/search_index_all_projects.yml b/config/feature_flags/development/search_index_all_projects.yml new file mode 100644 index 0000000000000..dc4ea2a175daf --- /dev/null +++ b/config/feature_flags/development/search_index_all_projects.yml @@ -0,0 +1,8 @@ +--- +name: search_index_all_projects +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134456 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/432489 +milestone: '16.7' +type: development +group: group::global search +default_enabled: false diff --git a/ee/app/models/concerns/elastic/projects_search.rb b/ee/app/models/concerns/elastic/projects_search.rb index eb4c6085c6d08..049028ab68748 100644 --- a/ee/app/models/concerns/elastic/projects_search.rb +++ b/ee/app/models/concerns/elastic/projects_search.rb @@ -16,6 +16,17 @@ def use_elasticsearch? ::Gitlab::CurrentSettings.elasticsearch_indexes_project?(self) end + def maintaining_indexed_associations? + use_elasticsearch? + end + + override :maintaining_elasticsearch? + def maintaining_elasticsearch? + return super if ::Feature.disabled?(:search_index_all_projects, self) + + ::Gitlab::CurrentSettings.elasticsearch_indexing? + end + override :maintain_elasticsearch_create def maintain_elasticsearch_create ::Elastic::ProcessInitialBookkeepingService.track!(self) diff --git a/ee/app/models/ee/issue_assignee.rb b/ee/app/models/ee/issue_assignee.rb index 7e36685fcd4ea..c149e27f2f95a 100644 --- a/ee/app/models/ee/issue_assignee.rb +++ b/ee/app/models/ee/issue_assignee.rb @@ -8,7 +8,7 @@ module IssueAssignee end def update_elasticsearch_index - if issue.project&.use_elasticsearch? && issue.maintaining_elasticsearch? + if issue.maintaining_elasticsearch? && issue.project&.maintaining_indexed_associations? issue.maintain_elasticsearch_update issue.maintain_elasticsearch_issue_notes_update # we need to propagate new permissions to notes end diff --git a/ee/app/models/ee/project_feature.rb b/ee/app/models/ee/project_feature.rb index 64fedb4426259..b74c21f353c5b 100644 --- a/ee/app/models/ee/project_feature.rb +++ b/ee/app/models/ee/project_feature.rb @@ -21,35 +21,41 @@ module ProjectFeature prepended do set_available_features(EE_FEATURES) - # Ensure changes to project visibility settings go to elasticsearch if the tracked field(s) change - after_commit on: :update do - if project.maintaining_elasticsearch? - project.maintain_elasticsearch_update - - associations_to_update = [] - associations_to_update << 'issues' if elasticsearch_project_issues_need_updating? - associations_to_update << 'merge_requests' if elasticsearch_project_merge_requests_need_updating? - associations_to_update << 'notes' if elasticsearch_project_notes_need_updating? - associations_to_update << 'milestones' if elasticsearch_project_milestones_need_updating? - - if associations_to_update.any? - ElasticAssociationIndexerWorker.perform_async(project.class.name, project_id, associations_to_update) - end - - if elasticsearch_project_blobs_need_updating? - ElasticCommitIndexerWorker.perform_async(project.id, false, { force: true }) - end - - if elasticsearch_project_wikis_need_updating? - ElasticWikiIndexerWorker.perform_async(project.id, project.class.name, { force: true }) - end - end - end + # Ensure changes to project visibility settings go to Elasticsearch if the tracked field(s) change + after_commit :update_project_in_index, on: :update, if: -> { project.maintaining_elasticsearch? } + after_commit :update_project_associations_in_index, on: :update, if: -> { + project.maintaining_elasticsearch? && project.maintaining_indexed_associations? + } attribute :requirements_access_level, default: Featurable::ENABLED private + def update_project_in_index + project.maintain_elasticsearch_update + end + + def update_project_associations_in_index + associations_to_update = [].tap do |associations| + associations << 'issues' if elasticsearch_project_issues_need_updating? + associations << 'merge_requests' if elasticsearch_project_merge_requests_need_updating? + associations << 'notes' if elasticsearch_project_notes_need_updating? + associations << 'milestones' if elasticsearch_project_milestones_need_updating? + end + + if associations_to_update.any? + ElasticAssociationIndexerWorker.perform_async(project.class.name, project_id, associations_to_update) + end + + if elasticsearch_project_blobs_need_updating? + ElasticCommitIndexerWorker.perform_async(project.id, false, { force: true }) + end + + return unless elasticsearch_project_wikis_need_updating? + + ElasticWikiIndexerWorker.perform_async(project.id, project.class.name, { force: true }) + end + def elasticsearch_project_milestones_need_updating? previous_changes.keys.any? { |key| MILESTONE_PERMISSION_TRACKED_FIELDS.include?(key) } end diff --git a/ee/app/services/elastic/process_initial_bookkeeping_service.rb b/ee/app/services/elastic/process_initial_bookkeeping_service.rb index 0e1ac7a1c922b..0f1b1dfe1ebbb 100644 --- a/ee/app/services/elastic/process_initial_bookkeeping_service.rb +++ b/ee/app/services/elastic/process_initial_bookkeeping_service.rb @@ -25,6 +25,8 @@ def backfill_projects!(*projects) projects.each do |project| raise ArgumentError, 'This method only accepts Projects' unless project.is_a?(Project) + next unless project.maintaining_indexed_associations? + maintain_indexed_associations(project, INDEXED_PROJECT_ASSOCIATIONS) ElasticCommitIndexerWorker.perform_async(project.id, false, { force: true }) diff --git a/ee/app/workers/elastic/project_transfer_worker.rb b/ee/app/workers/elastic/project_transfer_worker.rb index 31ade3e23182a..945f4f9933748 100644 --- a/ee/app/workers/elastic/project_transfer_worker.rb +++ b/ee/app/workers/elastic/project_transfer_worker.rb @@ -21,7 +21,7 @@ def perform(project_id, old_namespace_id, new_namespace_id) project.invalidate_elasticsearch_indexes_cache! end - if project.maintaining_elasticsearch? + if project.maintaining_elasticsearch? && project.maintaining_indexed_associations? # If the project is indexed in Elasticsearch, the project and all associated data are queued up for indexing # to make sure the namespace_ancestry field gets updated in each document. ::Elastic::ProcessInitialBookkeepingService.backfill_projects!(project) diff --git a/ee/spec/models/concerns/elastic/project_spec.rb b/ee/spec/models/concerns/elastic/project_spec.rb index eba33ac00417b..00308ca2729c1 100644 --- a/ee/spec/models/concerns/elastic/project_spec.rb +++ b/ee/spec/models/concerns/elastic/project_spec.rb @@ -2,13 +2,13 @@ require 'spec_helper' -RSpec.describe Project, :elastic, :clean_gitlab_redis_shared_state, feature_category: :global_search do +RSpec.describe Project, :elastic_delete_by_query, feature_category: :global_search do before do stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true) end context 'when limited indexing is on' do - let_it_be(:project) { create :project, name: 'main_project' } + let_it_be(:project) { create(:project, :empty_repo, name: 'main_project') } before do stub_ee_application_setting(elasticsearch_limit_indexing: true) @@ -18,51 +18,85 @@ describe '#maintaining_elasticsearch?' do subject(:maintaining_elasticsearch) { project.maintaining_elasticsearch? } - it { is_expected.to be(false) } + context 'when the search_index_all_projects FF is false' do + before do + stub_feature_flags(search_index_all_projects: false) + end + + it { is_expected.to be(false) } + end + + context 'when the search_index_all_projects FF is true' do + it { is_expected.to be(true) } + end end describe '#use_elasticsearch?' do - it 'returns false' do - expect(project.use_elasticsearch?).to be_falsey - end + subject(:use_elasticsearch) { project.use_elasticsearch? } + + it { is_expected.to be(false) } end end context 'when a project is enabled specifically' do before do - create :elasticsearch_indexed_project, project: project + create(:elasticsearch_indexed_project, project: project) end describe '#maintaining_elasticsearch?' do subject(:maintaining_elasticsearch) { project.maintaining_elasticsearch? } - it { is_expected.to be(true) } + context 'when the search_index_all_projects FF is false' do + before do + stub_feature_flags(search_index_all_projects: false) + end + + it { is_expected.to be(true) } + end + + context 'when the search_index_all_projects FF is true' do + it { is_expected.to be(true) } + end end describe '#use_elasticsearch?' do - it 'returns true' do - expect(project.use_elasticsearch?).to be_truthy - end + subject(:use_elasticsearch) { project.use_elasticsearch? } + + it { is_expected.to be(true) } end - it 'only indexes enabled projects' do - Sidekiq::Testing.inline! do - create :project, path: 'test_two', description: 'awesome project' - create :project + describe 'indexing', :sidekiq_inline do + context 'when the search_index_all_projects FF is false' do + before do + stub_feature_flags(search_index_all_projects: false) + end + + it 'only indexes enabled projects' do + create(:project, :empty_repo, path: 'test_two', description: 'awesome project') + ensure_elasticsearch_index! - ensure_elasticsearch_index! + expect(described_class.elastic_search('main_project', options: { project_ids: :any }).total_count).to eq(1) + expect(described_class.elastic_search('"test_two"', options: { project_ids: :any }).total_count).to eq(0) + end end - expect(described_class.elastic_search('main_pro*', options: { project_ids: :any }).total_count).to eq(1) - expect(described_class.elastic_search('"test_two"', options: { project_ids: :any }).total_count).to eq(0) + context 'when the search_index_all_projects FF is true' do + it 'indexes all projects' do + create(:project, :empty_repo, path: 'test_two', description: 'awesome project') + ensure_elasticsearch_index! + + expect(described_class.elastic_search('main_project', options: { project_ids: :any }).total_count).to eq(1) + expect(described_class.elastic_search('"test_two"', options: { project_ids: :any }).total_count).to eq(1) + end + end end end - context 'when a group is enabled' do + context 'when a group is enabled', :sidekiq_inline do let_it_be(:group) { create(:group) } - before do - create :elasticsearch_indexed_namespace, namespace: group + before_all do + create(:elasticsearch_indexed_namespace, namespace: group) end describe '#maintaining_elasticsearch?' do @@ -70,38 +104,60 @@ subject(:maintaining_elasticsearch) { project_in_group.maintaining_elasticsearch? } - it { is_expected.to be(true) } + context 'when the search_index_all_projects FF is false' do + before do + stub_feature_flags(search_index_all_projects: false) + end + + it { is_expected.to be(true) } + end + + context 'when the search_index_all_projects FF is true' do + it { is_expected.to be(true) } + end end - it 'indexes only projects under the group' do - Sidekiq::Testing.inline! do - create :project, name: 'group_test1', group: create(:group, parent: group) - create :project, name: 'group_test2', description: 'awesome project' - create :project, name: 'group_test3', group: group - create :project, path: 'someone_elses_project', name: 'test4' + describe 'indexing' do + context 'when the search_index_all_projects FF is false' do + before do + stub_feature_flags(search_index_all_projects: false) + end + + it 'indexes only projects under the group' do + create(:project, name: 'group_test1', group: create(:group, parent: group)) + create(:project, name: 'group_test2', description: 'awesome project') + create(:project, name: 'group_test3', group: group) + ensure_elasticsearch_index! - ensure_elasticsearch_index! + expect(described_class.elastic_search('group_test*', options: { project_ids: :any }).total_count).to eq(2) + expect(described_class.elastic_search('"group_test3"', options: { project_ids: :any }).total_count).to eq(1) + expect(described_class.elastic_search('"group_test2"', options: { project_ids: :any }).total_count).to eq(0) + end end - expect(described_class.elastic_search('group_test*', options: { project_ids: :any }).total_count).to eq(2) - expect(described_class.elastic_search('"group_test3"', options: { project_ids: :any }).total_count).to eq(1) - expect(described_class.elastic_search('"group_test2"', options: { project_ids: :any }).total_count).to eq(0) - expect(described_class.elastic_search('"group_test4"', options: { project_ids: :any }).total_count).to eq(0) + context 'when the search_index_all_projects FF is true' do + it 'indexes all projects' do + create(:project, name: 'group_test1', group: create(:group, parent: group)) + create(:project, name: 'group_test2', description: 'awesome project') + create(:project, name: 'group_test3', group: group) + ensure_elasticsearch_index! + + expect(described_class.elastic_search('group_test*', options: { project_ids: :any }).total_count).to eq(3) + expect(described_class.elastic_search('"group_test3"', options: { project_ids: :any }).total_count).to eq(1) + expect(described_class.elastic_search('"group_test2"', options: { project_ids: :any }).total_count).to eq(1) + end + end end context 'default_operator' do RSpec.shared_examples 'use correct default_operator' do |operator| - before do - Sidekiq::Testing.inline! do - create :project, name: 'project1', group: group, description: 'test foo' - create :project, name: 'project2', group: group, description: 'test' - create :project, name: 'project3', group: group, description: 'foo' + it 'uses correct operator', :sidekiq_inline do + create(:project, name: 'project1', group: group, description: 'test foo') + create(:project, name: 'project2', group: group, description: 'test') + create(:project, name: 'project3', group: group, description: 'foo') - ensure_elasticsearch_index! - end - end + ensure_elasticsearch_index! - it 'uses correct operator' do count_for_or = described_class.elastic_search('test | foo', options: { project_ids: :any }).total_count expect(count_for_or).to be > 0 @@ -142,15 +198,12 @@ end end - # This test is added to address the issues described in - context 'when projects and snippets co-exist', :elastic_clean, issue: 'https://gitlab.com/gitlab-org/gitlab/issues/36340' do - before do - create :project - create :snippet, :public - end - + context 'when projects and snippets co-exist', issue: 'https://gitlab.com/gitlab-org/gitlab/issues/36340' do context 'when searching with a wildcard' do it 'only returns projects', :sidekiq_inline do + create(:project) + create(:snippet, :public) + ensure_elasticsearch_index! response = described_class.elastic_search('*') @@ -160,21 +213,18 @@ end end - it 'finds projects' do + it 'finds projects', :sidekiq_inline do project_ids = [] - Sidekiq::Testing.inline! do - project = create :project, name: 'test1' - project1 = create :project, path: 'test2', description: 'awesome project' - project2 = create :project - create :project, path: 'someone_elses_project' - project_ids += [project.id, project1.id, project2.id] + project = create(:project, name: 'test1') + project1 = create(:project, path: 'test2', description: 'awesome project') + project2 = create(:project) + create(:project, path: 'someone_elses_project') + project_ids += [project.id, project1.id, project2.id] - # The project you have no access to except as an administrator - create :project, :private, name: 'test3' + create(:project, :private, name: 'test3') - ensure_elasticsearch_index! - end + ensure_elasticsearch_index! expect(described_class.elastic_search('"test1"', options: { project_ids: project_ids }).total_count).to eq(1) expect(described_class.elastic_search('"test2"', options: { project_ids: project_ids }).total_count).to eq(1) @@ -184,16 +234,12 @@ expect(described_class.elastic_search('"someone_elses_project"', options: { project_ids: project_ids }).total_count).to eq(0) end - it 'finds partial matches in project names' do - project_ids = [] - - Sidekiq::Testing.inline! do - project = create :project, name: 'tesla-model-s' - project1 = create :project, name: 'tesla_model_s' - project_ids += [project.id, project1.id] + it 'finds partial matches in project names', :sidekiq_inline do + project = create :project, name: 'tesla-model-s' + project1 = create :project, name: 'tesla_model_s' + project_ids = [project.id, project1.id] - ensure_elasticsearch_index! - end + ensure_elasticsearch_index! expect(described_class.elastic_search('tesla', options: { project_ids: project_ids }).total_count).to eq(2) end diff --git a/ee/spec/models/project_feature_spec.rb b/ee/spec/models/project_feature_spec.rb index 5f2560593bed6..36b18e240c280 100644 --- a/ee/spec/models/project_feature_spec.rb +++ b/ee/spec/models/project_feature_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' -RSpec.describe ProjectFeature do - let(:project) { create(:project, :public) } - let(:user) { create(:user) } +RSpec.describe ProjectFeature, feature_category: :groups_and_projects do + let_it_be_with_reload(:project) { create(:project, :public) } + let_it_be_with_reload(:user) { create(:user) } describe 'default values' do subject { Project.new.project_feature } @@ -27,36 +27,122 @@ end end - describe 'project visibility changes' do + describe 'project visibility changes', feature_category: :global_search do using RSpec::Parameterized::TableSyntax - before do - allow(project).to receive(:maintaining_elasticsearch?).and_return(true) + context 'for repository' do + where(:maintaining_elasticsearch, :maintaining_indexed_associations, :worker_expected) do + true | true | true + false | true | false + true | false | false + false | false | false + end + + with_them do + before do + allow(project).to receive(:maintaining_elasticsearch?).and_return(maintaining_elasticsearch) + allow(project).to receive(:maintaining_indexed_associations?).and_return(maintaining_indexed_associations) + end + + context 'when updating repository_access_level' do + it 'enqueues a worker to index commit data' do + if worker_expected + expect(ElasticCommitIndexerWorker).to receive(:perform_async).with(project.id, false, { force: true }) + else + expect(ElasticCommitIndexerWorker).not_to receive(:perform_async) + end + + project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED) + end + end + end end - where(:feature, :worker_expected, :associations) do - 'issues' | true | %w[issues notes milestones] - 'wiki' | false | nil - 'builds' | false | nil - 'merge_requests' | true | %w[merge_requests notes milestones] - 'repository' | true | %w[notes] - 'snippets' | true | %w[notes] - 'operations' | false | nil - 'security_and_compliance' | false | nil - 'pages' | false | nil + context 'for wiki' do + where(:maintaining_elasticsearch, :maintaining_indexed_associations, :worker_expected) do + true | true | true + false | true | false + true | false | false + false | false | false + end + + with_them do + before do + allow(project).to receive(:maintaining_elasticsearch?).and_return(maintaining_elasticsearch) + allow(project).to receive(:maintaining_indexed_associations?).and_return(maintaining_indexed_associations) + end + + context 'when updating wiki_access_level' do + it 'enqueues a worker to index commit data' do + if worker_expected + expect(ElasticWikiIndexerWorker).to receive(:perform_async).with(project.id, 'Project', { force: true }) + else + expect(ElasticWikiIndexerWorker).not_to receive(:perform_async) + end + + project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED) + end + end + end end - with_them do - it 're-indexes project and project associations on update' do - expect(project).to receive(:maintain_elasticsearch_update) + context 'for associations in the database' do + where(:feature, :maintaining_elasticsearch, :maintaining_indexed_associations, :worker_expected, :associations) do + 'issues' | true | true | true | %w[issues notes milestones] + 'issues' | false | true | false | nil + 'issues' | true | false | false | nil + 'issues' | false | false | false | nil + 'builds' | true | true | false | nil + 'builds' | false | true | false | nil + 'builds' | true | false | false | nil + 'builds' | false | false | false | nil + 'merge_requests' | true | true | true | %w[merge_requests notes milestones] + 'merge_requests' | false | true | false | nil + 'merge_requests' | true | false | false | nil + 'merge_requests' | false | false | false | nil + 'repository' | true | true | true | %w[notes] + 'repository' | false | true | false | nil + 'repository' | true | false | false | nil + 'repository' | false | false | false | nil + 'snippets' | true | true | true | %w[notes] + 'snippets' | false | true | false | nil + 'snippets' | true | false | false | nil + 'snippets' | false | false | false | nil + 'operations' | true | true | false | nil + 'operations' | false | true | false | nil + 'operations' | true | false | false | nil + 'operations' | false | false | false | nil + 'security_and_compliance' | true | true | false | nil + 'security_and_compliance' | false | true | false | nil + 'security_and_compliance' | true | false | false | nil + 'security_and_compliance' | false | false | false | nil + 'pages' | true | true | false | nil + 'pages' | false | true | false | nil + 'pages' | true | false | false | nil + 'pages' | false | false | false | nil + end - if worker_expected - expect(ElasticAssociationIndexerWorker).to receive(:perform_async).with('Project', project.id, associations) - else - expect(ElasticAssociationIndexerWorker).not_to receive(:perform_async) + with_them do + before do + allow(project).to receive(:maintaining_elasticsearch?).and_return(maintaining_elasticsearch) + allow(project).to receive(:maintaining_indexed_associations?).and_return(maintaining_indexed_associations) end - project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::DISABLED) + it 're-indexes project and project associations on update' do + if maintaining_elasticsearch + expect(project).to receive(:maintain_elasticsearch_update) + else + expect(project).not_to receive(:maintain_elasticsearch_update) + end + + if worker_expected + expect(ElasticAssociationIndexerWorker).to receive(:perform_async).with('Project', project.id, associations) + else + expect(ElasticAssociationIndexerWorker).not_to receive(:perform_async) + end + + project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::DISABLED) + end end end end diff --git a/ee/spec/services/elastic/process_initial_bookkeeping_service_spec.rb b/ee/spec/services/elastic/process_initial_bookkeeping_service_spec.rb index de2fa950f77c3..91a491a5a9df8 100644 --- a/ee/spec/services/elastic/process_initial_bookkeeping_service_spec.rb +++ b/ee/spec/services/elastic/process_initial_bookkeeping_service_spec.rb @@ -3,16 +3,23 @@ require 'spec_helper' RSpec.describe Elastic::ProcessInitialBookkeepingService, feature_category: :global_search do - let(:project) { create(:project) } - let(:issue) { create(:issue) } + let_it_be(:project) { create(:project) } + let_it_be(:issue) { create(:issue) } describe '.backfill_projects!' do - it 'calls ElasticCommitIndexerWorker and, ElasticWikiIndexerWorker' do - expect(described_class).to receive(:maintain_indexed_associations).with(project, Elastic::ProcessInitialBookkeepingService::INDEXED_PROJECT_ASSOCIATIONS) - expect(ElasticCommitIndexerWorker).to receive(:perform_async).with(project.id, false, { force: true }) - expect(ElasticWikiIndexerWorker).to receive(:perform_async).with(project.id, project.class.name, { force: true }) + context 'when project is maintaining indexed associations' do + before do + allow(project).to receive(:maintaining_indexed_associations?).and_return(true) + end - described_class.backfill_projects!(project) + it 'indexes itself and calls ElasticCommitIndexerWorker and, ElasticWikiIndexerWorker' do + expect(described_class).to receive(:track!).with(project) + expect(described_class).to receive(:maintain_indexed_associations).with(project, Elastic::ProcessInitialBookkeepingService::INDEXED_PROJECT_ASSOCIATIONS) + expect(ElasticCommitIndexerWorker).to receive(:perform_async).with(project.id, false, { force: true }) + expect(ElasticWikiIndexerWorker).to receive(:perform_async).with(project.id, project.class.name, { force: true }) + + described_class.backfill_projects!(project) + end end it 'raises an exception if non project is provided' do @@ -22,5 +29,19 @@ it 'uses a separate queue' do expect { described_class.backfill_projects!(project) }.not_to change { Elastic::ProcessBookkeepingService.queue_size } end + + context 'when project is not maintaining indexed associations' do + before do + allow(project).to receive(:maintaining_indexed_associations?).and_return(false) + end + + it 'indexes itself only' do + expect(described_class).not_to receive(:maintain_indexed_associations) + expect(ElasticCommitIndexerWorker).not_to receive(:perform_async) + expect(ElasticWikiIndexerWorker).not_to receive(:perform_async) + + described_class.backfill_projects!(project) + end + end end end -- GitLab