active_record_host_pool 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5238d6f59f78811a9e9972e613562d674d1137604438b46d0ef3b9a3eb1fa437
4
- data.tar.gz: 937e6d46772dc13b3b6faa62841053252aae17f92f8ef205b63927345408ed65
3
+ metadata.gz: 7baaf5e8c49a8308e3be6a9f52b41af880f0d8182a98a12be2ce8fa6d7ccbe60
4
+ data.tar.gz: aa68d84f75c00f608d13d475761f936722171f3d3d59a4ede471513279e8cda2
5
5
  SHA512:
6
- metadata.gz: bdbd8abf041ad7960996bbd0432fc19c90c0ae0c41328069798c401929cca9c3b9dd510f7160ab833895962b12e86937bf8eb592d851bea2197b94fcdf57b95a
7
- data.tar.gz: '068fff2a8be21b0e1ca192a49967c352d90b5c81aae27c11f36128ec825b4c03a9087f56269b10ca2cf406ddea445dc48662110284f1a3870eb4d48df0ceba7b'
6
+ metadata.gz: 072b56581266326d5decfd1221ccb9bf2ffde945cd04e1ce52278dd89d3b9e84b4d59d0a8d8e21808352d25a0f360ed67a006f71dc112751b7937a42f21aa455
7
+ data.tar.gz: c6075399e78ec309aeca872079044b042a5208dc1071c068eac01928a83b588d23cfc0d268907c0a9aa5c13a7d9b99005443e644b8142cb7ad9cb8a0e7cf013a
data/Readme.md CHANGED
@@ -5,6 +5,42 @@
5
5
  This gem allows for one ActiveRecord connection to be used to connect to multiple databases on a server.
6
6
  It accomplishes this by calling select_db() as necessary to switch databases between database calls.
7
7
 
8
+ ## How Connections Are Pooled
9
+
10
+ ARHP creates separate connection pools based on the pool key.
11
+
12
+ The pool key is defined as:
13
+
14
+ `host / port / socket / username / replica`
15
+
16
+ Therefore two databases with identical host, port, socket, username, and replica status will share a connection pool.
17
+ If any part (host, port, etc.) of the pool key differ, two databases will _not_ share a connection pool.
18
+
19
+ `replica` in the pool key is a boolean indicating if the database is a replica/reader (true) or writer database (false).
20
+
21
+ Below, `test_pool_1` and `test_pool_2` have identical host, username, socket, and replica status but the port information differs.
22
+ Here the database configurations are formatted as a table to give a visual example:
23
+
24
+ | | test_pool_1 | test_pool_2 |
25
+ |----------|----------------|----------------|
26
+ | host | 127.0.0.1 | 127.0.0.1 |
27
+ | port | | 3306 |
28
+ | socket | | |
29
+ | username | root | root |
30
+ | replica | false | false |
31
+
32
+ The configuration items must be explicitly defined or they will be blank in the pool key.
33
+ Configurations with matching _implicit_ items but differing _explicit_ items will create separate pools.
34
+ e.g. `test_pool_1` will default to port 3306 but because it is not explicitly defined it will not share a pool with `test_pool_2`
35
+
36
+ ARHP will therefore create the following pool keys:
37
+
38
+ ```
39
+ test_pool_1 => 127.0.0.1///root/false
40
+ test_pool_2 => 127.0.0.1/3306//root/false
41
+ ```
42
+
43
+
8
44
  ## Support
9
45
 
10
46
  For now, the only backend known to work is MySQL, with the mysql2 gem.
@@ -26,6 +26,15 @@ if ActiveRecord.version >= Gem::Version.new('6.0')
26
26
  # restore in case clearing the cache changed the database
27
27
  connection.unproxied._host_pool_current_database = host_pool_current_database_was
28
28
  end
29
+
30
+ def clear_on_handler(handler)
31
+ handler.all_connection_pools.each do |pool|
32
+ db_was = pool.connection.unproxied._host_pool_current_database
33
+ pool.connection.clear_query_cache if pool.active_connection?
34
+ ensure
35
+ pool.connection.unproxied._host_pool_current_database = db_was
36
+ end
37
+ end
29
38
  end
30
39
  end
31
40
 
@@ -10,7 +10,7 @@ module ActiveRecordHostPool
10
10
 
11
11
  def _host_pool_current_database=(database)
12
12
  @_host_pool_current_database = database
13
- @config[:database] = _host_pool_current_database if ActiveRecord::VERSION::MAJOR >= 5
13
+ @config[:database] = _host_pool_current_database
14
14
  end
15
15
 
16
16
  alias_method :execute_without_switching, :execute
@@ -83,13 +83,24 @@ module ActiveRecordHostPool
83
83
  super(_host_pool_current_database.to_s + "/" + sql, *args)
84
84
  end
85
85
  end
86
+
87
+ module PoolConfigPatch
88
+ def pool
89
+ ActiveSupport::ForkTracker.check!
90
+
91
+ @pool || synchronize { @pool ||= ActiveRecordHostPool::PoolProxy.new(self) }
92
+ end
93
+ end
86
94
  end
87
95
 
88
- # rubocop:disable Lint/DuplicateMethods
89
96
  module ActiveRecord
90
97
  module ConnectionAdapters
91
98
  class ConnectionHandler
92
99
  case "#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"
100
+ when '6.1'
101
+
102
+ :noop
103
+
93
104
  when '5.1', '5.2', '6.0'
94
105
 
95
106
  def establish_connection(spec)
