activerecord_autoreplica 1.2.0 → 1.3.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 +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
|