activerecord_autoreplica 1.2.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +17 -5
- data/Rakefile +1 -1
- data/activerecord_autoreplica.gemspec +3 -3
- data/lib/activerecord_autoreplica.rb +96 -52
- data/spec/activerecord_autoreplica_spec.rb +77 -34
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0dec3a7a02bfdd20f0428bdb405016f3b16d7c12
|
4
|
+
data.tar.gz: c84f381a3f3c1d01f2066aec82597333a7a8d2c8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9d0de5551b0801a3dd9149111e2610715f27a12619f56fdbb29e3108591c06678e326bb2e7e14836ccc89e6f9ab4fc7eef212d52074bba39b4a37f332e8c852b
|
7
|
+
data.tar.gz: e4efd1d3811ffd76b38acb6707b7dbe57babbcad919da69d49e67746236fc15c58219c5ca2bf0b41745c9501f5326a4c925613f55c636a337f4619f3e8a2c72d
|
data/README.md
CHANGED
@@ -17,7 +17,9 @@ The only dependency is ActiveRecord itself.
|
|
17
17
|
|
18
18
|
### Usage
|
19
19
|
|
20
|
-
|
20
|
+
There are two options.
|
21
|
+
|
22
|
+
The first is to pass a complete ActiveRecord connection specificaton hash, and
|
21
23
|
everything within the block is going to use the read slave connections when performing standard
|
22
24
|
ActiveRecord `SELECT` queries (not the hand-written ones).
|
23
25
|
|
@@ -27,11 +29,21 @@ ActiveRecord `SELECT` queries (not the hand-written ones).
|
|
27
29
|
end
|
28
30
|
|
29
31
|
Connection strings (URLs) are also supported, just like in ActiveRecord itself:
|
30
|
-
|
32
|
+
|
31
33
|
AutoReplica.using_read_replica_at('sqlite3:/replica_db_145.sqlite3') do
|
32
34
|
...
|
33
35
|
end
|
34
|
-
|
36
|
+
|
37
|
+
Note that this will create and disconnect a ConnectionPool each time the block is called.
|
38
|
+
|
39
|
+
The other option is to create the ConnectionPool yourself, and pass it to `using_read_replica_pool`:
|
40
|
+
|
41
|
+
AutoReplica.using_read_replica_pool(my_connection_pool) do
|
42
|
+
...
|
43
|
+
end
|
44
|
+
|
45
|
+
This will release connections to the pool at the end of the block, but not close them.
|
46
|
+
|
35
47
|
To use in Rails controller context (for all actions of this controller):
|
36
48
|
|
37
49
|
class SlowDataReportsController < ApplicationController
|
@@ -56,7 +68,7 @@ act accordingly.
|
|
56
68
|
|
57
69
|
The `using_read_replica_at` block will allocate a `ConnectionPool` like the standard `ActiveRecord` connection
|
58
70
|
manager does, and the pool is going to be closed and torn down at the end of the block. Since it only uses the basic
|
59
|
-
ActiveRecord facilities (including mutexes) it should be threadsafe (but _not_ thread-local since the connection
|
71
|
+
ActiveRecord facilities (including mutexes) it should be threadsafe (but _not_ thread-local since the connection
|
60
72
|
handler in ActiveRecord isn't).
|
61
73
|
|
62
74
|
### Running the specs
|
@@ -78,7 +90,7 @@ Rails 3.x support is likely to be dropped in the next major version.
|
|
78
90
|
The gem version is specified in the Rakefile.
|
79
91
|
|
80
92
|
### Contributing to activerecord_autoreplica
|
81
|
-
|
93
|
+
|
82
94
|
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
83
95
|
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
84
96
|
* Fork the project.
|
data/Rakefile
CHANGED
@@ -13,7 +13,7 @@ require 'rake'
|
|
13
13
|
|
14
14
|
require 'jeweler'
|
15
15
|
Jeweler::Tasks.new do |gem|
|
16
|
-
gem.version = '1.
|
16
|
+
gem.version = '1.3.0'
|
17
17
|
# gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
|
18
18
|
gem.name = "activerecord_autoreplica"
|
19
19
|
gem.homepage = "http://github.com/WeTransfer/activerecord_autoreplica"
|
@@ -2,16 +2,16 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: activerecord_autoreplica 1.
|
5
|
+
# stub: activerecord_autoreplica 1.3.0 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "activerecord_autoreplica"
|
9
|
-
s.version = "1.
|
9
|
+
s.version = "1.3.0"
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib"]
|
13
13
|
s.authors = ["Julik Tarkhanov"]
|
14
|
-
s.date = "2016-
|
14
|
+
s.date = "2016-11-07"
|
15
15
|
s.description = " Redirect all SELECT queries to a separate connection within a block "
|
16
16
|
s.email = "me@julik.nl"
|
17
17
|
s.extra_rdoc_files = [
|
@@ -14,10 +14,10 @@
|
|
14
14
|
# * SomeRecord.connection calls ActiveRecord::Base.connection_handler
|
15
15
|
# * It then asks the connection handler for a connection for this specific ActiveRecord subclass, or barring that
|
16
16
|
# - for the connection for one of it's ancestors, ending with ActiveRecord::Base itself
|
17
|
-
# * The ConnectionHandler, in turn, asks one of
|
17
|
+
# * The ConnectionHandler, in turn, asks one of its managed ConnectionPool objects to give it a connection for use.
|
18
18
|
# * The connection is returned and then the query is ran against it.
|
19
19
|
#
|
20
|
-
# This is why the only integration point for this is ++ActiveRecord::Base.
|
20
|
+
# This is why the only integration point for this is ++ActiveRecord::Base.connection_handler=++
|
21
21
|
# To make our trick work, here is what we do:
|
22
22
|
#
|
23
23
|
# First we wrap the original ConnectionHandler used by ActiveRecord in our own proxy. That proxy will ask the original
|
@@ -41,7 +41,7 @@ module AutoReplica
|
|
41
41
|
# Runs a given block with all SELECT statements being executed against the read slave
|
42
42
|
# database.
|
43
43
|
#
|
44
|
-
# AutoReplica.using_read_replica_at(:adapter => 'mysql2', :
|
44
|
+
# AutoReplica.using_read_replica_at(:adapter => 'mysql2', :database => 'read_replica', ...) do
|
45
45
|
# customer = Customer.find(3) # Will SELECT from the replica database at the connection spec passed to the block
|
46
46
|
# customer.register_complaint! # Will UPDATE to the master database connection
|
47
47
|
# end
|
@@ -49,63 +49,49 @@ module AutoReplica
|
|
49
49
|
# @param replica_connection_spec_hash_or_url[String, Hash] an ActiveRecord connection specification or a DSN URL
|
50
50
|
# @return [void]
|
51
51
|
def self.using_read_replica_at(replica_connection_spec_hash_or_url)
|
52
|
+
in_replica_context(replica_connection_spec_hash_or_url, AdHocConnectionHandler){ yield }
|
53
|
+
end
|
54
|
+
|
55
|
+
# Runs a given block with all SELECT statements being executed using the read slave
|
56
|
+
# connection pool.
|
57
|
+
#
|
58
|
+
# read_pool = ActiveRecord::ConnectionAdapters::ConnectionPool.new(:adapter => 'mysql2', :database => 'read_replica', ...)
|
59
|
+
# AutoReplica.using_read_replica_pool(read_pool) do
|
60
|
+
# customer = Customer.find(3) # Will SELECT from the replica database picked off the read pool
|
61
|
+
# customer.register_complaint! # Will UPDATE to the master database connection
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# @param replica_connection_pool[ActiveRecord::ConnectionAdapters::ConnectionPool] an ActiveRecord connection pool instance
|
65
|
+
# @return [void]
|
66
|
+
def self.using_read_replica_pool(replica_connection_pool)
|
67
|
+
in_replica_context(replica_connection_pool){ yield }
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.in_replica_context(handler_params, handler_class=ConnectionHandler)
|
52
71
|
return yield if @in_replica_context # This method should not be reentrant
|
53
|
-
|
54
|
-
# Resolve if there is a URL given
|
55
|
-
# Duplicate the hash so that we can change it if we have to
|
56
|
-
# (say by deleting :adapter)
|
57
|
-
config_hash = if replica_connection_spec_hash_or_url.is_a?(Hash)
|
58
|
-
replica_connection_spec_hash_or_url.dup
|
59
|
-
else
|
60
|
-
resolve_connection_url(replica_connection_spec_hash_or_url).dup
|
61
|
-
end
|
62
|
-
|
72
|
+
|
63
73
|
@in_replica_context = true
|
64
|
-
|
74
|
+
|
65
75
|
original_connection_handler = ActiveRecord::Base.connection_handler
|
66
|
-
custom_handler =
|
76
|
+
custom_handler = handler_class.new(original_connection_handler, handler_params)
|
67
77
|
begin
|
68
78
|
ActiveRecord::Base.connection_handler = custom_handler
|
69
79
|
yield
|
70
80
|
ensure
|
71
81
|
ActiveRecord::Base.connection_handler = original_connection_handler
|
72
|
-
custom_handler.
|
82
|
+
custom_handler.finish
|
73
83
|
@in_replica_context = false
|
74
84
|
end
|
75
85
|
end
|
76
|
-
|
77
|
-
# Resolve an ActiveRecord connection URL, from a string to a Hash.
|
78
|
-
#
|
79
|
-
# @param url_string[String] the connection URL (like `sqlite3://...`)
|
80
|
-
# @return [Hash] a symbol-keyed ActiveRecord connection specification
|
81
|
-
def self.resolve_connection_url(url_string)
|
82
|
-
# TODO: privatize this method.
|
83
|
-
if defined?(ActiveRecord::Base::ConnectionSpecification::Resolver) # AR3
|
84
|
-
resolver = ActiveRecord::Base::ConnectionSpecification::Resolver.new(url_string, {})
|
85
|
-
resolver.send(:connection_url_to_hash, url_string) # Because making this public was so hard
|
86
|
-
else # AR4
|
87
|
-
resolved = ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(url_string).to_hash
|
88
|
-
resolved["database"].gsub!(/^\//, '') # which is not done by the resolver
|
89
|
-
resolved.symbolize_keys # which is also not done by the resolver
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
86
|
+
|
93
87
|
# The connection handler that wraps the ActiveRecord one. Everything gets forwarded to the wrapped
|
94
88
|
# object, but a "spiked" connection adapter gets returned from retrieve_connection.
|
95
89
|
class ConnectionHandler # a proxy for ActiveRecord::ConnectionAdapters::ConnectionHandler
|
96
|
-
def initialize(original_handler,
|
90
|
+
def initialize(original_handler, read_pool)
|
97
91
|
@original_handler = original_handler
|
98
|
-
|
99
|
-
# aside from the one managed by Rails proper.
|
100
|
-
adapter_method = "%s_connection" % connection_specification_hash[:adapter]
|
101
|
-
connection_specification = begin
|
102
|
-
ConnectionSpecification.new('autoreplica', connection_specification_hash, adapter_method)
|
103
|
-
rescue ArgumentError # AR 4 and lower wants 2 arguments
|
104
|
-
ConnectionSpecification.new(connection_specification_hash, adapter_method)
|
105
|
-
end
|
106
|
-
@read_pool = ActiveRecord::ConnectionAdapters::ConnectionPool.new(connection_specification)
|
92
|
+
@read_pool = read_pool
|
107
93
|
end
|
108
|
-
|
94
|
+
|
109
95
|
# Overridden method which gets called by ActiveRecord to get a connection related to a specific
|
110
96
|
# ActiveRecord::Base subclass.
|
111
97
|
def retrieve_connection(for_ar_class)
|
@@ -113,18 +99,22 @@ module AutoReplica
|
|
113
99
|
connection_for_reads = @read_pool.connection
|
114
100
|
Adapter.new(connection_for_writes, connection_for_reads)
|
115
101
|
end
|
116
|
-
|
102
|
+
|
103
|
+
def release_read_pool_connection
|
104
|
+
@read_pool.release_connection
|
105
|
+
end
|
106
|
+
|
117
107
|
# Close all the connections maintained by the read pool
|
118
108
|
def disconnect_read_pool!
|
119
109
|
@read_pool.disconnect!
|
120
110
|
end
|
121
|
-
|
111
|
+
|
122
112
|
# Disconnect both the original handler AND the read pool
|
123
113
|
def clear_all_connections!
|
124
114
|
disconnect_read_pool!
|
125
115
|
@original_handler.clear_all_connections!
|
126
116
|
end
|
127
|
-
|
117
|
+
|
128
118
|
# The duo for method proxying without delegate
|
129
119
|
def respond_to_missing?(method_name)
|
130
120
|
@original_handler.respond_to?(method_name)
|
@@ -132,6 +122,60 @@ module AutoReplica
|
|
132
122
|
def method_missing(method_name, *args, &blk)
|
133
123
|
@original_handler.public_send(method_name, *args, &blk)
|
134
124
|
end
|
125
|
+
|
126
|
+
# When finishing, releases the borrowed connection back into the pool
|
127
|
+
def finish
|
128
|
+
release_read_pool_connection
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# A connection handler that creates an ad-hoc read connection pool, and disconnects it when finishing
|
133
|
+
class AdHocConnectionHandler < ConnectionHandler
|
134
|
+
def initialize(original_handler, replica_connection_spec_hash_or_url)
|
135
|
+
connection_specification_hash = parse_params(replica_connection_spec_hash_or_url)
|
136
|
+
# We need to maintain our own pool for read replica connections,
|
137
|
+
# aside from the one managed by Rails proper.
|
138
|
+
adapter_method = "%s_connection" % connection_specification_hash[:adapter]
|
139
|
+
connection_specification = begin
|
140
|
+
ConnectionSpecification.new('autoreplica', connection_specification_hash, adapter_method)
|
141
|
+
rescue ArgumentError # AR 4 and lower wants 2 arguments
|
142
|
+
ConnectionSpecification.new(connection_specification_hash, adapter_method)
|
143
|
+
end
|
144
|
+
read_pool = ActiveRecord::ConnectionAdapters::ConnectionPool.new(connection_specification)
|
145
|
+
super(original_handler, read_pool)
|
146
|
+
end
|
147
|
+
|
148
|
+
def parse_params(replica_connection_spec_hash_or_url)
|
149
|
+
# Resolve if there is a URL given
|
150
|
+
# Duplicate the hash so that we can change it if we have to
|
151
|
+
# (say by deleting :adapter)
|
152
|
+
if replica_connection_spec_hash_or_url.is_a?(Hash)
|
153
|
+
replica_connection_spec_hash_or_url.dup
|
154
|
+
else
|
155
|
+
resolve_connection_url(replica_connection_spec_hash_or_url).dup
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Resolve an ActiveRecord connection URL, from a string to a Hash.
|
160
|
+
#
|
161
|
+
# @param url_string[String] the connection URL (like `sqlite3://...`)
|
162
|
+
# @return [Hash] a symbol-keyed ActiveRecord connection specification
|
163
|
+
def resolve_connection_url(url_string)
|
164
|
+
# TODO: privatize this method.
|
165
|
+
if defined?(ActiveRecord::Base::ConnectionSpecification::Resolver) # AR3
|
166
|
+
resolver = ActiveRecord::Base::ConnectionSpecification::Resolver.new(url_string, {})
|
167
|
+
resolver.send(:connection_url_to_hash, url_string) # Because making this public was so hard
|
168
|
+
else # AR4
|
169
|
+
resolved = ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(url_string).to_hash
|
170
|
+
resolved["database"].gsub!(/^\//, '') # which is not done by the resolver
|
171
|
+
resolved.symbolize_keys # which is also not done by the resolver
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Disconnect all read pool connections, making the pool ready to be disposed.
|
176
|
+
def finish
|
177
|
+
disconnect_read_pool!
|
178
|
+
end
|
135
179
|
end
|
136
180
|
|
137
181
|
# Acts as a wrapping proxy that replaces an ActiveRecord Adapter object. This is the
|
@@ -146,7 +190,7 @@ module AutoReplica
|
|
146
190
|
@master_connection = master_connection_adapter
|
147
191
|
@read_connection = replica_connection_adapter
|
148
192
|
end
|
149
|
-
|
193
|
+
|
150
194
|
# Under the hood, ActiveRecord uses methods for the most common database statements
|
151
195
|
# like "select_all", "select_one", "select_value" and so on. Those can be overridden by concrete
|
152
196
|
# connection adapters, but in the basic abstract Adapter they get included from
|
@@ -160,7 +204,7 @@ module AutoReplica
|
|
160
204
|
@read_connection.send(select_method_name, *method_arguments)
|
161
205
|
end
|
162
206
|
end
|
163
|
-
|
207
|
+
|
164
208
|
# The duo for method proxying without delegate
|
165
209
|
def respond_to_missing?(method_name)
|
166
210
|
@master_connection.respond_to?(method_name)
|
@@ -169,11 +213,11 @@ module AutoReplica
|
|
169
213
|
@master_connection.public_send(method_name, *args, &blk)
|
170
214
|
end
|
171
215
|
end
|
172
|
-
|
216
|
+
|
173
217
|
# if respond_to?(:private_constant)
|
174
|
-
# private_constant :ConnectionSpecification
|
218
|
+
# private_constant :ConnectionSpecification
|
175
219
|
# private_constant :ConnectionHandler
|
176
220
|
# private_constant :Adapter
|
177
221
|
# end
|
178
|
-
|
222
|
+
|
179
223
|
end
|
@@ -2,16 +2,16 @@ require_relative 'helper'
|
|
2
2
|
require 'securerandom'
|
3
3
|
|
4
4
|
describe AutoReplica do
|
5
|
-
|
5
|
+
|
6
6
|
before :all do
|
7
7
|
test_seed_name = SecureRandom.hex(4)
|
8
8
|
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ('master_db_%s.sqlite3' % test_seed_name))
|
9
|
-
|
9
|
+
|
10
10
|
# Setup the master and replica connections
|
11
11
|
@master_connection_config = ActiveRecord::Base.connection_config.dup
|
12
12
|
@replica_connection_config = @master_connection_config.merge(database: ('replica_db_%s.sqlite3' % test_seed_name))
|
13
13
|
@replica_connection_config_url = 'sqlite3:/replica_db_%s.sqlite3' % test_seed_name
|
14
|
-
|
14
|
+
|
15
15
|
ActiveRecord::Migration.suppress_messages do
|
16
16
|
# Create both the master and the replica, with a simple small schema
|
17
17
|
[@master_connection_config, @replica_connection_config].each do | db_config |
|
@@ -25,25 +25,25 @@ describe AutoReplica do
|
|
25
25
|
end
|
26
26
|
end
|
27
27
|
end
|
28
|
-
|
28
|
+
|
29
29
|
after :all do
|
30
30
|
# Ensure database files get killed afterwards
|
31
31
|
[@master_connection_config, @replica_connection_config].map do | connection_config |
|
32
32
|
File.unlink(connection_config[:database]) rescue nil
|
33
33
|
end
|
34
34
|
end
|
35
|
-
|
35
|
+
|
36
36
|
before :each do
|
37
37
|
[@replica_connection_config, @master_connection_config].each do | config |
|
38
38
|
ActiveRecord::Base.establish_connection(config)
|
39
39
|
ActiveRecord::Base.connection.execute 'DELETE FROM things' # sqlite has no TRUNCATE
|
40
40
|
end
|
41
41
|
end
|
42
|
-
|
42
|
+
|
43
43
|
class TestThing < ActiveRecord::Base
|
44
44
|
self.table_name = 'things'
|
45
45
|
end
|
46
|
-
|
46
|
+
|
47
47
|
context 'using_read_replica_at' do
|
48
48
|
it 'has no reentrancy problems' do
|
49
49
|
id = described_class.using_read_replica_at(@replica_connection_config) do
|
@@ -53,7 +53,7 @@ describe AutoReplica do
|
|
53
53
|
expect {
|
54
54
|
TestThing.find(thing.id)
|
55
55
|
}.to raise_error(ActiveRecord::RecordNotFound)
|
56
|
-
|
56
|
+
|
57
57
|
thing.id # return to the outside of the block
|
58
58
|
end
|
59
59
|
end
|
@@ -61,7 +61,7 @@ describe AutoReplica do
|
|
61
61
|
found_on_master = TestThing.find(id)
|
62
62
|
expect(found_on_master.description).to eq('A nice Thing in the master database')
|
63
63
|
end
|
64
|
-
|
64
|
+
|
65
65
|
it 'executes the SELECT query against the replica database and returns the result of the block' do
|
66
66
|
id = described_class.using_read_replica_at(@replica_connection_config) do
|
67
67
|
thing = TestThing.create! description: 'A nice Thing in the master database'
|
@@ -72,20 +72,20 @@ describe AutoReplica do
|
|
72
72
|
end
|
73
73
|
found_on_master = TestThing.find(id)
|
74
74
|
expect(found_on_master.description).to eq('A nice Thing in the master database')
|
75
|
-
|
75
|
+
|
76
76
|
ActiveRecord::Base.establish_connection(@replica_connection_config)
|
77
77
|
thing_on_slave = TestThing.new(found_on_master.attributes)
|
78
78
|
thing_on_slave.id = found_on_master.id # gets ignored in attributes
|
79
79
|
thing_on_slave.description = 'A nice Thing that is in the slave database'
|
80
80
|
thing_on_slave.save!
|
81
|
-
|
81
|
+
|
82
82
|
ActiveRecord::Base.establish_connection(@master_connection_config)
|
83
83
|
described_class.using_read_replica_at(@replica_connection_config) do
|
84
84
|
thing_from_replica = TestThing.find(id)
|
85
85
|
expect(thing_from_replica.description).to eq('A nice Thing that is in the slave database')
|
86
86
|
end
|
87
87
|
end
|
88
|
-
|
88
|
+
|
89
89
|
it 'executes the SELECT query against the replica database with replica connection specification given as a URL' do
|
90
90
|
id = described_class.using_read_replica_at(@replica_connection_config_url) do
|
91
91
|
thing = TestThing.create! description: 'A nice Thing in the master database'
|
@@ -98,77 +98,120 @@ describe AutoReplica do
|
|
98
98
|
expect(found_on_master.description).to eq('A nice Thing in the master database')
|
99
99
|
end
|
100
100
|
end
|
101
|
-
|
101
|
+
|
102
102
|
describe AutoReplica::ConnectionHandler do
|
103
|
+
it 'proxies all methods' do
|
104
|
+
original_handler = double('ActiveRecord_ConnectionHandler')
|
105
|
+
expect(original_handler).to receive(:do_that_thing) { :yes }
|
106
|
+
pool_double = double('ConnectionPool')
|
107
|
+
subject = AutoReplica::ConnectionHandler.new(original_handler, pool_double)
|
108
|
+
expect(subject.do_that_thing).to eq(:yes)
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'enhances connection_for and returns an instance of the Adapter' do
|
112
|
+
original_handler = double('ActiveRecord_ConnectionHandler')
|
113
|
+
adapter_double = double('ActiveRecord_Adapter')
|
114
|
+
connection_double = double('Connection')
|
115
|
+
pool_double = double('ConnectionPool')
|
116
|
+
expect(original_handler).to receive(:retrieve_connection).with(TestThing) { adapter_double }
|
117
|
+
expect(pool_double).to receive(:connection) { connection_double }
|
118
|
+
|
119
|
+
subject = AutoReplica::ConnectionHandler.new(original_handler, pool_double)
|
120
|
+
connection = subject.retrieve_connection(TestThing)
|
121
|
+
expect(connection).to be_kind_of(AutoReplica::Adapter)
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'releases the the read pool connection when finishing' do
|
125
|
+
original_handler = double('ActiveRecord_ConnectionHandler')
|
126
|
+
pool_double = double('ConnectionPool')
|
127
|
+
subject = AutoReplica::ConnectionHandler.new(original_handler, pool_double)
|
128
|
+
|
129
|
+
expect(pool_double).to receive(:release_connection)
|
130
|
+
subject.finish
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'performs clear_all_connections! both on the contained handler and on the read pool' do
|
134
|
+
original_handler = double('ActiveRecord_ConnectionHandler')
|
135
|
+
pool_double = double('ConnectionPool')
|
136
|
+
|
137
|
+
expect(original_handler).to receive(:clear_all_connections!)
|
138
|
+
expect(pool_double).to receive(:disconnect!)
|
139
|
+
|
140
|
+
subject = AutoReplica::ConnectionHandler.new(original_handler, pool_double)
|
141
|
+
subject.clear_all_connections!
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
describe AutoReplica::AdHocConnectionHandler do
|
103
146
|
it 'creates a read pool with the replica connection specification hash' do
|
104
147
|
original_handler = double('ActiveRecord_ConnectionHandler')
|
105
148
|
expect(ActiveRecord::ConnectionAdapters::ConnectionPool).to receive(:new).
|
106
149
|
with(instance_of(AutoReplica::ConnectionSpecification))
|
107
|
-
|
108
|
-
AutoReplica::
|
150
|
+
|
151
|
+
AutoReplica::AdHocConnectionHandler.new(original_handler, @replica_connection_config)
|
109
152
|
end
|
110
|
-
|
153
|
+
|
111
154
|
it 'proxies all methods' do
|
112
155
|
original_handler = double('ActiveRecord_ConnectionHandler')
|
113
156
|
expect(original_handler).to receive(:do_that_thing) { :yes }
|
114
|
-
subject = AutoReplica::
|
157
|
+
subject = AutoReplica::AdHocConnectionHandler.new(original_handler, @replica_connection_config)
|
115
158
|
expect(subject.do_that_thing).to eq(:yes)
|
116
159
|
end
|
117
|
-
|
160
|
+
|
118
161
|
it 'enhances connection_for and returns an instance of the Adapter' do
|
119
162
|
original_handler = double('ActiveRecord_ConnectionHandler')
|
120
163
|
adapter_double = double('ActiveRecord_Adapter')
|
121
164
|
expect(original_handler).to receive(:retrieve_connection).with(TestThing) { adapter_double }
|
122
|
-
|
123
|
-
subject = AutoReplica::
|
165
|
+
|
166
|
+
subject = AutoReplica::AdHocConnectionHandler.new(original_handler, @replica_connection_config)
|
124
167
|
connection = subject.retrieve_connection(TestThing)
|
125
168
|
expect(connection).to be_kind_of(AutoReplica::Adapter)
|
126
169
|
end
|
127
|
-
|
128
|
-
it 'disconnects the read pool when
|
170
|
+
|
171
|
+
it 'disconnects the read pool when finishing' do
|
129
172
|
original_handler = double('ActiveRecord_ConnectionHandler')
|
130
173
|
pool_double = double('ConnectionPool')
|
131
174
|
expect(ActiveRecord::ConnectionAdapters::ConnectionPool).to receive(:new).
|
132
175
|
with(instance_of(AutoReplica::ConnectionSpecification)) { pool_double }
|
133
|
-
subject = AutoReplica::
|
134
|
-
|
176
|
+
subject = AutoReplica::AdHocConnectionHandler.new(original_handler, @replica_connection_config)
|
177
|
+
|
135
178
|
expect(pool_double).to receive(:disconnect!)
|
136
|
-
subject.
|
179
|
+
subject.finish
|
137
180
|
end
|
138
|
-
|
181
|
+
|
139
182
|
it 'performs clear_all_connections! both on the contained handler and on the read pool' do
|
140
183
|
original_handler = double('ActiveRecord_ConnectionHandler')
|
141
184
|
pool_double = double('ConnectionPool')
|
142
185
|
expect(ActiveRecord::ConnectionAdapters::ConnectionPool).to receive(:new).
|
143
186
|
with(instance_of(AutoReplica::ConnectionSpecification)) { pool_double }
|
144
|
-
|
187
|
+
|
145
188
|
expect(original_handler).to receive(:clear_all_connections!)
|
146
189
|
expect(pool_double).to receive(:disconnect!)
|
147
|
-
|
148
|
-
subject = AutoReplica::
|
190
|
+
|
191
|
+
subject = AutoReplica::AdHocConnectionHandler.new(original_handler, @replica_connection_config)
|
149
192
|
subject.clear_all_connections!
|
150
193
|
end
|
151
194
|
end
|
152
|
-
|
195
|
+
|
153
196
|
describe AutoReplica::Adapter do
|
154
197
|
it 'mirrors select_ prefixed database statement methods in ActiveRecord::ConnectionAdapters::DatabaseStatements' do
|
155
198
|
master = double()
|
156
199
|
expect(master).not_to receive(:respond_to?)
|
157
200
|
subject = AutoReplica::Adapter.new(master, double())
|
158
|
-
|
201
|
+
|
159
202
|
select_methods = ActiveRecord::ConnectionAdapters::DatabaseStatements.instance_methods.grep(/^select_/)
|
160
203
|
expect(select_methods.length).to be > 1
|
161
|
-
|
204
|
+
|
162
205
|
select_methods.each do | select_method_in_database_statements |
|
163
206
|
expect(subject).to respond_to(select_method_in_database_statements)
|
164
207
|
end
|
165
208
|
end
|
166
|
-
|
209
|
+
|
167
210
|
it 'redirects calls to all select_ methods to the read connection and others to the master connection' do
|
168
211
|
master_adapter = double('Connection to the master DB')
|
169
212
|
replica_adapter = double('Connection to the replica DB')
|
170
213
|
subject = AutoReplica::Adapter.new(master_adapter, replica_adapter)
|
171
|
-
|
214
|
+
|
172
215
|
expect(master_adapter).to receive(:some_arbitrary_method) { :from_master }
|
173
216
|
expect(replica_adapter).to receive(:select_values).with(:a, :b, :c) { :from_replica }
|
174
217
|
expect(subject.some_arbitrary_method).to eq(:from_master)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord_autoreplica
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-11-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|