@@ -99,15 +110,6 @@ module ActiveRecord
99
110
  owner_to_pool[spec.name] = ActiveRecordHostPool::PoolProxy.new(spec)
100
111
  end
101
112
 
102
- when '4.2'
103
-
104
- def establish_connection(owner, spec)
105
- @class_to_pool.clear
106
- raise "Anonymous class is not allowed." unless owner.name
107
-
108
- owner_to_pool[owner.name] = ActiveRecordHostPool::PoolProxy.new(spec)
109
- end
110
-
111
113
  else
112
114
 
113
115
  raise "Unsupported version of Rails (v#{ActiveRecord::VERSION::STRING})"
@@ -115,6 +117,11 @@ module ActiveRecord
115
117
  end
116
118
  end
117
119
  end
118
- # rubocop:enable Lint/DuplicateMethods
119
120
 
120
121
  ActiveRecord::ConnectionAdapters::Mysql2Adapter.include(ActiveRecordHostPool::DatabaseSwitch)
122
+
123
+ # In Rails 6.1 Connection Pools are no longer instantiated in #establish_connection but in a
124
+ # new pool method.
125
+ if "#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}" == '6.1'
126
+ ActiveRecord::ConnectionAdapters::PoolConfig.prepend(ActiveRecordHostPool::PoolConfigPatch)
127
+ end
@@ -1,158 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'delegate'
4
- require 'active_record'
5
- require 'active_record_host_pool/connection_adapter_mixin'
6
-
7
- # this module sits in between ConnectionHandler and a bunch of different ConnectionPools (one per host).
8
- # when a connection is requested, it goes like:
9
- # ActiveRecordClass -> ConnectionHandler#connection
10
- # ConnectionHandler#connection -> (find or create PoolProxy)
11
- # PoolProxy -> shared list of Pools
12
- # Pool actually gives back a connection, then PoolProxy turns this
13
- # into a ConnectionProxy that can inform (on execute) which db we should be on.
14
-
15
- module ActiveRecordHostPool
16
- class PoolProxy < Delegator
17
- def initialize(spec)
18
- super(spec)
19
- @spec = spec
20
- @config = spec.config
21
- end
22
-
23
- def __getobj__
24
- _connection_pool
25
- end
26
-
27
- def __setobj__(spec)
28
- @spec = spec
29
- @config = spec.config
30
- @_pool_key = nil
31
- end
32
-
33
- def spec
34
- @spec
35
- end
36
-
37
- def connection(*args)
38
- real_connection = _connection_pool.connection(*args)
39
- _connection_proxy_for(real_connection, @config[:database])
40
- rescue Exception => e
41
- if rescuable_errors.any? { |r| e.is_a?(r) }
42
- _connection_pools.delete(_pool_key)
43
- end
44
- Kernel.raise(e)
45
- end
46
-
47
- # by the time we are patched into ActiveRecord, the current thread has already established
48
- # a connection. thus we need to patch both connection and checkout/checkin
49
- def checkout(*args, &block)
50
- cx = _connection_pool.checkout(*args, &block)
51
- _connection_proxy_for(cx, @config[:database])
52
- end
53
-
54
- def checkin(cx)
55
- cx = cx.unproxied
56
- _connection_pool.checkin(cx)
57
- end
58
-
59
- def with_connection
60
- cx = checkout
61
- yield cx
62
- ensure
63
- checkin cx
64
- end
65
-
66
- def disconnect!
67
- p = _connection_pool(false)
68
- return unless p
69
-
70
- p.disconnect!
71
- p.automatic_reconnect = true if p.respond_to?(:automatic_reconnect=)
72
- _clear_connection_proxy_cache
73
- end
74
-
75
- def automatic_reconnect=(value)
76
- p = _connection_pool(false)
77
- return unless p
78
-
79
- p.automatic_reconnect = value if p.respond_to?(:automatic_reconnect=)
80
- end
81
-
82
- def clear_reloadable_connections!
83
- _connection_pool.clear_reloadable_connections!
84
- _clear_connection_proxy_cache
85
- end
86
-
87
- def release_connection(*args)
88
- p = _connection_pool(false)
89
- return unless p
90
-
91
- p.release_connection(*args)
92
- end
93
-
94
- def flush!
95
- p = _connection_pool(false)
96
- return unless p
97
-
98
- p.flush!
99
- end
100
-
101
- def discard!
102
- p = _connection_pool(false)
103
- return unless p
104
-
105
- p.discard!
106
-
107
- # All connections in the pool (even if they're currently
108
- # leased!) have just been discarded, along with the pool itself.
109
- # Any further interaction with the pool (except #spec and #schema_cache)
110
- # is undefined.
111
- # Remove the connection for the given key so a new one can be created in its place
112
- _connection_pools.delete(_pool_key)
113
- end
114
-
115
- private
116
-
117
- def rescuable_errors
118
- @rescuable_errors ||= begin
119
- e = [Mysql2::Error]
120
- if ActiveRecord.const_defined?("NoDatabaseError")
121
- e << ActiveRecord::NoDatabaseError
122
- end
123
- e
124
- end
125
- end
126
-
127
- def _connection_pools
128
- @@_connection_pools ||= {}
129
- end
130
-
131
- def _pool_key
132
- @_pool_key ||= "#{@config[:host]}/#{@config[:port]}/#{@config[:socket]}/#{@config[:username]}/#{@config[:slave] && 'slave'}"
133
- end
134
-
135
- def _connection_pool(auto_create = true)
136
- pool = _connection_pools[_pool_key]
137
- if pool.nil? && auto_create
138
- pool = _connection_pools[_pool_key] = ActiveRecord::ConnectionAdapters::ConnectionPool.new(@spec)
139
- end
140
- pool
141
- end
142
-
143
- def _connection_proxy_for(connection, database)
144
- @connection_proxy_cache ||= {}
145
- key = [connection, database]
146
-
147
- @connection_proxy_cache[key] ||= begin
148
- cx = ActiveRecordHostPool::ConnectionProxy.new(connection, database)
149
- cx.execute('select 1')
150
- cx
151
- end
152
- end
153
-
154
- def _clear_connection_proxy_cache
155
- @connection_proxy_cache = {}
156
- end
157
- end
3
+ if "#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}" == '6.1'
4
+ require 'active_record_host_pool/pool_proxy_6_1'
5
+ else
6
+ require 'active_record_host_pool/pool_proxy_legacy'
158
7
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordHostPool
4
- VERSION = "1.0.3"
4
+ VERSION = "1.1.0"
5
5
  end
