diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 2b72a4c4c89c458d4dfa497cf8be2f89f4b21f34..87adedf936e05a0e868f1704cb4158282f2c3b6f 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -194,7 +194,7 @@ def persisted_environment=(environment) after_save :stick_build_if_status_changed after_create unless: :importing? do |build| - run_after_commit { BuildHooksWorker.perform_async(build) } + run_after_commit { build.feature_flagged_execute_hooks } end class << self @@ -285,7 +285,7 @@ def clone_accessors build.run_after_commit do BuildQueueWorker.perform_async(id) - BuildHooksWorker.perform_async(build) + build.feature_flagged_execute_hooks end end @@ -313,7 +313,7 @@ def clone_accessors build.run_after_commit do build.ensure_persistent_ref - BuildHooksWorker.perform_async(build) + build.feature_flagged_execute_hooks end end @@ -780,10 +780,20 @@ def stuck? pending? && !any_runners_online? end + def feature_flagged_execute_hooks + if Feature.enabled?(:execute_build_hooks_inline, project) + execute_hooks + else + BuildHooksWorker.perform_async(self) + end + end + def execute_hooks return unless project return if user&.blocked? + ActiveRecord::Associations::Preloader.new.preload([self], { runner: :tags }) + project.execute_hooks(build_data.dup, :job_hooks) if project.has_active_hooks?(:job_hooks) project.execute_integrations(build_data.dup, :job_hooks) if project.has_active_integrations?(:job_hooks) end diff --git a/app/models/project.rb b/app/models/project.rb index 28dd51fc67d61ce560e415b3507067b856669cc8..b8a2fb13f9aa22a3a20877e5e7b20ccfeae600b1 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1682,7 +1682,13 @@ def execute_integrations(data, hooks_scope = :push_hooks) end def has_active_hooks?(hooks_scope = :push_hooks) - hooks.hooks_for(hooks_scope).any? || SystemHook.hooks_for(hooks_scope).any? || Gitlab::FileHook.any? + @has_active_hooks ||= {} # rubocop: disable Gitlab/PredicateMemoization + + return @has_active_hooks[hooks_scope] if @has_active_hooks.key?(hooks_scope) + + @has_active_hooks[hooks_scope] = hooks.hooks_for(hooks_scope).any? || + SystemHook.hooks_for(hooks_scope).any? || + Gitlab::FileHook.any? end def has_active_integrations?(hooks_scope = :push_hooks) diff --git a/app/workers/build_hooks_worker.rb b/app/workers/build_hooks_worker.rb index 5c08344bfe3ece6b86bdb4cd65fd71c698f17c15..2c62aed72d6f36f121a071a650eb074e98969fff 100644 --- a/app/workers/build_hooks_worker.rb +++ b/app/workers/build_hooks_worker.rb @@ -13,9 +13,9 @@ class BuildHooksWorker # rubocop:disable Scalability/IdempotentWorker # rubocop: disable CodeReuse/ActiveRecord def perform(build_id) - Ci::Build.includes({ runner: :tags }) - .find_by_id(build_id) - .try(:execute_hooks) + build = Ci::Build.find_by_id(build_id) + + build.execute_hooks if build end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/workers/ci/build_finished_worker.rb b/app/workers/ci/build_finished_worker.rb index 25c7637a79f146f8cc659e95259743d35c972705..36a50735fed2b2ab9933a4210663324ed236a176 100644 --- a/app/workers/ci/build_finished_worker.rb +++ b/app/workers/ci/build_finished_worker.rb @@ -36,8 +36,7 @@ def process_build(build) build.update_coverage Ci::BuildReportResultService.new.execute(build) - # We execute these async as these are independent operations. - BuildHooksWorker.perform_async(build) + build.feature_flagged_execute_hooks ChatNotificationWorker.perform_async(build.id) if build.pipeline.chat? build.track_deployment_usage build.track_verify_usage diff --git a/config/feature_flags/development/execute_build_hooks_inline.yml b/config/feature_flags/development/execute_build_hooks_inline.yml new file mode 100644 index 0000000000000000000000000000000000000000..0389fca3bb1b556a69e2a1ebf888fdaece9e7df0 --- /dev/null +++ b/config/feature_flags/development/execute_build_hooks_inline.yml @@ -0,0 +1,8 @@ +--- +name: execute_build_hooks_inline +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/93665 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/370387 +milestone: '15.3' +type: development +group: group::integrations +default_enabled: false diff --git a/ee/spec/workers/ee/ci/build_finished_worker_spec.rb b/ee/spec/workers/ee/ci/build_finished_worker_spec.rb index dffe4f85ca86fd2327b4c3631ea757244ec8a969..54f1a1afe2d905cab08ec9083a98e9a77a8b7111 100644 --- a/ee/spec/workers/ee/ci/build_finished_worker_spec.rb +++ b/ee/spec/workers/ee/ci/build_finished_worker_spec.rb @@ -4,7 +4,7 @@ RSpec.describe Ci::BuildFinishedWorker do let_it_be(:ci_runner) { create(:ci_runner) } - let_it_be(:build) { create(:ee_ci_build, :sast, :success, runner: ci_runner) } + let_it_be_with_reload(:build) { create(:ee_ci_build, :sast, :success, runner: ci_runner) } let_it_be(:project) { build.project } let_it_be(:namespace) { project.shared_runners_limit_namespace } @@ -34,13 +34,10 @@ def project_stats end context 'when exception is raised in `super`' do - before do - allow(::BuildHooksWorker) - .to receive(:perform_async) - .and_raise(ArgumentError) - end - it 'does not enqueue the worker in EE' do + allow(Ci::Build).to receive(:find_by_id).with(build.id).and_return(build) + allow(build).to receive(:execute_hooks).and_raise(ArgumentError) + expect { subject }.to raise_error(ArgumentError) expect(::Security::TrackSecureScansWorker).not_to receive(:perform_async) @@ -105,14 +102,6 @@ def project_stats project.add_reporter(user) end - shared_examples 'schedules processing of requirement reports' do - it do - expect(RequirementsManagement::ProcessRequirementsReportsWorker).to receive(:perform_async) - - subject - end - end - shared_examples 'does not schedule processing of requirement reports' do it do expect(RequirementsManagement::ProcessRequirementsReportsWorker).not_to receive(:perform_async) @@ -126,7 +115,11 @@ def project_stats stub_licensed_features(requirements: true) end - it_behaves_like 'schedules processing of requirement reports' + it 'schedules processing of requirement reports' do + expect(RequirementsManagement::ProcessRequirementsReportsWorker).to receive(:perform_async) + + subject + end context 'when user has insufficient permissions to create test reports' do before do diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 546cfd80fcf519f36c2965f121d1b3bfea0eb6d3..8b1d27687542d853a5270347a5beef40f21fa5c4 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -4,6 +4,8 @@ RSpec.describe Ci::Build do include Ci::TemplateHelpers + include AfterNextHelpers + let_it_be(:user) { create(:user) } let_it_be(:group, reload: true) { create(:group) } let_it_be(:project, reload: true) { create(:project, :repository, group: group) } @@ -60,11 +62,36 @@ describe 'callbacks' do context 'when running after_create callback' do - it 'triggers asynchronous build hooks worker' do - expect(BuildHooksWorker).to receive(:perform_async) + it 'executes hooks' do + expect_next(described_class).to receive(:execute_hooks) create(:ci_build) end + + context 'when the execute_build_hooks_inline flag is disabled' do + before do + stub_feature_flags(execute_build_hooks_inline: false) + end + + it 'uses the old job hooks worker' do + expect(::BuildHooksWorker).to receive(:perform_async).with(Ci::Build) + + create(:ci_build) + end + end + + context 'when the execute_build_hooks_inline flag is enabled for a project' do + before do + stub_feature_flags(execute_build_hooks_inline: project) + end + + it 'executes hooks inline' do + expect(::BuildHooksWorker).not_to receive(:perform_async) + expect_next(described_class).to receive(:execute_hooks) + + create(:ci_build, project: project) + end + end end end @@ -1663,20 +1690,18 @@ end it 'returns an expanded environment name with a list of variables' do - expect(build).to receive(:simple_variables).once.and_call_original - is_expected.to eq('review/host') end context 'when build metadata has already persisted the expanded environment name' do before do - build.metadata.expanded_environment_name = 'review/host' + build.metadata.expanded_environment_name = 'review/foo' end it 'returns a persisted expanded environment name without a list of variables' do expect(build).not_to receive(:simple_variables) - is_expected.to eq('review/host') + is_expected.to eq('review/foo') end end end @@ -3872,8 +3897,20 @@ build.enqueue end - it 'queues BuildHooksWorker' do - expect(BuildHooksWorker).to receive(:perform_async).with(build) + context 'when the execute_build_hooks_inline flag is disabled' do + before do + stub_feature_flags(execute_build_hooks_inline: false) + end + + it 'queues BuildHooksWorker' do + expect(BuildHooksWorker).to receive(:perform_async).with(build) + + build.enqueue + end + end + + it 'executes hooks' do + expect(build).to receive(:execute_hooks) build.enqueue end @@ -4624,6 +4661,7 @@ def run_job_without_exception end before do + allow(build).to receive(:execute_hooks) stub_artifacts_object_storage end @@ -5564,7 +5602,7 @@ def run_job_without_exception build.cancel_gracefully? end - let_it_be(:build) { create(:ci_build, pipeline: pipeline) } + let(:build) { create(:ci_build, pipeline: pipeline) } it 'cannot cancel gracefully' do expect(subject).to be false diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 2920278679ff0ec8f81595f6fe81af11963ae7d7..0d11586c487ed819ddd43a40a9672664db2dbac4 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -5889,7 +5889,7 @@ def has_external_wiki end describe '#has_active_hooks?' do - let_it_be(:project) { create(:project) } + let_it_be_with_refind(:project) { create(:project) } it { expect(project.has_active_hooks?).to be_falsey } diff --git a/spec/workers/build_hooks_worker_spec.rb b/spec/workers/build_hooks_worker_spec.rb index 426eb03638c22689a127e6442528cb908d63df81..80dc36d268faa79b1b5e9c41dd4071df7770b164 100644 --- a/spec/workers/build_hooks_worker_spec.rb +++ b/spec/workers/build_hooks_worker_spec.rb @@ -23,8 +23,8 @@ end end - describe '.perform_async' do - it 'sends a message to the application logger, before performing', :sidekiq_inline do + describe '.perform_async', :sidekiq_inline do + it 'sends a message to the application logger, before performing' do build = create(:ci_build) expect(Gitlab::AppLogger).to receive(:info).with( diff --git a/spec/workers/ci/build_finished_worker_spec.rb b/spec/workers/ci/build_finished_worker_spec.rb index 201182636e7ee06c2cd8ad7716927892e5438036..5ddaabc3938e2b45f1650a00e97706ac7667f0d8 100644 --- a/spec/workers/ci/build_finished_worker_spec.rb +++ b/spec/workers/ci/build_finished_worker_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Ci::BuildFinishedWorker do + include AfterNextHelpers + subject { described_class.new.perform(build.id) } describe '#perform' do @@ -16,17 +18,28 @@ it 'calculates coverage and calls hooks', :aggregate_failures do expect(build).to receive(:update_coverage).ordered - expect_next_instance_of(Ci::BuildReportResultService) do |build_report_result_service| - expect(build_report_result_service).to receive(:execute).with(build) - end + expect_next(Ci::BuildReportResultService).to receive(:execute).with(build) - expect(BuildHooksWorker).to receive(:perform_async) + expect(build).to receive(:execute_hooks) expect(ChatNotificationWorker).not_to receive(:perform_async) expect(Ci::ArchiveTraceWorker).to receive(:perform_in) subject end + context 'when the execute_build_hooks_inline feature flag is disabled' do + before do + stub_feature_flags(execute_build_hooks_inline: false) + end + + it 'uses the BuildHooksWorker' do + expect(build).not_to receive(:execute_hooks) + expect(BuildHooksWorker).to receive(:perform_async).with(build) + + subject + end + end + context 'when build is failed' do before do build.update!(status: :failed)