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