data/test/database.yml CHANGED
@@ -1,76 +1,107 @@
1
1
  <% mysql = URI(ENV['MYSQL_URL'] || 'mysql://root@127.0.0.1:3306') %>
2
- test_host_1_db_1:
2
+ # ARHP creates separate connection pools based on the pool key.
3
+ # The pool key is defined as:
4
+ # host / port / socket / username / replica
5
+ #
6
+ # Therefore two databases with identical host, port, socket, username, and replica status will share a connection pool.
7
+ # If any part (host, port, etc.) of the pool key differ, two databases will _not_ share a connection pool.
8
+ #
9
+ # Below, "test_pool_1..." and "test_pool_2..." have identical host, username, socket, and replica status but the port information differs.
10
+ # Here the yml configurations are reformatted as a table to give a visual example:
11
+ #
12
+ # |----------+----------------+----------------|
13
+ # | | test_pool_1 | test_pool_2 |
14
+ # |----------+----------------+----------------+
15
+ # | host | 127.0.0.1 | 127.0.0.1 |
16
+ # | port | | 3306 |
17
+ # | socket | | |
18
+ # | username | root | root |
19
+ # | replica | false | false |
20
+ # |----------+----------------+----------------|
21
+ #
22
+ # Note: The configuration items must be explicitly defined or will be blank in the pool key.
23
+ # Configurations with matching _implicit_ items but differing _explicit_ items will create separate pools.
24
+ # e.g. "test_pool_1" will default to port 3306 but because it is not explicitly defined it will not share a pool with test_pool_2
25
+ #
26
+ # ARHP will therefore create the following pool keys:
27
+ # test_pool_1 => 127.0.0.1///root/false
28
+ # test_pool_2 => 127.0.0.1/3306//root/false
29
+
30
+ test_pool_1_db_a:
3
31
  adapter: mysql2
4
32
  encoding: utf8
5
- database: arhp_test_1
33
+ database: arhp_test_db_a
6
34
  username: <%= mysql.user %>
7
35
  password: <%= mysql.password %>
8
36
  host: <%= mysql.host %>
9
37
  reconnect: true
10
38
 
11
39
  # Mimic configurations as read by active_record_shards/ar_flexmaster
12
- test_host_1_db_1_slave:
40
+ test_pool_1_db_a_replica:
13
41
  adapter: mysql2
14
42
  encoding: utf8
15
- database: arhp_test_1_slave
43
+ database: arhp_test_db_a_replica
16
44
  username: <%= mysql.user %>
17
45
  password: <%= mysql.password %>
18
46
  host: <%= mysql.host %>
19
47
  reconnect: true
20
48
  slave: true
21
49
 
22
- test_host_1_db_2:
50
+ test_pool_1_db_b:
23
51
  adapter: mysql2
24
52
  encoding: utf8
25
- database: arhp_test_2
53
+ database: arhp_test_db_b
26
54
  username: <%= mysql.user %>
27
55
  password: <%= mysql.password %>
28
56
  host: <%= mysql.host %>
29
57
  reconnect: true
30
58
 
31
- test_host_2_db_3:
59
+ test_pool_1_db_not_there:
32
60
  adapter: mysql2
33
61
  encoding: utf8
34
- database: arhp_test_3
62
+ database: arhp_test_db_not_there
35
63
  username: <%= mysql.user %>
36
64
  password: <%= mysql.password %>
37
65
  host: <%= mysql.host %>
38
- port: <%= mysql.port %>
39
66
  reconnect: true
40
67
 
41
- test_host_2_db_4:
68
+ test_pool_2_db_d:
42
69
  adapter: mysql2
43
70
  encoding: utf8
44
- database: arhp_test_4
71
+ database: arhp_test_db_d
45
72
  username: <%= mysql.user %>
46
73
  password: <%= mysql.password %>
47
74
  host: <%= mysql.host %>
48
75
  port: <%= mysql.port %>
49
76
  reconnect: true
50
77
 
51
- test_host_2_db_5:
78
+ test_pool_2_db_e:
52
79
  adapter: mysql2
53
80
  encoding: utf8
54
- database: arhp_test_4
55
- username: john-doe
56
- password:
81
+ database: arhp_test_db_e
82
+ username: <%= mysql.user %>
83
+ password: <%= mysql.password %>
57
84
  host: <%= mysql.host %>
58
85
  port: <%= mysql.port %>
59
86
  reconnect: true
60
87
 
