diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 24f3161bd35e8d9ce99a0cbbc6e6cd4c7ec47bc3..cdeccedc1471c50ad16baa2fdd47bd05546ab1f7 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -2,6 +2,7 @@ module Ci class Build < Ci::Processable + prepend Ci::BulkInsertableTags include Ci::Metadatable include Ci::Contextable include TokenAuthenticatable @@ -434,10 +435,6 @@ def runnable? true end - def save_tags - super unless Thread.current['ci_bulk_insert_tags'] - end - def archived? return true if degenerated? diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 61194c9b7d11c44fe2cf240b06a0c9179747e32d..e0180880760a4f7208b14d4ed12e2aa0c6797c8a 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -2,6 +2,7 @@ module Ci class Runner < Ci::ApplicationRecord + prepend Ci::BulkInsertableTags include Gitlab::SQL::Pattern include RedisCacheable include ChronicDurationAttribute diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index ac9d8c39bd2df8f5dd3957f5a811da681b6396ea..d08d303912d3d79ada646f3a10381075a767df35 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -220,10 +220,6 @@ def self.locking_enabled? false end - def self.bulk_insert_tags!(statuses) - Gitlab::Ci::Tags::BulkInsert.new(statuses).insert! - end - def locking_enabled? will_save_change_to_status? end diff --git a/app/models/concerns/ci/bulk_insertable_tags.rb b/app/models/concerns/ci/bulk_insertable_tags.rb new file mode 100644 index 0000000000000000000000000000000000000000..453b3b3fbc9b69beab7a5744812ab01224d65d39 --- /dev/null +++ b/app/models/concerns/ci/bulk_insertable_tags.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Ci + module BulkInsertableTags + extend ActiveSupport::Concern + + BULK_INSERT_TAG_THREAD_KEY = 'ci_bulk_insert_tags' + + class << self + def with_bulk_insert_tags + previous = Thread.current[BULK_INSERT_TAG_THREAD_KEY] + Thread.current[BULK_INSERT_TAG_THREAD_KEY] = true + yield + ensure + Thread.current[BULK_INSERT_TAG_THREAD_KEY] = previous + end + end + + # overrides save_tags from acts-as-taggable + def save_tags + super unless Thread.current[BULK_INSERT_TAG_THREAD_KEY] + end + end +end diff --git a/app/services/ci/runners/register_runner_service.rb b/app/services/ci/runners/register_runner_service.rb index 196d2de1a65df9abc7ccb79c39787085ca64be11..6588cd7e24810262e01c1f8baf0198753c640933 100644 --- a/app/services/ci/runners/register_runner_service.rb +++ b/app/services/ci/runners/register_runner_service.rb @@ -8,7 +8,19 @@ def execute(registration_token, attributes) return unless runner_type_attrs - ::Ci::Runner.create(attributes.merge(runner_type_attrs)) + runner = ::Ci::Runner.new(attributes.merge(runner_type_attrs)) + + Ci::BulkInsertableTags.with_bulk_insert_tags do + Ci::Runner.transaction do + if runner.save + Gitlab::Ci::Tags::BulkInsert.bulk_insert_tags!([runner]) + else + raise ActiveRecord::Rollback + end + end + end + + runner end private diff --git a/lib/gitlab/ci/pipeline/chain/create.rb b/lib/gitlab/ci/pipeline/chain/create.rb index 71dfc1a676cac623be70f57e075de3f9c52090b8..207b4b5ff8b0a1fddd8c059ab32e9d67d4534921 100644 --- a/lib/gitlab/ci/pipeline/chain/create.rb +++ b/lib/gitlab/ci/pipeline/chain/create.rb @@ -11,10 +11,10 @@ class Create < Chain::Base def perform! logger.instrument_with_sql(:pipeline_save) do BulkInsertableAssociations.with_bulk_insert do - with_bulk_insert_tags do + ::Ci::BulkInsertableTags.with_bulk_insert_tags do pipeline.transaction do pipeline.save! - CommitStatus.bulk_insert_tags!(statuses) + Gitlab::Ci::Tags::BulkInsert.bulk_insert_tags!(statuses) end end end @@ -29,14 +29,6 @@ def break? private - def with_bulk_insert_tags - previous = Thread.current['ci_bulk_insert_tags'] - Thread.current['ci_bulk_insert_tags'] = true - yield - ensure - Thread.current['ci_bulk_insert_tags'] = previous - end - def statuses strong_memoize(:statuses) do pipeline diff --git a/lib/gitlab/ci/tags/bulk_insert.rb b/lib/gitlab/ci/tags/bulk_insert.rb index 870bd0fc0a2aba24181dddb08ceecc0f745a77ec..2e56e47f5b8da3745fc8ed1c26c41bacf8e88352 100644 --- a/lib/gitlab/ci/tags/bulk_insert.rb +++ b/lib/gitlab/ci/tags/bulk_insert.rb @@ -9,33 +9,37 @@ class BulkInsert TAGGINGS_BATCH_SIZE = 1000 TAGS_BATCH_SIZE = 500 - def initialize(statuses) - @statuses = statuses + def self.bulk_insert_tags!(taggables) + Gitlab::Ci::Tags::BulkInsert.new(taggables).insert! + end + + def initialize(taggables) + @taggables = taggables end def insert! - return false if tag_list_by_status.empty? + return false if tag_list_by_taggable.empty? persist_build_tags! end private - attr_reader :statuses + attr_reader :taggables - def tag_list_by_status - strong_memoize(:tag_list_by_status) do - statuses.each.with_object({}) do |status, acc| - tag_list = status.tag_list + def tag_list_by_taggable + strong_memoize(:tag_list_by_taggable) do + taggables.each.with_object({}) do |taggable, acc| + tag_list = taggable.tag_list next unless tag_list - acc[status] = tag_list + acc[taggable] = tag_list end end end def persist_build_tags! - all_tags = tag_list_by_status.values.flatten.uniq.reject(&:blank?) + all_tags = tag_list_by_taggable.values.flatten.uniq.reject(&:blank?) tag_records_by_name = create_tags(all_tags).index_by(&:name) taggings = build_taggings_attributes(tag_records_by_name) @@ -65,24 +69,24 @@ def create_tags(tags) # rubocop: enable CodeReuse/ActiveRecord def build_taggings_attributes(tag_records_by_name) - taggings = statuses.flat_map do |status| - tag_list = tag_list_by_status[status] + taggings = taggables.flat_map do |taggable| + tag_list = tag_list_by_taggable[taggable] next unless tag_list tags = tag_records_by_name.values_at(*tag_list) - taggings_for(tags, status) + taggings_for(tags, taggable) end taggings.compact! taggings end - def taggings_for(tags, status) + def taggings_for(tags, taggable) tags.map do |tag| { tag_id: tag.id, - taggable_type: CommitStatus.name, - taggable_id: status.id, + taggable_type: taggable.class.base_class.name, + taggable_id: taggable.id, created_at: Time.current, context: 'tags' } diff --git a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb index 9057c4e99df2f948097df315d7abe2f64b762048..7b97dde3808a30732d25dfafed2c6686b1f4c32c 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb @@ -77,7 +77,7 @@ context 'without tags' do it 'extracts an empty tag list' do - expect(CommitStatus) + expect(Gitlab::Ci::Tags::BulkInsert) .to receive(:bulk_insert_tags!) .with([job]) .and_call_original @@ -95,7 +95,7 @@ end it 'bulk inserts tags' do - expect(CommitStatus) + expect(Gitlab::Ci::Tags::BulkInsert) .to receive(:bulk_insert_tags!) .with([job]) .and_call_original diff --git a/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb b/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb index 063376499e23d8f8fa1c027d2f7acb9533c08eef..72574d501763d2754c2f86695b56b639b11fa953 100644 --- a/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb +++ b/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb @@ -18,7 +18,7 @@ let(:error_message) do <<~MESSAGE A mechanism depending on internals of 'act-as-taggable-on` has been designed - to bulk insert tags for Ci::Build records. + to bulk insert tags for Ci::Build/Ci::Runner records. Please review the code carefully before updating the gem version https://gitlab.com/gitlab-org/gitlab/-/issues/350053 MESSAGE @@ -27,6 +27,21 @@ it { expect(ActsAsTaggableOn::VERSION).to eq(acceptable_version), error_message } end + describe '.bulk_insert_tags!' do + let(:inserter) { instance_double(described_class) } + + it 'delegates to bulk insert class' do + expect(Gitlab::Ci::Tags::BulkInsert) + .to receive(:new) + .with(statuses) + .and_return(inserter) + + expect(inserter).to receive(:insert!) + + described_class.bulk_insert_tags!(statuses) + end + end + describe '#insert!' do context 'without tags' do it { expect(service.insert!).to be_falsey } @@ -45,6 +60,17 @@ expect(other_job.reload.tag_list).to match_array(%w[tag2 tag3 tag4]) end + it 'persists taggings' do + service.insert! + + expect(job.taggings.size).to eq(2) + expect(other_job.taggings.size).to eq(3) + + expect(Ci::Build.tagged_with('tag1')).to include(job) + expect(Ci::Build.tagged_with('tag2')).to include(job, other_job) + expect(Ci::Build.tagged_with('tag3')).to include(other_job) + end + context 'when batching inserts for tags' do before do stub_const("#{described_class}::TAGS_BATCH_SIZE", 2) @@ -83,6 +109,15 @@ expect(job.reload.tag_list).to match_array(%w[tag1 tag2]) expect(other_job.reload.tag_list).to be_empty end + + it 'persists taggings' do + service.insert! + + expect(job.taggings.size).to eq(2) + + expect(Ci::Build.tagged_with('tag1')).to include(job) + expect(Ci::Build.tagged_with('tag2')).to include(job) + end end end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 6456ad3c1ece0ab91c6303693619d047243bca07..1ecf7e8b21646e23867bbe42acd6693a086484d1 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -2083,6 +2083,27 @@ end end + describe '#save_tags' do + let(:build) { create(:ci_build, tag_list: ['tag']) } + + it 'saves tags' do + build.save! + + expect(build.tags.count).to eq(1) + expect(build.tags.first.name).to eq('tag') + end + + context 'with BulkInsertableTags.with_bulk_insert_tags' do + it 'does not save_tags' do + Ci::BulkInsertableTags.with_bulk_insert_tags do + build.save! + end + + expect(build.tags).to be_empty + end + end + end + describe '#has_tags?' do context 'when build has tags' do subject { create(:ci_build, tag_list: ['tag']) } diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 74d8b012b29a89e6909edafaf79477bcbcf31d5c..c5cb67929e24ce6416ec8b24ad1a18bf911acc02 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -1193,6 +1193,40 @@ def does_db_update end end + describe '#save_tags' do + let(:runner) { build(:ci_runner, tag_list: ['tag']) } + + it 'saves tags' do + runner.save! + + expect(runner.tags.count).to eq(1) + expect(runner.tags.first.name).to eq('tag') + end + + context 'with BulkInsertableTags.with_bulk_insert_tags' do + it 'does not save_tags' do + Ci::BulkInsertableTags.with_bulk_insert_tags do + runner.save! + end + + expect(runner.tags).to be_empty + end + + context 'over TAG_LIST_MAX_LENGTH' do + let(:tag_list) { (1..described_class::TAG_LIST_MAX_LENGTH + 1).map { |i| "tag#{i}" } } + let(:runner) { build(:ci_runner, tag_list: tag_list) } + + it 'fails validation if over tag limit' do + Ci::BulkInsertableTags.with_bulk_insert_tags do + expect { runner.save! }.to raise_error(ActiveRecord::RecordInvalid) + end + + expect(runner.tags).to be_empty + end + end + end + end + describe '#has_tags?' do context 'when runner has tags' do subject { create(:ci_runner, tag_list: ['tag']) } diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index dbb15fad2469ad37790f327d5bf134fc42f1c626..9f58d3c1a80e9010dbd727d651c1e16655c74f52 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -984,22 +984,6 @@ def create_status(**opts) end end - describe '.bulk_insert_tags!' do - let(:statuses) { double('statuses') } - let(:inserter) { double('inserter') } - - it 'delegates to bulk insert class' do - expect(Gitlab::Ci::Tags::BulkInsert) - .to receive(:new) - .with(statuses) - .and_return(inserter) - - expect(inserter).to receive(:insert!) - - described_class.bulk_insert_tags!(statuses) - end - end - describe '#expire_etag_cache!' do it 'expires the etag cache' do expect_next_instance_of(Gitlab::EtagCaching::Store) do |etag_store| diff --git a/spec/models/concerns/ci/bulk_insertable_tags_spec.rb b/spec/models/concerns/ci/bulk_insertable_tags_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..23f0831403df81620f1296f4646c28c3808d8854 --- /dev/null +++ b/spec/models/concerns/ci/bulk_insertable_tags_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::BulkInsertableTags do + let(:taggable_class) do + Class.new do + prepend Ci::BulkInsertableTags + + attr_reader :tags_saved + + def save_tags + @tags_saved = true + end + end + end + + let(:record) { taggable_class.new } + + describe '.with_bulk_insert_tags' do + it 'changes the thread key to true' do + expect(Thread.current['ci_bulk_insert_tags']).to be_nil + + described_class.with_bulk_insert_tags do + expect(Thread.current['ci_bulk_insert_tags']).to eq(true) + end + + expect(Thread.current['ci_bulk_insert_tags']).to be_nil + end + end + + describe '#save_tags' do + it 'calls super' do + record.save_tags + + expect(record.tags_saved).to eq(true) + end + + it 'does not call super with BulkInsertableTags.with_bulk_insert_tags' do + described_class.with_bulk_insert_tags do + record.save_tags + end + + expect(record.tags_saved).to be_nil + end + + it 'isolates bulk insert behavior between threads' do + record2 = taggable_class.new + + t1 = Thread.new do + described_class.with_bulk_insert_tags do + record.save_tags + end + end + + t2 = Thread.new do + record2.save_tags + end + + [t1, t2].each(&:join) + + expect(record.tags_saved).to be_nil + expect(record2.tags_saved).to eq(true) + end + end +end diff --git a/spec/services/ci/runners/register_runner_service_spec.rb b/spec/services/ci/runners/register_runner_service_spec.rb index f43fd8230788d95803fc7aea8d7db963e8ddb038..03dcf851e5316d3c91eaba5f88f6e15e5828a55d 100644 --- a/spec/services/ci/runners/register_runner_service_spec.rb +++ b/spec/services/ci/runners/register_runner_service_spec.rb @@ -13,7 +13,7 @@ stub_application_setting(valid_runner_registrars: ApplicationSetting::VALID_RUNNER_REGISTRAR_TYPES) end - subject { described_class.new.execute(token, args) } + subject(:runner) { described_class.new.execute(token, args) } context 'when no token is provided' do let(:token) { '' } @@ -83,6 +83,9 @@ expect(subject.platform).to eq args[:platform] expect(subject.architecture).to eq args[:architecture] expect(subject.ip_address).to eq args[:ip_address] + + expect(Ci::Runner.tagged_with('tag1')).to include(subject) + expect(Ci::Runner.tagged_with('tag2')).to include(subject) end end @@ -230,5 +233,41 @@ end end end + + context 'when tags are provided' do + let(:token) { registration_token } + + let(:args) do + { tag_list: %w(tag1 tag2) } + end + + it 'creates runner with tags' do + expect(runner).to be_persisted + + expect(runner.tags).to contain_exactly( + an_object_having_attributes(name: 'tag1'), + an_object_having_attributes(name: 'tag2') + ) + end + + it 'creates tags in bulk' do + expect(Gitlab::Ci::Tags::BulkInsert).to receive(:bulk_insert_tags!).and_call_original + + expect(runner).to be_persisted + end + + context 'and tag list exceeds limit' do + let(:args) do + { tag_list: (1..Ci::Runner::TAG_LIST_MAX_LENGTH + 1).map { |i| "tag#{i}" } } + end + + it 'does not create any tags' do + expect(Gitlab::Ci::Tags::BulkInsert).not_to receive(:bulk_insert_tags!) + + expect(runner).not_to be_persisted + expect(runner.tags).to be_empty + end + end + end end end