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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8ed3fdce7e191b78e8bc24c13cc926cb9b625052
4
- data.tar.gz: 02bdd2ae70931f1a7376a9264bf6a5ac6442275e
3
+ metadata.gz: 0dec3a7a02bfdd20f0428bdb405016f3b16d7c12
4
+ data.tar.gz: c84f381a3f3c1d01f2066aec82597333a7a8d2c8
5
5
  SHA512:
6
- metadata.gz: fa680e51be925cbe09927aa323e44be88fd26cf82b799474761bbae59a135a2123e1f459e9e350f9949eb58462338e7dc433558aaa9156463d8881fe141f82cf
7
- data.tar.gz: 7db0c18d68ed48fdbf428d56bff0f49a1ec5137ef8f5c10e695eef9d41ec3e860a52ffff11256eb2525f98c7b004ecf1b4e465d8fa711e1d1dde932252250f5f
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
- The API consists of _one_ method. Pass it a complete ActiveRecord connection specificaton hash, and
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.2.0'
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.2.0 ruby lib
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.2.0"
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-10-23"
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 it's managed ConnectionPool objects to give it a connection for use.
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.connection_hanler=++
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', :datbase => 'read_replica', ...) do
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
- # Wrap the connection handler in our proxy
74
+
65
75
  original_connection_handler = ActiveRecord::Base.connection_handler
66
- custom_handler = ConnectionHandler.new(original_connection_handler, config_hash)
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.disconnect_read_pool!
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, connection_specification_hash)
90
+ def initialize(original_handler, read_pool)
97
91
  @original_handler = original_handler
98
- # We need to maintain our own pool for read replica connections,
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::ConnectionHandler.new(original_handler, @replica_connection_config)
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::ConnectionHandler.new(original_handler, @replica_connection_config)
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::ConnectionHandler.new(original_handler, @replica_connection_config)
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 asked to' do
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::ConnectionHandler.new(original_handler, @replica_connection_config)
134
-
176
+ subject = AutoReplica::AdHocConnectionHandler.new(original_handler, @replica_connection_config)
177
+
135
178
  expect(pool_double).to receive(:disconnect!)
136
- subject.disconnect_read_pool!
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::ConnectionHandler.new(original_handler, @replica_connection_config)
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.2.0
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-10-23 00:00:00.000000000 Z
11
+ date: 2016-11-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord