active_record_host_pool 1.2.5 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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