Merge pull request #37199 from seejohnrun/reduce-surface-area-of-connection-specification

Reduce surface area of ConnectionSpecification
This commit is contained in:
Eileen M. Uchitelle 2019-09-16 09:35:22 -04:00 committed by GitHub
commit 9b6433bb82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 345 additions and 346 deletions

View File

@ -10,6 +10,7 @@ module ActiveRecord
autoload :Column
autoload :ConnectionSpecification
autoload :Resolver
autoload_at "active_record/connection_adapters/abstract/schema_definitions" do
autoload :IndexDefinition

View File

@ -385,14 +385,14 @@ module ActiveRecord
@spec = spec
@checkout_timeout = (spec.underlying_configuration_hash[:checkout_timeout] && spec.underlying_configuration_hash[:checkout_timeout].to_f) || 5
if @idle_timeout = spec.underlying_configuration_hash.fetch(:idle_timeout, 300)
@checkout_timeout = (spec.db_config.configuration_hash[:checkout_timeout] && spec.db_config.configuration_hash[:checkout_timeout].to_f) || 5
if @idle_timeout = spec.db_config.configuration_hash.fetch(:idle_timeout, 300)
@idle_timeout = @idle_timeout.to_f
@idle_timeout = nil if @idle_timeout <= 0
end
# default max pool size to 5
@size = (spec.underlying_configuration_hash[:pool] && spec.underlying_configuration_hash[:pool].to_i) || 5
@size = (spec.db_config.configuration_hash[:pool] && spec.db_config.configuration_hash[:pool].to_i) || 5
# This variable tracks the cache of threads mapped to reserved connections, with the
# sole purpose of speeding up the +connection+ method. It is not the authoritative
@ -422,7 +422,7 @@ module ActiveRecord
# +reaping_frequency+ is configurable mostly for historical reasons, but it could
# also be useful if someone wants a very low +idle_timeout+.
reaping_frequency = spec.underlying_configuration_hash.fetch(:reaping_frequency, 60)
reaping_frequency = spec.db_config.configuration_hash.fetch(:reaping_frequency, 60)
@reaper = Reaper.new(self, reaping_frequency && reaping_frequency.to_f)
@reaper.run
end
@ -505,7 +505,7 @@ module ActiveRecord
# Raises:
# - ActiveRecord::ExclusiveConnectionTimeoutError if unable to gain ownership of all
# connections in the pool within a timeout interval (default duration is
# <tt>spec.underlying_configuration_hash[:checkout_timeout] * 2</tt> seconds).
# <tt>spec.db_config.configuration_hash[:checkout_timeout] * 2</tt> seconds).
def disconnect(raise_on_acquisition_timeout = true)
with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do
synchronize do
@ -526,7 +526,7 @@ module ActiveRecord
#
# The pool first tries to gain ownership of all connections. If unable to
# do so within a timeout interval (default duration is
# <tt>spec.underlying_configuration_hash[:checkout_timeout] * 2</tt> seconds), then the pool is forcefully
# <tt>spec.db_config.configuration_hash[:checkout_timeout] * 2</tt> seconds), then the pool is forcefully
# disconnected without any regard for other connection owning threads.
def disconnect!
disconnect(false)
@ -557,7 +557,7 @@ module ActiveRecord
# Raises:
# - ActiveRecord::ExclusiveConnectionTimeoutError if unable to gain ownership of all
# connections in the pool within a timeout interval (default duration is
# <tt>spec.underlying_configuration_hash[:checkout_timeout] * 2</tt> seconds).
# <tt>spec.db_config.configuration_hash[:checkout_timeout] * 2</tt> seconds).
def clear_reloadable_connections(raise_on_acquisition_timeout = true)
with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do
synchronize do
@ -579,7 +579,7 @@ module ActiveRecord
#
# The pool first tries to gain ownership of all connections. If unable to
# do so within a timeout interval (default duration is
# <tt>spec.underlying_configuration_hash[:checkout_timeout] * 2</tt> seconds), then the pool forcefully
# <tt>spec.db_config.configuration_hash[:checkout_timeout] * 2</tt> seconds), then the pool forcefully
# clears the cache and reloads connections without any regard for other
# connection owning threads.
def clear_reloadable_connections!
@ -899,7 +899,7 @@ module ActiveRecord
alias_method :release, :remove_connection_from_thread_cache
def new_connection
Base.send(spec.adapter_method, spec.underlying_configuration_hash).tap do |conn|
Base.send(spec.db_config.adapter_method, spec.db_config.configuration_hash).tap do |conn|
conn.check_version
end
end
@ -1063,7 +1063,7 @@ module ActiveRecord
alias :connection_pools :connection_pool_list
def establish_connection(config)
resolver = ConnectionSpecification::Resolver.new(Base.configurations)
resolver = Resolver.new(Base.configurations)
spec = resolver.spec(config)
remove_connection(spec.name)
@ -1074,7 +1074,7 @@ module ActiveRecord
}
if spec
payload[:spec_name] = spec.name
payload[:config] = spec.underlying_configuration_hash
payload[:config] = spec.db_config.configuration_hash
end
message_bus.instrument("!connection.active_record", payload) do
@ -1149,7 +1149,7 @@ module ActiveRecord
if pool = owner_to_pool.delete(spec_name)
pool.automatic_reconnect = false
pool.disconnect!
pool.spec.underlying_configuration_hash
pool.spec.db_config.configuration_hash
end
end
@ -1164,7 +1164,7 @@ module ActiveRecord
# A connection was established in an ancestor process that must have
# subsequently forked. We can't reuse the connection, but we can copy
# the specification and establish a new connection with it.
establish_connection(ancestor_pool.spec.to_hash).tap do |pool|
establish_connection(ancestor_pool.spec.db_config.configuration_hash).tap do |pool|
pool.schema_cache = ancestor_pool.schema_cache if ancestor_pool.schema_cache
end
else

