active_record_host_pool 1.0.1 → 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: a62e23b1cbccc5365b119d31329f4c227a6efebf729ce49edb5f2dd63d704e39
4
- data.tar.gz: 815dcd119bebc0ddb5ad7308ca41b4906e15cc72ede2048d164623cdd539df7f
3
+ metadata.gz: 7baaf5e8c49a8308e3be6a9f52b41af880f0d8182a98a12be2ce8fa6d7ccbe60
4
+ data.tar.gz: aa68d84f75c00f608d13d475761f936722171f3d3d59a4ede471513279e8cda2
5
5
  SHA512:
6
- metadata.gz: 8c4022f240849bdc6be2140d5b6abda363264e1f1011e6781436537516c356c2ac4a05b82d30891143274113cf18575b0817b76ccc5f22a6fcb0ebd190148db1
7
- data.tar.gz: dd7d2ffed45316302a921ab8f70c7ae067cef09e5ae01a0961b1f899cc34808e5b6077d488182e590bc88b767dd670d29ebb6b83aaf18222c370a5fdd3c06f12
6
+ metadata.gz: 072b56581266326d5decfd1221ccb9bf2ffde945cd04e1ce52278dd89d3b9e84b4d59d0a8d8e21808352d25a0f360ed67a006f71dc112751b7937a42f21aa455
7
+ data.tar.gz: c6075399e78ec309aeca872079044b042a5208dc1071c068eac01928a83b588d23cfc0d268907c0a9aa5c13a7d9b99005443e644b8142cb7ad9cb8a0e7cf013a
data/Readme.md CHANGED
@@ -1,10 +1,46 @@
1
- [![Build status](https://circleci.com/gh/zendesk/active_record_host_pool.svg?style=svg)](https://circleci.com/gh/zendesk/active_record_host_pool)
1
+ [![Build Status](https://github.com/zendesk/active_record_host_pool/workflows/CI/badge.svg)](https://github.com/zendesk/active_record_host_pool/actions?query=workflow%3ACI)
2
2
 
3
3
  # ActiveRecord host pooling
4
4
 
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.
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ if ActiveRecord.version >= Gem::Version.new('6.0')
4
+ module ActiveRecordHostPool
5
+ # ActiveRecord 6.0 introduced multiple database support. With that, an update
6
+ # has been made in https://github.com/rails/rails/pull/35089 to ensure that
7
+ # all query caches are cleared across connection handlers and pools. If you
8
+ # write on one connection, the other connection will have the update that
9
+ # occurred.
10
+ #
11
+ # This broke ARHP which implements its own pool, allowing you to access
12
+ # multiple databases with the same connection (e.g. 1 connection for 100
13
+ # shards on the same server).
14
+ #
15
+ # This patch maintains the reference to the database during the cache clearing
16
+ # to ensure that the database doesn't get swapped out mid-way into an
17
+ # operation.
18
+ #
19
+ # This is a private Rails API and may change in future releases as they're
20
+ # actively working on sharding in Rails 6 and above.
21
+ module ClearQueryCachePatch
22
+ def clear_query_caches_for_current_thread
23
+ host_pool_current_database_was = connection.unproxied._host_pool_current_database
24
+ super
25
+ ensure
26
+ # restore in case clearing the cache changed the database
27
+ connection.unproxied._host_pool_current_database = host_pool_current_database_was
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
38
+ end
39
+ end
40
+
41
+ ActiveRecord::Base.singleton_class.prepend ActiveRecordHostPool::ClearQueryCachePatch
42
+ end
@@ -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.1"
4
+ VERSION = "1.1.0"
5
5
  end
@@ -4,6 +4,7 @@ require 'active_record'
4
4
  require 'active_record/base'
5
5
  require 'active_record/connection_adapters/abstract_adapter'
6
6
 
7
+ require 'active_record_host_pool/clear_query_cache_patch'
7
8
  require 'active_record_host_pool/connection_proxy'
8
9
  require 'active_record_host_pool/pool_proxy'
9
10
  require 'active_record_host_pool/connection_adapter_mixin'
data/test/database.yml CHANGED
@@ -1,67 +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:
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:
23
60
  adapter: mysql2
24
61
  encoding: utf8
25
- database: arhp_test_2
62
+ database: arhp_test_db_not_there
26
63
  username: <%= mysql.user %>
27
64
  password: <%= mysql.password %>
28
65
  host: <%= mysql.host %>
29
66
  reconnect: true
30
67
 
31
- test_host_2_db_3:
68
+ test_pool_2_db_d:
32
69
  adapter: mysql2
33
70
  encoding: utf8
34
- database: arhp_test_3
71
+ database: arhp_test_db_d
35
72
  username: <%= mysql.user %>
36
73
  password: <%= mysql.password %>
37
74
  host: <%= mysql.host %>
38
75
  port: <%= mysql.port %>
39
76
  reconnect: true
40
77
 
41
- test_host_2_db_4:
78
+ test_pool_2_db_e:
42
79
  adapter: mysql2
43
80
  encoding: utf8
44
- database: arhp_test_4
81
+ database: arhp_test_db_e
45
82
  username: <%= mysql.user %>
46
83
  password: <%= mysql.password %>
47
84
  host: <%= mysql.host %>
48
85
  port: <%= mysql.port %>
49
86
  reconnect: true
50
87
 
51
- test_host_2_db_5:
88
+ test_pool_3_db_e:
52
89
  adapter: mysql2
53
90
  encoding: utf8
54
- database: arhp_test_4
91
+ database: arhp_test_db_e
55
92
  username: john-doe
56
93
  password:
57
94
  host: <%= mysql.host %>
58
95
  port: <%= mysql.port %>
59
96
  reconnect: true
60
97
 
61
- test_host_1_db_not_there:
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:
62
102
  adapter: mysql2
63
103
  encoding: utf8
64
- database: arhp_test_no_create
104
+ database: arhp_test_db_c
65
105
  username: <%= mysql.user %>
66
106
  password: <%= mysql.password %>
67
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,42 +48,148 @@ 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
- class Test1 < ActiveRecord::Base
29
- self.table_name = "tests"
30
- establish_connection(:test_host_1_db_1)
31
- end
32
-
33
- class Test1Slave < ActiveRecord::Base
34
- self.table_name = "tests"
35
- establish_connection(:test_host_1_db_1_slave)
36
- end
37
-
38
- class Test2 < ActiveRecord::Base
39
- self.table_name = "tests"
40
- establish_connection(:test_host_1_db_2)
41
- end
42
-
43
- class Test3 < ActiveRecord::Base
44
- self.table_name = "tests"
45
- establish_connection(:test_host_2_db_3)
46
- end
47
-
48
- class Test4 < ActiveRecord::Base
49
- self.table_name = "tests"
50
- establish_connection(:test_host_2_db_4)
51
- end
52
-
53
- class Test5 < ActiveRecord::Base
54
- self.table_name = "tests"
55
- establish_connection(:test_host_2_db_5)
56
- end
57
- 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
58
166
  end
59
167
 
60
168
  def current_database(klass)
61
169
  klass.connection.select_value('select DATABASE()')
62
170
  end
171
+
172
+ # Remove a method from a given module that fixes something.
173
+ # Execute the passed in block.
174
+ # Re-add the method back to the module.
175
+ def without_module_patch(mod, method_name)
176
+ method_body = mod.instance_method(method_name)
177
+ mod.remove_method(method_name)
178
+ yield if block_given?
179
+ ensure
180
+ mod.define_method(method_name, method_body)
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
63
195
  end
data/test/schema.rb CHANGED
@@ -1,8 +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' do |t|
6
- t.string 'val'
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
7
20
  end
21
+ ensure
22
+ ActiveRecordHostPool.allowing_writes = false
8
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
 
@@ -19,7 +25,7 @@ class ActiveRecordHostPoolTest < Minitest::Test
19
25
 
20
26
  # Verify that when we fork, the process doesn't crash
21
27
  pid = Process.fork do
22
- if ActiveRecord::VERSION::MAJOR == 5 && ActiveRecord::VERSION::MINOR == 2
28
+ if ActiveRecord.version >= Gem::Version.new('5.2')
23
29
  assert_equal(false, ActiveRecord::Base.connected?) # New to Rails 5.2
24
30
  else
25
31
  assert_equal(true, ActiveRecord::Base.connected?)
@@ -30,78 +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')
53
+ Pool1DbA.connection.send(:select_all, 'select 1')
54
+ assert_equal 'arhp_test_db_a', current_database(Pool1DbA)
55
+
56
+ Pool2DbD.connection.send(:select_all, 'select 1')
57
+ assert_equal 'arhp_test_db_d', current_database(Pool2DbD)
58
+
59
+ Pool3DbE.connection.send(:select_all, 'select 1')
60
+ assert_equal 'arhp_test_db_e', current_database(Pool3DbE)
52
61
  end
53
62
 
54
63
  def test_should_insert_on_correct_database
55
- assert_action_uses_correct_database(:insert, "insert into tests values(NULL, 'foo')")
64
+ Pool1DbA.connection.send(:insert, "insert into tests values(NULL, 'foo')")
65
+ assert_equal 'arhp_test_db_a', current_database(Pool1DbA)
66
+
67
+ Pool2DbD.connection.send(:insert, "insert into tests values(NULL, 'foo')")
68
+ assert_equal 'arhp_test_db_d', current_database(Pool2DbD)
69
+
70
+ Pool3DbE.connection.send(:insert, "insert into tests values(NULL, 'foo')")
71
+ assert_equal 'arhp_test_db_e', current_database(Pool3DbE)
56
72
  end
57
73
 
58
74
  def test_connection_returns_a_proxy
59
- assert_kind_of ActiveRecordHostPool::ConnectionProxy, Test1.connection
75
+ assert_kind_of ActiveRecordHostPool::ConnectionProxy, Pool1DbA.connection
60
76
  end
61
77
 
62
78
  def test_connection_proxy_handles_private_methods
63
79
  # Relies on connection.class returning the real class
64
- Test1.connection.class.class_eval do
80
+ Pool1DbA.connection.class.class_eval do
65
81
  private
66
82
 
67
83
  def test_private_method
68
84
  true
69
85
  end
70
86
  end
71
- assert Test1.connection.respond_to?(:test_private_method, true)
72
- refute Test1.connection.respond_to?(:test_private_method)
73
- assert_includes(Test1.connection.private_methods, :test_private_method)
74
- assert_equal true, Test1.connection.send(:test_private_method)
75
- end
76
-
77
- def test_should_not_share_a_query_cache
78
- Test1.create(val: 'foo')
79
- Test2.create(val: 'foobar')
80
- Test1.connection.cache do
81
- refute_equal Test1.first.val, Test2.first.val
82
- 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)
83
91
  end
84
92
 
85
93
  def test_object_creation
86
- Test1.create(val: 'foo')
87
- assert_equal('arhp_test_1', current_database(Test1))
94
+ Pool1DbA.create(val: 'foo')
95
+ assert_equal('arhp_test_db_a', current_database(Pool1DbA))
88
96
 
89
- Test3.create(val: 'bar')
90
- assert_equal('arhp_test_1', current_database(Test1))
91
- 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))
92
100
 
93
- Test2.create!(val: 'bar_distinct')
94
- assert_equal('arhp_test_2', current_database(Test2))
95
- assert Test2.find_by_val('bar_distinct')
96
- 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')
97
105
  end
98
106
 
99
107
  def test_disconnect
100
- Test1.create(val: 'foo')
101
- unproxied = Test1.connection.unproxied
102
- Test1.connection_handler.clear_all_connections!
103
- Test1.create(val: 'foo')
104
- 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)
105
113
  end
