summaryrefslogtreecommitdiff
path: root/spec/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables_spec.rb
diff options
context:
space:
mode:
Diffstat (limited to 'spec/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables_spec.rb')
-rw-r--r--spec/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables_spec.rb334
1 files changed, 334 insertions, 0 deletions
diff --git a/spec/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables_spec.rb b/spec/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables_spec.rb
new file mode 100644
index 00000000000..9fd49b312eb
--- /dev/null
+++ b/spec/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables_spec.rb
@@ -0,0 +1,334 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::MigrationHelpers::AutomaticLockWritesOnTables,
+ :reestablished_active_record_base, query_analyzers: false do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:schema_class) { Class.new(Gitlab::Database::Migration[2.1]) }
+ let(:gitlab_main_table_name) { :_test_gitlab_main_table }
+ let(:gitlab_ci_table_name) { :_test_gitlab_ci_table }
+ let(:gitlab_geo_table_name) { :_test_gitlab_geo_table }
+ let(:gitlab_shared_table_name) { :_test_table }
+
+ before do
+ stub_feature_flags(automatic_lock_writes_on_table: true)
+ reconfigure_db_connection(model: ActiveRecord::Base, config_model: config_model)
+ end
+
+ shared_examples 'does not lock writes on table' do |config_model|
+ let(:config_model) { config_model }
+
+ it 'allows deleting records from the table' do
+ allow_next_instance_of(Gitlab::Database::LockWritesManager) do |instance|
+ expect(instance).not_to receive(:lock_writes)
+ end
+
+ run_migration
+
+ expect do
+ migration_class.connection.execute("DELETE FROM #{table_name}")
+ end.not_to raise_error
+ end
+ end
+
+ shared_examples 'locks writes on table' do |config_model|
+ let(:config_model) { config_model }
+
+ it 'errors on deleting' do
+ allow_next_instance_of(Gitlab::Database::LockWritesManager) do |instance|
+ expect(instance).to receive(:lock_writes).and_call_original
+ end
+
+ run_migration
+
+ expect do
+ migration_class.connection.execute("DELETE FROM #{table_name}")
+ end.to raise_error(ActiveRecord::StatementInvalid, /is write protected/)
+ end
+ end
+
+ context 'when executing create_table migrations' do
+ let(:create_gitlab_main_table_migration_class) { create_table_migration(gitlab_main_table_name) }
+ let(:create_gitlab_ci_table_migration_class) { create_table_migration(gitlab_ci_table_name) }
+ let(:create_gitlab_shared_table_migration_class) { create_table_migration(gitlab_shared_table_name) }
+
+ context 'when single database' do
+ let(:config_model) { Gitlab::Database.database_base_models[:main] }
+
+ before do
+ skip_if_multiple_databases_are_setup
+ end
+
+ it 'does not lock any newly created tables' do
+ allow_next_instance_of(Gitlab::Database::LockWritesManager) do |instance|
+ expect(instance).not_to receive(:lock_writes)
+ end
+
+ create_gitlab_main_table_migration_class.migrate(:up)
+ create_gitlab_ci_table_migration_class.migrate(:up)
+ create_gitlab_shared_table_migration_class.migrate(:up)
+
+ expect do
+ create_gitlab_main_table_migration_class.connection.execute("DELETE FROM #{gitlab_main_table_name}")
+ create_gitlab_ci_table_migration_class.connection.execute("DELETE FROM #{gitlab_ci_table_name}")
+ create_gitlab_shared_table_migration_class.connection.execute("DELETE FROM #{gitlab_shared_table_name}")
+ end.not_to raise_error
+ end
+ end
+
+ context 'when multiple databases' do
+ before do
+ skip_if_multiple_databases_not_setup
+ end
+
+ let(:skip_automatic_lock_on_writes) { false }
+ let(:migration_class) { create_table_migration(table_name, skip_automatic_lock_on_writes) }
+ let(:run_migration) { migration_class.migrate(:up) }
+
+ context 'for creating a gitlab_main table' do
+ let(:table_name) { gitlab_main_table_name }
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:main]
+ it_behaves_like 'locks writes on table', Gitlab::Database.database_base_models[:ci]
+
+ context 'when table listed as a deleted table' do
+ before do
+ stub_const("Gitlab::Database::GitlabSchema::DELETED_TABLES", { table_name.to_s => :gitlab_main })
+ end
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:ci]
+ end
+
+ context 'when the migration skips automatic locking of tables' do
+ let(:skip_automatic_lock_on_writes) { true }
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:ci]
+ end
+
+ context 'when the SKIP_AUTOMATIC_LOCK_ON_WRITES feature flag is set' do
+ before do
+ stub_env('SKIP_AUTOMATIC_LOCK_ON_WRITES' => 'true')
+ end
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:ci]
+ end
+
+ context 'when the automatic_lock_writes_on_table feature flag is disabled' do
+ before do
+ stub_feature_flags(automatic_lock_writes_on_table: false)
+ end
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:ci]
+ end
+ end
+
+ context 'for creating a gitlab_ci table' do
+ let(:table_name) { gitlab_ci_table_name }
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:ci]
+ it_behaves_like 'locks writes on table', Gitlab::Database.database_base_models[:main]
+
+ context 'when table listed as a deleted table' do
+ before do
+ stub_const("Gitlab::Database::GitlabSchema::DELETED_TABLES", { table_name.to_s => :gitlab_ci })
+ end
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:main]
+ end
+
+ context 'when the migration skips automatic locking of tables' do
+ let(:skip_automatic_lock_on_writes) { true }
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:main]
+ end
+
+ context 'when the SKIP_AUTOMATIC_LOCK_ON_WRITES feature flag is set' do
+ before do
+ stub_env('SKIP_AUTOMATIC_LOCK_ON_WRITES' => 'true')
+ end
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:main]
+ end
+
+ context 'when the automatic_lock_writes_on_table feature flag is disabled' do
+ before do
+ stub_feature_flags(automatic_lock_writes_on_table: false)
+ end
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:main]
+ end
+ end
+
+ context 'for creating gitlab_shared table' do
+ let(:table_name) { gitlab_shared_table_name }
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:main]
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:ci]
+ end
+
+ context 'for creating a gitlab_geo table' do
+ before do
+ skip unless geo_configured?
+ end
+
+ let(:table_name) { gitlab_geo_table_name }
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:geo]
+ end
+
+ context 'for creating an unknown gitlab_schema table' do
+ let(:table_name) { :foobar } # no gitlab_schema defined
+ let(:config_model) { Gitlab::Database.database_base_models[:main] }
+
+ it "raises an error about undefined gitlab_schema" do
+ expected_error_message = <<~ERROR
+ No gitlab_schema is defined for the table #{table_name}. Please consider
+ adding it to the database dictionary.
+ More info: https://docs.gitlab.com/ee/development/database/database_dictionary.html
+ ERROR
+
+ expect { run_migration }.to raise_error(expected_error_message)
+ end
+ end
+ end
+ end
+
+ context 'when renaming a table' do
+ before do
+ skip_if_multiple_databases_not_setup
+ create_table_migration(old_table_name).migrate(:up) # create the table first before renaming it
+ end
+
+ let(:migration_class) { rename_table_migration(old_table_name, table_name) }
+ let(:run_migration) { migration_class.migrate(:up) }
+
+ context 'when a gitlab_main table' do
+ let(:old_table_name) { gitlab_main_table_name }
+ let(:table_name) { :_test_gitlab_main_new_table }
+ let(:database_base_model) { Gitlab::Database.database_base_models[:main] }
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:main]
+ it_behaves_like 'locks writes on table', Gitlab::Database.database_base_models[:ci]
+ end
+
+ context 'when a gitlab_ci table' do
+ let(:old_table_name) { gitlab_ci_table_name }
+ let(:table_name) { :_test_gitlab_ci_new_table }
+ let(:database_base_model) { Gitlab::Database.database_base_models[:ci] }
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:ci]
+ it_behaves_like 'locks writes on table', Gitlab::Database.database_base_models[:main]
+ end
+ end
+
+ context 'when reversing drop_table migrations' do
+ let(:drop_gitlab_main_table_migration_class) { drop_table_migration(gitlab_main_table_name) }
+ let(:drop_gitlab_ci_table_migration_class) { drop_table_migration(gitlab_ci_table_name) }
+ let(:drop_gitlab_shared_table_migration_class) { drop_table_migration(gitlab_shared_table_name) }
+
+ context 'when single database' do
+ let(:config_model) { Gitlab::Database.database_base_models[:main] }
+
+ before do
+ skip_if_multiple_databases_are_setup
+ end
+
+ it 'does not lock any newly created tables' do
+ allow_next_instance_of(Gitlab::Database::LockWritesManager) do |instance|
+ expect(instance).not_to receive(:lock_writes)
+ end
+
+ drop_gitlab_main_table_migration_class.connection.execute("CREATE TABLE #{gitlab_main_table_name}()")
+ drop_gitlab_ci_table_migration_class.connection.execute("CREATE TABLE #{gitlab_ci_table_name}()")
+ drop_gitlab_shared_table_migration_class.connection.execute("CREATE TABLE #{gitlab_shared_table_name}()")
+
+ drop_gitlab_main_table_migration_class.migrate(:up)
+ drop_gitlab_ci_table_migration_class.migrate(:up)
+ drop_gitlab_shared_table_migration_class.migrate(:up)
+
+ drop_gitlab_main_table_migration_class.migrate(:down)
+ drop_gitlab_ci_table_migration_class.migrate(:down)
+ drop_gitlab_shared_table_migration_class.migrate(:down)
+
+ expect do
+ drop_gitlab_main_table_migration_class.connection.execute("DELETE FROM #{gitlab_main_table_name}")
+ drop_gitlab_ci_table_migration_class.connection.execute("DELETE FROM #{gitlab_ci_table_name}")
+ drop_gitlab_shared_table_migration_class.connection.execute("DELETE FROM #{gitlab_shared_table_name}")
+ end.not_to raise_error
+ end
+ end
+
+ context 'when multiple databases' do
+ before do
+ skip_if_multiple_databases_not_setup
+ migration_class.connection.execute("CREATE TABLE #{table_name}()")
+ migration_class.migrate(:up)
+ end
+
+ let(:migration_class) { drop_table_migration(table_name) }
+ let(:run_migration) { migration_class.migrate(:down) }
+
+ context 'for re-creating a gitlab_main table' do
+ let(:table_name) { gitlab_main_table_name }
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:main]
+ it_behaves_like 'locks writes on table', Gitlab::Database.database_base_models[:ci]
+ end
+
+ context 'for re-creating a gitlab_ci table' do
+ let(:table_name) { gitlab_ci_table_name }
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:ci]
+ it_behaves_like 'locks writes on table', Gitlab::Database.database_base_models[:main]
+ end
+
+ context 'for re-creating a gitlab_shared table' do
+ let(:table_name) { gitlab_shared_table_name }
+
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:main]
+ it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:ci]
+ end
+ end
+ end
+
+ def create_table_migration(table_name, skip_lock_on_writes = false)
+ migration_class = Class.new(schema_class) do
+ class << self; attr_accessor :table_name; end
+ def change
+ create_table self.class.table_name
+ end
+ end
+ migration_class.skip_automatic_lock_on_writes = skip_lock_on_writes
+ migration_class.tap { |klass| klass.table_name = table_name }
+ end
+
+ def rename_table_migration(old_table_name, new_table_name)
+ migration_class = Class.new(schema_class) do
+ class << self; attr_accessor :old_table_name, :new_table_name; end
+ def change
+ rename_table self.class.old_table_name, self.class.new_table_name
+ end
+ end
+
+ migration_class.tap do |klass|
+ klass.old_table_name = old_table_name
+ klass.new_table_name = new_table_name
+ end
+ end
+
+ def drop_table_migration(table_name)
+ migration_class = Class.new(schema_class) do
+ class << self; attr_accessor :table_name; end
+ def change
+ drop_table(self.class.table_name) {}
+ end
+ end
+ migration_class.tap { |klass| klass.table_name = table_name }
+ end
+
+ def geo_configured?
+ !!ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: 'geo')
+ end
+end