View File

@ -4,259 +4,11 @@ require "uri"
module ActiveRecord
module ConnectionAdapters
class ConnectionSpecification #:nodoc:
attr_reader :name, :adapter_method, :db_config
class ConnectionSpecification # :nodoc:
attr_reader :name, :db_config
def initialize(name, db_config, adapter_method)
@name, @db_config, @adapter_method = name, db_config, adapter_method
end
def underlying_configuration_hash
@db_config.configuration_hash
end
def initialize_dup(original)
@db_config = original.db_config.dup
end
def to_hash
underlying_configuration_hash.dup.merge(name: @name)
end
# Expands a connection string into a hash.
class ConnectionUrlResolver # :nodoc:
# == Example
#
# url = "postgresql://foo:bar@localhost:9000/foo_test?pool=5&timeout=3000"
# ConnectionUrlResolver.new(url).to_hash
# # => {
# adapter: "postgresql",
# host: "localhost",
# port: 9000,
# database: "foo_test",
# username: "foo",
# password: "bar",
# pool: "5",
# timeout: "3000"
# }
def initialize(url)
raise "Database URL cannot be empty" if url.blank?
@uri = uri_parser.parse(url)
@adapter = @uri.scheme && @uri.scheme.tr("-", "_")
@adapter = "postgresql" if @adapter == "postgres"
if @uri.opaque
@uri.opaque, @query = @uri.opaque.split("?", 2)
else
@query = @uri.query
end
end
# Converts the given URL to a full connection hash.
def to_hash
config = raw_config.compact_blank
config.map { |key, value| config[key] = uri_parser.unescape(value) if value.is_a? String }
config
end
private
attr_reader :uri
def uri_parser
@uri_parser ||= URI::Parser.new
end
# Converts the query parameters of the URI into a hash.
#
# "localhost?pool=5&reaping_frequency=2"
# # => { pool: "5", reaping_frequency: "2" }
#
# returns empty hash if no query present.
#
# "localhost"
# # => {}
def query_hash
Hash[(@query || "").split("&").map { |pair| pair.split("=") }].symbolize_keys
end
def raw_config
if uri.opaque
query_hash.merge(
adapter: @adapter,
database: uri.opaque
)
else
query_hash.merge(
adapter: @adapter,
username: uri.user,
password: uri.password,
port: uri.port,
database: database_from_path,
host: uri.hostname
)
end
end
# Returns name of the database.
def database_from_path
if @adapter == "sqlite3"
# 'sqlite3:/foo' is absolute, because that makes sense. The
# corresponding relative version, 'sqlite3:foo', is handled
# elsewhere, as an "opaque".
uri.path
else
# Only SQLite uses a filename as the "database" name; for
# anything else, a leading slash would be silly.
uri.path.sub(%r{^/}, "")
end
end
end
##
# Builds a ConnectionSpecification from user input.
class Resolver # :nodoc:
attr_reader :configurations
# Accepts a list of db config objects.
def initialize(configurations)
@configurations = configurations
end
# Returns an instance of ConnectionSpecification for a given adapter.
# Accepts a hash one layer deep that contains all connection information.
#
# == Example
#
# config = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } }
# spec = Resolver.new(config).spec(:production)
# spec.adapter_method
# # => "sqlite3_connection"
# spec.underlying_configuration_hash
# # => { host: "localhost", database: "foo", adapter: "sqlite3" }
#
def spec(config)
pool_name = config if config.is_a?(Symbol)
db_config = resolve(config, pool_name)
spec = db_config.configuration_hash
raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter)
# Require the adapter itself and give useful feedback about
# 1. Missing adapter gems and
# 2. Adapter gems' missing dependencies.
path_to_adapter = "active_record/connection_adapters/#{spec[:adapter]}_adapter"
begin
require path_to_adapter
rescue LoadError => e
# We couldn't require the adapter itself. Raise an exception that
# points out config typos and missing gems.
if e.path == path_to_adapter
# We can assume that a non-builtin adapter was specified, so it's
# either misspelled or missing from Gemfile.
raise LoadError, "Could not load the '#{spec[:adapter]}' Active Record adapter. Ensure that the adapter is spelled correctly in config/database.yml and that you've added the necessary adapter gem to your Gemfile.", e.backtrace
# Bubbled up from the adapter require. Prefix the exception message
# with some guidance about how to address it and reraise.
else
raise LoadError, "Error loading the '#{spec[:adapter]}' Active Record adapter. Missing a gem it depends on? #{e.message}", e.backtrace
end
end
adapter_method = "#{spec[:adapter]}_connection"
unless ActiveRecord::Base.respond_to?(adapter_method)
raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter"
end
ConnectionSpecification.new(spec.delete(:name) || "primary", db_config, adapter_method)
end
# Returns fully resolved connection, accepts hash, string or symbol.
# Always returns a DatabaseConfiguration::DatabaseConfig
#
# == Examples
#
# Symbol representing current environment.
#
# Resolver.new("production" => {}).resolve(:production)
# # => DatabaseConfigurations::HashConfig.new(env_name: "production", config: {})
#
# One layer deep hash of connection values.
#
# Resolver.new({}).resolve("adapter" => "sqlite3")
# # => DatabaseConfigurations::HashConfig.new(config: {"adapter" => "sqlite3"})
#
# Connection URL.
#
# Resolver.new({}).resolve("postgresql://localhost/foo")
# # => DatabaseConfigurations::UrlConfig.new(config: {"adapter" => "postgresql", "host" => "localhost", "database" => "foo"})
#
def resolve(config_or_env, pool_name = nil)
env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s
case config_or_env
when Symbol
resolve_symbol_connection(config_or_env, pool_name)
when String
DatabaseConfigurations::UrlConfig.new(env, "primary", config_or_env)
when Hash
DatabaseConfigurations::HashConfig.new(env, "primary", config_or_env)
when DatabaseConfigurations::DatabaseConfig
config_or_env
else
raise TypeError, "Invalid type for configuration. Expected Symbol, String, or Hash. Got #{config_or_env.inspect}"
end
end
private
# Takes the environment such as +:production+ or +:development+ and a
# pool name the corresponds to the name given by the connection pool
# to the connection. That pool name is merged into the hash with the
# name key.
#
# This requires that the @configurations was initialized with a key that
# matches.
#
# configurations = #<ActiveRecord::DatabaseConfigurations:0x00007fd9fdace3e0
# @configurations=[
# #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd9fdace250
# @env_name="production", @spec_name="primary", @config={database: "my_db"}>
# ]>
#
# Resolver.new(configurations).resolve_symbol_connection(:production, "primary")
# # => DatabaseConfigurations::HashConfig(config: database: "my_db", env_name: "production", spec_name: "primary")
def resolve_symbol_connection(env_name, pool_name)
db_config = configurations.find_db_config(env_name)
if db_config
config = db_config.configuration_hash.merge(name: pool_name.to_s)
DatabaseConfigurations::HashConfig.new(db_config.env_name, db_config.spec_name, config)
else
raise AdapterNotSpecified, <<~MSG
The `#{env_name}` database is not configured for the `#{ActiveRecord::ConnectionHandling::DEFAULT_ENV.call}` environment.
Available databases configurations are:
#{build_configuration_sentence}
MSG
end
end
def build_configuration_sentence # :nodoc:
configs = configurations.configs_for(include_replicas: true)
configs.group_by(&:env_name).map do |env, config|
namespaces = config.map(&:spec_name)
if namespaces.size > 1
"#{env}: #{namespaces.join(", ")}"
else
env
end
end.join("\n")
end
def initialize(name, db_config)
@name, @db_config = name, db_config
end
end
end

