# frozen_string_literal: true require 'rake_helper' RSpec.describe 'gitlab:db:lock_writes', :silence_stdout, :reestablished_active_record_base, :suppress_gitlab_schemas_validate_connection do before :all do Rake.application.rake_require 'active_record/railties/databases' Rake.application.rake_require 'tasks/seed_fu' Rake.application.rake_require 'tasks/gitlab/db/validate_config' Rake.application.rake_require 'tasks/gitlab/db/lock_writes' # empty task as env is already loaded Rake::Task.define_task :environment end let!(:project) { create(:project) } let!(:ci_build) { create(:ci_build) } let(:main_connection) { ApplicationRecord.connection } let(:ci_connection) { Ci::ApplicationRecord.connection } let(:detached_partition_table) { '_test_gitlab_main_part_20220101' } before do create_detached_partition_sql = <<~SQL CREATE TABLE IF NOT EXISTS gitlab_partitions_dynamic._test_gitlab_main_part_20220101 ( id bigserial primary key not null ) SQL main_connection.execute(create_detached_partition_sql) ci_connection.execute(create_detached_partition_sql) Gitlab::Database::SharedModel.using_connection(main_connection) do Postgresql::DetachedPartition.create!( table_name: detached_partition_table, drop_after: Time.current ) end allow(Gitlab::Database::GitlabSchema).to receive(:table_schema).and_call_original allow(Gitlab::Database::GitlabSchema).to receive(:table_schema) .with(detached_partition_table).and_return(:gitlab_main) end context 'single database' do before do skip_if_multiple_databases_are_setup end context 'when locking writes' do it 'does not add any triggers to the main schema tables' do expect do run_rake_task('gitlab:db:lock_writes') end.to change { number_of_triggers(main_connection) }.by(0) end it 'will be still able to modify tables that belong to the main two schemas' do run_rake_task('gitlab:db:lock_writes') expect do Project.last.touch Ci::Build.last.touch end.not_to raise_error end end end context 'multiple databases' do before do skip_if_multiple_databases_not_setup Gitlab::Database::SharedModel.using_connection(ci_connection) do Postgresql::DetachedPartition.create!( table_name: detached_partition_table, drop_after: Time.current ) end end context 'when locking writes' do it 'still allows writes on the tables with the correct connections' do Project.update_all(updated_at: Time.now) Ci::Build.update_all(updated_at: Time.now) end it 'still allows writing to gitlab_shared schema on any connection' do connections = [main_connection, ci_connection] connections.each do |connection| Gitlab::Database::SharedModel.using_connection(connection) do LooseForeignKeys::DeletedRecord.create!( fully_qualified_table_name: "public.projects", primary_key_value: 1, cleanup_attempts: 0 ) end end end it 'prevents writes on the main tables on the ci database' do run_rake_task('gitlab:db:lock_writes') expect do ci_connection.execute("delete from projects") end.to raise_error(ActiveRecord::StatementInvalid, /Table: "projects" is write protected/) end it 'prevents writes on the ci tables on the main database' do run_rake_task('gitlab:db:lock_writes') expect do main_connection.execute("delete from ci_builds") end.to raise_error(ActiveRecord::StatementInvalid, /Table: "ci_builds" is write protected/) end it 'prevents truncating a ci table on the main database' do run_rake_task('gitlab:db:lock_writes') expect do main_connection.execute("truncate ci_build_needs") end.to raise_error(ActiveRecord::StatementInvalid, /Table: "ci_build_needs" is write protected/) end it 'prevents writes to detached partitions' do run_rake_task('gitlab:db:lock_writes') expect do ci_connection.execute("INSERT INTO gitlab_partitions_dynamic.#{detached_partition_table} DEFAULT VALUES") end.to raise_error(ActiveRecord::StatementInvalid, /Table: "#{detached_partition_table}" is write protected/) end end context 'when running in dry_run mode' do before do stub_env('DRY_RUN', 'true') end it 'allows writes on the main tables on the ci database' do run_rake_task('gitlab:db:lock_writes') expect do ci_connection.execute("delete from projects") end.not_to raise_error end it 'allows writes on the ci tables on the main database' do run_rake_task('gitlab:db:lock_writes') expect do main_connection.execute("delete from ci_builds") end.not_to raise_error end end context 'multiple shared databases' do 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 run_rake_task('gitlab:db:lock_writes') expect do ApplicationRecord.connection.execute("delete from ci_builds") Ci::ApplicationRecord.connection.execute("delete from users") end.not_to raise_error end end context 'when unlocking writes' do before do run_rake_task('gitlab:db:lock_writes') end it 'allows writes again on the gitlab_ci tables on the main database' do run_rake_task('gitlab:db:unlock_writes') expect do main_connection.execute("delete from ci_builds") end.not_to raise_error end it 'allows writes again to detached partitions' do run_rake_task('gitlab:db:unlock_writes') expect do ci_connection.execute("INSERT INTO gitlab_partitions_dynamic._test_gitlab_main_part_20220101 DEFAULT VALUES") end.not_to raise_error end end end def number_of_triggers(connection) connection.select_value("SELECT count(*) FROM information_schema.triggers") end end