activerecord_autoreplica 1.3.1 → 2.0.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/.travis.yml +0 -1
- data/Gemfile +1 -1
- data/README.md +0 -2
- data/Rakefile +1 -1
- data/activerecord_autoreplica.gemspec +6 -7
- data/lib/activerecord_autoreplica.rb +7 -28
- data/spec/activerecord_autoreplica_spec.rb +71 -85
- metadata +6 -7
- data/gemfiles/Gemfile.rails-3.2.x +0 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a5eb9206cedab199be0c3b122d345ca8c8373c3f
|
4
|
+
data.tar.gz: c1b40dc95b92b655585ead464030f171aca27ee0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f10210c92a90ba9ed00a26514e360936524996b5be63dddb719173789b15b96cec40d21ada5227252504c3e996cd7e7c8c5af0488873041d4e63e9d1cad9f585
|
7
|
+
data.tar.gz: cb56b501aab0cf34234f81be6d9f8070fb14a542ef96da46d173c6853f678c9fcc9a81dea6e79dc3014df23f237c6df0f106843155885e76432811ef0f431eba
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
@@ -6,7 +6,7 @@ source "http://rubygems.org"
|
|
6
6
|
# $ BUNDLE_GEMFILE=gemfiles/Gemfile.rails-4.1.x bundle exec rake
|
7
7
|
#
|
8
8
|
# etc.
|
9
|
-
gem 'activerecord', "
|
9
|
+
gem 'activerecord', ">= 4"
|
10
10
|
|
11
11
|
# Add dependencies to develop your gem here.
|
12
12
|
# Include everything needed to run rake, tests, features, etc.
|
data/README.md
CHANGED
@@ -79,10 +79,8 @@ There are Gemfiles for testing against various versions of ActiveRecord. To run
|
|
79
79
|
|
80
80
|
$ BUNDLE_GEMFILE=gemfiles/Gemfile.rails-5.0.x bundle exec rake
|
81
81
|
$ BUNDLE_GEMFILE=gemfiles/Gemfile.rails-4.1.x bundle exec rake
|
82
|
-
$ BUNDLE_GEMFILE=gemfiles/Gemfile.rails-3.2.x bundle exec rake
|
83
82
|
|
84
83
|
The library contains a couple of switches that allow it to function in all these Rails versions.
|
85
|
-
Rails 3.x support is likely to be dropped in the next major version.
|
86
84
|
|
87
85
|
### Versioning
|
88
86
|
|
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 = '
|
16
|
+
gem.version = '2.0.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
|
5
|
+
# stub: activerecord_autoreplica 2.0.0 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "activerecord_autoreplica"
|
9
|
-
s.version = "
|
9
|
+
s.version = "2.0.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-12-06"
|
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 = [
|
@@ -27,7 +27,6 @@ Gem::Specification.new do |s|
|
|
27
27
|
"README.md",
|
28
28
|
"Rakefile",
|
29
29
|
"activerecord_autoreplica.gemspec",
|
30
|
-
"gemfiles/Gemfile.rails-3.2.x",
|
31
30
|
"gemfiles/Gemfile.rails-4.1.x",
|
32
31
|
"gemfiles/Gemfile.rails-5.0.x",
|
33
32
|
"lib/activerecord_autoreplica.rb",
|
@@ -43,7 +42,7 @@ Gem::Specification.new do |s|
|
|
43
42
|
s.specification_version = 4
|
44
43
|
|
45
44
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
46
|
-
s.add_runtime_dependency(%q<activerecord>, ["
|
45
|
+
s.add_runtime_dependency(%q<activerecord>, [">= 4"])
|
47
46
|
s.add_development_dependency(%q<rake>, ["~> 10.0"])
|
48
47
|
s.add_development_dependency(%q<yard>, [">= 0"])
|
49
48
|
s.add_development_dependency(%q<sqlite3>, [">= 0"])
|
@@ -52,7 +51,7 @@ Gem::Specification.new do |s|
|
|
52
51
|
s.add_development_dependency(%q<bundler>, ["~> 1.0"])
|
53
52
|
s.add_development_dependency(%q<jeweler>, [">= 0"])
|
54
53
|
else
|
55
|
-
s.add_dependency(%q<activerecord>, ["
|
54
|
+
s.add_dependency(%q<activerecord>, [">= 4"])
|
56
55
|
s.add_dependency(%q<rake>, ["~> 10.0"])
|
57
56
|
s.add_dependency(%q<yard>, [">= 0"])
|
58
57
|
s.add_dependency(%q<sqlite3>, [">= 0"])
|
@@ -62,7 +61,7 @@ Gem::Specification.new do |s|
|
|
62
61
|
s.add_dependency(%q<jeweler>, [">= 0"])
|
63
62
|
end
|
64
63
|
else
|
65
|
-
s.add_dependency(%q<activerecord>, ["
|
64
|
+
s.add_dependency(%q<activerecord>, [">= 4"])
|
66
65
|
s.add_dependency(%q<rake>, ["~> 10.0"])
|
67
66
|
s.add_dependency(%q<yard>, [">= 0"])
|
68
67
|
s.add_dependency(%q<sqlite3>, [">= 0"])
|
@@ -31,14 +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
|
-
|
35
|
-
|
36
|
-
# The first one is used in ActiveRecord 3+, the second one in 4+
|
37
|
-
ConnectionSpecification = begin
|
38
|
-
ActiveRecord::Base::ConnectionSpecification
|
39
|
-
rescue
|
40
|
-
ActiveRecord::ConnectionAdapters::ConnectionSpecification
|
41
|
-
end
|
34
|
+
# Aliased since this class gets renamed sometimes between Rails versions
|
35
|
+
ConnectionSpecification = ActiveRecord::ConnectionAdapters::ConnectionSpecification
|
42
36
|
|
43
37
|
# Runs a given block with all SELECT statements being executed against the read slave
|
44
38
|
# database.
|
@@ -70,21 +64,13 @@ module AutoReplica
|
|
70
64
|
end
|
71
65
|
|
72
66
|
def self.in_replica_context(handler_params, handler_class=ConnectionHandler)
|
73
|
-
return yield if Thread.current[:autoreplica] # This method should not be reentrant
|
74
|
-
|
75
67
|
original_connection_handler = ActiveRecord::Base.connection_handler
|
76
68
|
custom_handler = handler_class.new(original_connection_handler, handler_params)
|
77
69
|
begin
|
78
|
-
|
79
|
-
Thread.current[:autoreplica] = true
|
80
|
-
ActiveRecord::Base.connection_handler = custom_handler
|
81
|
-
end
|
70
|
+
ActiveRecord::Base.connection_handler = custom_handler
|
82
71
|
yield
|
83
72
|
ensure
|
84
|
-
|
85
|
-
Thread.current[:autoreplica] = false
|
86
|
-
ActiveRecord::Base.connection_handler = original_connection_handler
|
87
|
-
end
|
73
|
+
ActiveRecord::Base.connection_handler = original_connection_handler
|
88
74
|
custom_handler.finish
|
89
75
|
end
|
90
76
|
end
|
@@ -100,16 +86,9 @@ module AutoReplica
|
|
100
86
|
# Overridden method which gets called by ActiveRecord to get a connection related to a specific
|
101
87
|
# ActiveRecord::Base subclass.
|
102
88
|
def retrieve_connection(for_ar_class)
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
89
|
+
connection_for_writes = @original_handler.retrieve_connection(for_ar_class)
|
90
|
+
connection_for_reads = @read_pool.connection
|
91
|
+
Adapter.new(connection_for_writes, connection_for_reads)
|
113
92
|
end
|
114
93
|
|
115
94
|
def release_read_pool_connection
|
@@ -5,11 +5,11 @@ 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', pool: 10, 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
|
-
@replica_connection_config = @master_connection_config.merge(database: ('replica_db_%s.sqlite3' % test_seed_name)
|
12
|
+
@replica_connection_config = @master_connection_config.merge(pool: 10, database: ('replica_db_%s.sqlite3' % test_seed_name))
|
13
13
|
@replica_connection_config_url = 'sqlite3:/replica_db_%s.sqlite3?pool=10' % test_seed_name
|
14
14
|
|
15
15
|
ActiveRecord::Migration.suppress_messages do
|
@@ -43,7 +43,74 @@ describe AutoReplica do
|
|
43
43
|
class TestThing < ActiveRecord::Base
|
44
44
|
self.table_name = 'things'
|
45
45
|
end
|
46
|
+
|
47
|
+
it 'does not contaminate other threads with the replica connection' do
|
48
|
+
ActiveRecord::Base.establish_connection(@master_connection_config)
|
49
|
+
TestThing.create! description: 'In master'
|
50
|
+
|
51
|
+
ActiveRecord::Base.establish_connection(@replica_connection_config)
|
52
|
+
TestThing.create! description: 'In replica'
|
53
|
+
|
54
|
+
ActiveRecord::Base.establish_connection(@master_connection_config)
|
55
|
+
expect(TestThing.first.description).to eq('In master')
|
56
|
+
|
57
|
+
ActiveRecord::Base.establish_connection(@replica_connection_config)
|
58
|
+
expect(TestThing.first.description).to eq('In replica')
|
59
|
+
|
60
|
+
ActiveRecord::Base.establish_connection(@master_connection_config)
|
61
|
+
|
62
|
+
Thread.abort_on_exception = true
|
63
|
+
failures = 0
|
64
|
+
successes = 0
|
65
|
+
lock = Mutex.new
|
66
|
+
|
67
|
+
n_threads = 4
|
68
|
+
n_iterations = 68
|
69
|
+
readers_from_slave = (1..4).map do |n|
|
70
|
+
Thread.new do
|
71
|
+
n_iterations.times do
|
72
|
+
sleep(rand / 3.0)
|
73
|
+
described_class.using_read_replica_at(**@replica_connection_config) do
|
74
|
+
description = TestThing.first.description
|
75
|
+
lock.synchronize do
|
76
|
+
if description == 'In replica'
|
77
|
+
successes += 1
|
78
|
+
else
|
79
|
+
failures += 1
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
readers_from_master = (1..n_threads).map do |n|
|
88
|
+
Thread.new do
|
89
|
+
n_iterations.times do
|
90
|
+
sleep(rand / 3.0)
|
91
|
+
description = TestThing.first.description
|
92
|
+
lock.synchronize do
|
93
|
+
if description == 'In master'
|
94
|
+
successes += 1
|
95
|
+
else
|
96
|
+
failures += 1
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
readers_from_slave.map(&:join)
|
104
|
+
readers_from_master.map(&:join)
|
46
105
|
|
106
|
+
# All the fetches should be correct
|
107
|
+
expect(successes).not_to be_zero
|
108
|
+
|
109
|
+
# There should be no fetches from master in the replica block, and no fetches
|
110
|
+
# from replica without the replica block
|
111
|
+
expect(failures).to eq(0)
|
112
|
+
end
|
113
|
+
|
47
114
|
context 'using_read_replica_at' do
|
48
115
|
it 'has no reentrancy problems' do
|
49
116
|
id = described_class.using_read_replica_at(@replica_connection_config) do
|
@@ -97,73 +164,6 @@ describe AutoReplica do
|
|
97
164
|
found_on_master = TestThing.find(id)
|
98
165
|
expect(found_on_master.description).to eq('A nice Thing in the master database')
|
99
166
|
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
|
167
167
|
end
|
168
168
|
|
169
169
|
describe AutoReplica::ConnectionHandler do
|
@@ -175,8 +175,7 @@ describe AutoReplica do
|
|
175
175
|
expect(subject.do_that_thing).to eq(:yes)
|
176
176
|
end
|
177
177
|
|
178
|
-
it 'enhances connection_for and returns an instance of the Adapter
|
179
|
-
Thread.current[:autoreplica] = true
|
178
|
+
it 'enhances connection_for and returns an instance of the Adapter' do
|
180
179
|
original_handler = double('ActiveRecord_ConnectionHandler')
|
181
180
|
adapter_double = double('ActiveRecord_Adapter')
|
182
181
|
connection_double = double('Connection')
|
@@ -187,19 +186,8 @@ describe AutoReplica do
|
|
187
186
|
subject = AutoReplica::ConnectionHandler.new(original_handler, pool_double)
|
188
187
|
connection = subject.retrieve_connection(TestThing)
|
189
188
|
expect(connection).to be_kind_of(AutoReplica::Adapter)
|
190
|
-
Thread.current[:autoreplica] = false
|
191
189
|
end
|
192
190
|
|
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
|
-
|
203
191
|
it 'releases the the read pool connection when finishing' do
|
204
192
|
original_handler = double('ActiveRecord_ConnectionHandler')
|
205
193
|
pool_double = double('ConnectionPool')
|
@@ -237,8 +225,7 @@ describe AutoReplica do
|
|
237
225
|
expect(subject.do_that_thing).to eq(:yes)
|
238
226
|
end
|
239
227
|
|
240
|
-
it 'enhances connection_for and returns an instance of the Adapter
|
241
|
-
Thread.current[:autoreplica] = true
|
228
|
+
it 'enhances connection_for and returns an instance of the Adapter' do
|
242
229
|
original_handler = double('ActiveRecord_ConnectionHandler')
|
243
230
|
adapter_double = double('ActiveRecord_Adapter')
|
244
231
|
expect(original_handler).to receive(:retrieve_connection).with(TestThing) { adapter_double }
|
@@ -246,7 +233,6 @@ describe AutoReplica do
|
|
246
233
|
subject = AutoReplica::AdHocConnectionHandler.new(original_handler, @replica_connection_config)
|
247
234
|
connection = subject.retrieve_connection(TestThing)
|
248
235
|
expect(connection).to be_kind_of(AutoReplica::Adapter)
|
249
|
-
Thread.current[:autoreplica] = false
|
250
236
|
end
|
251
237
|
|
252
238
|
it 'disconnects the read pool when finishing' do
|
metadata
CHANGED
@@ -1,29 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord_autoreplica
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.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-12-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '4'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '4'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -138,7 +138,6 @@ files:
|
|
138
138
|
- README.md
|
139
139
|
- Rakefile
|
140
140
|
- activerecord_autoreplica.gemspec
|
141
|
-
- gemfiles/Gemfile.rails-3.2.x
|
142
141
|
- gemfiles/Gemfile.rails-4.1.x
|
143
142
|
- gemfiles/Gemfile.rails-5.0.x
|
144
143
|
- lib/activerecord_autoreplica.rb
|
@@ -1,13 +0,0 @@
|
|
1
|
-
source "http://rubygems.org"
|
2
|
-
gem 'activerecord', "~> 3"
|
3
|
-
|
4
|
-
# Add dependencies to develop your gem here.
|
5
|
-
# Include everything needed to run rake, tests, features, etc.
|
6
|
-
group :development do
|
7
|
-
gem 'rake', '~> 10.0'
|
8
|
-
gem 'sqlite3'
|
9
|
-
gem "rspec", "~> 2.4"
|
10
|
-
gem "rdoc", "~> 3.12"
|
11
|
-
gem "bundler", "~> 1.0"
|
12
|
-
gem "jeweler", "1.8.4" # The last without Nokogiri
|
13
|
-
end
|