active_record_host_pool 1.0.2 → 1.1.1

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: 5eb359cbe2626874bf6836ee5b5b1826ccf02fef7b474f81f5eec273435232d0
4
- data.tar.gz: 40383e9f54da681dd340b7dba75aac65613a34fd083862bf96a905f766c9f7ea
3
+ metadata.gz: 2dc3e41230c1cc7b208056d7f6fe99abe717e190b55cdca3ff5df9f1513fcad1
4
+ data.tar.gz: 92f1982d46da0644184083a78d72ad878d54ff70413f347fcb67bc3890cfaeca
5
5
  SHA512:
6
- metadata.gz: 84d8eaafd2a6ebd88a42e89c8a52113b1ae3654123bf6d6735256e4787563862024af288b9755c254d445151c43d35502f52608e468beed6daad309225fb4480
7
- data.tar.gz: 9a6493969b78849cea22220f562371e25a8215015ee747c5bf8ca533dfca4db21a637336c127c7abc50e6eee7d31843dd75b14fcbe7f329e461e2a24dc0fbd35
6
+ metadata.gz: b3e4c62f5aee3ae968622ca9a7fef59fd9974c4153c244c312715036f6753a0ec58ba3083247bb9a4b9479db3a3c55a8182c85ae320d273ad90a1084ea745b46
7
+ data.tar.gz: 1fcec2e7e23272e50894a19bd4fc195e0360089d3a5e44f98fc6a589723f3a6bb8dae2276dbeeec31f05f87f89641308909412b28f73f929bc2358605fe30937
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.
@@ -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
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
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
+ # Sits between ConnectionHandler and a bunch of different ConnectionPools (one per host).
17
+ class PoolProxy < Delegator
18
+ def initialize(pool_config)
19
+ super(pool_config)
20
+ @pool_config = pool_config
21
+ @config = pool_config.db_config.configuration_hash
22
+ end
23
+
24
+ def __getobj__
25
+ _connection_pool
26
+ end
27
+
28
+ def __setobj__(pool_config)
29
+ @pool_config = pool_config
30
+ @config = pool_config.db_config.configuration_hash
31
+ @_pool_key = nil
32
+ end
33
+
34
+ attr_reader :pool_config
35
+
36
+ def connection(*args)
37
+ real_connection = _connection_pool.connection(*args)
38
+ _connection_proxy_for(real_connection, @config[:database])
39
+ rescue Mysql2::Error, ActiveRecord::NoDatabaseError
40
+ _connection_pools.delete(_pool_key)
41
+ Kernel.raise
42
+ end
43
+
44
+ # by the time we are patched into ActiveRecord, the current thread has already established
45
+ # a connection. thus we need to patch both connection and checkout/checkin
46
+ def checkout(*args, &block)
47
+ cx = _connection_pool.checkout(*args, &block)
48
+ _connection_proxy_for(cx, @config[:database])
49
+ end
50
+
51
+ def checkin(cx)
52
+ cx = cx.unproxied
53
+ _connection_pool.checkin(cx)
54
+ end
55
+
56
+ def with_connection
57
+ cx = checkout
58
+ yield cx
59
+ ensure
60
+ checkin cx
61
+ end
62
+
63
+ def disconnect!
64
+ p = _connection_pool(false)
65
+ return unless p
66
+
67
+ p.disconnect!
68
+ p.automatic_reconnect = true if p.respond_to?(:automatic_reconnect=)
69
+ _clear_connection_proxy_cache
70
+ end
71
+
72
+ def automatic_reconnect=(value)
73
+ p = _connection_pool(false)
74
+ return unless p
75
+
76
+ p.automatic_reconnect = value if p.respond_to?(:automatic_reconnect=)
77
+ end
78
+
79
+ def clear_reloadable_connections!
80
+ _connection_pool.clear_reloadable_connections!
81
+ _clear_connection_proxy_cache
82
+ end
83
+
84
+ def release_connection(*args)
85
+ p = _connection_pool(false)
86
+ return unless p
87
+
88
+ p.release_connection(*args)
89
+ end
90
+
91
+ def flush!
92
+ p = _connection_pool(false)
93
+ return unless p
94
+
95
+ p.flush!
96
+ end
97
+
98
+ def discard!
99
+ p = _connection_pool(false)
100
+ return unless p
101
+
102
+ p.discard!
103
+
104
+ # All connections in the pool (even if they're currently
105
+ # leased!) have just been discarded, along with the pool itself.
106
+ # Any further interaction with the pool (except #pool_config and #schema_cache)
107
+ # is undefined.
108
+ # Remove the connection for the given key so a new one can be created in its place
109
+ _connection_pools.delete(_pool_key)
110
+ end
111
+
112
+ private
113
+
114
+ def _connection_pools
115
+ @@_connection_pools ||= {}
116
+ end
117
+
118
+ def _pool_key
119
+ @_pool_key ||= "#{@config[:host]}/#{@config[:port]}/#{@config[:socket]}/" \
120
+ "#{@config[:username]}/#{replica_configuration? && 'replica'}"
121
+ end
122
+
123
+ def _connection_pool(auto_create = true)
124
+ pool = _connection_pools[_pool_key]
125
+ if pool.nil? && auto_create
126
+ pool = _connection_pools[_pool_key] = ActiveRecord::ConnectionAdapters::ConnectionPool.new(@pool_config)
127
+ end
128
+ pool
129
+ end
130
+
131
+ def _connection_proxy_for(connection, database)
132
+ @connection_proxy_cache ||= {}
133
+ key = [connection, database]
134
+
135
+ @connection_proxy_cache[key] ||= begin
136
+ cx = ActiveRecordHostPool::ConnectionProxy.new(connection, database)
137
+ cx.execute('select 1')
138
+ cx
139
+ end
140
+ end
141
+
142
+ def _clear_connection_proxy_cache
143
+ @connection_proxy_cache = {}
144
+ end
145
+
146
+ def replica_configuration?
147
+ @config[:replica] || @config[:slave]
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,152 @@
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 = _connection_pool.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
+ # by the time we are patched into ActiveRecord, the current thread has already established
47
+ # a connection. thus we need to patch both connection and checkout/checkin
48
+ def checkout(*args, &block)
49
+ cx = _connection_pool.checkout(*args, &block)
50
+ _connection_proxy_for(cx, @config[:database])
51
+ end
52
+
53
+ def checkin(cx)
54
+ cx = cx.unproxied
55
+ _connection_pool.checkin(cx)
56
+ end
57
+
58
+ def with_connection
59
+ cx = checkout
60
+ yield cx
61
+ ensure
62
+ checkin cx
63
+ end
64
+
65
+ def disconnect!
66
+ p = _connection_pool(false)
67
+ return unless p
68
+
69
+ p.disconnect!
70
+ p.automatic_reconnect = true if p.respond_to?(:automatic_reconnect=)
71
+ _clear_connection_proxy_cache
72
+ end
73
+
74
+ def automatic_reconnect=(value)
75
+ p = _connection_pool(false)
76
+ return unless p
77
+
78
+ p.automatic_reconnect = value if p.respond_to?(:automatic_reconnect=)
79
+ end
80
+
81
+ def clear_reloadable_connections!
82
+ _connection_pool.clear_reloadable_connections!
83
+ _clear_connection_proxy_cache
84
+ end
85
+
86
+ def release_connection(*args)
87
+ p = _connection_pool(false)
88
+ return unless p
89
+
90
+ p.release_connection(*args)
91
+ end
92
+
93
+ def flush!
94
+ p = _connection_pool(false)
95
+ return unless p
96
+
97
+ p.flush!
98
+ end
99
+
100
+ def discard!
101
+ p = _connection_pool(false)
102
+ return unless p
103
+
104
+ p.discard!
105
+
106
+ # All connections in the pool (even if they're currently
107
+ # leased!) have just been discarded, along with the pool itself.
108
+ # Any further interaction with the pool (except #spec and #schema_cache)
109
+ # is undefined.
110
+ # Remove the connection for the given key so a new one can be created in its place
111
+ _connection_pools.delete(_pool_key)
112
+ end
113
+
114
+ private
115
+
116
+ def _connection_pools
117
+ @@_connection_pools ||= {}
118
+ end
119
+
120
+ def _pool_key
121
+ @_pool_key ||= "#{@config[:host]}/#{@config[:port]}/#{@config[:socket]}/" \
122
+ "#{@config[:username]}/#{replica_configuration? && 'replica'}"
123
+ end
124
+
125
+ def _connection_pool(auto_create = true)
126
+ pool = _connection_pools[_pool_key]
127
+ if pool.nil? && auto_create
128
+ pool = _connection_pools[_pool_key] = ActiveRecord::ConnectionAdapters::ConnectionPool.new(@spec)
129
+ end
130
+ pool
131
+ end
132
+
133
+ def _connection_proxy_for(connection, database)
134
+ @connection_proxy_cache ||= {}
135
+ key = [connection, database]
136
+
137
+ @connection_proxy_cache[key] ||= begin
138
+ cx = ActiveRecordHostPool::ConnectionProxy.new(connection, database)
139
+ cx.execute('select 1')
140
+ cx
141
+ end
142
+ end
143
+
144
+ def _clear_connection_proxy_cache
145
+ @connection_proxy_cache = {}
146
+ end
147
+
148
+ def replica_configuration?
149
+ @config[:replica] || @config[:slave]
150
+ end
151
+ end
152
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordHostPool
4
- VERSION = "1.0.2"
4
+ VERSION = "1.1.1"
5
5
  end