Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/tasks/gitlab/db')
-rw-r--r--spec/tasks/gitlab/db/decomposition/rollback/bump_ci_sequences_rake_spec.rb103
-rw-r--r--spec/tasks/gitlab/db/lock_writes_rake_spec.rb177
-rw-r--r--spec/tasks/gitlab/db/validate_config_rake_spec.rb41
3 files changed, 321 insertions, 0 deletions
diff --git a/spec/tasks/gitlab/db/decomposition/rollback/bump_ci_sequences_rake_spec.rb b/spec/tasks/gitlab/db/decomposition/rollback/bump_ci_sequences_rake_spec.rb
new file mode 100644
index 00000000000..29b80176ef8
--- /dev/null
+++ b/spec/tasks/gitlab/db/decomposition/rollback/bump_ci_sequences_rake_spec.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+require 'rake_helper'
+
+RSpec.describe 'gitlab:db:decomposition:rollback:bump_ci_sequences', :silence_stdout do
+ before :all do
+ Rake.application.rake_require 'tasks/gitlab/db/decomposition/rollback/bump_ci_sequences'
+
+ # empty task as env is already loaded
+ Rake::Task.define_task :environment
+ end
+
+ let(:expected_error_message) do
+ <<-EOS.strip_heredoc
+ Please specify a positive integer `increase_by` value
+ Example: rake gitlab:db:decomposition:rollback:bump_ci_sequences[100000]
+ EOS
+ end
+
+ let(:main_sequence_name) { 'issues_id_seq' }
+ let(:ci_sequence_name) { 'ci_build_needs_id_seq' }
+
+ # This is just to make sure that all of the sequences start with `is_called=True`
+ # which means that the next call to nextval() is going to increment the sequence.
+ # To give predictable test results.
+ before do
+ ApplicationRecord.connection.select_value("select nextval($1)", nil, [ci_sequence_name])
+ end
+
+ context 'when passing wrong argument' do
+ it 'will print an error message and exit when passing no argument' do
+ expect do
+ run_rake_task('gitlab:db:decomposition:rollback:bump_ci_sequences')
+ end.to raise_error(SystemExit) { |error| expect(error.status).to eq(1) }
+ .and output(expected_error_message).to_stdout
+ end
+
+ it 'will print an error message and exit when passing a non positive integer value' do
+ expect do
+ run_rake_task('gitlab:db:decomposition:rollback:bump_ci_sequences', '-5')
+ end.to raise_error(SystemExit) { |error| expect(error.status).to eq(1) }
+ .and output(expected_error_message).to_stdout
+ end
+ end
+
+ context 'when bumping the ci sequences' do
+ it 'changes ci sequences by the passed argument `increase_by` value on the main database' do
+ expect do
+ run_rake_task('gitlab:db:decomposition:rollback:bump_ci_sequences', '15')
+ end.to change {
+ last_value_of_sequence(ApplicationRecord.connection, ci_sequence_name)
+ }.by(16) # the +1 is because the sequence has is_called = true
+ end
+
+ it 'will still increase the value of sequences that have is_called = False' do
+ # see `is_called`: https://www.postgresql.org/docs/12/functions-sequence.html
+ # choosing a new arbitrary value for the sequence
+ new_value = last_value_of_sequence(ApplicationRecord.connection, ci_sequence_name) + 1000
+ ApplicationRecord.connection.select_value("select setval($1, $2, false)", nil, [ci_sequence_name, new_value])
+ expect do
+ run_rake_task('gitlab:db:decomposition:rollback:bump_ci_sequences', '15')
+ end.to change {
+ last_value_of_sequence(ApplicationRecord.connection, ci_sequence_name)
+ }.by(15)
+ end
+
+ it 'resets the INCREMENT value of the sequences back to 1 for the following calls to nextval()' do
+ run_rake_task('gitlab:db:decomposition:rollback:bump_ci_sequences', '15')
+ value_1 = ApplicationRecord.connection.select_value("select nextval($1)", nil, [ci_sequence_name])
+ value_2 = ApplicationRecord.connection.select_value("select nextval($1)", nil, [ci_sequence_name])
+ expect(value_2 - value_1).to eq(1)
+ end
+
+ it 'does not change the sequences on the gitlab_main tables' do
+ expect do
+ run_rake_task('gitlab:db:decomposition:rollback:bump_ci_sequences', '10')
+ end.to change {
+ last_value_of_sequence(ApplicationRecord.connection, main_sequence_name)
+ }.by(0)
+ .and change {
+ last_value_of_sequence(ApplicationRecord.connection, ci_sequence_name)
+ }.by(11) # the +1 is because the sequence has is_called = true
+ end
+ end
+
+ context 'when multiple databases' do
+ before do
+ skip_if_multiple_databases_not_setup
+ end
+
+ it 'does not change ci sequences on the ci database' do
+ expect do
+ run_rake_task('gitlab:db:decomposition:rollback:bump_ci_sequences', '10')
+ end.to change {
+ last_value_of_sequence(Ci::ApplicationRecord.connection, ci_sequence_name)
+ }.by(0)
+ end
+ end
+end
+
+def last_value_of_sequence(connection, sequence_name)
+ connection.select_value("select last_value from #{sequence_name}")
+end
diff --git a/spec/tasks/gitlab/db/lock_writes_rake_spec.rb b/spec/tasks/gitlab/db/lock_writes_rake_spec.rb
new file mode 100644
index 00000000000..034c520887e
--- /dev/null
+++ b/spec/tasks/gitlab/db/lock_writes_rake_spec.rb
@@ -0,0 +1,177 @@
+# frozen_string_literal: true
+
+require 'rake_helper'
+
+RSpec.describe 'gitlab:db:lock_writes', :silence_stdout, :reestablished_active_record_base 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 }
+
+ 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
+ end
+
+ context 'when locking writes' do
+ it 'adds 3 triggers to the ci schema tables on the main database' do
+ expect do
+ run_rake_task('gitlab:db:lock_writes')
+ end.to change {
+ number_of_triggers_on(main_connection, Ci::Build.table_name)
+ }.by(3) # Triggers to block INSERT / UPDATE / DELETE
+ # Triggers on TRUNCATE are not added to the information_schema.triggers
+ # See https://www.postgresql.org/message-id/16934.1568989957%40sss.pgh.pa.us
+ end
+
+ it 'adds 3 triggers to the main schema tables on the ci database' do
+ expect do
+ run_rake_task('gitlab:db:lock_writes')
+ end.to change {
+ number_of_triggers_on(ci_connection, Project.table_name)
+ }.by(3) # Triggers to block INSERT / UPDATE / DELETE
+ # Triggers on TRUNCATE are not added to the information_schema.triggers
+ # See https://www.postgresql.org/message-id/16934.1568989957%40sss.pgh.pa.us
+ end
+
+ 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 'retries again if it receives a statement_timeout a few number of times' do
+ error_message = "PG::QueryCanceled: ERROR: canceling statement due to statement timeout"
+ call_count = 0
+ allow(main_connection).to receive(:execute) do |statement|
+ if statement.include?("CREATE TRIGGER")
+ call_count += 1
+ raise(ActiveRecord::QueryCanceled, error_message) if call_count.even?
+ end
+ end
+ run_rake_task('gitlab:db:lock_writes')
+ end
+
+ it 'raises the exception if it happened many times' do
+ error_message = "PG::QueryCanceled: ERROR: canceling statement due to statement timeout"
+ allow(main_connection).to receive(:execute) do |statement|
+ if statement.include?("CREATE TRIGGER")
+ raise(ActiveRecord::QueryCanceled, error_message)
+ end
+ end
+
+ expect do
+ run_rake_task('gitlab:db:lock_writes')
+ end.to raise_error(ActiveRecord::QueryCanceled)
+ end
+ end
+
+ context 'when unlocking writes' do
+ before do
+ run_rake_task('gitlab:db:lock_writes')
+ end
+
+ it 'removes the write protection triggers from the gitlab_main tables on the ci database' do
+ expect do
+ run_rake_task('gitlab:db:unlock_writes')
+ end.to change {
+ number_of_triggers_on(ci_connection, Project.table_name)
+ }.by(-3) # Triggers to block INSERT / UPDATE / DELETE
+ # Triggers on TRUNCATE are not added to the information_schema.triggers
+ # See https://www.postgresql.org/message-id/16934.1568989957%40sss.pgh.pa.us
+
+ expect do
+ ci_connection.execute("delete from projects")
+ end.not_to raise_error
+ end
+
+ it 'removes the write protection triggers from the gitlab_ci tables on the main database' do
+ expect do
+ run_rake_task('gitlab:db:unlock_writes')
+ end.to change {
+ number_of_triggers_on(main_connection, Ci::Build.table_name)
+ }.by(-3)
+
+ expect do
+ main_connection.execute("delete from ci_builds")
+ end.not_to raise_error
+ end
+ end
+ end
+
+ def number_of_triggers(connection)
+ connection.select_value("SELECT count(*) FROM information_schema.triggers")
+ end
+
+ def number_of_triggers_on(connection, table_name)
+ connection
+ .select_value("SELECT count(*) FROM information_schema.triggers WHERE event_object_table=$1", nil, [table_name])
+ end
+end
diff --git a/spec/tasks/gitlab/db/validate_config_rake_spec.rb b/spec/tasks/gitlab/db/validate_config_rake_spec.rb
index 0b2c844a91f..03d7504e8b1 100644
--- a/spec/tasks/gitlab/db/validate_config_rake_spec.rb
+++ b/spec/tasks/gitlab/db/validate_config_rake_spec.rb
@@ -3,6 +3,10 @@
require 'rake_helper'
RSpec.describe 'gitlab:db:validate_config', :silence_stdout do
+ # We don't need to delete this data since it only modifies `ar_internal_metadata`
+ # which would not be cleaned either by `DbCleaner`
+ self.use_transactional_tests = false
+
before :all do
Rake.application.rake_require 'active_record/railties/databases'
Rake.application.rake_require 'tasks/seed_fu'
@@ -111,6 +115,26 @@ RSpec.describe 'gitlab:db:validate_config', :silence_stdout do
end
it_behaves_like 'validates successfully'
+
+ context 'when config is pointing to incorrect server' do
+ let(:test_config) do
+ {
+ main: main_database_config.merge(port: 11235)
+ }
+ end
+
+ it_behaves_like 'validates successfully'
+ end
+
+ context 'when config is pointing to non-existent database' do
+ let(:test_config) do
+ {
+ main: main_database_config.merge(database: 'non_existent_database')
+ }
+ end
+
+ it_behaves_like 'validates successfully'
+ end
end
context 'when main: uses database_tasks=false' do
@@ -181,6 +205,23 @@ RSpec.describe 'gitlab:db:validate_config', :silence_stdout do
it_behaves_like 'raises an error', /The 'ci' since it is using 'database_tasks: false' should share database with 'main:'/
end
end
+
+ context 'one of the databases is in read-only mode' do
+ let(:test_config) do
+ {
+ main: main_database_config
+ }
+ end
+
+ let(:exception) { ActiveRecord::StatementInvalid.new("READONLY") }
+
+ before do
+ allow(exception).to receive(:cause).and_return(PG::ReadOnlySqlTransaction.new("cannot execute INSERT in a read-only transaction"))
+ allow(ActiveRecord::InternalMetadata).to receive(:upsert).at_least(:once).and_raise(exception)
+ end
+
+ it_behaves_like 'validates successfully'
+ end
end
%w[db:migrate db:schema:load db:schema:dump].each do |task|