From a728acad8fdc60577f0fe4400b05e5eef93906a9 Mon Sep 17 00:00:00 2001 From: Alex Kalderimis <akalderimis@gitlab.com> Date: Sun, 14 Aug 2022 23:44:55 +0000 Subject: [PATCH] Make the build hooks worker idempotent Prior to this change, the payload was computed in the sidekiq worker which may have different data due to replication lag. This change computes the payload when we _enqueue_ the job, and skips the use of the build-hooks worker, which is retained for backwards compatibility, but is otherwise redundant. Added: Adds execute_build_hooks_inline feature-flag This adds a feature flag around the inline execution of job hooks. --- app/models/ci/build.rb | 16 +++++- app/models/project.rb | 8 ++- app/workers/build_hooks_worker.rb | 6 +- app/workers/ci/build_finished_worker.rb | 3 +- .../execute_build_hooks_inline.yml | 8 +++ .../ee/ci/build_finished_worker_spec.rb | 25 +++------ spec/models/ci/build_spec.rb | 56 ++++++++++++++++--- spec/models/project_spec.rb | 2 +- spec/workers/build_hooks_worker_spec.rb | 4 +- spec/workers/ci/build_finished_worker_spec.rb | 21 +++++-- 10 files changed, 108 insertions(+), 41 deletions(-) create mode 100644 config/feature_flags/development/execute_build_hooks_inline.yml diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 2b72a4c4c89c4..87adedf936e05 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 28dd51fc67d61..b8a2fb13f9aa2 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 5c08344bfe3ec..2c62aed72d6f3 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 25c7637a79f14..36a50735fed2b 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 0000000000000..0389fca3bb1b5 --- /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 dffe4f85ca86f..54f1a1afe2d90 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 546cfd80fcf51..8b1d27687542d 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 2920278679ff0..0d11586c487ed 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 426eb03638c22..80dc36d268faa 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 201182636e7ee..5ddaabc3938e2 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) -- GitLab