View File

@ -0,0 +1,145 @@
# frozen_string_literal: true
module ActiveRecord
module ConnectionAdapters
# Builds a ConnectionSpecification from user input.
class Resolver # :nodoc:
attr_reader :configurations
# Accepts a list of db config objects.
def initialize(configurations)
@configurations = configurations
end
# Returns an instance of ConnectionSpecification for a given adapter.
# Accepts a hash one layer deep that contains all connection information.
#
# == Example
#
# config = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } }
# spec = Resolver.new(config).spec(:production)
# spec.db_config.configuration_hash
# # => { host: "localhost", database: "foo", adapter: "sqlite3" }
#
def spec(config)
pool_name = config if config.is_a?(Symbol)
db_config = resolve(config, pool_name)
spec = db_config.configuration_hash
raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter)
# Require the adapter itself and give useful feedback about
# 1. Missing adapter gems and
# 2. Adapter gems' missing dependencies.
path_to_adapter = "active_record/connection_adapters/#{spec[:adapter]}_adapter"
begin
require path_to_adapter
rescue LoadError => e
# We couldn't require the adapter itself. Raise an exception that
# points out config typos and missing gems.
if e.path == path_to_adapter
# We can assume that a non-builtin adapter was specified, so it's
# either misspelled or missing from Gemfile.
raise LoadError, "Could not load the '#{spec[:adapter]}' Active Record adapter. Ensure that the adapter is spelled correctly in config/database.yml and that you've added the necessary adapter gem to your Gemfile.", e.backtrace
# Bubbled up from the adapter require. Prefix the exception message
# with some guidance about how to address it and reraise.
else
raise LoadError, "Error loading the '#{spec[:adapter]}' Active Record adapter. Missing a gem it depends on? #{e.message}", e.backtrace
end
end
unless ActiveRecord::Base.respond_to?(db_config.adapter_method)
raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter"
end
ConnectionSpecification.new(spec.delete(:name) || "primary", db_config)
end
# Returns fully resolved connection, accepts hash, string or symbol.
# Always returns a DatabaseConfiguration::DatabaseConfig
#
# == Examples
#
# Symbol representing current environment.
#
# Resolver.new("production" => {}).resolve(:production)
# # => DatabaseConfigurations::HashConfig.new(env_name: "production", config: {})
#
# One layer deep hash of connection values.
#
# Resolver.new({}).resolve("adapter" => "sqlite3")
# # => DatabaseConfigurations::HashConfig.new(config: {"adapter" => "sqlite3"})
#
# Connection URL.
#
# Resolver.new({}).resolve("postgresql://localhost/foo")
# # => DatabaseConfigurations::UrlConfig.new(config: {"adapter" => "postgresql", "host" => "localhost", "database" => "foo"})
#
def resolve(config_or_env, pool_name = nil)
env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s
case config_or_env
when Symbol
resolve_symbol_connection(config_or_env, pool_name)
when String
DatabaseConfigurations::UrlConfig.new(env, "primary", config_or_env)
when Hash
DatabaseConfigurations::HashConfig.new(env, "primary", config_or_env)
when DatabaseConfigurations::DatabaseConfig
config_or_env
else
raise TypeError, "Invalid type for configuration. Expected Symbol, String, or Hash. Got #{config_or_env.inspect}"
end
end
private
# Takes the environment such as +:production+ or +:development+ and a
# pool name the corresponds to the name given by the connection pool
# to the connection. That pool name is merged into the hash with the
# name key.
#
# This requires that the @configurations was initialized with a key that
# matches.
#
# configurations = #<ActiveRecord::DatabaseConfigurations:0x00007fd9fdace3e0
# @configurations=[
# #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd9fdace250
# @env_name="production", @spec_name="primary", @config={database: "my_db"}>
# ]>
#
# Resolver.new(configurations).resolve_symbol_connection(:production, "primary")
# # => DatabaseConfigurations::HashConfig(config: database: "my_db", env_name: "production", spec_name: "primary")
def resolve_symbol_connection(env_name, pool_name)
db_config = configurations.find_db_config(env_name)
if db_config
config = db_config.configuration_hash.merge(name: pool_name.to_s)
DatabaseConfigurations::HashConfig.new(db_config.env_name, db_config.spec_name, config)
else
raise AdapterNotSpecified, <<~MSG
The `#{env_name}` database is not configured for the `#{ActiveRecord::ConnectionHandling::DEFAULT_ENV.call}` environment.
Available databases configurations are:
#{build_configuration_sentence}
MSG
end
end
def build_configuration_sentence # :nodoc:
configs = configurations.configs_for(include_replicas: true)
configs.group_by(&:env_name).map do |env, config|
namespaces = config.map(&:spec_name)
if namespaces.size > 1
"#{env}: #{namespaces.join(", ")}"
else
env
end
end.join("\n")
end
end
end
end

