active_record_host_pool 1.0.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Readme.md +36 -0
- data/lib/active_record_host_pool/clear_query_cache_patch.rb +9 -0
- data/lib/active_record_host_pool/connection_adapter_mixin.rb +19 -12
- data/lib/active_record_host_pool/pool_proxy.rb +4 -155
- data/lib/active_record_host_pool/version.rb +1 -1
- data/test/database.yml +52 -21
- data/test/helper.rb +157 -43
- data/test/schema.rb +16 -9
- data/test/test_arhp.rb +56 -103
- metadata +26 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7baaf5e8c49a8308e3be6a9f52b41af880f0d8182a98a12be2ce8fa6d7ccbe60
|
4
|
+
data.tar.gz: aa68d84f75c00f608d13d475761f936722171f3d3d59a4ede471513279e8cda2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 072b56581266326d5decfd1221ccb9bf2ffde945cd04e1ce52278dd89d3b9e84b4d59d0a8d8e21808352d25a0f360ed67a006f71dc112751b7937a42f21aa455
|
7
|
+
data.tar.gz: c6075399e78ec309aeca872079044b042a5208dc1071c068eac01928a83b588d23cfc0d268907c0a9aa5c13a7d9b99005443e644b8142cb7ad9cb8a0e7cf013a
|
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.
|
@@ -26,6 +26,15 @@ if ActiveRecord.version >= Gem::Version.new('6.0')
|
|
26
26
|
# restore in case clearing the cache changed the database
|
27
27
|
connection.unproxied._host_pool_current_database = host_pool_current_database_was
|
28
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
|
29
38
|
end
|
30
39
|
end
|
31
40
|
|
@@ -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
|
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
|
-
|
4
|
-
require '
|
5
|
-
|
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
|
data/test/database.yml
CHANGED
@@ -1,76 +1,107 @@
|
|
1
1
|
<% mysql = URI(ENV['MYSQL_URL'] || 'mysql://root@127.0.0.1:3306') %>
|
2
|
-
|
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:
|
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
|
-
|
40
|
+
test_pool_1_db_a_replica:
|
13
41
|
adapter: mysql2
|
14
42
|
encoding: utf8
|
15
|
-
database:
|
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
|
-
|
50
|
+
test_pool_1_db_b:
|
23
51
|
adapter: mysql2
|
24
52
|
encoding: utf8
|
25
|
-
database:
|
53
|
+
database: arhp_test_db_b
|
26
54
|
username: <%= mysql.user %>
|
27
55
|
password: <%= mysql.password %>
|
28
56
|
host: <%= mysql.host %>
|
29
57
|
reconnect: true
|
30
58
|
|
31
|
-
|
59
|
+
test_pool_1_db_not_there:
|
32
60
|
adapter: mysql2
|
33
61
|
encoding: utf8
|
34
|
-
database:
|
62
|
+
database: arhp_test_db_not_there
|
35
63
|
username: <%= mysql.user %>
|
36
64
|
password: <%= mysql.password %>
|
37
65
|
host: <%= mysql.host %>
|
38
|
-
port: <%= mysql.port %>
|
39
66
|
reconnect: true
|
40
67
|
|
41
|
-
|
68
|
+
test_pool_2_db_d:
|
42
69
|
adapter: mysql2
|
43
70
|
encoding: utf8
|
44
|
-
database:
|
71
|
+
database: arhp_test_db_d
|
45
72
|
username: <%= mysql.user %>
|
46
73
|
password: <%= mysql.password %>
|
47
74
|
host: <%= mysql.host %>
|
48
75
|
port: <%= mysql.port %>
|
49
76
|
reconnect: true
|
50
77
|
|
51
|
-
|
78
|
+
test_pool_2_db_e:
|
52
79
|
adapter: mysql2
|
53
80
|
encoding: utf8
|
54
|
-
database:
|
55
|
-
username:
|
56
|
-
password:
|
81
|
+
database: arhp_test_db_e
|
82
|
+
username: <%= mysql.user %>
|
83
|
+
password: <%= mysql.password %>
|
57
84
|
host: <%= mysql.host %>
|
58
85
|
port: <%= mysql.port %>
|
59
86
|
reconnect: true
|
60
87
|
|
61
|
-
|
88
|
+
test_pool_3_db_e:
|
62
89
|
adapter: mysql2
|
63
90
|
encoding: utf8
|
64
|
-
database:
|
65
|
-
username:
|
66
|
-
password:
|
91
|
+
database: arhp_test_db_e
|
92
|
+
username: john-doe
|
93
|
+
password:
|
67
94
|
host: <%= mysql.host %>
|
95
|
+
port: <%= mysql.port %>
|
68
96
|
reconnect: true
|
69
97
|
|
70
|
-
|
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:
|
71
102
|
adapter: mysql2
|
72
103
|
encoding: utf8
|
73
|
-
database:
|
104
|
+
database: arhp_test_db_c
|
74
105
|
username: <%= mysql.user %>
|
75
106
|
password: <%= mysql.password %>
|
76
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/
|
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
|
-
|
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,46 +48,121 @@ module ARHPTestSetup
|
|
22
48
|
private
|
23
49
|
|
24
50
|
def arhp_create_models
|
25
|
-
return if ARHPTestSetup.const_defined?('
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
65
166
|
end
|
66
167
|
|
67
168
|
def current_database(klass)
|
@@ -78,4 +179,17 @@ module ARHPTestSetup
|
|
78
179
|
ensure
|
79
180
|
mod.define_method(method_name, method_body)
|
80
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
|
81
195
|
end
|
data/test/schema.rb
CHANGED
@@ -1,16 +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', force: true do |t|
|
6
|
-
t.string 'val'
|
7
|
-
end
|
8
4
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
14
19
|
end
|
15
20
|
end
|
21
|
+
ensure
|
22
|
+
ActiveRecordHostPool.allowing_writes = false
|
16
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
|
-
|
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
|
|
@@ -30,110 +36,80 @@ class ActiveRecordHostPoolTest < Minitest::Test
|
|
30
36
|
ActiveRecord::Base.connection_handler.clear_all_connections!
|
31
37
|
end
|
32
38
|
|
33
|
-
def
|
34
|
-
assert_equal(
|
35
|
-
assert_equal(
|
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
|
43
|
-
refute_equal(
|
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
|
47
|
-
refute_equal(
|
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
|
-
|
52
|
-
|
53
|
+
Pool1DbA.connection.send(:select_all, 'select 1')
|
54
|
+
assert_equal 'arhp_test_db_a', current_database(Pool1DbA)
|
53
55
|
|
54
|
-
|
55
|
-
|
56
|
-
end
|
56
|
+
Pool2DbD.connection.send(:select_all, 'select 1')
|
57
|
+
assert_equal 'arhp_test_db_d', current_database(Pool2DbD)
|
57
58
|
|
58
|
-
|
59
|
-
|
60
|
-
assert_equal(Test1.connection.raw_connection, Test1Shard.connection.raw_connection)
|
59
|
+
Pool3DbE.connection.send(:select_all, 'select 1')
|
60
|
+
assert_equal 'arhp_test_db_e', current_database(Pool3DbE)
|
61
61
|
end
|
62
62
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
# Remove patch that fixes an issue in Rails 6+ to ensure it still
|
68
|
-
# exists. If this begins to fail then it may mean that Rails has fixed
|
69
|
-
# the issue so that it no longer occurs.
|
70
|
-
without_module_patch(ActiveRecordHostPool::ClearQueryCachePatch, :clear_query_caches_for_current_thread) do
|
71
|
-
exception = assert_raises(ActiveRecord::StatementInvalid) do
|
72
|
-
ActiveRecord::Base.cache { Test1Shard.create! }
|
73
|
-
end
|
74
|
-
|
75
|
-
assert_equal("Mysql2::Error: Table 'arhp_test_2.test1_shards' doesn't exist", exception.message)
|
76
|
-
end
|
77
|
-
end
|
63
|
+
def test_should_insert_on_correct_database
|
64
|
+
Pool1DbA.connection.send(:insert, "insert into tests values(NULL, 'foo')")
|
65
|
+
assert_equal 'arhp_test_db_a', current_database(Pool1DbA)
|
78
66
|
|
79
|
-
|
80
|
-
|
67
|
+
Pool2DbD.connection.send(:insert, "insert into tests values(NULL, 'foo')")
|
68
|
+
assert_equal 'arhp_test_db_d', current_database(Pool2DbD)
|
81
69
|
|
82
|
-
|
83
|
-
|
84
|
-
# handlers which can cause the database to change.
|
85
|
-
# See ActiveRecordHostPool::ClearQueryCachePatch
|
86
|
-
ActiveRecord::Base.cache { Test1Shard.create! }
|
87
|
-
end
|
70
|
+
Pool3DbE.connection.send(:insert, "insert into tests values(NULL, 'foo')")
|
71
|
+
assert_equal 'arhp_test_db_e', current_database(Pool3DbE)
|
88
72
|
end
|
89
73
|
|
90
74
|
def test_connection_returns_a_proxy
|
91
|
-
assert_kind_of ActiveRecordHostPool::ConnectionProxy,
|
75
|
+
assert_kind_of ActiveRecordHostPool::ConnectionProxy, Pool1DbA.connection
|
92
76
|
end
|
93
77
|
|
94
78
|
def test_connection_proxy_handles_private_methods
|
95
79
|
# Relies on connection.class returning the real class
|
96
|
-
|
80
|
+
Pool1DbA.connection.class.class_eval do
|
97
81
|
private
|
98
82
|
|
99
83
|
def test_private_method
|
100
84
|
true
|
101
85
|
end
|
102
86
|
end
|
103
|
-
assert
|
104
|
-
refute
|
105
|
-
assert_includes(
|
106
|
-
assert_equal true,
|
107
|
-
end
|
108
|
-
|
109
|
-
def test_should_not_share_a_query_cache
|
110
|
-
Test1.create(val: 'foo')
|
111
|
-
Test2.create(val: 'foobar')
|
112
|
-
Test1.connection.cache do
|
113
|
-
refute_equal Test1.first.val, Test2.first.val
|
114
|
-
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)
|
115
91
|
end
|
116
92
|
|
117
93
|
def test_object_creation
|
118
|
-
|
119
|
-
assert_equal('
|
94
|
+
Pool1DbA.create(val: 'foo')
|
95
|
+
assert_equal('arhp_test_db_a', current_database(Pool1DbA))
|
120
96
|
|
121
|
-
|
122
|
-
assert_equal('
|
123
|
-
assert_equal('
|
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))
|
124
100
|
|
125
|
-
|
126
|
-
assert_equal('
|
127
|
-
assert
|
128
|
-
refute
|
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')
|
129
105
|
end
|
130
106
|
|
131
107
|
def test_disconnect
|
132
|
-
|
133
|
-
unproxied =
|
134
|
-
|
135
|
-
|
136
|
-
assert(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)
|
137
113
|
end
|
138
114
|
|
139
115
|
def test_checkout
|
@@ -145,7 +121,7 @@ class ActiveRecordHostPoolTest < Minitest::Test
|
|
145
121
|
end
|
146
122
|
|
147
123
|
def test_no_switch_when_creating_db
|
148
|
-
conn =
|
124
|
+
conn = Pool1DbA.connection
|
149
125
|
conn.expects(:execute_without_switching)
|
150
126
|
conn.expects(:_switch_connection).never
|
151
127
|
assert conn._host_pool_current_database
|
@@ -153,7 +129,7 @@ class ActiveRecordHostPoolTest < Minitest::Test
|
|
153
129
|
end
|
154
130
|
|
155
131
|
def test_no_switch_when_dropping_db
|
156
|
-
conn =
|
132
|
+
conn = Pool1DbA.connection
|
157
133
|
conn.expects(:execute_without_switching)
|
158
134
|
conn.expects(:_switch_connection).never
|
159
135
|
assert conn._host_pool_current_database
|
@@ -163,17 +139,17 @@ class ActiveRecordHostPoolTest < Minitest::Test
|
|
163
139
|
def test_underlying_assumption_about_test_db
|
164
140
|
debug_me = false
|
165
141
|
# ensure connection
|
166
|
-
|
142
|
+
Pool1DbA.first
|
167
143
|
|
168
144
|
# which is the "default" DB to connect to?
|
169
|
-
first_db =
|
145
|
+
first_db = Pool1DbA.connection.unproxied.instance_variable_get(:@_cached_current_database)
|
170
146
|
puts "\nOk, we started on #{first_db}" if debug_me
|
171
147
|
|
172
148
|
switch_to_klass = case first_db
|
173
|
-
when '
|
174
|
-
|
175
|
-
when '
|
176
|
-
|
149
|
+
when 'arhp_test_db_b'
|
150
|
+
Pool1DbA
|
151
|
+
when 'arhp_test_db_a'
|
152
|
+
Pool1DbB
|
177
153
|
else
|
178
154
|
raise "Expected a database name, got #{first_db.inspect}"
|
179
155
|
end
|
@@ -188,7 +164,7 @@ class ActiveRecordHostPoolTest < Minitest::Test
|
|
188
164
|
|
189
165
|
# now, disable our auto-switching and trigger a mysql reconnect
|
190
166
|
switch_to_klass.connection.unproxied.stubs(:_switch_connection).returns(true)
|
191
|
-
|
167
|
+
Pool2DbD.connection.execute("KILL #{thread_id}")
|
192
168
|
|
193
169
|
# and finally, did mysql reconnect correctly?
|
194
170
|
puts "\nAnd now we end up on #{current_database(switch_to_klass)}" if debug_me
|
@@ -201,27 +177,4 @@ class ActiveRecordHostPoolTest < Minitest::Test
|
|
201
177
|
pool.expects(:checkin).with(conn)
|
202
178
|
pool.release_connection
|
203
179
|
end
|
204
|
-
|
205
|
-
private
|
206
|
-
|
207
|
-
def assert_action_uses_correct_database(action, sql)
|
208
|
-
(1..4).each do |i|
|
209
|
-
klass = ARHPTestSetup.const_get("Test#{i}")
|
210
|
-
desired_db = "arhp_test_#{i}"
|
211
|
-
klass.connection.send(action, sql)
|
212
|
-
assert_equal desired_db, current_database(klass)
|
213
|
-
end
|
214
|
-
end
|
215
|
-
|
216
|
-
def simulate_rails_app_active_record_railties
|
217
|
-
if ActiveRecord.version >= Gem::Version.new('6.0')
|
218
|
-
# Necessary for testing ActiveRecord 6.0 which uses the connection
|
219
|
-
# handlers when clearing query caches across all handlers when
|
220
|
-
# an operation that dirties the cache is involved (e.g. create/insert,
|
221
|
-
# update, delete/destroy, truncate, etc.)
|
222
|
-
ActiveRecord::Base.connection_handlers = {
|
223
|
-
ActiveRecord::Base.writing_role => ActiveRecord::Base.default_connection_handler
|
224
|
-
}
|
225
|
-
end
|
226
|
-
end
|
227
180
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_record_host_pool
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Benjamin Quorning
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date:
|
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:
|
22
|
+
version: 5.1.0
|
23
23
|
- - "<"
|
24
24
|
- !ruby/object:Gem::Version
|
25
|
-
version: '
|
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:
|
32
|
+
version: 5.1.0
|
33
33
|
- - "<"
|
34
34
|
- !ruby/object:Gem::Version
|
35
|
-
version: '
|
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:
|
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:
|
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:
|
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:
|
105
|
+
version: 1.0.1
|
92
106
|
- !ruby/object:Gem::Dependency
|
93
107
|
name: rake
|
94
108
|
requirement: !ruby/object:Gem::Requirement
|
@@ -166,14 +180,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
166
180
|
requirements:
|
167
181
|
- - ">="
|
168
182
|
- !ruby/object:Gem::Version
|
169
|
-
version:
|
183
|
+
version: 2.6.0
|
170
184
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
171
185
|
requirements:
|
172
186
|
- - ">="
|
173
187
|
- !ruby/object:Gem::Version
|
174
188
|
version: '0'
|
175
189
|
requirements: []
|
176
|
-
rubygems_version: 3.
|
190
|
+
rubygems_version: 3.1.6
|
177
191
|
signing_key:
|
178
192
|
specification_version: 4
|
179
193
|
summary: Allow ActiveRecord to share a connection to multiple databases on the same
|