61
- test_host_1_db_not_there:
88
+ test_pool_3_db_e:
62
89
  adapter: mysql2
63
90
  encoding: utf8
64
- database: arhp_test_no_create
65
- username: <%= mysql.user %>
66
- password: <%= mysql.password %>
91
+ database: arhp_test_db_e
92
+ username: john-doe
93
+ password:
67
94
  host: <%= mysql.host %>
95
+ port: <%= mysql.port %>
68
96
  reconnect: true
69
97
 
70
- test_host_1_db_shard:
98
+ # test_pool_1_db_c needs to be the last database defined in the file
99
+ # otherwise the test_models_with_matching_hosts_and_non_matching_databases_issue_exists_without_arhp_patch
100
+ # test fails
101
+ test_pool_1_db_c:
71
102
  adapter: mysql2
72
103
  encoding: utf8
73
- database: arhp_test_1_shard
104
+ database: arhp_test_db_c
74
105
  username: <%= mysql.user %>
75
106
  password: <%= mysql.password %>
76
107
  host: <%= mysql.host %>
data/test/helper.rb CHANGED
@@ -5,15 +5,41 @@ require 'minitest/autorun'
5
5
 
6
6
  require 'active_record_host_pool'
7
7
  require 'logger'
8
- require 'mocha/setup'
8
+ require 'mocha/minitest'
9
9
  require 'phenix'
10
10
 
11
- RAILS_ENV = 'test'
11
+ ENV['RAILS_ENV'] = 'test'
12
+ ENV['LEGACY_CONNECTION_HANDLING'] = 'true' if ENV['LEGACY_CONNECTION_HANDLING'].nil?
12
13
 
13
- Minitest::Test = MiniTest::Unit::TestCase unless defined?(::Minitest::Test)
14
+ if ActiveRecord.version >= Gem::Version.new('6.1')
15
+ ActiveRecord::Base.legacy_connection_handling = (ENV['LEGACY_CONNECTION_HANDLING'] == 'true')
16
+ end
17
+
18
+ RAILS_6_1_WITH_NON_LEGACY_CONNECTION_HANDLING =
19
+ ActiveRecord.version >= Gem::Version.new('6.1') && !ActiveRecord::Base.legacy_connection_handling
14
20
 
15
21
  ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + '/test.log')
16
22
 
23
+ # BEGIN preventing_writes? patch
24
+ ## Rails 6.1 by default does not allow writing to replica databases which prevents
25
+ ## us from properly setting up the test databases. This patch is used in test/schema.rb
26
+ ## to allow us to write to the replicas but only during migrations
27
+ module ActiveRecordHostPool
28
+ cattr_accessor :allowing_writes
29
+ module PreventWritesPatch
30
+ def preventing_writes?
31
+ return false if ActiveRecordHostPool.allowing_writes && replica?
32
+
33
+ super
34
+ end
35
+ end
36
+ end
37
+
38
+ if ActiveRecord.version >= Gem::Version.new('6.1')
39
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend ActiveRecordHostPool::PreventWritesPatch
40
+ end
41
+ # END preventing_writes? patch
42
+
17
43
  Phenix.configure do |config|
18
44
  config.skip_database = ->(name, conf) { name =~ /not_there/ || conf['username'] == 'john-doe' }
19
45
  end
@@ -22,46 +48,121 @@ module ARHPTestSetup
22
48
  private
23
49
 
24
50
  def arhp_create_models