View File

@ -183,7 +183,7 @@ module ActiveRecord
pool_name = primary_class? ? "primary" : name
self.connection_specification_name = pool_name
resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(Base.configurations)
resolver = ConnectionAdapters::Resolver.new(Base.configurations)
config_hash = resolver.resolve(config_or_env, pool_name).configuration_hash
config_hash[:name] = pool_name
@ -227,7 +227,7 @@ module ActiveRecord
#
# Please use only for reading.
def connection_config
connection_pool.spec.underlying_configuration_hash
connection_pool.spec.db_config.configuration_hash
end
def connection_pool

View File

@ -3,6 +3,7 @@
require "active_record/database_configurations/database_config"
require "active_record/database_configurations/hash_config"
require "active_record/database_configurations/url_config"
require "active_record/database_configurations/connection_url_resolver"
module ActiveRecord
# ActiveRecord::DatabaseConfigurations returns an array of DatabaseConfig

View File

@ -0,0 +1,96 @@
# frozen_string_literal: true
module ActiveRecord
class DatabaseConfigurations
# Expands a connection string into a hash.
class ConnectionUrlResolver # :nodoc:
# == Example
#
# url = "postgresql://foo:bar@localhost:9000/foo_test?pool=5&timeout=3000"
# ConnectionUrlResolver.new(url).to_hash
# # => {
# adapter: "postgresql",
# host: "localhost",
# port: 9000,
# database: "foo_test",
# username: "foo",
# password: "bar",
# pool: "5",
# timeout: "3000"
# }
def initialize(url)
raise "Database URL cannot be empty" if url.blank?
@uri = uri_parser.parse(url)
@adapter = @uri.scheme && @uri.scheme.tr("-", "_")
@adapter = "postgresql" if @adapter == "postgres"
if @uri.opaque
@uri.opaque, @query = @uri.opaque.split("?", 2)
else
@query = @uri.query
end
end
# Converts the given URL to a full connection hash.
def to_hash
config = raw_config.compact_blank
config.map { |key, value| config[key] = uri_parser.unescape(value) if value.is_a? String }
config
end
private
attr_reader :uri
def uri_parser
@uri_parser ||= URI::Parser.new
end
# Converts the query parameters of the URI into a hash.
#
# "localhost?pool=5&reaping_frequency=2"
# # => { pool: "5", reaping_frequency: "2" }
#
# returns empty hash if no query present.
#
# "localhost"
# # => {}
def query_hash
Hash[(@query || "").split("&").map { |pair| pair.split("=") }].symbolize_keys
end
def raw_config
if uri.opaque
query_hash.merge(
adapter: @adapter,
database: uri.opaque
)
else
query_hash.merge(
adapter: @adapter,
username: uri.user,
password: uri.password,
port: uri.port,
database: database_from_path,
host: uri.hostname
)
end
end
# Returns name of the database.
def database_from_path
if @adapter == "sqlite3"
# 'sqlite3:/foo' is absolute, because that makes sense. The
# corresponding relative version, 'sqlite3:foo', is handled
# elsewhere, as an "opaque".
uri.path
else
# Only SQLite uses a filename as the "database" name; for
# anything else, a leading slash would be silly.
uri.path.sub(%r{^/}, "")
end
end
end
end
end

