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/lock_writes_rake_spec.rb')
-rw-r--r--spec/tasks/gitlab/db/lock_writes_rake_spec.rb177
1 files changed, 177 insertions, 0 deletions
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