106
114
 
107
115
  def test_checkout
@@ -113,7 +121,7 @@ class ActiveRecordHostPoolTest < Minitest::Test
113
121
  end
114
122
 
115
123
  def test_no_switch_when_creating_db
116
- conn = Test1.connection
124
+ conn = Pool1DbA.connection
117
125
  conn.expects(:execute_without_switching)
118
126
  conn.expects(:_switch_connection).never
119
127
  assert conn._host_pool_current_database
@@ -121,7 +129,7 @@ class ActiveRecordHostPoolTest < Minitest::Test
121
129
  end
122
130
 
123
131
  def test_no_switch_when_dropping_db
124
- conn = Test1.connection
132
+ conn = Pool1DbA.connection
125
133
  conn.expects(:execute_without_switching)
126
134
  conn.expects(:_switch_connection).never
127
135
  assert conn._host_pool_current_database
@@ -131,17 +139,17 @@ class ActiveRecordHostPoolTest < Minitest::Test
131
139
  def test_underlying_assumption_about_test_db
132
140
  debug_me = false
133
141
  # ensure connection
134
- Test1.first
142
+ Pool1DbA.first
135
143
 
136
144
  # which is the "default" DB to connect to?
137
- first_db = Test1.connection.unproxied.instance_variable_get(:@_cached_current_database)
145
+ first_db = Pool1DbA.connection.unproxied.instance_variable_get(:@_cached_current_database)
138
146
  puts "\nOk, we started on #{first_db}" if debug_me
