active_record_host_pool 1.2.5 → 2.0.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.
@@ -1,159 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'delegate'
4
- require 'active_record'
5
- require 'active_record_host_pool/connection_adapter_mixin'
6
- require 'mutex_m'
7
-
8
- # this module sits in between ConnectionHandler and a bunch of different ConnectionPools (one per host).
9
- # when a connection is requested, it goes like:
10
- # ActiveRecordClass -> ConnectionHandler#connection
11
- # ConnectionHandler#connection -> (find or create PoolProxy)
12
- # PoolProxy -> shared list of Pools
13
- # Pool actually gives back a connection, then PoolProxy turns this
14
- # into a ConnectionProxy that can inform (on execute) which db we should be on.
15
-
16
- module ActiveRecordHostPool
17
- # Sits between ConnectionHandler and a bunch of different ConnectionPools (one per host).
18
- class PoolProxy < Delegator
19
- include Mutex_m
20
-
21
- def initialize(pool_config)
22
- super(pool_config)
23
- @pool_config = pool_config
24
- @config = pool_config.db_config.configuration_hash
25
- end
26
-
27
- def __getobj__
28
- _connection_pool
29
- end
30
-
31
- def __setobj__(pool_config)
32
- @pool_config = pool_config
33
- @config = pool_config.db_config.configuration_hash
34
- @_pool_key = nil
35
- end
36
-
37
- attr_reader :pool_config
38
-
39
- def connection(*args)
40
- real_connection = _unproxied_connection(*args)
41
- _connection_proxy_for(real_connection, @config[:database])
42
- rescue Mysql2::Error, ActiveRecord::NoDatabaseError
43
- _connection_pools.delete(_pool_key)
44
- Kernel.raise
45
- end
46
-
47
- def _unproxied_connection(*args)
48
- _connection_pool.connection(*args)
49
- end
50
-
51
- # by the time we are patched into ActiveRecord, the current thread has already established
52
- # a connection. thus we need to patch both connection and checkout/checkin
53
- def checkout(*args, &block)
54
- cx = _connection_pool.checkout(*args, &block)
55
- _connection_proxy_for(cx, @config[:database])
56
- end
57
-
58
- def checkin(cx)
59
- cx = cx.unproxied
60
- _connection_pool.checkin(cx)
61
- end
62
-
63
- def with_connection
64
- cx = checkout
65
- yield cx
66
- ensure
67
- checkin cx
68
- end
69
-
70
- def disconnect!
71
- p = _connection_pool(false)
72
- return unless p
73
-
74
- synchronize do
75
- p.disconnect!
76
- p.automatic_reconnect = true
77
- _clear_connection_proxy_cache
78
- end
79
- end
80
-
81
- def automatic_reconnect=(value)
82
- p = _connection_pool(false)
83
- return unless p
84
-
85
- p.automatic_reconnect = value
86
- end
87
-
88
- def clear_reloadable_connections!
89
- _connection_pool.clear_reloadable_connections!
90
- _clear_connection_proxy_cache
91
- end
92
-
93
- def release_connection(*args)
94
- p = _connection_pool(false)
95
- return unless p
96
-
97
- p.release_connection(*args)
98
- end
99
-
100
- def flush!
101
- p = _connection_pool(false)
102
- return unless p
103
-
104
- p.flush!
105
- end
106
-
107
- def discard!
108
- p = _connection_pool(false)
109
- return unless p
110
-
111
- p.discard!
112
-
113
- # All connections in the pool (even if they're currently
114
- # leased!) have just been discarded, along with the pool itself.
115
- # Any further interaction with the pool (except #pool_config and #schema_cache)
116
- # is undefined.
117
- # Remove the connection for the given key so a new one can be created in its place
118
- _connection_pools.delete(_pool_key)
119
- end
120
-
121
- private
122
-
123
- def _connection_pools
124
- @@_connection_pools ||= {}
125
- end
126
-
127
- def _pool_key
128
- @_pool_key ||= "#{@config[:host]}/#{@config[:port]}/#{@config[:socket]}/" \
129
- "#{@config[:username]}/#{replica_configuration? && 'replica'}"
130
- end
131
-
132
- def _connection_pool(auto_create = true)
133
- pool = _connection_pools[_pool_key]
134
- if pool.nil? && auto_create
135
- pool = _connection_pools[_pool_key] = ActiveRecord::ConnectionAdapters::ConnectionPool.new(@pool_config)
136
- end
137
- pool
138
- end
139
-
140
- def _connection_proxy_for(connection, database)
141
- @connection_proxy_cache ||= {}
142
- key = [connection, database]
143
-
144
- @connection_proxy_cache[key] ||= begin
145
- cx = ActiveRecordHostPool::ConnectionProxy.new(connection, database)
146
- cx.execute('select 1')
147
- cx
148
- end
149
- end
150
-
151
- def _clear_connection_proxy_cache
152
- @connection_proxy_cache = {}
153
- end
154
-
155
- def replica_configuration?
156
- @config[:replica] || @config[:slave]
157
- end
158
- end
159
- end
@@ -1,156 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # For versions of Rails < 6.1
4
-
5
- require 'delegate'
6
- require 'active_record'
7
- require 'active_record_host_pool/connection_adapter_mixin'
8
-
9
- # this module sits in between ConnectionHandler and a bunch of different ConnectionPools (one per host).
10
- # when a connection is requested, it goes like:
11
- # ActiveRecordClass -> ConnectionHandler#connection
12
- # ConnectionHandler#connection -> (find or create PoolProxy)
13
- # PoolProxy -> shared list of Pools
14
- # Pool actually gives back a connection, then PoolProxy turns this
15
- # into a ConnectionProxy that can inform (on execute) which db we should be on.
16
-
17
- module ActiveRecordHostPool
18
- # Sits between ConnectionHandler and a bunch of different ConnectionPools (one per host).
19
- class PoolProxy < Delegator
20
- def initialize(spec)
21
- super(spec)
22
- @spec = spec
23
- @config = spec.config
24
- end
25
-
26
- def __getobj__
27
- _connection_pool
28
- end
29
-
30
- def __setobj__(spec)
31
- @spec = spec
32
- @config = spec.config
33
- @_pool_key = nil
34
- end
35
-
36
- attr_reader :spec
37
-
38
- def connection(*args)
39
- real_connection = _unproxied_connection(*args)
40
- _connection_proxy_for(real_connection, @config[:database])
41
- rescue Mysql2::Error, ActiveRecord::NoDatabaseError
42
- _connection_pools.delete(_pool_key)
43
- Kernel.raise
44
- end
45
-
46
- def _unproxied_connection(*args)
47
- _connection_pool.connection(*args)
48
- end
49
-
50
- # by the time we are patched into ActiveRecord, the current thread has already established
51
- # a connection. thus we need to patch both connection and checkout/checkin
52
- def checkout(*args, &block)
53
- cx = _connection_pool.checkout(*args, &block)
54
- _connection_proxy_for(cx, @config[:database])
55
- end
56
-
57
- def checkin(cx)
58
- cx = cx.unproxied
59
- _connection_pool.checkin(cx)
60
- end
61
-
62
- def with_connection
63
- cx = checkout
64
- yield cx
65
- ensure
66
- checkin cx
67
- end
68
-
69
- def disconnect!
70
- p = _connection_pool(false)
71
- return unless p
72
-
73
- p.disconnect!
74
- p.automatic_reconnect = true
75
- _clear_connection_proxy_cache
76
- end
77
-
78
- def automatic_reconnect=(value)
79
- p = _connection_pool(false)
80
- return unless p
81
-
82
- p.automatic_reconnect = value if p.respond_to?(:automatic_reconnect=)
83
- end
84
-
85
- def clear_reloadable_connections!
86
- _connection_pool.clear_reloadable_connections!
87
- _clear_connection_proxy_cache
88
- end
89
-
90
- def release_connection(*args)
91
- p = _connection_pool(false)
92
- return unless p
93
-
94
- p.release_connection(*args)
95
- end
96
-
97
- def flush!
98
- p = _connection_pool(false)
99
- return unless p
100
-
101
- p.flush!
102
- end
103
-
104
- def discard!
105
- p = _connection_pool(false)
106
- return unless p
107
-
108
- p.discard!
109
-
110
- # All connections in the pool (even if they're currently
111
- # leased!) have just been discarded, along with the pool itself.
112
- # Any further interaction with the pool (except #spec and #schema_cache)
113
- # is undefined.
114
- # Remove the connection for the given key so a new one can be created in its place
115
- _connection_pools.delete(_pool_key)
116
- end
117
-
118
- private
119
-
120
- def _connection_pools
121
- @@_connection_pools ||= {}
122
- end
123
-
124
- def _pool_key
125
- @_pool_key ||= "#{@config[:host]}/#{@config[:port]}/#{@config[:socket]}/" \
126
- "#{@config[:username]}/#{replica_configuration? && 'replica'}"
127
- end
128
-
129
- def _connection_pool(auto_create = true)
130
- pool = _connection_pools[_pool_key]
131
- if pool.nil? && auto_create
132
- pool = _connection_pools[_pool_key] = ActiveRecord::ConnectionAdapters::ConnectionPool.new(@spec)
133
- end
134
- pool
135
- end
136
-
137
- def _connection_proxy_for(connection, database)
138
- @connection_proxy_cache ||= {}
139
- key = [connection, database]
140
-
141
- @connection_proxy_cache[key] ||= begin
142
- cx = ActiveRecordHostPool::ConnectionProxy.new(connection, database)
143
- cx.execute('select 1')
144
- cx
145
- end
146
- end
147
-
148
- def _clear_connection_proxy_cache
149
- @connection_proxy_cache = {}
150
- end
151
-
152
- def replica_configuration?
153
- @config[:replica] || @config[:slave]
154
- end
155
- end
156
- end
data/test/database.yml DELETED
@@ -1,108 +0,0 @@
1
- <% mysql = URI(ENV['MYSQL_URL'] || 'mysql://root@127.0.0.1:3306') %>
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:
31
- adapter: mysql2
32
- encoding: utf8
33
- database: arhp_test_db_a
34
- username: <%= mysql.user %>
35
- password: <%= mysql.password %>
36
- host: <%= mysql.host %>
37
- reconnect: true
38
-
39
- # Mimic configurations as read by active_record_shards/ar_flexmaster
40
- test_pool_1_db_a_replica:
41
- adapter: mysql2
42
- encoding: utf8
43
- database: arhp_test_db_a_replica
44
- username: <%= mysql.user %>
45
- password: <%= mysql.password %>
46
- host: <%= mysql.host %>
47
- reconnect: true
48
- slave: true
49
-
50
- test_pool_1_db_b:
51
- adapter: mysql2
52
- encoding: utf8
53
- database: arhp_test_db_b
54
- username: <%= mysql.user %>
55
- password: <%= mysql.password %>
56
- host: <%= mysql.host %>
57
- reconnect: true
58
-
59
- test_pool_1_db_not_there:
60
- adapter: mysql2
61
- encoding: utf8
62
- database: arhp_test_db_not_there
63
- username: <%= mysql.user %>
64
- password: <%= mysql.password %>
65
- host: <%= mysql.host %>
66
- reconnect: true
67
-
68
- test_pool_2_db_d:
69
- adapter: mysql2
70
- encoding: utf8
71
- database: arhp_test_db_d
72
- username: <%= mysql.user %>
73
- password: <%= mysql.password %>
74
- host: <%= mysql.host %>
75
- port: <%= mysql.port %>
76
- reconnect: true
77
-
78
- test_pool_2_db_e:
79
- adapter: mysql2
80
- encoding: utf8
81
- database: arhp_test_db_e
82
- username: <%= mysql.user %>
83
- password: <%= mysql.password %>
84
- host: <%= mysql.host %>
85
- port: <%= mysql.port %>
86
- reconnect: true
87
-
88
- test_pool_3_db_e:
89
- adapter: mysql2
90
- encoding: utf8
91
- database: arhp_test_db_e
92
- username: john-doe
93
- password:
94
- host: <%= mysql.host %>
95
- port: <%= mysql.port %>
96
- reconnect: true
97
-
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:
102
- adapter: mysql2
103
- encoding: utf8
104
- database: arhp_test_db_c
105
- username: <%= mysql.user %>
106
- password: <%= mysql.password %>
107
- host: <%= mysql.host %>
108
- reconnect: true
data/test/helper.rb DELETED
@@ -1,198 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'bundler/setup'
4
- require 'minitest/autorun'
5
- require 'pry-byebug'
6
-
7
- require 'active_record_host_pool'
8
- require 'logger'
9
- require 'minitest/mock_expectations'
10
- require 'phenix'
11
-
12
- ENV['RAILS_ENV'] = 'test'
13
- ENV['LEGACY_CONNECTION_HANDLING'] = 'true' if ENV['LEGACY_CONNECTION_HANDLING'].nil?
14
-
15
- if ActiveRecord.version >= Gem::Version.new('6.1')
16
- ActiveRecord::Base.legacy_connection_handling = (ENV['LEGACY_CONNECTION_HANDLING'] == 'true')
17
- end
18
-
19
- RAILS_6_1_WITH_NON_LEGACY_CONNECTION_HANDLING =
20
- ActiveRecord.version >= Gem::Version.new('6.1') && !ActiveRecord::Base.legacy_connection_handling
21
-
22
- ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + '/test.log')
23
-
24
- Thread.abort_on_exception = true
25
-
26
- # BEGIN preventing_writes? patch
27
- ## Rails 6.1 by default does not allow writing to replica databases which prevents
28
- ## us from properly setting up the test databases. This patch is used in test/schema.rb
29
- ## to allow us to write to the replicas but only during migrations
30
- module ActiveRecordHostPool
31
- cattr_accessor :allowing_writes
32
- module PreventWritesPatch
33
- def preventing_writes?
34
- return false if ActiveRecordHostPool.allowing_writes && replica?
35
-
36
- super
37
- end
38
- end
39
- end
40
-
41
- if ActiveRecord.version >= Gem::Version.new('6.1')
42
- ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend ActiveRecordHostPool::PreventWritesPatch
43
- end
44
- # END preventing_writes? patch
45
-
46
- Phenix.configure do |config|
47
- config.skip_database = ->(name, conf) { name =~ /not_there/ || conf['username'] == 'john-doe' }
48
- end
49
-
50
- module ARHPTestSetup
51
- private
52
-
53
- def arhp_create_models
54
- return if ARHPTestSetup.const_defined?('Pool1DbA')
55
-
56
- if RAILS_6_1_WITH_NON_LEGACY_CONNECTION_HANDLING
57
- eval <<-RUBY
58
- class AbstractPool1DbC < ActiveRecord::Base
59
- self.abstract_class = true
60
- connects_to database: { writing: :test_pool_1_db_c }
61
- end
62
-
63
- # The placement of the Pool1DbC class is important so that its
64
- # connection will not be the most recent connection established
65
- # for test_pool_1.
66
- class Pool1DbC < AbstractPool1DbC
67
- end
68
-
69
- class AbstractPool1DbA < ActiveRecord::Base
70
- self.abstract_class = true
71
- connects_to database: { writing: :test_pool_1_db_a, reading: :test_pool_1_db_a_replica }
72
- end
73
-
74
- class Pool1DbA < AbstractPool1DbA
75
- self.table_name = "tests"
76
- end
77
-
78
- class AbstractPool1DbB < ActiveRecord::Base
79
- self.abstract_class = true
80
- connects_to database: { writing: :test_pool_1_db_b }
81
- end
82
-
83
- class Pool1DbB < AbstractPool1DbB
84
- self.table_name = "tests"
85
- end
86
-
87
- class AbstractPool2DbD < ActiveRecord::Base
88
- self.abstract_class = true
89
- connects_to database: { writing: :test_pool_2_db_d }
90
- end
91
-
92
- class Pool2DbD < AbstractPool2DbD
93
- self.table_name = "tests"
94
- end
95
-
96
- class AbstractPool2DbE < ActiveRecord::Base
97
- self.abstract_class = true
98
- connects_to database: { writing: :test_pool_2_db_e }
99
- end
100
-
101
- class Pool2DbE < AbstractPool2DbE
102
- self.table_name = "tests"
103
- end
104
-
105
- class AbstractPool3DbE < ActiveRecord::Base
106
- self.abstract_class = true
107
- connects_to database: { writing: :test_pool_3_db_e }
108
- end
109
-
110
- class Pool3DbE < AbstractPool3DbE
111
- self.table_name = "tests"
112
- end
113
-
114
- # Test ARHP with Rails 6.1+ horizontal sharding functionality
115
- class AbstractShardedModel < ActiveRecord::Base
116
- self.abstract_class = true
117
- connects_to shards: {
118
- default: { writing: :test_pool_1_db_shard_a },
119
- shard_b: { writing: :test_pool_1_db_shard_b, reading: :test_pool_1_db_shard_b_replica },
120
- shard_c: { writing: :test_pool_1_db_shard_c, reading: :test_pool_1_db_shard_c_replica },
121
- shard_d: { writing: :test_pool_2_db_shard_d, reading: :test_pool_2_db_shard_d_replica }
122
- }
123
- end
124
-
125
- class ShardedModel < AbstractShardedModel
126
- self.table_name = "tests"
127
- end
128
- RUBY
129
- else
130
- eval <<-RUBY
131
- # The placement of the Pool1DbC class is important so that its
132
- # connection will not be the most recent connection established
133
- # for test_pool_1.
134
- class Pool1DbC < ActiveRecord::Base
135
- establish_connection(:test_pool_1_db_c)
136
- end
137
-
138
- class Pool1DbA < ActiveRecord::Base
139
- self.table_name = "tests"
140
- establish_connection(:test_pool_1_db_a)
141
- end
142
-
143
- class Pool1DbAReplica < ActiveRecord::Base
144
- self.table_name = "tests"
145
- establish_connection(:test_pool_1_db_a_replica)
146
- end
147
-
148
- class Pool1DbB < ActiveRecord::Base
149
- self.table_name = "tests"
150
- establish_connection(:test_pool_1_db_b)
151
- end
152
-
153
- class Pool2DbD < ActiveRecord::Base
154
- self.table_name = "tests"
155
- establish_connection(:test_pool_2_db_d)
156
- end
157
-
158
- class Pool2DbE < ActiveRecord::Base
159
- self.table_name = "tests"
160
- establish_connection(:test_pool_2_db_e)
161
- end
162
-
163
- class Pool3DbE < ActiveRecord::Base
164
- self.table_name = "tests"
165
- establish_connection(:test_pool_3_db_e)
166
- end
167
- RUBY
168
- end
169
- end
170
-
171
- def current_database(klass)
172
- klass.connection.select_value('select DATABASE()')
173
- end
174
-
175
- # Remove a method from a given module that fixes something.
176
- # Execute the passed in block.
177
- # Re-add the method back to the module.
178
- def without_module_patch(mod, method_name)
179
- method_body = mod.instance_method(method_name)
180
- mod.remove_method(method_name)
181
- yield if block_given?
182
- ensure
183
- mod.define_method(method_name, method_body)
184
- end
185
-
186
- def simulate_rails_app_active_record_railties
187
- if ActiveRecord.version >= Gem::Version.new('6.0') && !RAILS_6_1_WITH_NON_LEGACY_CONNECTION_HANDLING
188
- # Necessary for testing ActiveRecord 6.0 which uses the connection
189
- # handlers when clearing query caches across all handlers when
190
- # an operation that dirties the cache is involved (e.g. create/insert,
191
- # update, delete/destroy, truncate, etc.)
192
- # In Rails 6.1 this is only present when legacy_connection_handling=true
193
- ActiveRecord::Base.connection_handlers = {
194
- ActiveRecord::Base.writing_role => ActiveRecord::Base.default_connection_handler
195
- }
196
- end
197
- end
198
- end
data/test/schema.rb DELETED
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'helper'
4
-
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
19
- end
20
- end
21
- ensure
22
- ActiveRecordHostPool.allowing_writes = false
23
- end