activerecord_autoreplica 1.3.0 → 1.3.1

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: 0dec3a7a02bfdd20f0428bdb405016f3b16d7c12
4
- data.tar.gz: c84f381a3f3c1d01f2066aec82597333a7a8d2c8
3
+ metadata.gz: db7ac2cf097115489206cf2c5ff942e644972585
4
+ data.tar.gz: 7ce126147d70a5f1726d82c4b6e2946d197b25df
5
5
  SHA512:
6
- metadata.gz: 9d0de5551b0801a3dd9149111e2610715f27a12619f56fdbb29e3108591c06678e326bb2e7e14836ccc89e6f9ab4fc7eef212d52074bba39b4a37f332e8c852b
7
- data.tar.gz: e4efd1d3811ffd76b38acb6707b7dbe57babbcad919da69d49e67746236fc15c58219c5ca2bf0b41745c9501f5326a4c925613f55c636a337f4619f3e8a2c72d
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) it should be threadsafe (but _not_ thread-local since the connection
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.0'
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.0 ruby lib
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.0"
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-07"
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 @in_replica_context # This method should not be reentrant
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
- ActiveRecord::Base.connection_handler = custom_handler
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
- ActiveRecord::Base.connection_handler = original_connection_handler
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
- connection_for_writes = @original_handler.retrieve_connection(for_ar_class)
99
- connection_for_reads = @read_pool.connection
100
- Adapter.new(connection_for_writes, connection_for_reads)
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.0
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-07 00:00:00.000000000 Z
11
+ date: 2016-11-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord