2017-07-09 13:41:28 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
require "config"
|
2009-02-03 21:25:37 -05:00
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
require "stringio"
|
2008-01-21 12:20:51 -05:00
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
require "active_record"
|
|
|
|
require "cases/test_case"
|
|
|
|
require "active_support/dependencies"
|
|
|
|
require "active_support/logger"
|
2020-05-09 14:43:05 -04:00
|
|
|
require "active_support/core_ext/kernel/singleton_class"
|
2011-06-04 18:19:17 -04:00
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
require "support/config"
|
|
|
|
require "support/connection"
|
2011-06-04 18:19:17 -04:00
|
|
|
|
|
|
|
# TODO: Move all these random hacks into the ARTest namespace and into the support/ dir
|
2008-01-21 12:20:51 -05:00
|
|
|
|
2013-01-24 05:10:58 -05:00
|
|
|
Thread.abort_on_exception = true
|
|
|
|
|
2008-01-21 12:20:51 -05:00
|
|
|
# Show backtraces for deprecated behavior for quicker cleanup.
|
|
|
|
ActiveSupport::Deprecation.debug = true
|
|
|
|
|
2013-12-16 13:04:07 -05:00
|
|
|
# Disable available locale checks to avoid warnings running the test suite.
|
|
|
|
I18n.enforce_available_locales = false
|
|
|
|
|
2011-06-09 15:15:34 -04:00
|
|
|
# Connect to the database
|
|
|
|
ARTest.connect
|
|
|
|
|
|
|
|
# Quote "type" if it's a reserved word for the current connection.
|
2016-08-06 12:26:20 -04:00
|
|
|
QUOTED_TYPE = ActiveRecord::Base.connection.quote_column_name("type")
|
2011-06-09 15:15:34 -04:00
|
|
|
|
2008-01-21 12:20:51 -05:00
|
|
|
def current_adapter?(*types)
|
|
|
|
types.any? do |type|
|
|
|
|
ActiveRecord::ConnectionAdapters.const_defined?(type) &&
|
|
|
|
ActiveRecord::Base.connection.is_a?(ActiveRecord::ConnectionAdapters.const_get(type))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2011-01-08 13:33:33 -05:00
|
|
|
def in_memory_db?
|
2012-04-27 02:31:37 -04:00
|
|
|
current_adapter?(:SQLite3Adapter) &&
|
2019-09-18 06:26:59 -04:00
|
|
|
ActiveRecord::Base.connection_pool.db_config.database == ":memory:"
|
2011-01-08 13:33:33 -05:00
|
|
|
end
|
|
|
|
|
2014-09-02 01:35:56 -04:00
|
|
|
def mysql_enforcing_gtid_consistency?
|
2016-08-06 12:26:20 -04:00
|
|
|
current_adapter?(:Mysql2Adapter) && "ON" == ActiveRecord::Base.connection.show_variable("enforce_gtid_consistency")
|
2014-09-02 01:35:56 -04:00
|
|
|
end
|
|
|
|
|
2018-10-24 21:11:31 -04:00
|
|
|
def supports_default_expression?
|
|
|
|
if current_adapter?(:PostgreSQLAdapter)
|
|
|
|
true
|
|
|
|
elsif current_adapter?(:Mysql2Adapter)
|
|
|
|
conn = ActiveRecord::Base.connection
|
2019-03-29 11:18:48 -04:00
|
|
|
!conn.mariadb? && conn.database_version >= "8.0.13"
|
2018-10-24 21:11:31 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-03-05 14:16:44 -05:00
|
|
|
%w[
|
|
|
|
supports_savepoints?
|
|
|
|
supports_partial_index?
|
2019-10-29 07:54:03 -04:00
|
|
|
supports_partitioned_indexes?
|
2020-06-02 18:18:27 -04:00
|
|
|
supports_expression_index?
|
2019-03-05 14:16:44 -05:00
|
|
|
supports_insert_returning?
|
|
|
|
supports_insert_on_duplicate_skip?
|
|
|
|
supports_insert_on_duplicate_update?
|
|
|
|
supports_insert_conflict_target?
|
2019-03-12 20:29:00 -04:00
|
|
|
supports_optimizer_hints?
|
2020-04-24 13:56:53 -04:00
|
|
|
supports_datetime_with_precision?
|
2019-03-05 14:16:44 -05:00
|
|
|
].each do |method_name|
|
|
|
|
define_method method_name do
|
|
|
|
ActiveRecord::Base.connection.public_send(method_name)
|
|
|
|
end
|
2011-01-09 10:52:41 -05:00
|
|
|
end
|
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
def with_env_tz(new_tz = "US/Eastern")
|
|
|
|
old_tz, ENV["TZ"] = ENV["TZ"], new_tz
|
2010-09-20 15:15:18 -04:00
|
|
|
yield
|
|
|
|
ensure
|
2016-08-06 12:26:20 -04:00
|
|
|
old_tz ? ENV["TZ"] = old_tz : ENV.delete("TZ")
|
2010-09-20 15:15:18 -04:00
|
|
|
end
|
|
|
|
|
2013-10-24 15:26:23 -04:00
|
|
|
def with_timezone_config(cfg)
|
|
|
|
verify_default_timezone_config
|
|
|
|
|
|
|
|
old_default_zone = ActiveRecord::Base.default_timezone
|
|
|
|
old_awareness = ActiveRecord::Base.time_zone_aware_attributes
|
|
|
|
old_zone = Time.zone
|
|
|
|
|
|
|
|
if cfg.has_key?(:default)
|
|
|
|
ActiveRecord::Base.default_timezone = cfg[:default]
|
|
|
|
end
|
|
|
|
if cfg.has_key?(:aware_attributes)
|
|
|
|
ActiveRecord::Base.time_zone_aware_attributes = cfg[:aware_attributes]
|
|
|
|
end
|
|
|
|
if cfg.has_key?(:zone)
|
|
|
|
Time.zone = cfg[:zone]
|
|
|
|
end
|
2010-09-20 15:15:18 -04:00
|
|
|
yield
|
|
|
|
ensure
|
2013-10-24 15:26:23 -04:00
|
|
|
ActiveRecord::Base.default_timezone = old_default_zone
|
|
|
|
ActiveRecord::Base.time_zone_aware_attributes = old_awareness
|
|
|
|
Time.zone = old_zone
|
|
|
|
end
|
|
|
|
|
|
|
|
# This method makes sure that tests don't leak global state related to time zones.
|
|
|
|
EXPECTED_ZONE = nil
|
|
|
|
EXPECTED_DEFAULT_TIMEZONE = :utc
|
|
|
|
EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES = false
|
|
|
|
def verify_default_timezone_config
|
|
|
|
if Time.zone != EXPECTED_ZONE
|
2013-10-25 09:59:04 -04:00
|
|
|
$stderr.puts <<-MSG
|
2014-10-29 21:18:48 -04:00
|
|
|
\n#{self}
|
2013-10-24 15:26:23 -04:00
|
|
|
Global state `Time.zone` was leaked.
|
|
|
|
Expected: #{EXPECTED_ZONE}
|
|
|
|
Got: #{Time.zone}
|
|
|
|
MSG
|
|
|
|
end
|
|
|
|
if ActiveRecord::Base.default_timezone != EXPECTED_DEFAULT_TIMEZONE
|
2013-10-25 09:59:04 -04:00
|
|
|
$stderr.puts <<-MSG
|
2014-10-29 21:18:48 -04:00
|
|
|
\n#{self}
|
2013-10-24 15:26:23 -04:00
|
|
|
Global state `ActiveRecord::Base.default_timezone` was leaked.
|
|
|
|
Expected: #{EXPECTED_DEFAULT_TIMEZONE}
|
|
|
|
Got: #{ActiveRecord::Base.default_timezone}
|
|
|
|
MSG
|
|
|
|
end
|
|
|
|
if ActiveRecord::Base.time_zone_aware_attributes != EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES
|
2013-10-25 09:59:04 -04:00
|
|
|
$stderr.puts <<-MSG
|
2014-10-29 21:18:48 -04:00
|
|
|
\n#{self}
|
2013-10-24 15:26:23 -04:00
|
|
|
Global state `ActiveRecord::Base.time_zone_aware_attributes` was leaked.
|
|
|
|
Expected: #{EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES}
|
|
|
|
Got: #{ActiveRecord::Base.time_zone_aware_attributes}
|
|
|
|
MSG
|
|
|
|
end
|
2010-09-20 15:15:18 -04:00
|
|
|
end
|
|
|
|
|
2014-07-09 17:10:49 -04:00
|
|
|
def enable_extension!(extension, connection)
|
2013-08-25 05:22:36 -04:00
|
|
|
return false unless connection.supports_extensions?
|
2014-07-09 17:10:49 -04:00
|
|
|
return connection.reconnect! if connection.extension_enabled?(extension)
|
2013-08-25 05:22:36 -04:00
|
|
|
|
2014-07-09 17:10:49 -04:00
|
|
|
connection.enable_extension extension
|
2015-02-12 08:44:47 -05:00
|
|
|
connection.commit_db_transaction if connection.transaction_open?
|
2013-08-25 05:22:36 -04:00
|
|
|
connection.reconnect!
|
|
|
|
end
|
|
|
|
|
2014-07-09 17:10:49 -04:00
|
|
|
def disable_extension!(extension, connection)
|
|
|
|
return false unless connection.supports_extensions?
|
|
|
|
return true unless connection.extension_enabled?(extension)
|
|
|
|
|
|
|
|
connection.disable_extension extension
|
|
|
|
connection.reconnect!
|
|
|
|
end
|
|
|
|
|
Implement granular role and shard swapping
This change allows for a connection to be swapped on role or shard for a
class. Previously calling `connected_to` would swap all the connections
to a particular role or shard. Granular connection swapping is useful
for swapping one connection to reading while leaving all other
connection classes on writing.
The public methods on connection handler have been updated to behave the
same as they did previously on the different handlers. The difference
however is instead of calling
`ActiveRecord::Base.connection_handlers[:reading].clear_all_connections!`
you now call
`ActiveRecord::Base.connection_handler.clear_all_connections!` which
will clear based on current role set by a `connected_to` block. Outside
the context of a `connected_to` block, `clear_all_connections!` can take
an optional parameter to clear specific connections by role.
The major changes in this PR are:
* We introduced a `legacy_connection_handling` configuration option that
is set to true by default. It will be set to `false` for all new
applications.
* In the new connection handling there will be one only connection
handler. Previously there was a connection handler for each role. Now
the role is stored in the `PoolManager`. In order to maintain backwards
compatibility we introduced a `LegacyPoolManager` to avoid duplicate
conditionals. See diagram in PR body for changes to connection
management.
* `connected_to` will now use a stacked concurrent map to keep track of
the connection for each class. For each opened block the `class`,
`role`, and `shard` will be added to the stack, when the block is exited
the `class`, `role`, `shard` array will be removed from the stack.
* With these changes `ActiveRecord::Base.connected_to` will remain
global. If called all connections in the block will use the `role` and
`shard` that was switched to. If called with a parent class like
`AnimalsRecord.connected_to` only models under `AnimalsRecord` will be
switched and everything else will remain the same.
Examples:
Given an application we have a `User` model that inherits from
`ApplicationRecord` and a `Dog` model that inherits from
`AnimalsRecord`. `AnimalsRecord` and `ApplicationRecord` have writing
and reading connections as well as shard `default`, `one`, and `two`.
```ruby
ActiveRecord::Base.connected_to(role: :reading) do
User.first # reads from default replica
Dog.first # reads from default replica
AnimalsRecord.connected_to(role: :writing, shard: :one) do
User.first # reads from default replica
Dog.first # reads from shard one primary
end
User.first # reads from default replica
Dog.first # reads from default replica
ApplicationRecord.connected_to(role: :writing, shard: :two) do
User.first # reads from shard two primary
Dog.first # reads from default replica
end
end
```
Things this PR does not solve:
* Currently there is no API for swapping more than one but not all
connections. Apps with many primaries may want to swap 3 but not all 10
connections. We plan to build an API for that in a followup PR.
* The middleware remains the same and is using the global switching
methods. Therefore at this time to use this new feature applications
must manually switch connections. We will also address this in a
followup PR.
* The `schema_cache` is currently on the `PoolConfig`. We plan on trying
to move this up to the `PoolManager` or elsewhere later on so each
`PoolConfig` doesn't need to hold a reference to the `schema_cache`.
Co-authored-by: John Crepezzi <john.crepezzi@gmail.com>
2020-09-11 16:06:25 -04:00
|
|
|
def clean_up_legacy_connection_handlers
|
|
|
|
handler = ActiveRecord::Base.default_connection_handler
|
|
|
|
ActiveRecord::Base.connection_handlers = {}
|
|
|
|
|
|
|
|
handler.connection_pool_names.each do |name|
|
|
|
|
next if ["ActiveRecord::Base", "ARUnit2Model", "Contact", "ContactSti"].include?(name)
|
|
|
|
|
|
|
|
handler.send(:owner_to_pool_manager).delete(name)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-08-10 09:51:43 -04:00
|
|
|
def clean_up_connection_handler
|
Implement granular role and shard swapping
This change allows for a connection to be swapped on role or shard for a
class. Previously calling `connected_to` would swap all the connections
to a particular role or shard. Granular connection swapping is useful
for swapping one connection to reading while leaving all other
connection classes on writing.
The public methods on connection handler have been updated to behave the
same as they did previously on the different handlers. The difference
however is instead of calling
`ActiveRecord::Base.connection_handlers[:reading].clear_all_connections!`
you now call
`ActiveRecord::Base.connection_handler.clear_all_connections!` which
will clear based on current role set by a `connected_to` block. Outside
the context of a `connected_to` block, `clear_all_connections!` can take
an optional parameter to clear specific connections by role.
The major changes in this PR are:
* We introduced a `legacy_connection_handling` configuration option that
is set to true by default. It will be set to `false` for all new
applications.
* In the new connection handling there will be one only connection
handler. Previously there was a connection handler for each role. Now
the role is stored in the `PoolManager`. In order to maintain backwards
compatibility we introduced a `LegacyPoolManager` to avoid duplicate
conditionals. See diagram in PR body for changes to connection
management.
* `connected_to` will now use a stacked concurrent map to keep track of
the connection for each class. For each opened block the `class`,
`role`, and `shard` will be added to the stack, when the block is exited
the `class`, `role`, `shard` array will be removed from the stack.
* With these changes `ActiveRecord::Base.connected_to` will remain
global. If called all connections in the block will use the `role` and
`shard` that was switched to. If called with a parent class like
`AnimalsRecord.connected_to` only models under `AnimalsRecord` will be
switched and everything else will remain the same.
Examples:
Given an application we have a `User` model that inherits from
`ApplicationRecord` and a `Dog` model that inherits from
`AnimalsRecord`. `AnimalsRecord` and `ApplicationRecord` have writing
and reading connections as well as shard `default`, `one`, and `two`.
```ruby
ActiveRecord::Base.connected_to(role: :reading) do
User.first # reads from default replica
Dog.first # reads from default replica
AnimalsRecord.connected_to(role: :writing, shard: :one) do
User.first # reads from default replica
Dog.first # reads from shard one primary
end
User.first # reads from default replica
Dog.first # reads from default replica
ApplicationRecord.connected_to(role: :writing, shard: :two) do
User.first # reads from shard two primary
Dog.first # reads from default replica
end
end
```
Things this PR does not solve:
* Currently there is no API for swapping more than one but not all
connections. Apps with many primaries may want to swap 3 but not all 10
connections. We plan to build an API for that in a followup PR.
* The middleware remains the same and is using the global switching
methods. Therefore at this time to use this new feature applications
must manually switch connections. We will also address this in a
followup PR.
* The `schema_cache` is currently on the `PoolConfig`. We plan on trying
to move this up to the `PoolManager` or elsewhere later on so each
`PoolConfig` doesn't need to hold a reference to the `schema_cache`.
Co-authored-by: John Crepezzi <john.crepezzi@gmail.com>
2020-09-11 16:06:25 -04:00
|
|
|
handler = ActiveRecord::Base.connection_handler
|
|
|
|
handler.instance_variable_get(:@owner_to_pool_manager).each do |owner, pool_manager|
|
|
|
|
pool_manager.role_names.each do |role_name|
|
|
|
|
next if role_name == ActiveRecord::Base.default_role
|
|
|
|
pool_manager.remove_role(role_name)
|
|
|
|
end
|
|
|
|
end
|
2020-08-10 09:51:43 -04:00
|
|
|
end
|
|
|
|
|
2011-01-08 13:33:33 -05:00
|
|
|
def load_schema
|
|
|
|
# silence verbose schema loading
|
|
|
|
original_stdout = $stdout
|
|
|
|
$stdout = StringIO.new
|
2009-05-17 14:51:35 -04:00
|
|
|
|
|
|
|
adapter_name = ActiveRecord::Base.connection.adapter_name.downcase
|
|
|
|
adapter_specific_schema_file = SCHEMA_ROOT + "/#{adapter_name}_specific_schema.rb"
|
|
|
|
|
|
|
|
load SCHEMA_ROOT + "/schema.rb"
|
|
|
|
|
2013-11-01 04:49:57 -04:00
|
|
|
if File.exist?(adapter_specific_schema_file)
|
2009-05-17 14:51:35 -04:00
|
|
|
load adapter_specific_schema_file
|
|
|
|
end
|
2017-06-03 02:58:54 -04:00
|
|
|
|
|
|
|
ActiveRecord::FixtureSet.reset_cache
|
2009-05-17 14:51:35 -04:00
|
|
|
ensure
|
|
|
|
$stdout = original_stdout
|
|
|
|
end
|
2010-07-26 08:06:14 -04:00
|
|
|
|
2011-01-08 13:33:33 -05:00
|
|
|
load_schema
|
|
|
|
|
2013-10-04 18:16:49 -04:00
|
|
|
class SQLSubscriber
|
|
|
|
attr_reader :logged
|
2013-10-04 19:51:35 -04:00
|
|
|
attr_reader :payloads
|
2013-10-04 18:16:49 -04:00
|
|
|
|
|
|
|
def initialize
|
|
|
|
@logged = []
|
2013-10-04 19:51:35 -04:00
|
|
|
@payloads = []
|
2013-10-04 18:16:49 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def start(name, id, payload)
|
2013-10-04 19:51:35 -04:00
|
|
|
@payloads << payload
|
2014-03-24 23:38:10 -04:00
|
|
|
@logged << [payload[:sql].squish, payload[:name], payload[:binds]]
|
2013-10-04 18:16:49 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def finish(name, id, payload); end
|
|
|
|
end
|
|
|
|
|
2012-11-17 11:01:35 -05:00
|
|
|
module InTimeZone
|
|
|
|
private
|
2016-08-06 13:55:02 -04:00
|
|
|
def in_time_zone(zone)
|
|
|
|
old_zone = Time.zone
|
|
|
|
old_tz = ActiveRecord::Base.time_zone_aware_attributes
|
|
|
|
|
|
|
|
Time.zone = zone ? ActiveSupport::TimeZone[zone] : nil
|
|
|
|
ActiveRecord::Base.time_zone_aware_attributes = !zone.nil?
|
|
|
|
yield
|
|
|
|
ensure
|
|
|
|
Time.zone = old_zone
|
|
|
|
ActiveRecord::Base.time_zone_aware_attributes = old_tz
|
|
|
|
end
|
2012-11-17 10:35:39 -05:00
|
|
|
end
|