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