active_record_host_pool 1.0.2 → 1.1.1

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: 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