View File

@ -18,8 +18,12 @@ module ActiveRecord
configuration_hash.stringify_keys
end
def initialize_dup(original)
@config = original.configuration_hash.dup
def adapter_method
"#{adapter}_connection"
end
def adapter
configuration_hash[:adapter]
end
def replica?

View File

@ -53,7 +53,7 @@ module ActiveRecord
private
def resolve_url_key
if configuration_hash[:url] && !configuration_hash[:url].match?(/^jdbc:/)
connection_hash = ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(configuration_hash[:url]).to_hash
connection_hash = ConnectionUrlResolver.new(configuration_hash[:url]).to_hash
configuration_hash.merge!(connection_hash)
end
end

View File

@ -64,7 +64,7 @@ module ActiveRecord
if url.nil? || /^jdbc:/.match?(url)
{ url: url }
else
ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(url).to_hash
ConnectionUrlResolver.new(url).to_hash
end
end

View File

@ -116,7 +116,7 @@ module ActiveRecord
if options.has_key?(:config)
@current_config = options[:config]
else
@current_config ||= ActiveRecord::Base.configurations.configs_for(env_name: options[:env], spec_name: options[:spec]).underlying_configuration_hash
@current_config ||= ActiveRecord::Base.configurations.configs_for(env_name: options[:env], spec_name: options[:spec]).db_config.configuration_hash
end
end
@ -136,7 +136,7 @@ module ActiveRecord
old_pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base.connection_specification_name)
each_local_configuration { |configuration| create configuration }
if old_pool
ActiveRecord::Base.connection_handler.establish_connection(old_pool.spec.to_hash)
ActiveRecord::Base.connection_handler.establish_connection(old_pool.spec.db_config.configuration_hash)
end
end

View File

@ -11,7 +11,7 @@ module ActiveRecord
def setup
@connection = ActiveRecord::Base.connection
db = Post.connection_pool.spec.underlying_configuration_hash[:database]
db = Post.connection_pool.spec.db_config.configuration_hash[:database]
table = Post.table_name
@db_name = db

View File

@ -40,7 +40,7 @@ module ActiveRecord
def test_close
db_config = ActiveRecord::DatabaseConfigurations::HashConfig.new("test", "primary", {})
pool = Pool.new(ConnectionSpecification.new("primary", db_config, nil))
pool = Pool.new(ConnectionSpecification.new("primary", db_config))
pool.insert_connection_for_test! @adapter
@adapter.pool = pool

View File

