activerecord-bogacs 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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