25
- return if ARHPTestSetup.const_defined?('Test1')
26
-
27
- eval <<-RUBY
28
- # The placement of the Test1Shard class is important so that its
29
- # connection will not be the most recent connection established
30
- # for test_host_1.
31
- class Test1Shard < ::ActiveRecord::Base
32
- establish_connection(:test_host_1_db_shard)
33
- end
34
-
35
- class Test1 < ActiveRecord::Base
36
- self.table_name = "tests"
37
- establish_connection(:test_host_1_db_1)
38
- end
39
-
40
- class Test1Slave < ActiveRecord::Base
41
- self.table_name = "tests"
42
- establish_connection(:test_host_1_db_1_slave)
43
- end
44
-
45
- class Test2 < ActiveRecord::Base
46
- self.table_name = "tests"
47
- establish_connection(:test_host_1_db_2)
48
- end
49
-
50
- class Test3 < ActiveRecord::Base
51
- self.table_name = "tests"
52
- establish_connection(:test_host_2_db_3)
53
- end
54
-
55
- class Test4 < ActiveRecord::Base
56
- self.table_name = "tests"
57
- establish_connection(:test_host_2_db_4)
58
- end
59
-
60
- class Test5 < ActiveRecord::Base
61
- self.table_name = "tests"
62
- establish_connection(:test_host_2_db_5)
63
- end
64
- RUBY
51
+ return if ARHPTestSetup.const_defined?('Pool1DbA')
52
+
53
+ if RAILS_6_1_WITH_NON_LEGACY_CONNECTION_HANDLING
54
+ eval <<-RUBY
55
+ class AbstractPool1DbC < ActiveRecord::Base
56
+ self.abstract_class = true
57
+ connects_to database: { writing: :test_pool_1_db_c }
58
+ end
59
+
60
+ # The placement of the Pool1DbC class is important so that its
61
+ # connection will not be the most recent connection established
62
+ # for test_pool_1.
63
+ class Pool1DbC < AbstractPool1DbC
64
+ end
65
+
66
+ class AbstractPool1DbA < ActiveRecord::Base
67
+ self.abstract_class = true
68
+ connects_to database: { writing: :test_pool_1_db_a, reading: :test_pool_1_db_a_replica }
69
+ end
70
+
71
+ class Pool1DbA < AbstractPool1DbA
72
+ self.table_name = "tests"
73
+ end
74
+
75
+ class AbstractPool1DbB < ActiveRecord::Base
76
+ self.abstract_class = true
77
+ connects_to database: { writing: :test_pool_1_db_b }
78
+ end
79
+
80
+ class Pool1DbB < AbstractPool1DbB
81
+ self.table_name = "tests"
82
+ end
83
+
84
+ class AbstractPool2DbD < ActiveRecord::Base
85
+ self.abstract_class = true
86
+ connects_to database: { writing: :test_pool_2_db_d }
87
+ end
88
+
89
+ class Pool2DbD < AbstractPool2DbD
90
+ self.table_name = "tests"
91
+ end
92
+
93
+ class AbstractPool2DbE < ActiveRecord::Base
94
+ self.abstract_class = true
95
+ connects_to database: { writing: :test_pool_2_db_e }
96
+ end
97
+
98
+ class Pool2DbE < AbstractPool2DbE
99
+ self.table_name = "tests"
100
+ end
101
+
102
+ class AbstractPool3DbE < ActiveRecord::Base
103
+ self.abstract_class = true
104
+ connects_to database: { writing: :test_pool_3_db_e }
105
+ end
106
+
107
+ class Pool3DbE < AbstractPool3DbE
108
+ self.table_name = "tests"
109
+ end
110
+
111
+ # Test ARHP with Rails 6.1+ horizontal sharding functionality
112
+ class AbstractShardedModel < ActiveRecord::Base
113
+ self.abstract_class = true
114
+ connects_to shards: {
115
+ default: { writing: :test_pool_1_db_shard_a },
116
+ shard_b: { writing: :test_pool_1_db_shard_b, reading: :test_pool_1_db_shard_b_replica },
117
+ shard_c: { writing: :test_pool_1_db_shard_c, reading: :test_pool_1_db_shard_c_replica },
118
+ shard_d: { writing: :test_pool_2_db_shard_d, reading: :test_pool_2_db_shard_d_replica }
119
+ }
120
+ end
121
+
122
+ class ShardedModel < AbstractShardedModel
123
+ self.table_name = "tests"
124
+ end
125
+ RUBY
126
+ else
127
+ eval <<-RUBY
128
+ # The placement of the Pool1DbC class is important so that its
129
+ # connection will not be the most recent connection established
130
+ # for test_pool_1.
131
+ class Pool1DbC < ActiveRecord::Base
132
+ establish_connection(:test_pool_1_db_c)
133
+ end
134
+
135
+ class Pool1DbA < ActiveRecord::Base
136
+ self.table_name = "tests"
137
+ establish_connection(:test_pool_1_db_a)
138
+ end
139
+
140
+ class Pool1DbAReplica < ActiveRecord::Base
141
+ self.table_name = "tests"
142
+ establish_connection(:test_pool_1_db_a_replica)
143
+ end
144
+
145
+ class Pool1DbB < ActiveRecord::Base
146
+ self.table_name = "tests"
147
+ establish_connection(:test_pool_1_db_b)
148
+ end
149
+
150
+ class Pool2DbD < ActiveRecord::Base
151
+ self.table_name = "tests"
152
+ establish_connection(:test_pool_2_db_d)
153
+ end
154
+
155
+ class Pool2DbE < ActiveRecord::Base
156
+ self.table_name = "tests"
157
+ establish_connection(:test_pool_2_db_e)
158
+ end
159
+
160
+ class Pool3DbE < ActiveRecord::Base
161
+ self.table_name = "tests"
162
+ establish_connection(:test_pool_3_db_e)
163
+ end
164
+ RUBY
165
+ end
65
166
  end
66
167
 
67
168
  def current_database(klass)
@@ -78,4 +179,17 @@ module ARHPTestSetup
78
179
  ensure
79
180
  mod.define_method(method_name, method_body)
80
181
  end
182
+
183
+ def simulate_rails_app_active_record_railties
184
+ if ActiveRecord.version >= Gem::Version.new('6.0') && !RAILS_6_1_WITH_NON_LEGACY_CONNECTION_HANDLING
185
+ # Necessary for testing ActiveRecord 6.0 which uses the connection
186
+ # handlers when clearing query caches across all handlers when
187
+ # an operation that dirties the cache is involved (e.g. create/insert,
188
+ # update, delete/destroy, truncate, etc.)
189
+ # In Rails 6.1 this is only present when legacy_connection_handling=true
190
+ ActiveRecord::Base.connection_handlers = {
191
+ ActiveRecord::Base.writing_role => ActiveRecord::Base.default_connection_handler
192
+ }
193
+ end
194
+ end
81
195
  end
data/test/schema.rb CHANGED
@@ -1,16 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'helper'
4
- ActiveRecord::Schema.define(version: 1) do
5
- create_table 'tests', force: true do |t|
6
- t.string 'val'
7
- end
8
4
 
