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 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