139
147
 
140
148
  switch_to_klass = case first_db
141
- when 'arhp_test_2'
142
- Test1
143
- when 'arhp_test_1'
144
- Test2
149
+ when 'arhp_test_db_b'
150
+ Pool1DbA
151
+ when 'arhp_test_db_a'
152
+ Pool1DbB
145
153
  else
146
154
  raise "Expected a database name, got #{first_db.inspect}"
147
155
  end
@@ -156,7 +164,7 @@ class ActiveRecordHostPoolTest < Minitest::Test
156
164
 
157
165
  # now, disable our auto-switching and trigger a mysql reconnect
158
166
  switch_to_klass.connection.unproxied.stubs(:_switch_connection).returns(true)
159
- Test3.connection.execute("KILL #{thread_id}")
167
+ Pool2DbD.connection.execute("KILL #{thread_id}")
160
168
 
161
169
  # and finally, did mysql reconnect correctly?
162
170
  puts "\nAnd now we end up on #{current_database(switch_to_klass)}" if debug_me
@@ -169,15 +177,4 @@ class ActiveRecordHostPoolTest < Minitest::Test
169
177
  pool.expects(:checkin).with(conn)
170
178
  pool.release_connection
171
179
  end
172
-
173
- private
174
-
175
- def assert_action_uses_correct_database(action, sql)
176
- (1..4).each do |i|
177
- klass = ARHPTestSetup.const_get("Test#{i}")
178
- desired_db = "arhp_test_#{i}"
179
- klass.connection.send(action, sql)
180
- assert_equal desired_db, current_database(klass)
181
- end
182
- end
183
180
  end
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_record_host_pool
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Benjamin Quorning
8
8
  - Gabe Martin-Dempesy