@ -27,13 +27,14 @@ module ActiveRecord
ENV["RACK_ENV"] = original_rack_env
end
def test_establish_connection_uses_spec_name
def test_establish_connection_uses_config_hash_with_spec_name
old_config = ActiveRecord::Base.configurations
config = { "readonly" => { "adapter" => "sqlite3", "pool" => "5" } }
ActiveRecord::Base.configurations = config
resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(ActiveRecord::Base.configurations)
spec = resolver.spec(:readonly)
@handler.establish_connection(spec.to_hash)
resolver = ConnectionAdapters::Resolver.new(ActiveRecord::Base.configurations)
config_hash = resolver.resolve(config["readonly"], "readonly").configuration_hash
config_hash[:name] = "readonly"
@handler.establish_connection(config_hash)
assert_not_nil @handler.retrieve_connection_pool("readonly")
ensure
@ -62,13 +63,13 @@ module ActiveRecord
@handler.establish_connection(:readonly)
assert_not_nil pool = @handler.retrieve_connection_pool("readonly")
assert_equal "db/readonly.sqlite3", pool.spec.underlying_configuration_hash[:database]
assert_equal "db/readonly.sqlite3", pool.spec.db_config.configuration_hash[:database]
assert_not_nil pool = @handler.retrieve_connection_pool("primary")
assert_equal "db/primary.sqlite3", pool.spec.underlying_configuration_hash[:database]
assert_equal "db/primary.sqlite3", pool.spec.db_config.configuration_hash[:database]
assert_not_nil pool = @handler.retrieve_connection_pool("common")
assert_equal "db/common.sqlite3", pool.spec.underlying_configuration_hash[:database]
assert_equal "db/common.sqlite3", pool.spec.db_config.configuration_hash[:database]
ensure
ActiveRecord::Base.configurations = @prev_configs
ENV["RAILS_ENV"] = previous_env
@ -92,7 +93,7 @@ module ActiveRecord
ActiveRecord::Base.establish_connection
assert_match "db/primary.sqlite3", ActiveRecord::Base.connection.pool.spec.underlying_configuration_hash[:database]
assert_match "db/primary.sqlite3", ActiveRecord::Base.connection.pool.spec.db_config.configuration_hash[:database]
ensure
ActiveRecord::Base.configurations = @prev_configs
ENV["RAILS_ENV"] = previous_env
@ -115,7 +116,7 @@ module ActiveRecord
ActiveRecord::Base.establish_connection
assert_match "db/primary.sqlite3", ActiveRecord::Base.connection.pool.spec.underlying_configuration_hash[:database]
assert_match "db/primary.sqlite3", ActiveRecord::Base.connection.pool.spec.db_config.configuration_hash[:database]
ensure
ActiveRecord::Base.configurations = @prev_configs
ENV["RAILS_ENV"] = previous_env
@ -131,7 +132,7 @@ module ActiveRecord
@handler.establish_connection(:development)
assert_not_nil pool = @handler.retrieve_connection_pool("development")
assert_equal "db/primary.sqlite3", pool.spec.underlying_configuration_hash[:database]
assert_equal "db/primary.sqlite3", pool.spec.db_config.configuration_hash[:database]
ensure
ActiveRecord::Base.configurations = @prev_configs
end
@ -146,7 +147,7 @@ module ActiveRecord
@handler.establish_connection(:development_readonly)
assert_not_nil pool = @handler.retrieve_connection_pool("development_readonly")
assert_equal "db/readonly.sqlite3", pool.spec.underlying_configuration_hash[:database]
assert_equal "db/readonly.sqlite3", pool.spec.db_config.configuration_hash[:database]
ensure
ActiveRecord::Base.configurations = @prev_configs
end
@ -210,7 +211,7 @@ module ActiveRecord
assert_same klass2.connection, ActiveRecord::Base.connection
pool = klass2.establish_connection(ActiveRecord::Base.connection_pool.spec.underlying_configuration_hash)
pool = klass2.establish_connection(ActiveRecord::Base.connection_pool.spec.db_config.configuration_hash)
assert_same klass2.connection, pool.connection
assert_not_same klass2.connection, ActiveRecord::Base.connection

View File

