activerecord_autoreplica 1.3.1 → 2.0.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: db7ac2cf097115489206cf2c5ff942e644972585
4
- data.tar.gz: 7ce126147d70a5f1726d82c4b6e2946d197b25df
3
+ metadata.gz: a5eb9206cedab199be0c3b122d345ca8c8373c3f
4
+ data.tar.gz: c1b40dc95b92b655585ead464030f171aca27ee0
5
5
  SHA512:
6
- metadata.gz: c480d4ad56372e3ab3439ae6bd3cffb63eaed11ac243d9cb6ce4bdf5f5b813648c749243cba44396ab09e930d5412f35179fcbff4a7c9cbac47d4209e097cd77
7
- data.tar.gz: cf90d567e36776e7fc277ddd0b18d315e0fdb64ce05a0e1ad471fb0b9adbdf9b6cf3b08ea6f17e90aa5c6310ddd5d9bdc1301ea25c15f62f327fc0621ed67d45
6
+ metadata.gz: f10210c92a90ba9ed00a26514e360936524996b5be63dddb719173789b15b96cec40d21ada5227252504c3e996cd7e7c8c5af0488873041d4e63e9d1cad9f585
7
+ data.tar.gz: cb56b501aab0cf34234f81be6d9f8070fb14a542ef96da46d173c6853f678c9fcc9a81dea6e79dc3014df23f237c6df0f106843155885e76432811ef0f431eba
@@ -1,5 +1,4 @@
1
1
  gemfile:
2
- - gemfiles/Gemfile.rails-3.2.x
3
2
  - gemfiles/Gemfile.rails-4.1.x
4
3
  - gemfiles/Gemfile.rails-5.0.x
5
4
  rvm:
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', "> 3.0"
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 = '1.3.1'
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 1.3.1 ruby lib
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 = "1.3.1"
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-11-21"
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>, ["> 3.0"])
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>, ["> 3.0"])
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>, ["> 3.0"])
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
- CONNECTION_SWITCHING_MUTEX = Mutex.new
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
- CONNECTION_SWITCHING_MUTEX.synchronize do
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
- CONNECTION_SWITCHING_MUTEX.synchronize do
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
- # 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
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), pool: 10)
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), pool: 10)
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 if the thread-local :autoreplica is set' do
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 if the thread-local :autoreplica is set' do
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: 1.3.1
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-21 00:00:00.000000000 Z
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: '3.0'
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: '3.0'
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