9
9
  - Pierre Schambacher
10
10
  - Ben Osheroff
11
- autorequire:
11
+ autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2020-03-30 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
@@ -145,6 +159,7 @@ files:
145
159
  - MIT-LICENSE
146
160
  - Readme.md
147
161
  - lib/active_record_host_pool.rb
162
+ - lib/active_record_host_pool/clear_query_cache_patch.rb
148
163
  - lib/active_record_host_pool/connection_adapter_mixin.rb
149
164
  - lib/active_record_host_pool/connection_proxy.rb
150
165
  - lib/active_record_host_pool/pool_proxy.rb
@@ -157,7 +172,7 @@ homepage: https://github.com/zendesk/active_record_host_pool
157
172
  licenses:
158
173
  - MIT
159
174
  metadata: {}
160
- post_install_message:
175
+ post_install_message:
161
176
  rdoc_options: []
162
177
  require_paths:
163
178
  - lib
@@ -165,15 +180,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
165
180
  requirements:
166
181
  - - ">="
167
182
  - !ruby/object:Gem::Version
168
- version: '0'
183
+ version: 2.6.0
169
184
  required_rubygems_version: !ruby/object:Gem::Requirement
170
185
  requirements:
171
186
  - - ">="
172
187
  - !ruby/object:Gem::Version
173
188
  version: '0'
174
189
  requirements: []
175
- rubygems_version: 3.1.1
176
- signing_key:
190
+ rubygems_version: 3.1.6
191
+ signing_key:
177
192
  specification_version: 4
178
193
  summary: Allow ActiveRecord to share a connection to multiple databases on the same
179
194
  host