From 081d95c02163f5f2ac594df320c6b8a374e33c43 Mon Sep 17 00:00:00 2001
From: Yorick Peterse <git@yorickpeterse.com>
Date: Wed, 15 Dec 2021 17:10:37 +0100
Subject: [PATCH] Disallow the use of X.establish_connection

This adds a RuboCop rule that disallows the usage of
`SomeModel.establish_connection`, as this pattern can break database
configurations and slows down the tests.

See https://gitlab.com/gitlab-org/gitlab/-/issues/338653 for more
information.
---
 .../initializers/active_record_lifecycle.rb   |  2 +-
 config/initializers/database_config.rb        |  2 +-
 ee/lib/gitlab/geo/database_tasks.rb           |  4 +--
 lib/tasks/gitlab/db.rake                      |  2 +-
 rubocop/cop/database/establish_connection.rb  | 20 +++++++++++++
 scripts/insert-rspec-profiling-data           |  2 +-
 spec/lib/gitlab/database/bulk_update_spec.rb  |  2 +-
 .../cop/database/establish_connection_spec.rb | 29 +++++++++++++++++++
 spec/support/db_cleaner.rb                    |  2 +-
 .../database/multiple_databases_spec.rb       |  8 ++---
 10 files changed, 61 insertions(+), 12 deletions(-)
 create mode 100644 rubocop/cop/database/establish_connection.rb
 create mode 100644 spec/rubocop/cop/database/establish_connection_spec.rb

diff --git a/config/initializers/active_record_lifecycle.rb b/config/initializers/active_record_lifecycle.rb
index 8d4b6d61abe3..92cc1d816179 100644
--- a/config/initializers/active_record_lifecycle.rb
+++ b/config/initializers/active_record_lifecycle.rb
@@ -5,7 +5,7 @@
 if defined?(ActiveRecord::Base) && !Gitlab::Runtime.sidekiq?
   Gitlab::Cluster::LifecycleEvents.on_worker_start do
     ActiveSupport.on_load(:active_record) do
-      ActiveRecord::Base.establish_connection
+      ActiveRecord::Base.establish_connection # rubocop: disable Database/EstablishConnection
 
       Gitlab::AppLogger.debug("ActiveRecord connection established")
     end
diff --git a/config/initializers/database_config.rb b/config/initializers/database_config.rb
index a3172fae027c..050ab1d9b3ec 100644
--- a/config/initializers/database_config.rb
+++ b/config/initializers/database_config.rb
@@ -13,6 +13,6 @@
     # The Geo::TrackingBase model does not yet use connects_to. So,
     # this will not properly support geo: from config/databse.yml
     # file yet. This is ACK of the current state and will be fixed.
-    Geo::TrackingBase.establish_connection(Gitlab::Database.geo_db_config_with_default_pool_size)
+    Geo::TrackingBase.establish_connection(Gitlab::Database.geo_db_config_with_default_pool_size) # rubocop: disable Database/EstablishConnection
   end
 end
diff --git a/ee/lib/gitlab/geo/database_tasks.rb b/ee/lib/gitlab/geo/database_tasks.rb
index 31d8e97d10f5..7a4b7a5154b5 100644
--- a/ee/lib/gitlab/geo/database_tasks.rb
+++ b/ee/lib/gitlab/geo/database_tasks.rb
@@ -132,7 +132,7 @@ def load
             ActiveRecord::Tasks::DatabaseTasks.load_schema(ActiveRecord::Base.configurations.configs_for(env_name: 'test').first, :sql, ENV['SCHEMA'])
           ensure
             if should_reconnect
-              ActiveRecord::Base.establish_connection(Gitlab::Geo::DatabaseTasks.db_config)
+              ActiveRecord::Base.establish_connection(Gitlab::Geo::DatabaseTasks.db_config) # rubocop: disable Database/EstablishConnection
             end
           end
         end
@@ -204,7 +204,7 @@ def set_db_env(settings)
         ActiveRecord::Migrator.migrations_paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths
         Gitlab::Database::SchemaMigrations::Context.default_schema_migrations_path = settings[:schema_migrations_path]
 
-        ActiveRecord::Base.establish_connection(db_config)
+        ActiveRecord::Base.establish_connection(db_config) # rubocop: disable Database/EstablishConnection
       end
 
       class SeedLoader
diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake
index 9e733fc3a0fc..efb0e1ef1e1a 100644
--- a/lib/tasks/gitlab/db.rake
+++ b/lib/tasks/gitlab/db.rake
@@ -170,7 +170,7 @@ namespace :gitlab do
       # the `ActiveRecord::Base.connection` might be switched to another one
       # This is due to `if should_reconnect`:
       # https://github.com/rails/rails/blob/a81aeb63a007ede2fe606c50539417dada9030c7/activerecord/lib/active_record/railties/databases.rake#L622
-      ActiveRecord::Base.establish_connection :main
+      ActiveRecord::Base.establish_connection :main # rubocop: disable Database/EstablishConnection
 
       Rake::Task['gitlab:db:create_dynamic_partitions'].invoke
     end
