active_record_host_pool 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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