activerecord_autoreplica 1.3.0 → 1.3.1
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 +1 -2
- data/Rakefile +1 -1
- data/activerecord_autoreplica.gemspec +3 -3
- data/lib/activerecord_autoreplica.rb +21 -9
- data/spec/activerecord_autoreplica_spec.rb +86 -5
- 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: db7ac2cf097115489206cf2c5ff942e644972585
|
4
|
+
data.tar.gz: 7ce126147d70a5f1726d82c4b6e2946d197b25df
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c480d4ad56372e3ab3439ae6bd3cffb63eaed11ac243d9cb6ce4bdf5f5b813648c749243cba44396ab09e930d5412f35179fcbff4a7c9cbac47d4209e097cd77
|
7
|
+
data.tar.gz: cf90d567e36776e7fc277ddd0b18d315e0fdb64ce05a0e1ad471fb0b9adbdf9b6cf3b08ea6f17e90aa5c6310ddd5d9bdc1301ea25c15f62f327fc0621ed67d45
|
data/README.md
CHANGED
@@ -68,8 +68,7 @@ act accordingly.
|
|
68
68
|
|
69
69
|
The `using_read_replica_at` block will allocate a `ConnectionPool` like the standard `ActiveRecord` connection
|
70
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
|
71
|
-
ActiveRecord facilities (including mutexes)
|
72
|
-
handler in ActiveRecord isn't).
|
71
|
+
ActiveRecord facilities (including mutexes). It should be threadsafe _and_ thread local.
|
73
72
|
|
74
73
|
### Running the specs
|
75
74
|
|
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.3.
|
16
|
+
gem.version = '1.3.1'
|
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.3.
|
5
|
+
# stub: activerecord_autoreplica 1.3.1 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "activerecord_autoreplica"
|
9
|
-
s.version = "1.3.
|
9
|
+
s.version = "1.3.1"
|
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-11-
|
14
|
+
s.date = "2016-11-21"
|
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 = [
|
@@ -31,6 +31,8 @@
|
|
31
31
|
#
|
32
32
|
# Once the block exits, the original connection handler is reassigned to the AR connection_pool.
|
33
33
|
module AutoReplica
|
34
|
+
CONNECTION_SWITCHING_MUTEX = Mutex.new
|
35
|
+
|
34
36
|
# The first one is used in ActiveRecord 3+, the second one in 4+
|
35
37
|
ConnectionSpecification = begin
|
36
38
|
ActiveRecord::Base::ConnectionSpecification
|
@@ -68,19 +70,22 @@ module AutoReplica
|
|
68
70
|
end
|
69
71
|
|
70
72
|
def self.in_replica_context(handler_params, handler_class=ConnectionHandler)
|
71
|
-
return yield if
|
72
|
-
|
73
|
-
@in_replica_context = true
|
73
|
+
return yield if Thread.current[:autoreplica] # This method should not be reentrant
|
74
74
|
|
75
75
|
original_connection_handler = ActiveRecord::Base.connection_handler
|
76
76
|
custom_handler = handler_class.new(original_connection_handler, handler_params)
|
77
77
|
begin
|
78
|
-
|
78
|
+
CONNECTION_SWITCHING_MUTEX.synchronize do
|
79
|
+
Thread.current[:autoreplica] = true
|
80
|
+
ActiveRecord::Base.connection_handler = custom_handler
|
81
|
+
end
|
79
82
|
yield
|
80
83
|
ensure
|
81
|
-
|
84
|
+
CONNECTION_SWITCHING_MUTEX.synchronize do
|
85
|
+
Thread.current[:autoreplica] = false
|
86
|
+
ActiveRecord::Base.connection_handler = original_connection_handler
|
87
|
+
end
|
82
88
|
custom_handler.finish
|
83
|
-
@in_replica_context = false
|
84
89
|
end
|
85
90
|
end
|
86
91
|
|
@@ -95,9 +100,16 @@ module AutoReplica
|
|
95
100
|
# Overridden method which gets called by ActiveRecord to get a connection related to a specific
|
96
101
|
# ActiveRecord::Base subclass.
|
97
102
|
def retrieve_connection(for_ar_class)
|
98
|
-
|
99
|
-
|
100
|
-
|
103
|
+
# See which thread is calling us. If it is the thread that initiated the `in_replica_context`
|
104
|
+
# block, we return a wrapper proxy. If it is not, then it is a different thread willing to
|
105
|
+
# use a connection, and we have to give it the original adapter instead
|
106
|
+
if Thread.current[:autoreplica]
|
107
|
+
connection_for_writes = @original_handler.retrieve_connection(for_ar_class)
|
108
|
+
connection_for_reads = @read_pool.connection
|
109
|
+
Adapter.new(connection_for_writes, connection_for_reads)
|
110
|
+
else
|
111
|
+
@original_handler.retrieve_connection(for_ar_class)
|
112
|
+
end
|
101
113
|
end
|
102
114
|
|
103
115
|
def release_read_pool_connection
|
@@ -5,12 +5,12 @@ describe AutoReplica do
|
|
5
5
|
|
6
6
|
before :all do
|
7
7
|
test_seed_name = SecureRandom.hex(4)
|
8
|
-
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ('master_db_%s.sqlite3' % test_seed_name))
|
8
|
+
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ('master_db_%s.sqlite3' % test_seed_name), pool: 10)
|
9
9
|
|
10
10
|
# Setup the master and replica connections
|
11
11
|
@master_connection_config = ActiveRecord::Base.connection_config.dup
|
12
|
-
@replica_connection_config = @master_connection_config.merge(database: ('replica_db_%s.sqlite3' % test_seed_name))
|
13
|
-
@replica_connection_config_url = 'sqlite3:/replica_db_%s.sqlite3' % test_seed_name
|
12
|
+
@replica_connection_config = @master_connection_config.merge(database: ('replica_db_%s.sqlite3' % test_seed_name), pool: 10)
|
13
|
+
@replica_connection_config_url = 'sqlite3:/replica_db_%s.sqlite3?pool=10' % 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
|
@@ -97,6 +97,73 @@ describe AutoReplica do
|
|
97
97
|
found_on_master = TestThing.find(id)
|
98
98
|
expect(found_on_master.description).to eq('A nice Thing in the master database')
|
99
99
|
end
|
100
|
+
|
101
|
+
it 'does not contaminate other threads with the replica connection' do
|
102
|
+
ActiveRecord::Base.establish_connection(@master_connection_config)
|
103
|
+
TestThing.create! description: 'In master'
|
104
|
+
|
105
|
+
ActiveRecord::Base.establish_connection(@replica_connection_config)
|
106
|
+
TestThing.create! description: 'In replica'
|
107
|
+
|
108
|
+
ActiveRecord::Base.establish_connection(@master_connection_config)
|
109
|
+
expect(TestThing.first.description).to eq('In master')
|
110
|
+
|
111
|
+
ActiveRecord::Base.establish_connection(@replica_connection_config)
|
112
|
+
expect(TestThing.first.description).to eq('In replica')
|
113
|
+
|
114
|
+
ActiveRecord::Base.establish_connection(@master_connection_config)
|
115
|
+
|
116
|
+
Thread.abort_on_exception = true
|
117
|
+
failures = 0
|
118
|
+
successes = 0
|
119
|
+
lock = Mutex.new
|
120
|
+
|
121
|
+
n_threads = 4
|
122
|
+
n_iterations = 68
|
123
|
+
readers_from_slave = (1..4).map do |n|
|
124
|
+
Thread.new do
|
125
|
+
n_iterations.times do
|
126
|
+
sleep(rand / 3.0)
|
127
|
+
described_class.using_read_replica_at(**@replica_connection_config) do
|
128
|
+
description = TestThing.first.description
|
129
|
+
lock.synchronize do
|
130
|
+
if description == 'In replica'
|
131
|
+
successes += 1
|
132
|
+
else
|
133
|
+
failures += 1
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
readers_from_master = (1..n_threads).map do |n|
|
142
|
+
Thread.new do
|
143
|
+
n_iterations.times do
|
144
|
+
sleep(rand / 3.0)
|
145
|
+
description = TestThing.first.description
|
146
|
+
lock.synchronize do
|
147
|
+
if description == 'In master'
|
148
|
+
successes += 1
|
149
|
+
else
|
150
|
+
failures += 1
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
readers_from_slave.map(&:join)
|
158
|
+
readers_from_master.map(&:join)
|
159
|
+
|
160
|
+
# All the fetches should be correct
|
161
|
+
expect(successes).not_to be_zero
|
162
|
+
|
163
|
+
# There should be no fetches from master in the replica block, and no fetches
|
164
|
+
# from replica without the replica block
|
165
|
+
expect(failures).to be_zero
|
166
|
+
end
|
100
167
|
end
|
101
168
|
|
102
169
|
describe AutoReplica::ConnectionHandler do
|
@@ -108,7 +175,8 @@ describe AutoReplica do
|
|
108
175
|
expect(subject.do_that_thing).to eq(:yes)
|
109
176
|
end
|
110
177
|
|
111
|
-
it 'enhances connection_for and returns an instance of the Adapter' do
|
178
|
+
it 'enhances connection_for and returns an instance of the Adapter if the thread-local :autoreplica is set' do
|
179
|
+
Thread.current[:autoreplica] = true
|
112
180
|
original_handler = double('ActiveRecord_ConnectionHandler')
|
113
181
|
adapter_double = double('ActiveRecord_Adapter')
|
114
182
|
connection_double = double('Connection')
|
@@ -119,8 +187,19 @@ describe AutoReplica do
|
|
119
187
|
subject = AutoReplica::ConnectionHandler.new(original_handler, pool_double)
|
120
188
|
connection = subject.retrieve_connection(TestThing)
|
121
189
|
expect(connection).to be_kind_of(AutoReplica::Adapter)
|
190
|
+
Thread.current[:autoreplica] = false
|
122
191
|
end
|
123
192
|
|
193
|
+
it 'returns the original connection without the wrapper if the thread-local :autoreplica is not set' do
|
194
|
+
Thread.current[:autoreplica] = false
|
195
|
+
original_handler = double('ActiveRecord_ConnectionHandler')
|
196
|
+
pool_double = double('Read replica pool')
|
197
|
+
expect(original_handler).to receive(:retrieve_connection).and_return(:original_connection)
|
198
|
+
subject = AutoReplica::ConnectionHandler.new(original_handler, pool_double)
|
199
|
+
connection = subject.retrieve_connection(TestThing)
|
200
|
+
expect(connection).to eq(:original_connection)
|
201
|
+
end
|
202
|
+
|
124
203
|
it 'releases the the read pool connection when finishing' do
|
125
204
|
original_handler = double('ActiveRecord_ConnectionHandler')
|
126
205
|
pool_double = double('ConnectionPool')
|
@@ -158,7 +237,8 @@ describe AutoReplica do
|
|
158
237
|
expect(subject.do_that_thing).to eq(:yes)
|
159
238
|
end
|
160
239
|
|
161
|
-
it 'enhances connection_for and returns an instance of the Adapter' do
|
240
|
+
it 'enhances connection_for and returns an instance of the Adapter if the thread-local :autoreplica is set' do
|
241
|
+
Thread.current[:autoreplica] = true
|
162
242
|
original_handler = double('ActiveRecord_ConnectionHandler')
|
163
243
|
adapter_double = double('ActiveRecord_Adapter')
|
164
244
|
expect(original_handler).to receive(:retrieve_connection).with(TestThing) { adapter_double }
|
@@ -166,6 +246,7 @@ describe AutoReplica do
|
|
166
246
|
subject = AutoReplica::AdHocConnectionHandler.new(original_handler, @replica_connection_config)
|
167
247
|
connection = subject.retrieve_connection(TestThing)
|
168
248
|
expect(connection).to be_kind_of(AutoReplica::Adapter)
|
249
|
+
Thread.current[:autoreplica] = false
|
169
250
|
end
|
170
251
|
|
171
252
|
it 'disconnects the read pool when finishing' do
|
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.3.
|
4
|
+
version: 1.3.1
|
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-
|
11
|
+
date: 2016-11-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|