diff --git a/rubocop/cop/database/establish_connection.rb b/rubocop/cop/database/establish_connection.rb
new file mode 100644
index 000000000000..20454887ce2d
--- /dev/null
+++ b/rubocop/cop/database/establish_connection.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module RuboCop
+  module Cop
+    module Database
+      class EstablishConnection < RuboCop::Cop::Cop
+        MSG = "Don't establish new database connections, as this slows down " \
+          'tests and may result in new connections using an incorrect configuration'
+
+        def_node_matcher :establish_connection?, <<~PATTERN
+          (send (const ...) :establish_connection ...)
+        PATTERN
+
+        def on_send(node)
+          add_offense(node, location: :expression) if establish_connection?(node)
+        end
+      end
+    end
+  end
+end
diff --git a/scripts/insert-rspec-profiling-data b/scripts/insert-rspec-profiling-data
index b20118585588..be25972644c8 100755
--- a/scripts/insert-rspec-profiling-data
+++ b/scripts/insert-rspec-profiling-data
@@ -12,7 +12,7 @@ module RspecProfiling
         # This disables the automatic creation of the database and
         # table. In the future, we may want a way to specify the host of
         # the database to connect so that we can call #install.
-        Result.establish_connection(results_url)
+        Result.establish_connection(results_url) # rubocop: disable Database/EstablishConnection
       end
 
       def results_url
diff --git a/spec/lib/gitlab/database/bulk_update_spec.rb b/spec/lib/gitlab/database/bulk_update_spec.rb
index 9a6463c99fa0..08b4d50f83b5 100644
--- a/spec/lib/gitlab/database/bulk_update_spec.rb
+++ b/spec/lib/gitlab/database/bulk_update_spec.rb
@@ -101,7 +101,7 @@
     before do
       configuration_hash = ActiveRecord::Base.connection_db_config.configuration_hash
 
-      ActiveRecord::Base.establish_connection(
+      ActiveRecord::Base.establish_connection( # rubocop: disable Database/EstablishConnection
         configuration_hash.merge(prepared_statements: prepared_statements)
       )
     end
diff --git a/spec/rubocop/cop/database/establish_connection_spec.rb b/spec/rubocop/cop/database/establish_connection_spec.rb
new file mode 100644
index 000000000000..a3c27d33cb0e
--- /dev/null
+++ b/spec/rubocop/cop/database/establish_connection_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_relative '../../../../rubocop/cop/database/establish_connection'
+
+RSpec.describe RuboCop::Cop::Database::EstablishConnection do
+  subject(:cop) { described_class.new }
+
+  it 'flags the use of ActiveRecord::Base.establish_connection' do
+    expect_offense(<<~CODE)
+      ActiveRecord::Base.establish_connection
+      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't establish new database [...]
+    CODE
+  end
+
+  it 'flags the use of ActiveRecord::Base.establish_connection with arguments' do
+    expect_offense(<<~CODE)
+      ActiveRecord::Base.establish_connection(:foo)
+      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't establish new database [...]
+    CODE
+  end
+
+  it 'flags the use of SomeModel.establish_connection' do
+    expect_offense(<<~CODE)
+      SomeModel.establish_connection
+      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't establish new database [...]
+    CODE
+  end
+end
diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb
index 316d645f99ff..fb70f82ef875 100644
--- a/spec/support/db_cleaner.rb
+++ b/spec/support/db_cleaner.rb
@@ -67,7 +67,7 @@ def recreate_all_databases!
     # Migrate each database individually
     with_reestablished_active_record_base do
       all_connection_classes.each do |connection_class|
-        ActiveRecord::Base.establish_connection(connection_class.connection_db_config)
+        ActiveRecord::Base.establish_connection(connection_class.connection_db_config) # rubocop: disable Database/EstablishConnection
 
         ActiveRecord::Tasks::DatabaseTasks.migrate
       end
diff --git a/spec/support_specs/database/multiple_databases_spec.rb b/spec/support_specs/database/multiple_databases_spec.rb
index 660563594587..b4cfa2538131 100644
--- a/spec/support_specs/database/multiple_databases_spec.rb
+++ b/spec/support_specs/database/multiple_databases_spec.rb
@@ -7,13 +7,13 @@
     context 'when doing establish_connection' do
       context 'on ActiveRecord::Base' do
         it 'raises exception' do
-          expect { ActiveRecord::Base.establish_connection(:main) }.to raise_error /Cannot re-establish/
+          expect { ActiveRecord::Base.establish_connection(:main) }.to raise_error /Cannot re-establish/ # rubocop: disable Database/EstablishConnection
         end
 
         context 'when using with_reestablished_active_record_base' do
           it 'does not raise exception' do
             with_reestablished_active_record_base do
-              expect { ActiveRecord::Base.establish_connection(:main) }.not_to raise_error
+              expect { ActiveRecord::Base.establish_connection(:main) }.not_to raise_error # rubocop: disable Database/EstablishConnection
             end
           end
         end
@@ -25,13 +25,13 @@
         end
 
         it 'raises exception' do
-          expect { Ci::ApplicationRecord.establish_connection(:ci) }.to raise_error /Cannot re-establish/
+          expect { Ci::ApplicationRecord.establish_connection(:ci) }.to raise_error /Cannot re-establish/ # rubocop: disable Database/EstablishConnection
         end
 
         context 'when using with_reestablished_active_record_base' do
           it 'does not raise exception' do
             with_reestablished_active_record_base do
-              expect { Ci::ApplicationRecord.establish_connection(:main) }.not_to raise_error
+              expect { Ci::ApplicationRecord.establish_connection(:main) }.not_to raise_error # rubocop: disable Database/EstablishConnection
             end
           end
         end
-- 
GitLab