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 'lib/gitlab/database/connection.rb')
-rw-r--r--lib/gitlab/database/connection.rb249
1 files changed, 249 insertions, 0 deletions
diff --git a/lib/gitlab/database/connection.rb b/lib/gitlab/database/connection.rb
new file mode 100644
index 00000000000..21861e4fba8
--- /dev/null
+++ b/lib/gitlab/database/connection.rb
@@ -0,0 +1,249 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ # Configuration settings and methods for interacting with a PostgreSQL
+ # database, with support for multiple databases.
+ class Connection
+ DEFAULT_POOL_HEADROOM = 10
+
+ attr_reader :scope
+
+ # Initializes a new `Database`.
+ #
+ # The `scope` argument must be an object (such as `ActiveRecord::Base`)
+ # that supports retrieving connections and connection pools.
+ def initialize(scope = ActiveRecord::Base)
+ @config = nil
+ @scope = scope
+ @version = nil
+ @open_transactions_baseline = 0
+ end
+
+ # We configure the database connection pool size automatically based on
+ # the configured concurrency. We also add some headroom, to make sure we
+ # don't run out of connections when more threads besides the 'user-facing'
+ # ones are running.
+ #
+ # Read more about this in
+ # doc/development/database/client_side_connection_pool.md
+ def default_pool_size
+ headroom =
+ (ENV["DB_POOL_HEADROOM"].presence || DEFAULT_POOL_HEADROOM).to_i
+
+ Gitlab::Runtime.max_threads + headroom
+ end
+
+ def config
+ # The result of this method must not be cached, as other methods may use
+ # it after making configuration changes and expect those changes to be
+ # present. For example, `disable_prepared_statements` expects the
+ # configuration settings to always be up to date.
+ #
+ # See the following for more information:
+ #
+ # - https://gitlab.com/gitlab-org/release/retrospectives/-/issues/39
+ # - https://gitlab.com/gitlab-com/gl-infra/production/-/issues/5238
+ scope.connection_db_config.configuration_hash.with_indifferent_access
+ end
+
+ def pool_size
+ config[:pool] || default_pool_size
+ end
+
+ def username
+ config[:username] || ENV['USER']
+ end
+
+ def database_name
+ config[:database]
+ end
+
+ def adapter_name
+ config[:adapter]
+ end
+
+ def human_adapter_name
+ if postgresql?
+ 'PostgreSQL'
+ else
+ 'Unknown'
+ end
+ end
+
+ def postgresql?
+ adapter_name.casecmp('postgresql') == 0
+ end
+
+ def db_config_with_default_pool_size
+ db_config_object = scope.connection_db_config
+ config = db_config_object.configuration_hash.merge(pool: default_pool_size)
+
+ ActiveRecord::DatabaseConfigurations::HashConfig.new(
+ db_config_object.env_name,
+ db_config_object.name,
+ config
+ )
+ end
+
+ # Disables prepared statements for the current database connection.
+ def disable_prepared_statements
+ scope.establish_connection(config.merge(prepared_statements: false))
+ end
+
+ # Check whether the underlying database is in read-only mode
+ def db_read_only?
+ pg_is_in_recovery =
+ scope
+ .connection
+ .execute('SELECT pg_is_in_recovery()')
+ .first
+ .fetch('pg_is_in_recovery')
+
+ Gitlab::Utils.to_boolean(pg_is_in_recovery)
+ end
+
+ def db_read_write?
+ !db_read_only?
+ end
+
+ def version
+ @version ||= database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
+ end
+
+ def database_version
+ connection.execute("SELECT VERSION()").first['version']
+ end
+
+ def postgresql_minimum_supported_version?
+ version.to_f >= MINIMUM_POSTGRES_VERSION
+ end
+
+ # Bulk inserts a number of rows into a table, optionally returning their
+ # IDs.
+ #
+ # table - The name of the table to insert the rows into.
+ # rows - An Array of Hash instances, each mapping the columns to their
+ # values.
+ # return_ids - When set to true the return value will be an Array of IDs of
+ # the inserted rows
+ # disable_quote - A key or an Array of keys to exclude from quoting (You
+ # become responsible for protection from SQL injection for
+ # these keys!)
+ # on_conflict - Defines an upsert. Values can be: :disabled (default) or
+ # :do_nothing
+ def bulk_insert(table, rows, return_ids: false, disable_quote: [], on_conflict: nil)
+ return if rows.empty?
+
+ keys = rows.first.keys
+ columns = keys.map { |key| connection.quote_column_name(key) }
+
+ disable_quote = Array(disable_quote).to_set
+ tuples = rows.map do |row|
+ keys.map do |k|
+ disable_quote.include?(k) ? row[k] : connection.quote(row[k])
+ end
+ end
+
+ sql = <<-EOF
+ INSERT INTO #{table} (#{columns.join(', ')})
+ VALUES #{tuples.map { |tuple| "(#{tuple.join(', ')})" }.join(', ')}
+ EOF
+
+ sql = "#{sql} ON CONFLICT DO NOTHING" if on_conflict == :do_nothing
+
+ sql = "#{sql} RETURNING id" if return_ids
+
+ result = connection.execute(sql)
+
+ if return_ids
+ result.values.map { |tuple| tuple[0].to_i }
+ else
+ []
+ end
+ end
+
+ def cached_column_exists?(table_name, column_name)
+ connection
+ .schema_cache.columns_hash(table_name)
+ .has_key?(column_name.to_s)
+ end
+
+ def cached_table_exists?(table_name)
+ exists? && connection.schema_cache.data_source_exists?(table_name)
+ end
+
+ def exists?
+ connection
+
+ true
+ rescue StandardError
+ false
+ end
+
+ def system_id
+ row = connection
+ .execute('SELECT system_identifier FROM pg_control_system()')
+ .first
+
+ row['system_identifier']
+ end
+
+ # @param [ActiveRecord::Connection] ar_connection
+ # @return [String]
+ def get_write_location(ar_connection)
+ use_new_load_balancer_query = Gitlab::Utils
+ .to_boolean(ENV['USE_NEW_LOAD_BALANCER_QUERY'], default: true)
+
+ sql =
+ if use_new_load_balancer_query
+ <<~NEWSQL
+ SELECT CASE
+ WHEN pg_is_in_recovery() = true AND EXISTS (SELECT 1 FROM pg_stat_get_wal_senders())
+ THEN pg_last_wal_replay_lsn()::text
+ WHEN pg_is_in_recovery() = false
+ THEN pg_current_wal_insert_lsn()::text
+ ELSE NULL
+ END AS location;
+ NEWSQL
+ else
+ <<~SQL
+ SELECT pg_current_wal_insert_lsn()::text AS location
+ SQL
+ end
+
+ row = ar_connection.select_all(sql).first
+ row['location'] if row
+ end
+
+ # inside_transaction? will return true if the caller is running within a
+ # transaction. Handles special cases when running inside a test
+ # environment, where tests may be wrapped in transactions
+ def inside_transaction?
+ base = Rails.env.test? ? @open_transactions_baseline : 0
+
+ scope.connection.open_transactions > base
+ end
+
+ # These methods that access @open_transactions_baseline are not
+ # thread-safe. These are fine though because we only call these in
+ # RSpec's main thread. If we decide to run specs multi-threaded, we would
+ # need to use something like ThreadGroup to keep track of this value
+ def set_open_transactions_baseline
+ @open_transactions_baseline = scope.connection.open_transactions
+ end
+
+ def reset_open_transactions_baseline
+ @open_transactions_baseline = 0
+ end
+
+ private
+
+ def connection
+ scope.connection
+ end
+ end
+ end
+end
+
+Gitlab::Database::Connection.prepend_mod_with('Gitlab::Database::Connection')