diff --git a/lib/gitlab/database/background_migration/batched_background_migration_dictionary.rb b/lib/gitlab/database/background_migration/batched_background_migration_dictionary.rb deleted file mode 100644 index a6efa09afdaae674b952fe071b3685af44bfbd77..0000000000000000000000000000000000000000 --- a/lib/gitlab/database/background_migration/batched_background_migration_dictionary.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Database - module BackgroundMigration - class BatchedBackgroundMigrationDictionary - def self.entry(migration_job_name) - entries_by_migration_job_name[migration_job_name] - end - - private_class_method def self.entries_by_migration_job_name - @entries_by_migration_job_name ||= Dir.glob(dict_path).to_h do |file_path| - entry = Entry.new(file_path) - [entry.migration_job_name, entry] - end - end - - private_class_method def self.dict_path - Rails.root.join('db/docs/batched_background_migrations/*.yml') - end - - class Entry - def initialize(file_path) - @file_path = file_path - @data = YAML.load_file(file_path) - end - - def migration_job_name - data['migration_job_name'] - end - - def finalized_by - data['finalized_by'] - end - - private - - attr_reader :file_path, :data - end - end - end - end -end diff --git a/lib/gitlab/database/migrations/batched_background_migration_helpers.rb b/lib/gitlab/database/migrations/batched_background_migration_helpers.rb index 32b02422903834c37b64fab48f862c63e1d0c8d3..970e20279c964e0d94247654d581326022dcaa27 100644 --- a/lib/gitlab/database/migrations/batched_background_migration_helpers.rb +++ b/lib/gitlab/database/migrations/batched_background_migration_helpers.rb @@ -19,6 +19,14 @@ module BatchedBackgroundMigrationHelpers BATCH_MIN_VALUE = 1 # Default minimum value for batched migrations BATCH_MIN_DELAY = 2.minutes.freeze # Minimum delay between batched migrations + ENFORCE_EARLY_FINALIZATION_FROM_VERSION = '20240905124117' + EARLY_FINALIZATION_ERROR = <<-MESSAGE.squeeze(' ').strip + Batched migration should be finalized only after at-least one required stop from queuing it. + This is to ensure that we are not breaking the upgrades for self-managed instances. + + For more info visit: https://docs.gitlab.com/ee/development/database/batched_background_migrations.html#finalize-a-batched-background-migration + MESSAGE + # Creates a batched background migration for the given table. A batched migration runs one job # at a time, computing the bounds of the next batch based on the current migration settings and the previous # batch bounds. Each job's execution status is tracked in the database as the migration runs. The given job @@ -188,7 +196,14 @@ def gitlab_schema_from_context end end - def ensure_batched_background_migration_is_finished(job_class_name:, table_name:, column_name:, job_arguments:, finalize: true) + def ensure_batched_background_migration_is_finished( + job_class_name:, + table_name:, + column_name:, + job_arguments:, + finalize: true, + skip_early_finalization_validation: false + ) Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas.require_dml_mode! if transaction_open? @@ -216,6 +231,10 @@ def ensure_batched_background_migration_is_finished(job_class_name:, table_name: return Gitlab::AppLogger.warn "Could not find batched background migration for the given configuration: #{configuration}" if migration.nil? + if migration.respond_to?(:queued_migration_version) && !skip_early_finalization_validation + prevent_early_finalization!(migration.queued_migration_version, version) + end + return if migration.finalized? if migration.finished? @@ -269,6 +288,21 @@ def assign_attributes_safely(migration, max_batch_size, batch_table_name, gitlab end # rubocop:enable GitlabSecurity/PublicSend end + + def prevent_early_finalization!(queued_migration_version, version) + return if version.to_s <= ENFORCE_EARLY_FINALIZATION_FROM_VERSION || queued_migration_version.blank? + + queued_migration_milestone = Gitlab::Utils::BatchedBackgroundMigrationsDictionary + .new(queued_migration_version) + .milestone + + return unless queued_migration_milestone.present? + + queued_migration_milestone = Gitlab::VersionInfo.parse_from_milestone(queued_migration_milestone) + last_required_stop = Gitlab::Database.upgrade_path.last_required_stop + + raise EARLY_FINALIZATION_ERROR unless queued_migration_milestone <= last_required_stop + end end end end diff --git a/lib/gitlab/utils/batched_background_migrations_dictionary.rb b/lib/gitlab/utils/batched_background_migrations_dictionary.rb new file mode 100644 index 0000000000000000000000000000000000000000..8dd903e79af9e49b33aade8bd5ffc45adc686952 --- /dev/null +++ b/lib/gitlab/utils/batched_background_migrations_dictionary.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module Gitlab + module Utils + class BatchedBackgroundMigrationsDictionary + DICTIONARY_BASE_DIR = 'db/docs/batched_background_migrations' + + attr_reader :queued_migration_version + + class << self + def entries + return @entries if @entries.present? && !Rails.env.test? + + @entries = Dir.glob("*.yml", base: DICTIONARY_BASE_DIR).each_with_object({}) do |file_name, data| + dictionary = YAML.load_file(File.join(DICTIONARY_BASE_DIR, file_name)) + + next unless dictionary['queued_migration_version'].present? + + data[dictionary['queued_migration_version'].to_s] = { + migration_job_name: dictionary['migration_job_name'], + introduced_by_url: dictionary['introduced_by_url'], + finalized_by: dictionary['finalized_by'].to_s, + milestone: dictionary['milestone'] + } + + data[dictionary['migration_job_name']] = data[dictionary['queued_migration_version'].to_s].merge( + queued_migration_version: dictionary['queued_migration_version'] + ) + end + end + + def entry(migration_job_name) + return unless entries&.dig(migration_job_name) + + new(entries[migration_job_name][:queued_migration_version]) + end + + # Used by BackgroundMigration/DictionaryFile cop to invalidate its cache + # if the contents of `db/docs/batched_background_migrations` changes. + def checksum(skip_memoization: false) + return @checksum if @checksum.present? && !skip_memoization + + @checksum = Digest::SHA256.hexdigest(entries.to_s) + end + end + + def initialize(queued_migration_version) + @queued_migration_version = queued_migration_version + end + + def finalized_by + entry&.dig(:finalized_by) + end + + def introduced_by_url + entry&.dig(:introduced_by_url) + end + + def milestone + entry&.dig(:milestone) + end + + def migration_job_name + entry&.dig(:migration_job_name) + end + + private + + def entry + @entry ||= self.class.entries[queued_migration_version.to_s] + end + end + end +end diff --git a/rubocop/batched_background_migrations_dictionary.rb b/rubocop/batched_background_migrations_dictionary.rb deleted file mode 100644 index 4778a382112c0e6aef53ce21bc933076ca482fcd..0000000000000000000000000000000000000000 --- a/rubocop/batched_background_migrations_dictionary.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -module RuboCop - class BatchedBackgroundMigrationsDictionary - DICTIONARY_BASE_DIR = 'db/docs/batched_background_migrations' - - attr_reader :queued_migration_version - - class << self - def dictionary_data - @dictionary_data ||= Dir.glob("*.yml", base: DICTIONARY_BASE_DIR).each_with_object({}) do |file_name, data| - dictionary = YAML.load_file(File.join(DICTIONARY_BASE_DIR, file_name)) - - next unless dictionary['queued_migration_version'].present? - - data[dictionary['queued_migration_version'].to_s] = { - introduced_by_url: dictionary['introduced_by_url'], - finalized_by: dictionary['finalized_by'].to_s, - milestone: dictionary['milestone'] - } - end - end - - def checksum - @checksum ||= Digest::SHA256.hexdigest(dictionary_data.to_s) - end - end - - def initialize(queued_migration_version) - @queued_migration_version = queued_migration_version - end - - def finalized_by - dictionary_data&.dig(:finalized_by) - end - - def introduced_by_url - dictionary_data&.dig(:introduced_by_url) - end - - def milestone - dictionary_data&.dig(:milestone) - end - - private - - def dictionary_data - @dictionary_data ||= self.class.dictionary_data[queued_migration_version.to_s] - end - end -end diff --git a/rubocop/cop/background_migration/dictionary_file.rb b/rubocop/cop/background_migration/dictionary_file.rb index d0c02c7c488275fe3cf7775c905462e54a1661f5..a7b4726e64357be7e174704f86b24b94b29bdb0e 100644 --- a/rubocop/cop/background_migration/dictionary_file.rb +++ b/rubocop/cop/background_migration/dictionary_file.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require_relative '../../migration_helpers' -require_relative '../../batched_background_migrations_dictionary' +require_relative '../../../lib/gitlab/utils/batched_background_migrations_dictionary' URL_PATTERN = %r{\Ahttps://gitlab\.com/gitlab-org/gitlab/-/merge_requests/\d+\z} @@ -52,7 +52,7 @@ def on_class(node) end def external_dependency_checksum - RuboCop::BatchedBackgroundMigrationsDictionary.checksum + ::Gitlab::Utils::BatchedBackgroundMigrationsDictionary.checksum end private @@ -78,7 +78,7 @@ def validate_dictionary_file(migration_name, node) return [:missing_dictionary, { file_name: dictionary_file_path(migration_name) }] end - bbm_dictionary = RuboCop::BatchedBackgroundMigrationsDictionary.new(version(node)) + bbm_dictionary = ::Gitlab::Utils::BatchedBackgroundMigrationsDictionary.new(version(node)) return [:missing_key, { key: :milestone }] unless bbm_dictionary.milestone.present? diff --git a/rubocop/cop/migration/unfinished_dependencies.rb b/rubocop/cop/migration/unfinished_dependencies.rb index 56ba7d405c599a3fefd442417ebf9e67460ffdde..1dda5dc507c96d86679364e9a5c7520cd80af430 100644 --- a/rubocop/cop/migration/unfinished_dependencies.rb +++ b/rubocop/cop/migration/unfinished_dependencies.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require_relative '../../migration_helpers' -require_relative '../../batched_background_migrations_dictionary' +require_relative '../../../lib/gitlab/utils/batched_background_migrations_dictionary' module RuboCop module Cop @@ -43,7 +43,9 @@ def on_casgn(node) private def fetch_finalized_by(queued_migration_version) - BatchedBackgroundMigrationsDictionary.new(queued_migration_version).finalized_by + ::Gitlab::Utils::BatchedBackgroundMigrationsDictionary + .new(queued_migration_version) + .finalized_by end end end diff --git a/spec/lib/gitlab/database/background_migration/batched_background_migration_dictionary_spec.rb b/spec/lib/gitlab/database/background_migration/batched_background_migration_dictionary_spec.rb deleted file mode 100644 index b3aa0c194d226f6ee4e62c025d93d1362a45fe53..0000000000000000000000000000000000000000 --- a/spec/lib/gitlab/database/background_migration/batched_background_migration_dictionary_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ::Gitlab::Database::BackgroundMigration::BatchedBackgroundMigrationDictionary, feature_category: :database do - describe '.entry' do - it 'returns a single dictionary entry for the given migration job' do - entry = described_class.entry('MigrateHumanUserType') - expect(entry.migration_job_name).to eq('MigrateHumanUserType') - expect(entry.finalized_by).to eq(20230523101514) - end - end -end diff --git a/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb b/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb index 404abbe90e10e2e97c78ef5c2e2bc20d18f51c3c..4cc92283430366690d46f71c270593737c651766 100644 --- a/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb @@ -443,6 +443,18 @@ def self.name end end + shared_examples 'invalid early finalization' do + it 'throws an early finalization error' do + expect { ensure_batched_background_migration_is_finished }.to raise_error(described_class::EARLY_FINALIZATION_ERROR) + end + end + + shared_examples 'valid finalization' do + it 'does not throw any error' do + expect { ensure_batched_background_migration_is_finished }.not_to raise_error + end + end + describe '#ensure_batched_background_migration_is_finished' do let(:job_class_name) { 'CopyColumnUsingBackgroundMigrationJob' } let(:table_name) { '_test_table' } @@ -461,12 +473,14 @@ def self.name let(:migration_attributes) do configuration.merge( - gitlab_schema: gitlab_schema + gitlab_schema: gitlab_schema, + queued_migration_version: Time.now.utc.strftime("%Y%m%d%H%M%S") ) end before do allow(migration).to receive(:transaction_open?).and_return(false) + allow(migration).to receive(:version).and_return('20240905124118') end subject(:ensure_batched_background_migration_is_finished) { migration.ensure_batched_background_migration_is_finished(**configuration) } @@ -588,5 +602,79 @@ def self.name expect { migration.ensure_batched_background_migration_is_finished(**configuration.merge(finalize: false)) }.to raise_error(RuntimeError) end end + + context 'with finalized migration' do + let(:migration_attributes) do + configuration + .except(:skip_early_finalization_validation) + .merge(queued_migration_version: '20240905124118') + end + + before do + allow(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_dml_mode!) + + create(:batched_background_migration, :finalized, migration_attributes) + end + + context 'when the migration does not have queued_migration_version attr' do + let(:migration_attributes) { configuration.merge(queued_migration_version: nil) } + + it_behaves_like 'valid finalization' + end + + context 'when the migration version is before ENFORCE_EARLY_FINALIZATION_FROM_VERSION' do + let(:migration_attributes) { configuration.merge(queued_migration_version: '20240905124116') } + + it_behaves_like 'valid finalization' + end + + context 'when the migration is queued after the last required stop' do + before do + stub_bbm_stops('16.11', '17.2') + end + + it_behaves_like 'invalid early finalization' + + context 'with skip_early_finalization_validation enabled' do + let(:configuration) do + { + job_class_name: job_class_name, + table_name: table_name, + column_name: column_name, + job_arguments: job_arguments, + skip_early_finalization_validation: true + } + end + + it_behaves_like 'valid finalization' + end + end + + context 'when the migration is queued on the last required stop' do + before do + stub_bbm_stops('16.11', '16.11') + end + + it_behaves_like 'valid finalization' + end + + context 'when the migration is queued before the last required stop' do + before do + stub_bbm_stops('16.11', '16.10') + end + + it_behaves_like 'valid finalization' + end + end + end + + def stub_bbm_stops(last_required_stop, queued_milestone) + allow_next_instance_of(Gitlab::Utils::BatchedBackgroundMigrationsDictionary) do |dict| + allow(dict).to receive(:milestone).and_return(queued_milestone) + end + + allow_next_instance_of(Gitlab::Utils::UpgradePath) do |ugrade_path| + allow(ugrade_path).to receive(:last_required_stop).and_return(Gitlab::VersionInfo.parse_from_milestone(last_required_stop)) + end end end diff --git a/spec/lib/gitlab/utils/batched_background_migrations_dictionary_spec.rb b/spec/lib/gitlab/utils/batched_background_migrations_dictionary_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..6161b6bcc83c10a9d1c653a40a2728df6ce9d989 --- /dev/null +++ b/spec/lib/gitlab/utils/batched_background_migrations_dictionary_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Utils::BatchedBackgroundMigrationsDictionary, feature_category: :database do + let(:bbm_dictionary_file_name) { "#{described_class::DICTIONARY_BASE_DIR}/test_migration.yml" } + let(:migration_version) { 20230307160250 } + let(:finalized_by) { '20230307160255' } + let(:introduced_by_url) { 'https://test_url' } + let(:milestone) { '16.5' } + let(:migration_job_name) { 'TestMigration' } + + let(:bbm_dictionary_data) do + { + migration_job_name: migration_job_name, + feature_category: :database, + introduced_by_url: introduced_by_url, + milestone: milestone, + queued_migration_version: migration_version, + finalized_by: finalized_by + } + end + + before do + File.open(bbm_dictionary_file_name, 'w') do |file| + file.write(bbm_dictionary_data.stringify_keys.to_yaml) + end + end + + after do + FileUtils.rm(bbm_dictionary_file_name) + end + + subject(:batched_background_migration) { described_class.new(migration_version) } + + describe '.entry' do + it 'returns a single dictionary entry for the given migration job' do + entry = described_class.entry('TestMigration') + expect(entry.migration_job_name).to eq('TestMigration') + expect(entry.finalized_by.to_s).to eq(finalized_by) + end + end + + shared_examples 'safely returns bbm attribute' do |attribute| + it 'returns the attr of the bbm' do + expect(batched_background_migration.public_send(attribute)).to eq(public_send(attribute)) + end + + it 'returns nothing for non-existing bbm dictionary' do + expect(described_class.new('random').public_send(attribute)).to be_nil + end + end + + describe '#introduced_by_url' do + it_behaves_like 'safely returns bbm attribute', :introduced_by_url + end + + describe '#milestone' do + it_behaves_like 'safely returns bbm attribute', :milestone + end + + describe '#migration_job_name' do + it_behaves_like 'safely returns bbm attribute', :migration_job_name + end + + describe '.checksum' do + let(:entries) { { c: "d", a: "b" } } + + it 'returns a checksum of the entries' do + allow(described_class).to receive(:entries).and_return(entries) + + expect(described_class.checksum(skip_memoization: true)).to eq(Digest::SHA256.hexdigest(entries.to_s)) + end + end +end diff --git a/spec/rubocop/batched_background_migrations_dictionary_spec.rb b/spec/rubocop/batched_background_migrations_dictionary_spec.rb deleted file mode 100644 index c6bb9d45fee754152556b087aab96f0efe1c6279..0000000000000000000000000000000000000000 --- a/spec/rubocop/batched_background_migrations_dictionary_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -# frozen_string_literal: true - -require 'rubocop_spec_helper' - -require_relative '../../rubocop/batched_background_migrations_dictionary' - -RSpec.describe RuboCop::BatchedBackgroundMigrationsDictionary, feature_category: :database do - let(:bbm_dictionary_file_name) { "#{described_class::DICTIONARY_BASE_DIR}/test_migration.yml" } - let(:migration_version) { 20230307160250 } - let(:finalized_by_version) { 20230307160255 } - let(:introduced_by_url) { 'https://test_url' } - - let(:bbm_dictionary_data) do - { - migration_job_name: 'TestMigration', - feature_category: :database, - introduced_by_url: introduced_by_url, - milestone: 16.5, - queued_migration_version: migration_version, - finalized_by: finalized_by_version - } - end - - before do - File.open(bbm_dictionary_file_name, 'w') do |file| - file.write(bbm_dictionary_data.stringify_keys.to_yaml) - end - end - - after do - FileUtils.rm(bbm_dictionary_file_name) - end - - subject(:batched_background_migration) { described_class.new(migration_version) } - - describe '#finalized_by' do - it 'returns the finalized_by version of the bbm with given version', - quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/456913' do - expect(batched_background_migration.finalized_by).to eq(finalized_by_version.to_s) - end - - it 'returns nothing for non-existing bbm dictionary' do - expect(described_class.new('random').finalized_by).to be_nil - end - end - - describe '#introduced_by_url' do - it 'returns the introduced_by_url of the bbm with given version', - quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/456912' do - expect(batched_background_migration.introduced_by_url).to eq(introduced_by_url) - end - - it 'returns nothing for non-existing bbm dictionary' do - expect(described_class.new('random').introduced_by_url).to be_nil - end - end - - describe '.checksum' do - let(:dictionary_data) { { c: "d", a: "b" } } - - it 'returns a checksum of the dictionary_data' do - allow(described_class).to receive(:dictionary_data).and_return(dictionary_data) - - expect(described_class.checksum).to eq(Digest::SHA256.hexdigest(dictionary_data.to_s)) - end - end -end diff --git a/spec/rubocop/cop/background_migration/dictionary_file_spec.rb b/spec/rubocop/cop/background_migration/dictionary_file_spec.rb index 38e6aaf619174e788d99937237a817eec63b7280..3c061136e17158ffa46f5d59d3bdb3ba9b013fdf 100644 --- a/spec/rubocop/cop/background_migration/dictionary_file_spec.rb +++ b/spec/rubocop/cop/background_migration/dictionary_file_spec.rb @@ -140,12 +140,13 @@ def up before do allow(File).to receive(:exist?).and_call_original allow(File).to receive(:exist?).with(dictionary_file_path).and_return(true) - allow(::RuboCop::BatchedBackgroundMigrationsDictionary).to receive(:dictionary_data).and_return({ - '20231118100907' => { - introduced_by_url: introduced_by_url, - milestone: milestone - } - }) + allow(Gitlab::Utils::BatchedBackgroundMigrationsDictionary) + .to receive(:entries).and_return({ + '20231118100907' => { + introduced_by_url: introduced_by_url, + milestone: milestone + } + }) end context 'without introduced_by_url' do @@ -221,8 +222,9 @@ def up end describe '#external_dependency_checksum' do - it 'uses the RuboCop::BatchedBackgroundMigrationsDictionary.checksum' do - allow(RuboCop::BatchedBackgroundMigrationsDictionary).to receive(:checksum).and_return('aaaaa') + it 'uses the Utils::BatchedBackgroundMigrationsDictionary.checksum' do + allow(Gitlab::Utils::BatchedBackgroundMigrationsDictionary) + .to receive(:checksum).and_return('aaaaa') expect(cop.external_dependency_checksum).to eq('aaaaa') end diff --git a/spec/rubocop/cop/migration/unfinished_dependencies_spec.rb b/spec/rubocop/cop/migration/unfinished_dependencies_spec.rb index f2e963ad3222d254fef46973ea039cdd0a746d08..05ac56105f69f8da7d469b5da39919f8ce96de61 100644 --- a/spec/rubocop/cop/migration/unfinished_dependencies_spec.rb +++ b/spec/rubocop/cop/migration/unfinished_dependencies_spec.rb @@ -99,7 +99,7 @@ def perform; end context 'with properly finalized dependent background migrations' do before do - allow_next_instance_of(RuboCop::BatchedBackgroundMigrationsDictionary) do |bbms| + allow_next_instance_of(Gitlab::Utils::BatchedBackgroundMigrationsDictionary) do |bbms| allow(bbms).to receive(:finalized_by).and_return(version - 5) end end diff --git a/spec/support/helpers/migrations_helpers.rb b/spec/support/helpers/migrations_helpers.rb index 97869fc12fc660ee9673a2c9b81b0a273a4863cb..1140daa2acda3280273ef36271d975b8074ca081 100644 --- a/spec/support/helpers/migrations_helpers.rb +++ b/spec/support/helpers/migrations_helpers.rb @@ -141,10 +141,10 @@ def previous_migration(steps_back = 2) end def finalized_by_version - finalized_by = ::Gitlab::Database::BackgroundMigration::BatchedBackgroundMigrationDictionary + finalized_by = ::Gitlab::Utils::BatchedBackgroundMigrationsDictionary .entry(described_class.to_s.demodulize)&.finalized_by - finalized_by.to_i if finalized_by + finalized_by.to_i if finalized_by.present? end def migration_schema_version diff --git a/spec/support_specs/helpers/migrations_helpers_spec.rb b/spec/support_specs/helpers/migrations_helpers_spec.rb index 6587234a90589630795b3ff215462d796fdc6abe..216a3fc373f69aba1b1b2b1a8c6611acdf8312d0 100644 --- a/spec/support_specs/helpers/migrations_helpers_spec.rb +++ b/spec/support_specs/helpers/migrations_helpers_spec.rb @@ -113,7 +113,7 @@ before do allow(helper).to receive(:described_class) - allow(::Gitlab::Database::BackgroundMigration::BatchedBackgroundMigrationDictionary).to( + allow(::Gitlab::Utils::BatchedBackgroundMigrationsDictionary).to( receive(:entry).and_return(dictionary_entry) ) end @@ -125,7 +125,7 @@ context 'when finalized_by is a string' do let(:dictionary_entry) do instance_double( - ::Gitlab::Database::BackgroundMigration::BatchedBackgroundMigrationDictionary::Entry, + ::Gitlab::Utils::BatchedBackgroundMigrationsDictionary, finalized_by: '20240104155616' ) end