@ -82,10 +82,10 @@ module ActiveRecord
ActiveRecord::Base.connects_to(database: { writing: :primary, reading: :readonly })
assert_not_nil pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("primary")
assert_equal "db/primary.sqlite3", pool.spec.underlying_configuration_hash[:database]
assert_equal "db/primary.sqlite3", pool.spec.db_config.configuration_hash[:database]
assert_not_nil pool = ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("primary")
assert_equal "db/readonly.sqlite3", pool.spec.underlying_configuration_hash[:database]
assert_equal "db/readonly.sqlite3", pool.spec.db_config.configuration_hash[:database]
ensure
ActiveRecord::Base.configurations = @prev_configs
ActiveRecord::Base.establish_connection(:arunit)
@ -140,10 +140,10 @@ module ActiveRecord
ActiveRecord::Base.connects_to(database: { default: :primary, readonly: :readonly })
assert_not_nil pool = ActiveRecord::Base.connection_handlers[:default].retrieve_connection_pool("primary")
assert_equal "db/primary.sqlite3", pool.spec.underlying_configuration_hash[:database]
assert_equal "db/primary.sqlite3", pool.spec.db_config.configuration_hash[:database]
assert_not_nil pool = ActiveRecord::Base.connection_handlers[:readonly].retrieve_connection_pool("primary")
assert_equal "db/readonly.sqlite3", pool.spec.underlying_configuration_hash[:database]
assert_equal "db/readonly.sqlite3", pool.spec.db_config.configuration_hash[:database]
ensure
ActiveRecord::Base.configurations = @prev_configs
ActiveRecord::Base.establish_connection(:arunit)
@ -162,7 +162,7 @@ module ActiveRecord
assert_equal handler, ActiveRecord::Base.connection_handlers[:writing]
assert_not_nil pool = handler.retrieve_connection_pool("primary")
assert_equal({ adapter: "postgresql", database: "bar", host: "localhost" }, pool.spec.underlying_configuration_hash)
assert_equal({ adapter: "postgresql", database: "bar", host: "localhost" }, pool.spec.db_config.configuration_hash)
end
ensure
ActiveRecord::Base.establish_connection(:arunit)
@ -182,7 +182,7 @@ module ActiveRecord
assert_equal handler, ActiveRecord::Base.connection_handlers[:writing]
assert_not_nil pool = handler.retrieve_connection_pool("primary")
assert_equal(config, pool.spec.underlying_configuration_hash)
assert_equal(config, pool.spec.db_config.configuration_hash)
end
ensure
ActiveRecord::Base.establish_connection(:arunit)
@ -222,7 +222,7 @@ module ActiveRecord
assert_equal handler, ActiveRecord::Base.connection_handlers[:writing]
assert_not_nil pool = handler.retrieve_connection_pool("primary")
assert_equal(config["default_env"]["animals"], pool.spec.underlying_configuration_hash)
assert_equal(config["default_env"]["animals"], pool.spec.db_config.configuration_hash)
end
ensure
ActiveRecord::Base.configurations = @prev_configs
@ -249,7 +249,7 @@ module ActiveRecord
assert_equal handler, ActiveRecord::Base.connection_handlers[:writing]
assert_not_nil pool = handler.retrieve_connection_pool("primary")
assert_equal(config["default_env"]["primary"], pool.spec.underlying_configuration_hash)
assert_equal(config["default_env"]["primary"], pool.spec.db_config.configuration_hash)
end
ensure
ActiveRecord::Base.configurations = @prev_configs
@ -284,7 +284,7 @@ module ActiveRecord
ActiveRecord::Base.connects_to database: { writing: :development, reading: :development_readonly }
assert_not_nil pool = ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("primary")
assert_equal "db/readonly.sqlite3", pool.spec.underlying_configuration_hash[:database]
assert_equal "db/readonly.sqlite3", pool.spec.db_config.configuration_hash[:database]
ensure
ActiveRecord::Base.configurations = @prev_configs
ActiveRecord::Base.establish_connection(:arunit)

View File

@ -1,15 +0,0 @@
# frozen_string_literal: true
require "cases/helper"
module ActiveRecord
module ConnectionAdapters
class ConnectionSpecificationTest < ActiveRecord::TestCase
def test_dup_deep_copy_config
db_config = ActiveRecord::DatabaseConfigurations::HashConfig.new("development", "primary", { a: :b })
spec = ConnectionSpecification.new("primary", db_config, "bar")
assert_not_equal(spec.underlying_configuration_hash.object_id, spec.dup.underlying_configuration_hash.object_id)
end
end
end
end

View File

@ -24,7 +24,7 @@ module ActiveRecord
def resolve_spec(spec, config)
configs = ActiveRecord::DatabaseConfigurations.new(config)
resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(configs)
resolver = ConnectionAdapters::Resolver.new(configs)
resolver.resolve(spec, spec).configuration_hash
end

View File

@ -199,7 +199,7 @@ module ActiveRecord
def test_idle_timeout_configuration
@pool.disconnect!
spec = ActiveRecord::Base.connection_pool.spec
spec.underlying_configuration_hash.merge!(idle_timeout: "0.02")
spec.db_config.configuration_hash.merge!(idle_timeout: "0.02")
@pool = ConnectionPool.new(spec)
idle_conn = @pool.checkout
@pool.checkin(idle_conn)
@ -224,7 +224,7 @@ module ActiveRecord
def test_disable_flush
@pool.disconnect!
spec = ActiveRecord::Base.connection_pool.spec
spec.underlying_configuration_hash.merge!(idle_timeout: -5)
spec.db_config.configuration_hash.merge!(idle_timeout: -5)
@pool = ConnectionPool.new(spec)
idle_conn = @pool.checkout
@pool.checkin(idle_conn)
@ -718,8 +718,12 @@ module ActiveRecord
private
def with_single_connection_pool
one_conn_spec = ActiveRecord::Base.connection_pool.spec.dup
one_conn_spec.underlying_configuration_hash[:pool] = 1 # this is safe to do, because .dupped ConnectionSpecification also auto-dups its config
old_config = ActiveRecord::Base.connection_pool.spec.db_config.configuration_hash
db_config = ActiveRecord::DatabaseConfigurations::HashConfig.new("arunit", "primary", old_config.dup)
one_conn_spec = ConnectionSpecification.new("primary", db_config)
one_conn_spec.db_config.configuration_hash[:pool] = 1 # this is safe to do, because .dupped ConnectionSpecification also auto-dups its config
yield(pool = ConnectionPool.new(one_conn_spec))
ensure
pool.disconnect! if pool