9
- # Add a table only the shard database will have. Conditional
10
- # exists since Phenix loads the schema file for every database.
11
- if ActiveRecord::Base.connection.current_database == 'arhp_test_1_shard'
12
- create_table 'test1_shards' do |t|
13
- t.string 'name'
5
+ begin
6
+ ActiveRecordHostPool.allowing_writes = true
7
+
8
+ ActiveRecord::Schema.define(version: 1) do
9
+ create_table 'tests', force: true do |t|
10
+ t.string 'val'
11
+ end
12
+
13
+ # Add a table only the shard database will have. Conditional
14
+ # exists since Phenix loads the schema file for every database.
15
+ if ActiveRecord::Base.connection.current_database == 'arhp_test_db_c'
16
+ create_table 'pool1_db_cs' do |t|
17
+ t.string 'name'
18
+ end
14
19
  end
15
20
  end
21
+ ensure
22
+ ActiveRecordHostPool.allowing_writes = false
16
23
  end
data/test/test_arhp.rb CHANGED
@@ -5,11 +5,17 @@ require_relative 'helper'
5
5
  class ActiveRecordHostPoolTest < Minitest::Test
6
6
  include ARHPTestSetup
7
7
  def setup
8
- Phenix.rise!
8
+ if RAILS_6_1_WITH_NON_LEGACY_CONNECTION_HANDLING
9
+ Phenix.rise! config_path: 'test/three_tier_database.yml'
10
+ else
11
+ Phenix.rise!
12
+ end
9
13
  arhp_create_models
10
14
  end
11
15
 
12
16
  def teardown
17
+ ActiveRecord::Base.connection.disconnect!
18
+ ActiveRecordHostPool::PoolProxy.class_variable_set(:@@_connection_pools, {})
13
19
  Phenix.burn!
14
20
  end
15
21
 
@@ -30,110 +36,80 @@ class ActiveRecordHostPoolTest < Minitest::Test
30
36
  ActiveRecord::Base.connection_handler.clear_all_connections!
31
37
  end
32
38
 
33
- def test_models_with_matching_hosts_should_share_a_connection
34
- assert_equal(Test1.connection.raw_connection, Test2.connection.raw_connection)
35
- assert_equal(Test3.connection.raw_connection, Test4.connection.raw_connection)
36
- end
37
-
38
- def test_models_without_matching_hosts_should_not_share_a_connection
39
- refute_equal(Test1.connection.raw_connection, Test4.connection.raw_connection)
39
+ def test_models_with_matching_hosts_ports_sockets_usernames_and_replica_status_should_share_a_connection
40
+ assert_equal(Pool1DbA.connection.raw_connection, Pool1DbB.connection.raw_connection)
41
+ assert_equal(Pool2DbD.connection.raw_connection, Pool2DbE.connection.raw_connection)
40
42
  end
41
43
 
42
- def test_models_without_matching_usernames_should_not_share_a_connection
43
- refute_equal(Test4.connection.raw_connection, Test5.connection.raw_connection)
44
+ def test_models_with_different_ports_should_not_share_a_connection
45
+ refute_equal(Pool1DbA.connection.raw_connection, Pool2DbD.connection.raw_connection)
44
46
  end
45
47
 
46
- def test_models_without_match_slave_status_should_not_share_a_connection
47
- refute_equal(Test1.connection.raw_connection, Test1Slave.connection.raw_connection)
48
+ def test_models_with_different_usernames_should_not_share_a_connection
49
+ refute_equal(Pool2DbE.connection.raw_connection, Pool3DbE.connection.raw_connection)
48
50
  end
49
51
 
50
52
  def test_should_select_on_correct_database
51
- assert_action_uses_correct_database(:select_all, 'select 1')
52
- end
53
+ Pool1DbA.connection.send(:select_all, 'select 1')
54
+ assert_equal 'arhp_test_db_a', current_database(Pool1DbA)
53
55
 
54
- def test_should_insert_on_correct_database
55
- assert_action_uses_correct_database(:insert, "insert into tests values(NULL, 'foo')")
56
- end
56
+ Pool2DbD.connection.send(:select_all, 'select 1')
57
+ assert_equal 'arhp_test_db_d', current_database(Pool2DbD)
57
58
 
58
- def test_models_with_matching_hosts_and_non_matching_databases_should_share_a_connection
59
- simulate_rails_app_active_record_railties
60
- assert_equal(Test1.connection.raw_connection, Test1Shard.connection.raw_connection)
59
+ Pool3DbE.connection.send(:select_all, 'select 1')
60
+ assert_equal 'arhp_test_db_e', current_database(Pool3DbE)
61
61
  end
62
62
 
63
- if ActiveRecord.version >= Gem::Version.new('6.0')
64
- def test_models_with_matching_hosts_and_non_matching_databases_issue_exists_without_arhp_patch
65
- simulate_rails_app_active_record_railties
66
-
67
- # Remove patch that fixes an issue in Rails 6+ to ensure it still
68
- # exists. If this begins to fail then it may mean that Rails has fixed
69
- # the issue so that it no longer occurs.
70
- without_module_patch(ActiveRecordHostPool::ClearQueryCachePatch, :clear_query_caches_for_current_thread) do
71
- exception = assert_raises(ActiveRecord::StatementInvalid) do
72
- ActiveRecord::Base.cache { Test1Shard.create! }
73
- end
74
-
75
- assert_equal("Mysql2::Error: Table 'arhp_test_2.test1_shards' doesn't exist", exception.message)
76
- end
77
- end
63
+ def test_should_insert_on_correct_database
64
+ Pool1DbA.connection.send(:insert, "insert into tests values(NULL, 'foo')")
65
+ assert_equal 'arhp_test_db_a', current_database(Pool1DbA)
78
66
 
