diff --git a/Changelog.md b/Changelog.md index fd2dfe9b..653d6a4f 100644 --- a/Changelog.md +++ b/Changelog.md @@ -6,6 +6,7 @@ and as of v1.0.0 this project adheres to [Semantic Versioning](https://semver.or ## [Unreleased] +- Remove `mysql2` as a direct dependency, test Rails 7.0 with `mysql2` and `activerecord-trilogy-adapter` - Drop Ruby 2.6. - Start testing with Ruby 3.2. diff --git a/Readme.md b/Readme.md index 98a6d9fa..e15e41f9 100644 --- a/Readme.md +++ b/Readme.md @@ -43,7 +43,7 @@ test_pool_2 => 127.0.0.1/3306//root/false ## Support -For now, the only backend known to work is MySQL, with the mysql2 gem. +For now, the only backend known to work is MySQL, with the mysql2 or activerecord-trilogy-adapter gem. Postgres, from an informal reading of the docs, will never support the concept of one server connection sharing multiple dbs. ## Installation diff --git a/lib/active_record_host_pool.rb b/lib/active_record_host_pool.rb index 1ca2b04c..cb6066ea 100644 --- a/lib/active_record_host_pool.rb +++ b/lib/active_record_host_pool.rb @@ -1,5 +1,19 @@ # frozen_string_literal: true +module ActiveRecordHostPool + class << self + attr_accessor :loaded_db_adapter + end +end + +begin + require 'mysql2' + ActiveRecordHostPool.loaded_db_adapter = :mysql2 +rescue LoadError + require 'activerecord-trilogy-adapter' + ActiveRecordHostPool.loaded_db_adapter = :trilogy +end + require 'active_record' require 'active_record/base' require 'active_record/connection_adapters/abstract_adapter' @@ -9,6 +23,3 @@ require 'active_record_host_pool/pool_proxy' require 'active_record_host_pool/connection_adapter_mixin' require 'active_record_host_pool/version' - -module ActiveRecordHostPool -end diff --git a/lib/active_record_host_pool/connection_adapter_mixin.rb b/lib/active_record_host_pool/connection_adapter_mixin.rb index 28a22d80..7479cba3 100644 --- a/lib/active_record_host_pool/connection_adapter_mixin.rb +++ b/lib/active_record_host_pool/connection_adapter_mixin.rb @@ -1,6 +1,13 @@ # frozen_string_literal: true -require "active_record/connection_adapters/mysql2_adapter" +case ActiveRecordHostPool.loaded_db_adapter +when :mysql2 + require "active_record/connection_adapters/mysql2_adapter" +when :trilogy + require 'trilogy_adapter/connection' + require "trilogy_adapter/errors" + ActiveRecord::Base.extend TrilogyAdapter::Connection +end module ActiveRecordHostPool module DatabaseSwitch @@ -75,13 +82,24 @@ def _switch_connection ) log("select_db #{_host_pool_current_database}", "SQL") do clear_cache! if respond_to?(:clear_cache!) - raw_connection.select_db(_host_pool_current_database) + _arhp_select_db(_host_pool_current_database) end @_cached_current_database = _host_pool_current_database @_cached_connection_object_id = @connection.object_id end end + case ActiveRecordHostPool.loaded_db_adapter + when :mysql2 + def _arhp_select_db(database) + raw_connection.select_db(database) + end + when :trilogy + def _arhp_select_db(database) # rubocop:disable Lint/DuplicateMethods + raw_connection.change_db(database) + end + end + # prevent different databases from sharing the same query cache def cache_sql(sql, *args) super(_host_pool_current_database.to_s + "/" + sql, *args) @@ -122,7 +140,12 @@ def establish_connection(spec) end end -ActiveRecord::ConnectionAdapters::Mysql2Adapter.include(ActiveRecordHostPool::DatabaseSwitch) +case ActiveRecordHostPool.loaded_db_adapter +when :mysql2 + ActiveRecord::ConnectionAdapters::Mysql2Adapter.include(ActiveRecordHostPool::DatabaseSwitch) +when :trilogy + ActiveRecord::ConnectionAdapters::TrilogyAdapter.include(ActiveRecordHostPool::DatabaseSwitch) +end # In Rails 6.1 Connection Pools are no longer instantiated in #establish_connection but in a # new pool method. diff --git a/lib/active_record_host_pool/pool_proxy_6_1.rb b/lib/active_record_host_pool/pool_proxy_6_1.rb index d8e197f2..5df8025b 100644 --- a/lib/active_record_host_pool/pool_proxy_6_1.rb +++ b/lib/active_record_host_pool/pool_proxy_6_1.rb @@ -15,6 +15,13 @@ module ActiveRecordHostPool # Sits between ConnectionHandler and a bunch of different ConnectionPools (one per host). class PoolProxy < Delegator + case ActiveRecordHostPool.loaded_db_adapter + when :mysql2 + RESCUABLE_DB_ERROR = Mysql2::Error + when :trilogy + RESCUABLE_DB_ERROR = Trilogy::ProtocolError + end + def initialize(pool_config) super(pool_config) @pool_config = pool_config @@ -36,7 +43,7 @@ def __setobj__(pool_config) def connection(*args) real_connection = _unproxied_connection(*args) _connection_proxy_for(real_connection, @config[:database]) - rescue Mysql2::Error, ActiveRecord::NoDatabaseError + rescue RESCUABLE_DB_ERROR, ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid _connection_pools.delete(_pool_key) Kernel.raise end diff --git a/test/database.yml b/test/database.yml index 2546a861..e8f529e1 100644 --- a/test/database.yml +++ b/test/database.yml @@ -1,4 +1,6 @@ <% mysql = URI(ENV['MYSQL_URL'] || 'mysql://root@127.0.0.1:3306') %> +<% adapter = ActiveRecordHostPool.loaded_db_adapter %> + # ARHP creates separate connection pools based on the pool key. # The pool key is defined as: # host / port / socket / username / replica @@ -28,65 +30,65 @@ # test_pool_2 => 127.0.0.1/3306//root/false test_pool_1_db_a: - adapter: mysql2 + adapter: <%= adapter %> encoding: utf8 database: arhp_test_db_a username: <%= mysql.user %> - password: <%= mysql.password %> + password: "<%= mysql.password %>" host: <%= mysql.host %> reconnect: true # Mimic configurations as read by active_record_shards/ar_flexmaster test_pool_1_db_a_replica: - adapter: mysql2 + adapter: <%= adapter %> encoding: utf8 database: arhp_test_db_a_replica username: <%= mysql.user %> - password: <%= mysql.password %> + password: "<%= mysql.password %>" host: <%= mysql.host %> reconnect: true slave: true test_pool_1_db_b: - adapter: mysql2 + adapter: <%= adapter %> encoding: utf8 database: arhp_test_db_b username: <%= mysql.user %> - password: <%= mysql.password %> + password: "<%= mysql.password %>" host: <%= mysql.host %> reconnect: true test_pool_1_db_not_there: - adapter: mysql2 + adapter: <%= adapter %> encoding: utf8 database: arhp_test_db_not_there username: <%= mysql.user %> - password: <%= mysql.password %> + password: "<%= mysql.password %>" host: <%= mysql.host %> reconnect: true test_pool_2_db_d: - adapter: mysql2 + adapter: <%= adapter %> encoding: utf8 database: arhp_test_db_d username: <%= mysql.user %> - password: <%= mysql.password %> + password: "<%= mysql.password %>" host: <%= mysql.host %> port: <%= mysql.port %> reconnect: true test_pool_2_db_e: - adapter: mysql2 + adapter: <%= adapter %> encoding: utf8 database: arhp_test_db_e username: <%= mysql.user %> - password: <%= mysql.password %> + password: "<%= mysql.password %>" host: <%= mysql.host %> port: <%= mysql.port %> reconnect: true test_pool_3_db_e: - adapter: mysql2 + adapter: <%= adapter %> encoding: utf8 database: arhp_test_db_e username: john-doe @@ -99,10 +101,10 @@ test_pool_3_db_e: # otherwise the test_models_with_matching_hosts_and_non_matching_databases_issue_exists_without_arhp_patch # test fails test_pool_1_db_c: - adapter: mysql2 + adapter: <%= adapter %> encoding: utf8 database: arhp_test_db_c username: <%= mysql.user %> - password: <%= mysql.password %> + password: "<%= mysql.password %>" host: <%= mysql.host %> reconnect: true diff --git a/test/test_arhp_caching.rb b/test/test_arhp_caching.rb index ee9c4973..cd795e9c 100644 --- a/test/test_arhp_caching.rb +++ b/test/test_arhp_caching.rb @@ -63,7 +63,13 @@ def test_models_with_matching_hosts_and_non_matching_databases_issue_exists_with end cached_db = Pool1DbC.connection.unproxied.pool.connections.first.instance_variable_get(:@_cached_current_database) - assert_equal("Mysql2::Error: Table '#{cached_db}.pool1_db_cs' doesn't exist", exception.message) + + case ActiveRecordHostPool.loaded_db_adapter + when :mysql2 + assert_equal("Mysql2::Error: Table '#{cached_db}.pool1_db_cs' doesn't exist", exception.message) + when :trilogy + assert_equal("Trilogy::ProtocolError: 1146: Table '#{cached_db}.pool1_db_cs' doesn't exist", exception.message) + end end end diff --git a/test/test_arhp_wrong_db.rb b/test/test_arhp_wrong_db.rb index 300fa6c6..be7b476e 100644 --- a/test/test_arhp_wrong_db.rb +++ b/test/test_arhp_wrong_db.rb @@ -10,10 +10,10 @@ def setup else Phenix.load_database_config end - ActiveRecordHostPool::PoolProxy.class_variable_set(:@@_connection_pools, {}) end def teardown + ActiveRecordHostPool::PoolProxy.class_variable_set(:@@_connection_pools, {}) Phenix.burn! end diff --git a/test/three_tier_database.yml b/test/three_tier_database.yml index d5678f76..44ab0db8 100644 --- a/test/three_tier_database.yml +++ b/test/three_tier_database.yml @@ -1,4 +1,5 @@ <% mysql = URI(ENV['MYSQL_URL'] || 'mysql://root@127.0.0.1:3306') %> +<% adapter = ActiveRecordHostPool.loaded_db_adapter %> # This .yml file is loaded in Rails 6.1 when ActiveRecord::Base.legacy_connection_handling = false # @@ -36,142 +37,142 @@ test: test_pool_1_db_a: - adapter: mysql2 + adapter: <%= adapter %> encoding: utf8 database: arhp_test_db_a username: <%= mysql.user %> - password: <%= mysql.password %> + password: "<%= mysql.password %>" host: <%= mysql.host %> reconnect: true # Mimic configurations as read by active_record_shards/ar_flexmaster test_pool_1_db_a_replica: - adapter: mysql2 + adapter: <%= adapter %> encoding: utf8 database: arhp_test_db_a_replica username: <%= mysql.user %> - password: <%= mysql.password %> + password: "<%= mysql.password %>" host: <%= mysql.host %> reconnect: true replica: true test_pool_1_db_b: - adapter: mysql2 + adapter: <%= adapter %> encoding: utf8 database: arhp_test_db_b username: <%= mysql.user %> - password: <%= mysql.password %> + password: "<%= mysql.password %>" host: <%= mysql.host %> reconnect: true test_pool_1_db_c: - adapter: mysql2 + adapter: <%= adapter %> encoding: utf8 database: arhp_test_db_c username: <%= mysql.user %> - password: <%= mysql.password %> + password: "<%= mysql.password %>" host: <%= mysql.host %> reconnect: true test_pool_1_db_not_there: - adapter: mysql2 + adapter: <%= adapter %> encoding: utf8 database: arhp_test_db_not_there username: <%= mysql.user %> - password: <%= mysql.password %> + password: "<%= mysql.password %>" host: <%= mysql.host %> reconnect: true test_pool_1_db_shard_a: - adapter: mysql2 + adapter: <%= adapter %> encoding: utf8 database: arhp_test_db_shard_a username: <%= mysql.user %> - password: <%= mysql.password %> + password: "<%= mysql.password %>" host: <%= mysql.host %> reconnect: true test_pool_1_db_shard_b: - adapter: mysql2 + adapter: <%= adapter %> encoding: utf8 database: arhp_test_db_shard_b username: <%= mysql.user %> - password: <%= mysql.password %> + password: "<%= mysql.password %>" host: <%= mysql.host %> reconnect: true test_pool_1_db_shard_b_replica: - adapter: mysql2 + adapter: <%= adapter %> encoding: utf8 database: arhp_test_db_shard_b_replica username: <%= mysql.user %> - password: <%= mysql.password %> + password: "<%= mysql.password %>" host: <%= mysql.host %> reconnect: true replica: true test_pool_1_db_shard_c: - adapter: mysql2 + adapter: <%= adapter %> encoding: utf8 database: arhp_test_db_shard_c username: <%= mysql.user %> - password: <%= mysql.password %> + password: "<%= mysql.password %>" host: <%= mysql.host %> reconnect: true test_pool_1_db_shard_c_replica: - adapter: mysql2 + adapter: <%= adapter %> encoding: utf8 database: arhp_test_db_shard_c_replica username: <%= mysql.user %> - password: <%= mysql.password %> + password: "<%= mysql.password %>" host: <%= mysql.host %> reconnect: true replica: true test_pool_2_db_shard_d: - adapter: mysql2 + adapter: <%= adapter %> encoding: utf8 database: arhp_test_db_shard_d username: <%= mysql.user %> - password: <%= mysql.password %> + password: "<%= mysql.password %>" host: <%= mysql.host %> port: <%= mysql.port %> reconnect: true test_pool_2_db_shard_d_replica: - adapter: mysql2 + adapter: <%= adapter %> encoding: utf8 database: arhp_test_db_shard_d_replica username: <%= mysql.user %> - password: <%= mysql.password %> + password: "<%= mysql.password %>" host: <%= mysql.host %> port: <%= mysql.port %> reconnect: true replica: true test_pool_2_db_d: - adapter: mysql2 + adapter: <%= adapter %> encoding: utf8 database: arhp_test_db_d username: <%= mysql.user %> - password: <%= mysql.password %> + password: "<%= mysql.password %>" host: <%= mysql.host %> port: <%= mysql.port %> reconnect: true test_pool_2_db_e: - adapter: mysql2 + adapter: <%= adapter %> encoding: utf8 database: arhp_test_db_e username: <%= mysql.user %> - password: <%= mysql.password %> + password: "<%= mysql.password %>" host: <%= mysql.host %> port: <%= mysql.port %> reconnect: true test_pool_3_db_e: - adapter: mysql2 + adapter: <%= adapter %> encoding: utf8 database: arhp_test_db_e username: john-doe