View File

@ -8,13 +8,13 @@ module ActiveRecord
class ResolverTest < ActiveRecord::TestCase
def resolve(spec, config = {})
configs = ActiveRecord::DatabaseConfigurations.new(config)
resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(configs)
resolver = ConnectionAdapters::Resolver.new(configs)
resolver.resolve(spec, spec).configuration_hash
end
def spec(spec, config = {})
configs = ActiveRecord::DatabaseConfigurations.new(config)
resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(configs)
resolver = ConnectionAdapters::Resolver.new(configs)
resolver.spec(spec)
end

View File

@ -37,7 +37,7 @@ end
def in_memory_db?
current_adapter?(:SQLite3Adapter) &&
ActiveRecord::Base.connection_pool.spec.underlying_configuration_hash[:database] == ":memory:"
ActiveRecord::Base.connection_pool.spec.db_config.configuration_hash[:database] == ":memory:"
end
def subsecond_precision_supported?

View File

@ -58,7 +58,8 @@ module ActiveRecord
end
def test_pool_has_reaper
spec = ActiveRecord::Base.connection_pool.spec.dup
config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", spec_name: "primary")
spec = ConnectionSpecification.new("primary", config)
pool = ConnectionPool.new spec
assert pool.reaper
@ -67,17 +68,19 @@ module ActiveRecord
end
def test_reaping_frequency_configuration
spec = ActiveRecord::Base.connection_pool.spec.dup
spec.underlying_configuration_hash[:reaping_frequency] = "10.01"
spec = duplicated_spec
spec.db_config.configuration_hash[:reaping_frequency] = "10.01"
pool = ConnectionPool.new spec
assert_equal 10.01, pool.reaper.frequency
ensure
pool.discard!
end
def test_connection_pool_starts_reaper
spec = ActiveRecord::Base.connection_pool.spec.dup
spec.underlying_configuration_hash[:reaping_frequency] = "0.0001"
spec = duplicated_spec
spec.db_config.configuration_hash[:reaping_frequency] = "0.0001"
pool = ConnectionPool.new spec
@ -94,8 +97,8 @@ module ActiveRecord
end
def test_reaper_works_after_pool_discard
spec = ActiveRecord::Base.connection_pool.spec.dup
spec.underlying_configuration_hash[:reaping_frequency] = "0.0001"
spec = duplicated_spec
spec.db_config.configuration_hash[:reaping_frequency] = "0.0001"
2.times do
pool = ConnectionPool.new spec
@ -116,7 +119,7 @@ module ActiveRecord
# This doesn't test the reaper directly, but we want to test the action
# it would take on a discarded pool
def test_reap_flush_on_discarded_pool
spec = ActiveRecord::Base.connection_pool.spec.dup
spec = duplicated_spec
pool = ConnectionPool.new spec
pool.discard!
@ -125,8 +128,8 @@ module ActiveRecord
end
def test_connection_pool_starts_reaper_in_fork
spec = ActiveRecord::Base.connection_pool.spec.dup
spec.underlying_configuration_hash[:reaping_frequency] = "0.0001"
spec = duplicated_spec
spec.db_config.configuration_hash[:reaping_frequency] = "0.0001"
pool = ConnectionPool.new spec
pool.checkout
@ -169,26 +172,33 @@ module ActiveRecord
pool.discard!
end
def new_conn_in_thread(pool)
event = Concurrent::Event.new
conn = nil
child = Thread.new do
conn = pool.checkout
event.set
Thread.stop
private
def duplicated_spec
old_config = ActiveRecord::Base.connection_pool.spec.db_config.configuration_hash
db_config = ActiveRecord::DatabaseConfigurations::HashConfig.new("arunit", "primary", old_config.dup)
ConnectionSpecification.new("primary", db_config)
end
event.wait
[conn, child]
end
def new_conn_in_thread(pool)
event = Concurrent::Event.new
conn = nil
def wait_for_conn_idle(conn, timeout = 5)
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
while conn.in_use? && Process.clock_gettime(Process::CLOCK_MONOTONIC) - start < timeout
Thread.pass
child = Thread.new do
conn = pool.checkout
event.set
Thread.stop
end
event.wait
[conn, child]
end
def wait_for_conn_idle(conn, timeout = 5)
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
while conn.in_use? && Process.clock_gettime(Process::CLOCK_MONOTONIC) - start < timeout
Thread.pass
end
end
end
end
end
end

View File

@ -359,7 +359,7 @@ module ApplicationTests
db_migrate_and_schema_dump_and_load "schema"
app_file "db/seeds.rb", <<-RUBY
print Book.connection.pool.spec.underlying_configuration_hash[:database]
print Book.connection.pool.spec.db_config.configuration_hash[:database]
RUBY
output = rails("db:seed")