activerecord-bogacs 0.1.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 +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +26 -0
- data/Gemfile +33 -0
- data/LICENSE.txt +22 -0
- data/README.md +124 -0
- data/Rakefile +167 -0
- data/activerecord-bogacs.gemspec +26 -0
- data/lib/active_record/bogacs.rb +55 -0
- data/lib/active_record/bogacs/default_pool.rb +672 -0
- data/lib/active_record/bogacs/false_pool.rb +259 -0
- data/lib/active_record/bogacs/pool_support.rb +21 -0
- data/lib/active_record/bogacs/shareable_pool.rb +255 -0
- data/lib/active_record/bogacs/version.rb +5 -0
- data/lib/active_record/connection_adapters/adapter_compat.rb +57 -0
- data/lib/active_record/shared_connection.rb +24 -0
- data/test/active_record/bogacs/default_pool_test.rb +34 -0
- data/test/active_record/bogacs/false_pool_test.rb +200 -0
- data/test/active_record/bogacs/shareable_pool/connection_pool_test.rb +186 -0
- data/test/active_record/bogacs/shareable_pool/connection_sharing_test.rb +429 -0
- data/test/active_record/bogacs/shareable_pool_helper.rb +81 -0
- data/test/active_record/builtin_pool_test.rb +18 -0
- data/test/active_record/connection_pool_test_methods.rb +336 -0
- data/test/test_helper.rb +304 -0
- metadata +130 -0
@@ -0,0 +1,429 @@
|
|
1
|
+
require File.expand_path('../shareable_pool_helper', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Bogacs
|
5
|
+
class ShareablePool
|
6
|
+
|
7
|
+
class ConnectionSharingTest < TestBase
|
8
|
+
include TestHelper
|
9
|
+
|
10
|
+
def setup
|
11
|
+
connection_pool.disconnect!
|
12
|
+
@_pool_size = set_pool_size(10, 5)
|
13
|
+
end
|
14
|
+
|
15
|
+
def teardown
|
16
|
+
clear_active_connections!; clear_shared_connections!
|
17
|
+
set_pool_size(*@_pool_size)
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_do_not_share_if_theres_an_active_connection
|
21
|
+
existing_conn = ActiveRecord::Base.connection
|
22
|
+
begin
|
23
|
+
with_shared_connection do |connection|
|
24
|
+
assert connection
|
25
|
+
refute shared_connection?(connection)
|
26
|
+
assert_nil current_shared_pool_connection
|
27
|
+
assert_equal existing_conn, ActiveRecord::Base.connection
|
28
|
+
end
|
29
|
+
assert_equal existing_conn, ActiveRecord::Base.connection
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_acquires_new_shared_connection_if_none_active
|
34
|
+
begin
|
35
|
+
with_shared_connection do |connection|
|
36
|
+
assert shared_connection?(connection)
|
37
|
+
assert_equal connection, current_shared_pool_connection
|
38
|
+
|
39
|
+
assert_equal connection, ActiveRecord::Base.connection # only during block
|
40
|
+
end
|
41
|
+
assert_nil current_shared_pool_connection
|
42
|
+
# - they actually might be the same - no need to check-out a new :
|
43
|
+
# refute_equal shared_connection, ActiveRecord::Base.connection
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_acquires_new_shared_connection_if_none_active_with_nested_block
|
48
|
+
begin
|
49
|
+
with_shared_connection do |connection|
|
50
|
+
assert shared_connection?(connection)
|
51
|
+
assert_equal connection, current_shared_pool_connection
|
52
|
+
|
53
|
+
assert_equal connection, ActiveRecord::Base.connection # only during block
|
54
|
+
|
55
|
+
# test nested block :
|
56
|
+
with_shared_connection do |connection2|
|
57
|
+
assert_equal connection, connection2
|
58
|
+
assert_equal connection2, current_shared_pool_connection
|
59
|
+
end
|
60
|
+
assert_equal connection, current_shared_pool_connection
|
61
|
+
end
|
62
|
+
assert_nil current_shared_pool_connection
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def test_acquires_same_shared_connection_for_two_threads_when_pool_occupied
|
67
|
+
thread_connection = Atomic.new(nil); shared_thread = nil
|
68
|
+
begin
|
69
|
+
shared_connection = nil
|
70
|
+
block_connections_in_threads(9) do # only one connection left
|
71
|
+
|
72
|
+
shared_thread = shared_connection_thread(thread_connection, :wait)
|
73
|
+
|
74
|
+
with_shared_connection do |connection|
|
75
|
+
shared_connection = connection
|
76
|
+
|
77
|
+
assert shared_connection?(connection)
|
78
|
+
assert_equal connection, current_shared_pool_connection
|
79
|
+
|
80
|
+
# just test that selects are fine :
|
81
|
+
connection.exec_query(sample_query)
|
82
|
+
|
83
|
+
assert_equal connection, thread_connection.value
|
84
|
+
end
|
85
|
+
assert_nil current_shared_pool_connection
|
86
|
+
end
|
87
|
+
|
88
|
+
refute_equal shared_connection, ActiveRecord::Base.connection
|
89
|
+
|
90
|
+
ensure
|
91
|
+
stop_shared_connection_threads thread_connection => shared_thread
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_shared_connections_get_reused_fairly
|
96
|
+
thread1_holder = Atomic.new(nil)
|
97
|
+
thread2_holder = Atomic.new(nil)
|
98
|
+
thread3_holder = Atomic.new(nil)
|
99
|
+
shared_conn_threads = {}
|
100
|
+
begin
|
101
|
+
block_connections_in_threads(8) do # only 2 (shareable) connections left
|
102
|
+
|
103
|
+
#puts "\nshared_conn thread 1"
|
104
|
+
shared_conn_threads[thread1_holder] =
|
105
|
+
shared_connection_thread(thread1_holder, :wait)
|
106
|
+
shared_conn1 = thread1_holder.value
|
107
|
+
|
108
|
+
#puts "\nAR-BASE with_shared 1"
|
109
|
+
with_shared_connection do |connection|
|
110
|
+
assert shared_connection?(connection)
|
111
|
+
# here it should checkout a new connection from the pool
|
112
|
+
assert connection != shared_conn1, '' << # connection == shared_conn2
|
113
|
+
"with_shared_connection reused a previously shared connection " <<
|
114
|
+
"instead of checking out another from the (non-filled) pool"
|
115
|
+
# just test that selects are fine :
|
116
|
+
connection.exec_query(sample_query)
|
117
|
+
end
|
118
|
+
#puts "AR-BASE with_shared 1 DONE"
|
119
|
+
|
120
|
+
#puts "\nshared_conn thread 2"
|
121
|
+
shared_conn_threads[thread2_holder] =
|
122
|
+
shared_connection_thread(thread2_holder, :wait)
|
123
|
+
shared_conn2 = thread2_holder.value
|
124
|
+
assert shared_conn2 != shared_conn1, "shared connections are same"
|
125
|
+
|
126
|
+
#puts "\nAR-BASE with_shared 2"
|
127
|
+
with_shared_connection do |connection|
|
128
|
+
assert shared_connection?(connection)
|
129
|
+
|
130
|
+
#puts "\n - shared_conn thread 3"
|
131
|
+
shared_conn_threads[thread3_holder] =
|
132
|
+
shared_connection_thread(thread3_holder, :wait)
|
133
|
+
shared_conn3 = thread3_holder.value
|
134
|
+
|
135
|
+
# but now it needs to reuse since pool "full" :
|
136
|
+
if connection != shared_conn1
|
137
|
+
assert_equal connection, shared_conn2
|
138
|
+
|
139
|
+
assert shared_conn3 == shared_conn1,
|
140
|
+
"expected thread3's connection #{shared_conn3.to_s} to == " <<
|
141
|
+
"#{shared_conn1.to_s} (but did not thread2's connection = #{shared_conn2.to_s})"
|
142
|
+
else
|
143
|
+
assert shared_conn3 == shared_conn2,
|
144
|
+
"expected thread3's connection #{shared_conn3.to_s} to == " <<
|
145
|
+
"#{shared_conn2.to_s} (but did not thread1's connection = #{shared_conn1.to_s})"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
#puts "AR-BASE with_shared 2 DONE"
|
149
|
+
end
|
150
|
+
ensure
|
151
|
+
stop_shared_connection_threads shared_conn_threads
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def test_shared_connections_get_reused_fairly_with_pool_prefilled
|
156
|
+
prefill_pool_with_connections
|
157
|
+
test_shared_connections_get_reused_fairly
|
158
|
+
end
|
159
|
+
|
160
|
+
def test_does_not_use_more_shared_connections_than_configured_shared_size
|
161
|
+
shared_conn_threads = {}
|
162
|
+
begin
|
163
|
+
block_connections_in_threads(2) do # only 2 connections left
|
164
|
+
|
165
|
+
shared_conn_threads = start_shared_connection_threads(7, :wait)
|
166
|
+
|
167
|
+
# 10 connections but shared pool is limited to max 50% :
|
168
|
+
shared_conns = shared_conn_threads.keys.map do |conn_holder|
|
169
|
+
assert conn = conn_holder.value
|
170
|
+
assert shared_connection?(conn)
|
171
|
+
conn
|
172
|
+
end
|
173
|
+
assert_equal 5, shared_conns.uniq.size
|
174
|
+
|
175
|
+
# still one left for normal connections :
|
176
|
+
assert_equal 9, connection_pool.connections.size
|
177
|
+
|
178
|
+
conn = ActiveRecord::Base.connection
|
179
|
+
refute shared_connection?(conn)
|
180
|
+
assert_equal 10, connection_pool.connections.size
|
181
|
+
|
182
|
+
end
|
183
|
+
ensure # release created threads
|
184
|
+
stop_shared_connection_threads shared_conn_threads
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def test_starts_blocking_when_sharing_max_is_reached
|
189
|
+
shared_conn_threads = {}
|
190
|
+
begin
|
191
|
+
block_connections_in_threads(6) do # assuming pool size 10
|
192
|
+
|
193
|
+
shared_conn_threads = start_shared_connection_threads(20, :wait)
|
194
|
+
|
195
|
+
# getting another one will block - timeout error :
|
196
|
+
assert_raise ActiveRecord::ConnectionTimeoutError do
|
197
|
+
with_shared_connection do |connection|
|
198
|
+
flunk "not expected to get a shared-connection"
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
ensure # release created threads
|
203
|
+
stop_shared_connection_threads shared_conn_threads
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def test_starts_blocking_when_sharing_max_is_reached_with_pool_prefilled
|
208
|
+
prefill_pool_with_connections
|
209
|
+
test_starts_blocking_when_sharing_max_is_reached
|
210
|
+
end
|
211
|
+
|
212
|
+
def test_releases_shared_connections_back_as_blocks_are_done
|
213
|
+
block_connections_in_threads(5) do # assuming pool size 10
|
214
|
+
assert_equal 5, initialized_connections.size
|
215
|
+
shared_conn_threads = start_shared_connection_threads(20, false) # no :wait
|
216
|
+
# ... 5 threads shared ~ 4-times - still have 5 "left"
|
217
|
+
begin
|
218
|
+
threads = []
|
219
|
+
|
220
|
+
stop_condition = Atomic.new(false)
|
221
|
+
4.times do
|
222
|
+
threads << Thread.new(self) do |test|
|
223
|
+
test.with_shared_connection do
|
224
|
+
sleep(0.005) while ( ! stop_condition.value )
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
sleep(0.01)
|
230
|
+
|
231
|
+
with_shared_connection do |conn1|
|
232
|
+
assert shared_connection? conn1
|
233
|
+
|
234
|
+
conns = shared_conn_threads.keys.map do |holder|
|
235
|
+
sleep(0.001) while ! holder.value
|
236
|
+
assert shared_connection? holder.value
|
237
|
+
holder.value # the connection
|
238
|
+
end
|
239
|
+
assert_equal 20, conns.size
|
240
|
+
assert_equal 5, conns.uniq.size
|
241
|
+
|
242
|
+
sleep(0.005)
|
243
|
+
|
244
|
+
with_shared_connection do |conn2|
|
245
|
+
assert conn1 == conn2
|
246
|
+
|
247
|
+
# we can not do any-more here without a timeout :
|
248
|
+
failed = nil
|
249
|
+
Thread.new(self) do |test|
|
250
|
+
begin
|
251
|
+
test.with_shared_connection { failed = false }
|
252
|
+
rescue => e
|
253
|
+
failed = e
|
254
|
+
end
|
255
|
+
end.join
|
256
|
+
#assert failed
|
257
|
+
assert_instance_of ActiveRecord::ConnectionTimeoutError, failed
|
258
|
+
|
259
|
+
stop_condition.swap(true); threads.each(&:join); threads = []
|
260
|
+
|
261
|
+
# but now we released 4 "shared" connections :
|
262
|
+
stop_condition.swap(false)
|
263
|
+
|
264
|
+
failures = []
|
265
|
+
4.times do
|
266
|
+
threads << Thread.new(self) do |test|
|
267
|
+
begin
|
268
|
+
test.with_shared_connection do
|
269
|
+
ActiveRecord::Base.connection.exec_query test_query # 'select 42'
|
270
|
+
sleep(0.005) while ( ! stop_condition.value )
|
271
|
+
end
|
272
|
+
rescue => e
|
273
|
+
failures << e
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
stop_condition.swap(true); threads.each(&:join); threads = []
|
279
|
+
assert failures.empty?, "got failures: #{failures.map(&:inspect).join}"
|
280
|
+
|
281
|
+
end # nested with_shared_connection
|
282
|
+
end # with_shared_connection
|
283
|
+
|
284
|
+
ensure
|
285
|
+
stop_shared_connection_threads shared_conn_threads
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
protected
|
291
|
+
|
292
|
+
def current_shared_pool_connection
|
293
|
+
Thread.current[:shared_pool_connection]
|
294
|
+
end
|
295
|
+
|
296
|
+
def set_pool_size(size, shared_size = nil)
|
297
|
+
prev_size = connection_pool.size
|
298
|
+
prev_shared_size = connection_pool.shared_size
|
299
|
+
connection_pool.size = size
|
300
|
+
connection_pool.shared_size = shared_size if shared_size
|
301
|
+
if block_given?
|
302
|
+
begin
|
303
|
+
yield
|
304
|
+
ensure
|
305
|
+
connection_pool.size = prev_size
|
306
|
+
connection_pool.shared_size = prev_shared_size
|
307
|
+
end
|
308
|
+
else
|
309
|
+
shared_size ? [ prev_size, prev_shared_size ] : prev_size
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
def set_shared_size(size)
|
314
|
+
prev_size = connection_pool.shared_size
|
315
|
+
connection_pool.shared_size = size
|
316
|
+
if block_given?
|
317
|
+
begin
|
318
|
+
yield
|
319
|
+
ensure
|
320
|
+
connection_pool.shared_size = prev_size
|
321
|
+
end
|
322
|
+
else
|
323
|
+
prev_size
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
private
|
328
|
+
|
329
|
+
@@counter = 0
|
330
|
+
STDOUT.sync = true
|
331
|
+
|
332
|
+
def shared_connection_thread(connection_holder, wait = true)
|
333
|
+
test_name = _test_name
|
334
|
+
|
335
|
+
debug = test_name == 'test_starts_blocking_when_sharing_max_is_reached_with_pool_prefilled'
|
336
|
+
if debug && false
|
337
|
+
@@counter += 1
|
338
|
+
puts "\n"
|
339
|
+
puts "#{@@counter} initialized-connections: #{connections.size}"
|
340
|
+
puts "#{@@counter} shared-connections: #{shared_connections.size}"
|
341
|
+
puts "#{@@counter} available-connections #{available_connections.size}"
|
342
|
+
puts "#{@@counter} reserved-connections #{reserved_connections.size}"
|
343
|
+
puts "\n"
|
344
|
+
end
|
345
|
+
|
346
|
+
thread = Thread.new do
|
347
|
+
begin
|
348
|
+
ActiveRecord::Base.connection_pool.with_shared_connection do |connection|
|
349
|
+
# just test that selects are fine :
|
350
|
+
connection.exec_query(test_query)
|
351
|
+
|
352
|
+
connection_holder.swap(connection)
|
353
|
+
while connection_holder.value != false
|
354
|
+
# connection.select_value('SELECT version()')
|
355
|
+
sleep(0.001)
|
356
|
+
end
|
357
|
+
# just test that selects are fine :
|
358
|
+
connection.select_value(sample_query)
|
359
|
+
end
|
360
|
+
rescue => e
|
361
|
+
puts "\n#{test_name} acquire shared thread failed: #{e.inspect} \n #{e.backtrace.join(" \n")}\n"
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
while connection_holder.value.nil?
|
366
|
+
sleep(0.005)
|
367
|
+
end if wait
|
368
|
+
|
369
|
+
thread
|
370
|
+
end
|
371
|
+
|
372
|
+
def start_shared_connection_threads(count, wait = true)
|
373
|
+
holder_2_thread = {}
|
374
|
+
count.times do
|
375
|
+
conn_holder = Atomic.new(nil)
|
376
|
+
thread = shared_connection_thread(conn_holder, wait)
|
377
|
+
holder_2_thread[conn_holder] = thread
|
378
|
+
end
|
379
|
+
holder_2_thread
|
380
|
+
end
|
381
|
+
|
382
|
+
def stop_shared_connection_threads(holder_2_threads)
|
383
|
+
holder_2_threads.keys.each { |holder| holder.swap(false) }
|
384
|
+
holder_2_threads.values.each(&:join)
|
385
|
+
end
|
386
|
+
|
387
|
+
def block_connections_in_threads(count)
|
388
|
+
block = Atomic.new(0); threads = []
|
389
|
+
count.times do
|
390
|
+
threads << Thread.new do
|
391
|
+
begin
|
392
|
+
ActiveRecord::Base.connection
|
393
|
+
block.update { |v| v + 1 }
|
394
|
+
while block.value <= count
|
395
|
+
sleep(0.005)
|
396
|
+
end
|
397
|
+
rescue => e
|
398
|
+
puts "block thread failed: #{e.inspect}"
|
399
|
+
ensure
|
400
|
+
ActiveRecord::Base.clear_active_connections!
|
401
|
+
end
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
while block.value < count
|
406
|
+
sleep(0.001) # wait till connections are blocked
|
407
|
+
end
|
408
|
+
|
409
|
+
outcome = yield
|
410
|
+
|
411
|
+
block.update { |v| v + 42 }; threads.each(&:join)
|
412
|
+
|
413
|
+
outcome
|
414
|
+
end
|
415
|
+
|
416
|
+
def prefill_pool_with_connections(size = connection_pool.size)
|
417
|
+
conns = []
|
418
|
+
begin
|
419
|
+
size.times { conns << connection_pool.checkout }
|
420
|
+
ensure
|
421
|
+
conns.each { |conn| connection_pool.checkin(conn) }
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
end
|
426
|
+
|
427
|
+
end
|
428
|
+
end
|
429
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
|
2
|
+
require File.expand_path('../../test_helper', File.dirname(__FILE__))
|
3
|
+
|
4
|
+
ActiveRecord::Bogacs::ShareablePool.class_eval do
|
5
|
+
attr_reader :shared_connections
|
6
|
+
attr_writer :size, :shared_size
|
7
|
+
end
|
8
|
+
|
9
|
+
module ActiveRecord
|
10
|
+
module Bogacs
|
11
|
+
class ShareablePool
|
12
|
+
|
13
|
+
module TestHelpers
|
14
|
+
|
15
|
+
def teardown; connection_pool.disconnect! end
|
16
|
+
|
17
|
+
def connection_pool
|
18
|
+
ActiveRecord::Base.connection_pool
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialized_connections
|
22
|
+
ActiveRecord::Base.connection_pool.connections.dup
|
23
|
+
end
|
24
|
+
alias_method :connections, :initialized_connections
|
25
|
+
|
26
|
+
def reserved_connections
|
27
|
+
ActiveRecord::Base.connection_pool.reserved_connections
|
28
|
+
end
|
29
|
+
|
30
|
+
def available_connections
|
31
|
+
connection_pool.available.instance_variable_get(:'@queue').dup
|
32
|
+
end
|
33
|
+
|
34
|
+
def available_connection? connection
|
35
|
+
available_connections.include? connection
|
36
|
+
end
|
37
|
+
|
38
|
+
def shared_connections
|
39
|
+
ActiveRecord::Base.connection_pool.shared_connections
|
40
|
+
end
|
41
|
+
|
42
|
+
def shared_connection? connection
|
43
|
+
!!shared_connections.get(connection)
|
44
|
+
end
|
45
|
+
|
46
|
+
def with_shared_connection(&block)
|
47
|
+
ActiveRecord::Base.connection_pool.with_shared_connection(&block)
|
48
|
+
end
|
49
|
+
|
50
|
+
def clear_active_connections!
|
51
|
+
ActiveRecord::Base.clear_active_connections!
|
52
|
+
end
|
53
|
+
|
54
|
+
def clear_shared_connections!
|
55
|
+
connection_pool = ActiveRecord::Base.connection_pool
|
56
|
+
shared_connections.keys.each do |connection|
|
57
|
+
connection_pool.release_shared_connection(connection)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
class TestBase < ::Test::Unit::TestCase
|
64
|
+
include TestHelpers
|
65
|
+
|
66
|
+
def self.startup
|
67
|
+
ConnectionAdapters::ConnectionHandler.connection_pool_class = ShareablePool
|
68
|
+
|
69
|
+
ActiveRecord::Base.establish_connection AR_CONFIG
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.shutdown
|
73
|
+
ActiveRecord::Base.connection_pool.disconnect!
|
74
|
+
ConnectionAdapters::ConnectionHandler.connection_pool_class = ConnectionAdapters::ConnectionPool
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|