79
- def test_models_with_matching_hosts_and_non_matching_databases_do_not_mix_up_underlying_database
80
- simulate_rails_app_active_record_railties
67
+ Pool2DbD.connection.send(:insert, "insert into tests values(NULL, 'foo')")
68
+ assert_equal 'arhp_test_db_d', current_database(Pool2DbD)
81
69
 
82
- # ActiveRecord 6.0 introduced a change that surfaced a problematic code
83
- # path in active_record_host_pool when clearing caches across connection
84
- # handlers which can cause the database to change.
85
- # See ActiveRecordHostPool::ClearQueryCachePatch
86
- ActiveRecord::Base.cache { Test1Shard.create! }
87
- end
70
+ Pool3DbE.connection.send(:insert, "insert into tests values(NULL, 'foo')")
71
+ assert_equal 'arhp_test_db_e', current_database(Pool3DbE)
88
72
  end
89
73
 
90
74
  def test_connection_returns_a_proxy
91
- assert_kind_of ActiveRecordHostPool::ConnectionProxy, Test1.connection
75
+ assert_kind_of ActiveRecordHostPool::ConnectionProxy, Pool1DbA.connection
92
76
  end
93
77
 
94
78
  def test_connection_proxy_handles_private_methods
95
79
  # Relies on connection.class returning the real class
96
- Test1.connection.class.class_eval do
80
+ Pool1DbA.connection.class.class_eval do
97
81
  private
98
82
 
99
83
  def test_private_method
100
84
  true
101
85
  end
102
86
  end
103
- assert Test1.connection.respond_to?(:test_private_method, true)
104
- refute Test1.connection.respond_to?(:test_private_method)
105
- assert_includes(Test1.connection.private_methods, :test_private_method)
106
- assert_equal true, Test1.connection.send(:test_private_method)
107
- end
108
-
109
- def test_should_not_share_a_query_cache
110
- Test1.create(val: 'foo')
111
- Test2.create(val: 'foobar')
112
- Test1.connection.cache do
113
- refute_equal Test1.first.val, Test2.first.val
114
- end
87
+ assert Pool1DbA.connection.respond_to?(:test_private_method, true)
88
+ refute Pool1DbA.connection.respond_to?(:test_private_method)
89
+ assert_includes(Pool1DbA.connection.private_methods, :test_private_method)
90
+ assert_equal true, Pool1DbA.connection.send(:test_private_method)
115
91
  end
116
92
 
117
93
  def test_object_creation
118
- Test1.create(val: 'foo')
119
- assert_equal('arhp_test_1', current_database(Test1))
94
+ Pool1DbA.create(val: 'foo')
95
+ assert_equal('arhp_test_db_a', current_database(Pool1DbA))
120
96
 
121
- Test3.create(val: 'bar')
122
- assert_equal('arhp_test_1', current_database(Test1))
123
- assert_equal('arhp_test_3', current_database(Test3))
97
+ Pool2DbD.create(val: 'bar')
98
+ assert_equal('arhp_test_db_a', current_database(Pool1DbA))
99
+ assert_equal('arhp_test_db_d', current_database(Pool2DbD))
124
100
 
125
- Test2.create!(val: 'bar_distinct')
126
- assert_equal('arhp_test_2', current_database(Test2))
127
- assert Test2.find_by_val('bar_distinct')
128
- refute Test1.find_by_val('bar_distinct')
101
+ Pool1DbB.create!(val: 'bar_distinct')
102
+ assert_equal('arhp_test_db_b', current_database(Pool1DbB))
103
+ assert Pool1DbB.find_by_val('bar_distinct')
104
+ refute Pool1DbA.find_by_val('bar_distinct')
129
105
  end
130
106
 
131
107
  def test_disconnect
132
- Test1.create(val: 'foo')
133
- unproxied = Test1.connection.unproxied
134
- Test1.connection_handler.clear_all_connections!
135
- Test1.create(val: 'foo')
136
- assert(unproxied != Test1.connection.unproxied)
108
+ Pool1DbA.create(val: 'foo')
109
+ unproxied = Pool1DbA.connection.unproxied
110
+ Pool1DbA.connection_handler.clear_all_connections!
111
+ Pool1DbA.create(val: 'foo')
112
+ assert(unproxied != Pool1DbA.connection.unproxied)
137
113
  end
138
114
 
139
115
  def test_checkout
@@ -145,7 +121,7 @@ class ActiveRecordHostPoolTest < Minitest::Test
145
121
  end
146
122
 
147
123
  def test_no_switch_when_creating_db
148
- conn = Test1.connection
124
+ conn = Pool1DbA.connection
149
125
  conn.expects(:execute_without_switching)
150
126
  conn.expects(:_switch_connection).never
151
127
  assert conn._host_pool_current_database
@@ -153,7 +129,7 @@ class ActiveRecordHostPoolTest < Minitest::Test
153
129
  end
154
130
 
155
131
  def test_no_switch_when_dropping_db
156
- conn = Test1.connection
132
+ conn = Pool1DbA.connection
157
133
  conn.expects(:execute_without_switching)
158
134
  conn.expects(:_switch_connection).never
159
135
  assert conn._host_pool_current_database
@@ -163,17 +139,17 @@ class ActiveRecordHostPoolTest < Minitest::Test
163
139
  def test_underlying_assumption_about_test_db
164
140
  debug_me = false
165
141
  # ensure connection
166
- Test1.first
142
+ Pool1DbA.first
167
143
 
168
144
  # which is the "default" DB to connect to?
