# frozen_string_literal: true require 'spec_helper' RSpec.describe Gitlab::Database::TablesLocker, :suppress_gitlab_schemas_validate_connection, :silence_stdout, feature_category: :cell do let(:default_lock_writes_manager) do instance_double( Gitlab::Database::LockWritesManager, lock_writes: { action: 'any action' }, unlock_writes: { action: 'unlocked' } ) end before do allow(Gitlab::Database::LockWritesManager).to receive(:new).with(any_args).and_return(default_lock_writes_manager) # Limiting the scope of the tests to a subset of the database tables allow(Gitlab::Database::GitlabSchema).to receive(:tables_to_schema).and_return({ 'application_setttings' => :gitlab_main_clusterwide, 'projects' => :gitlab_main, 'security_findings' => :gitlab_main, 'ci_builds' => :gitlab_ci, 'ci_jobs' => :gitlab_ci, 'loose_foreign_keys_deleted_records' => :gitlab_shared, 'ar_internal_metadata' => :gitlab_internal }) end before(:all) do create_partition_sql = <<~SQL CREATE TABLE IF NOT EXISTS #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.security_findings_test_partition PARTITION OF security_findings FOR VALUES IN (0) SQL ApplicationRecord.connection.execute(create_partition_sql) Ci::ApplicationRecord.connection.execute(create_partition_sql) create_detached_partition_sql = <<~SQL CREATE TABLE IF NOT EXISTS #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_gitlab_main_part_202201 ( id bigserial primary key not null ) SQL ApplicationRecord.connection.execute(create_detached_partition_sql) Ci::ApplicationRecord.connection.execute(create_detached_partition_sql) Gitlab::Database::SharedModel.using_connection(ApplicationRecord.connection) do Postgresql::DetachedPartition.create!( table_name: '_test_gitlab_main_part_20220101', drop_after: Time.current ) end Gitlab::Database::SharedModel.using_connection(Ci::ApplicationRecord.connection) do Postgresql::DetachedPartition.create!( table_name: '_test_gitlab_main_part_20220101', drop_after: Time.current ) end end shared_examples "lock tables" do |gitlab_schema, database_name| let(:connection) { Gitlab::Database.database_base_models[database_name].connection } let(:tables_to_lock) do Gitlab::Database::GitlabSchema .tables_to_schema.filter_map { |table_name, schema| table_name if schema == gitlab_schema } end it "locks table in schema #{gitlab_schema} and database #{database_name}" do expect(tables_to_lock).not_to be_empty tables_to_lock.each do |table_name| lock_writes_manager = instance_double(Gitlab::Database::LockWritesManager, lock_writes: nil) expect(Gitlab::Database::LockWritesManager).to receive(:new).with( table_name: table_name, connection: connection, database_name: database_name, with_retries: true, logger: anything, dry_run: anything ).once.and_return(lock_writes_manager) expect(lock_writes_manager).to receive(:lock_writes).once end subject end it 'returns list of actions' do expect(subject).to include({ action: 'any action' }) end end shared_examples "unlock tables" do |gitlab_schema, database_name| let(:connection) { Gitlab::Database.database_base_models[database_name].connection } let(:tables_to_unlock) do Gitlab::Database::GitlabSchema .tables_to_schema.filter_map { |table_name, schema| table_name if schema == gitlab_schema } end it "unlocks table in schema #{gitlab_schema} and database #{database_name}" do expect(tables_to_unlock).not_to be_empty tables_to_unlock.each do |table_name| lock_writes_manager = instance_double(Gitlab::Database::LockWritesManager, unlock_writes: nil) expect(Gitlab::Database::LockWritesManager).to receive(:new).with( table_name: table_name, connection: anything, database_name: database_name, with_retries: true, logger: anything, dry_run: anything ).once.and_return(lock_writes_manager) expect(lock_writes_manager).to receive(:unlock_writes) end subject end it 'returns list of actions' do expect(subject).to include({ action: 'unlocked' }) end end shared_examples "lock partitions" do |partition_identifier, database_name| let(:connection) { Gitlab::Database.database_base_models[database_name].connection } it 'locks the partition' do lock_writes_manager = instance_double(Gitlab::Database::LockWritesManager, lock_writes: nil) expect(Gitlab::Database::LockWritesManager).to receive(:new).with( table_name: partition_identifier, connection: connection, database_name: database_name, with_retries: true, logger: anything, dry_run: anything ).once.and_return(lock_writes_manager) expect(lock_writes_manager).to receive(:lock_writes) subject end end shared_examples "unlock partitions" do |partition_identifier, database_name| let(:connection) { Gitlab::Database.database_base_models[database_name].connection } it 'unlocks the partition' do lock_writes_manager = instance_double(Gitlab::Database::LockWritesManager, unlock_writes: nil) expect(Gitlab::Database::LockWritesManager).to receive(:new).with( table_name: partition_identifier, connection: connection, database_name: database_name, with_retries: true, logger: anything, dry_run: anything ).once.and_return(lock_writes_manager) expect(lock_writes_manager).to receive(:unlock_writes) subject end end context 'when running on single database' do before do skip_if_database_exists(:ci) end describe '#lock_writes' do subject { described_class.new.lock_writes } it 'does not lock any table' do expect(Gitlab::Database::LockWritesManager).to receive(:new) .with(any_args).and_return(default_lock_writes_manager) expect(default_lock_writes_manager).not_to receive(:lock_writes) subject end it_behaves_like 'unlock tables', :gitlab_main, 'main' it_behaves_like 'unlock tables', :gitlab_ci, 'main' it_behaves_like 'unlock tables', :gitlab_main_clusterwide, 'main' it_behaves_like 'unlock tables', :gitlab_shared, 'main' it_behaves_like 'unlock tables', :gitlab_internal, 'main' end describe '#unlock_writes' do subject { described_class.new.lock_writes } it 'does call Gitlab::Database::LockWritesManager.unlock_writes' do expect(Gitlab::Database::LockWritesManager).to receive(:new) .with(any_args).and_return(default_lock_writes_manager) expect(default_lock_writes_manager).to receive(:unlock_writes) expect(default_lock_writes_manager).not_to receive(:lock_writes) subject end end end context 'when running on multiple databases' do before do skip_if_shared_database(:ci) end describe '#lock_writes' do subject { described_class.new.lock_writes } it_behaves_like 'lock tables', :gitlab_ci, 'main' it_behaves_like 'lock tables', :gitlab_main, 'ci' it_behaves_like 'lock tables', :gitlab_main_clusterwide, 'ci' it_behaves_like 'unlock tables', :gitlab_main_clusterwide, 'main' it_behaves_like 'unlock tables', :gitlab_main, 'main' it_behaves_like 'unlock tables', :gitlab_ci, 'ci' it_behaves_like 'unlock tables', :gitlab_shared, 'main' it_behaves_like 'unlock tables', :gitlab_shared, 'ci' it_behaves_like 'unlock tables', :gitlab_internal, 'main' it_behaves_like 'unlock tables', :gitlab_internal, 'ci' gitlab_main_partition = "#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.security_findings_test_partition" it_behaves_like 'unlock partitions', gitlab_main_partition, 'main' it_behaves_like 'lock partitions', gitlab_main_partition, 'ci' gitlab_main_detached_partition = "#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_gitlab_main_part_20220101" it_behaves_like 'unlock partitions', gitlab_main_detached_partition, 'main' it_behaves_like 'lock partitions', gitlab_main_detached_partition, 'ci' end describe '#unlock_writes' do subject { described_class.new.unlock_writes } it_behaves_like "unlock tables", :gitlab_ci, 'main' it_behaves_like "unlock tables", :gitlab_main, 'ci' it_behaves_like "unlock tables", :gitlab_main, 'main' it_behaves_like "unlock tables", :gitlab_ci, 'ci' it_behaves_like "unlock tables", :gitlab_shared, 'main' it_behaves_like "unlock tables", :gitlab_shared, 'ci' it_behaves_like "unlock tables", :gitlab_internal, 'main' it_behaves_like "unlock tables", :gitlab_internal, 'ci' gitlab_main_partition = "#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.security_findings_test_partition" it_behaves_like 'unlock partitions', gitlab_main_partition, 'main' it_behaves_like 'unlock partitions', gitlab_main_partition, 'ci' gitlab_main_detached_partition = "#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_gitlab_main_part_20220101" it_behaves_like 'unlock partitions', gitlab_main_detached_partition, 'main' it_behaves_like 'unlock partitions', gitlab_main_detached_partition, 'ci' end context 'when running in dry_run mode' do subject { described_class.new(dry_run: true).lock_writes } it 'passes dry_run flag to LockWritesManager' do expect(Gitlab::Database::LockWritesManager).to receive(:new).with( table_name: 'security_findings', connection: anything, database_name: 'ci', with_retries: true, logger: anything, dry_run: true ).and_return(default_lock_writes_manager) expect(default_lock_writes_manager).to receive(:lock_writes) subject end end context 'when running on multiple shared databases' do subject { described_class.new.lock_writes } before do allow(::Gitlab::Database).to receive(:db_config_share_with).and_return(nil) ci_db_config = Ci::ApplicationRecord.connection_db_config allow(::Gitlab::Database).to receive(:db_config_share_with).with(ci_db_config).and_return('main') end it 'does not lock any tables if the ci database is shared with main database' do expect(Gitlab::Database::LockWritesManager).to receive(:new) .with(any_args).and_return(default_lock_writes_manager) expect(default_lock_writes_manager).not_to receive(:lock_writes) subject end end end context 'when geo database is configured' do let(:geo_table) do Gitlab::Database::GitlabSchema .tables_to_schema.filter_map { |table_name, schema| table_name if schema == :gitlab_geo } .first end subject { described_class.new.unlock_writes } before do skip "Geo database is not configured" unless Gitlab::Database.has_config?(:geo) end it 'does not lock table in geo database' do expect(Gitlab::Database::LockWritesManager).not_to receive(:new).with( table_name: geo_table, connection: anything, database_name: 'geo', with_retries: true, logger: anything, dry_run: anything ) subject end end end