169
- first_db = Test1.connection.unproxied.instance_variable_get(:@_cached_current_database)
145
+ first_db = Pool1DbA.connection.unproxied.instance_variable_get(:@_cached_current_database)
170
146
  puts "\nOk, we started on #{first_db}" if debug_me
171
147
 
172
148
  switch_to_klass = case first_db
173
- when 'arhp_test_2'
174
- Test1
175
- when 'arhp_test_1'
176
- Test2
149
+ when 'arhp_test_db_b'
150
+ Pool1DbA
151
+ when 'arhp_test_db_a'
152
+ Pool1DbB
177
153
  else
178
154
  raise "Expected a database name, got #{first_db.inspect}"
179
155
  end
@@ -188,7 +164,7 @@ class ActiveRecordHostPoolTest < Minitest::Test
188
164
 
189
165
  # now, disable our auto-switching and trigger a mysql reconnect
190
166
  switch_to_klass.connection.unproxied.stubs(:_switch_connection).returns(true)
191
- Test3.connection.execute("KILL #{thread_id}")
167
+ Pool2DbD.connection.execute("KILL #{thread_id}")
192
168
 
193
169
  # and finally, did mysql reconnect correctly?
194
170
  puts "\nAnd now we end up on #{current_database(switch_to_klass)}" if debug_me
@@ -201,27 +177,4 @@ class ActiveRecordHostPoolTest < Minitest::Test
201
177
  pool.expects(:checkin).with(conn)
202
178
  pool.release_connection
203
179
  end
204
-
205
- private
206
-
207
- def assert_action_uses_correct_database(action, sql)
208
- (1..4).each do |i|
209
- klass = ARHPTestSetup.const_get("Test#{i}")
210
- desired_db = "arhp_test_#{i}"
211
- klass.connection.send(action, sql)
212
- assert_equal desired_db, current_database(klass)
213
- end
214
- end
215
-
216
- def simulate_rails_app_active_record_railties
217
- if ActiveRecord.version >= Gem::Version.new('6.0')
218
- # Necessary for testing ActiveRecord 6.0 which uses the connection
219
- # handlers when clearing query caches across all handlers when
220
- # an operation that dirties the cache is involved (e.g. create/insert,
221
- # update, delete/destroy, truncate, etc.)
222
- ActiveRecord::Base.connection_handlers = {
223
- ActiveRecord::Base.writing_role => ActiveRecord::Base.default_connection_handler
224
- }
225
- end
226
- end
227
180
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_record_host_pool
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Benjamin Quorning
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2021-02-09 00:00:00.000000000 Z
14
+ date: 2022-08-26 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: activerecord
@@ -19,20 +19,20 @@ dependencies:
19
19
  requirements:
20
20
  - - ">="
21
21
  - !ruby/object:Gem::Version
22
- version: 4.2.0
22
+ version: 5.1.0
23
23
  - - "<"
24
24
  - !ruby/object:Gem::Version
25
- version: '6.1'
25
+ version: '7.0'
26
26
  type: :runtime
27
27
  prerelease: false
28
28
  version_requirements: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: 4.2.0
32
+ version: 5.1.0
33
33
  - - "<"
34
34
  - !ruby/object:Gem::Version
35
- version: '6.1'
35
+ version: '7.0'
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: mysql2
38
38
  requirement: !ruby/object:Gem::Requirement
@@ -61,34 +61,48 @@ dependencies:
61
61
  - - ">="
62
62
  - !ruby/object:Gem::Version
63
63
  version: '0'
64
+ - !ruby/object:Gem::Dependency
65
+ name: minitest
66
+ requirement: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: 5.10.0
71
+ type: :development
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: 5.10.0
64
78
  - !ruby/object:Gem::Dependency
65
79
  name: mocha
66
80
  requirement: !ruby/object:Gem::Requirement
67
81
  requirements:
68
82
  - - ">="
69
83
  - !ruby/object:Gem::Version
70
- version: '0'
84
+ version: 1.4.0
71
85
  type: :development
72
86
  prerelease: false
73
87
  version_requirements: !ruby/object:Gem::Requirement
74
88
  requirements:
75
89
  - - ">="
76
90
  - !ruby/object:Gem::Version
77
- version: '0'
91
+ version: 1.4.0
78
92
  - !ruby/object:Gem::Dependency
79
93
  name: phenix
80
94
  requirement: !ruby/object:Gem::Requirement
81
95
  requirements:
82
96
  - - ">="
83
97
  - !ruby/object:Gem::Version
84
- version: '0'
98
+ version: 1.0.1
85
99
  type: :development
86
100
  prerelease: false
87
101
  version_requirements: !ruby/object:Gem::Requirement
88
102
  requirements:
89
103
  - - ">="
90
104
  - !ruby/object:Gem::Version
91
- version: '0'
105
+ version: 1.0.1
92
106
  - !ruby/object:Gem::Dependency
93
107
  name: rake
94
108
  requirement: !ruby/object:Gem::Requirement
@@ -166,14 +180,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
166
180
  requirements:
167
181
  - - ">="
168
182
  - !ruby/object:Gem::Version
169
- version: '0'
183
+ version: 2.6.0
170
184
  required_rubygems_version: !ruby/object:Gem::Requirement
171
185
  requirements:
172
186
  - - ">="
173
187
  - !ruby/object:Gem::Version
174
188
  version: '0'
175
189
  requirements: []
176
- rubygems_version: 3.2.2
190
+ rubygems_version: 3.1.6
177
191
  signing_key:
178
192
  specification_version: 4
179
193
  summary: Allow ActiveRecord to share a connection to multiple databases on the same