Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Ticket #2162: connection_pool_test.rb

File connection_pool_test.rb, 14.8 kB (added by tomafro, 3 years ago)

Tests and code, all in one nasty file

Line 
1 require 'abstract_unit'
2 require 'fixtures/topic'
3 require 'monitor'
4
5 # todo change configuration to use a hash within a hash
6
7 module ActiveRecord
8   class Base
9     def self.adapter_pool_connection(config)
10       ConnectionAdapters::AdapterPool.new(config)
11     end
12    
13     def self.mock_connection(config)
14       ConnectionAdapters::MockAdapter.new(config)
15     end
16   end
17  
18   class ConnectionAdapters::MockAdapter < ::ActiveRecord::ConnectionAdapters::AbstractAdapter
19     attr_writer :active
20    
21     def self.live_connection_count
22       @@live_connection_count ||= 0
23     end
24    
25     def initialize(config)
26       @@live_connection_count = self.class.live_connection_count + 1
27       @active = true
28     end
29    
30     def pretend_to_do_something(&block)
31       yield if block_given?
32     end
33    
34     def raise_connection_failed
35       raise ActiveRecord::ConnectionFailed
36     end
37    
38     def active?
39       @active
40     end
41   end
42
43   class ConnectionAdapters::AdapterPool
44     include MonitorMixin
45  
46     def initialize(config)
47       super()
48       @config = config
49       @max_signal = new_cond
50       start_reaper_thread
51     end
52    
53     def kind_of?(klass)
54       klass == ConnectionAdapters::AdapterPool || method_missing(:kind_of?, klass)
55     end
56    
57     # Pass all undefined methods through to a local instance of the underlying
58     # database adapter
59    
60     def method_missing(method, *args, &block)
61       use_local_connection do |connection|
62         connection.send(method, *args, &block)
63       end
64     end
65  
66     def current_local_connection
67       Thread.current[:current_local_connection]
68     end   
69
70     def idle_connections
71       @idle_connections ||= []
72     end
73    
74     def all_connections
75       @all_connections ||= []
76     end
77
78     # ensure there aren't too many/too few idle connections
79
80     def rebalance_pool
81       synchronize do
82         while max_idle && max_idle < idle_connections.size
83           remove_connection(idle_connections.shift)
84         end
85        
86         while min_idle && min_idle > idle_connections.size
87           idle_connections << add_connection
88         end
89       end
90     end
91    
92     # remove any stale connections from the pool
93    
94     def refresh_pool
95       synchronize do
96         rejected = idle_connections.reject {|c| c.active? }
97         rejected.each {|c| remove_connection c }
98       end
99     end
100    
101     def refresh_and_rebalance_pool
102       synchronize do
103         refresh_pool
104         rebalance_pool
105       end
106     end
107    
108     def reaper_thread
109       @reaper_thread
110     end
111    
112     private
113       def max_idle
114         @config[:max_idle]
115       end
116      
117       def min_idle
118         @config[:min_idle]
119       end
120      
121       def max_count
122         @config[:max_count]
123       end
124      
125       def reaper_siesta_period
126         @config[:reaper_siesta] || 30
127       end
128    
129       def add_connection
130         connection = ActiveRecord::Base.send("#{@config[:underlying_adapter]}_connection", @config)
131         all_connections << connection
132         connection
133       end
134      
135       def remove_connection(connection)
136         # @TODO we should clean this up in some more meaningful way before discarding it?
137         synchronize do
138           all_connections.delete(connection)
139           idle_connections.delete(connection)
140         end
141        
142         if current_local_connection == connection
143           Thread.current[:current_local_connection] = nil
144         end
145       end
146      
147       # Uses a local connection for a block of work
148
149       def use_local_connection(&block)
150         if !current_local_connection
151           acquire_and_use_local_connection(&block)
152         else
153           yield current_local_connection
154         end
155       end
156      
157       # Aquire a new local connection and use it for the given block of work
158      
159       def acquire_and_use_local_connection(&block)
160         begin
161           Thread.current[:current_local_connection]=(acquire_connection)
162           yield current_local_connection
163         rescue ActiveRecord::ConnectionFailed
164           # if the connection has failed, then reset it (so it isn't returned to the pool)
165           remove_connection(current_local_connection)
166           raise
167         ensure
168           if current_local_connection
169             # return the connection back to the pool
170             synchronize do
171               idle_connections << Thread.current[:current_local_connection]
172               @max_signal.signal
173             end
174           end
175           Thread.current[:current_local_connection]= nil
176         end
177       end
178      
179       def acquire_connection
180         synchronize do
181           unless result = idle_connections.pop
182             if max_count.nil? || all_connections.size < max_count
183               result = add_connection
184             else
185               @max_signal.wait
186             end
187           end
188
189           result || acquire_connection
190         end
191       end
192      
193       def start_reaper_thread
194         unless @config[:prevent_reaper_thread]
195           @reaper_thread = Thread.new do
196             Thread.current[:continue] = true
197             while Thread.current[:continue]
198               sleep(reaper_siesta_period)
199               refresh_and_rebalance_pool
200             end
201           end
202         end   
203       end
204   end
205 end
206
207 class AdapterPoolWithLiveConnectionTest < Test::Unit::TestCase
208   include MonitorMixin
209  
210   fixtures :topics
211
212   def setup
213     super
214     ActiveRecord::Base.allow_concurrency = false
215     @original_config = ActiveRecord::Base.remove_connection
216     @pooled_config = @original_config.dup
217     @pooled_config[:underlying_adapter]= @pooled_config[:adapter]
218     @pooled_config[:adapter]= "adapter_pool"
219     @pooled_config[:prevent_reaper_thread]= true
220     ActiveRecord::Base.establish_connection(@pooled_config)
221   end
222  
223   def teardown
224     ActiveRecord::Base.establish_connection(@original_config)
225   end
226  
227   def test_basic_connection_works
228     assert_kind_of(ActiveRecord::ConnectionAdapters::AdapterPool, ActiveRecord::Base.connection)
229     assert_equal 2, Topic.count
230   end
231 end
232
233 class AdapterPoolWithMockConnectionTest < Test::Unit::TestCase
234   def setup
235     super
236     @original_config = ActiveRecord::Base.remove_connection
237     @pooled_config = @original_config.dup
238     @pooled_config[:underlying_adapter]= "mock"
239     @pooled_config[:adapter]= "adapter_pool"
240     @pooled_config[:prevent_reaper_thread]= true
241     ActiveRecord::Base.establish_connection(@pooled_config)
242   end
243  
244   def teardown
245     ActiveRecord::Base.establish_connection(@original_config)
246   end
247  
248   def test_basic_connection_works
249     assert_equal(0, live_connection_count)
250    
251     assert_kind_of(ActiveRecord::ConnectionAdapters::AdapterPool, ActiveRecord::Base.connection)
252    
253     assert_equal(1, live_connection_count)
254     assert_equal(1, idle_connection_count)
255    
256     ActiveRecord::Base.connection.pretend_to_do_something
257    
258     assert_equal(1, live_connection_count)
259     assert_equal(1, idle_connection_count)
260    
261     assert ActiveRecord::Base.connection.reaper_thread.nil?
262   end
263  
264   def test_transaction_support
265     ActiveRecord::Base.transaction {
266       assert_equal(0, idle_connection_count)
267       first = current_local_connection
268      
269       ActiveRecord::Base.transaction {
270         assert_equal(0, idle_connection_count)
271         assert_equal(1, all_connection_count)
272         assert_same first, current_local_connection
273       }
274     }
275    
276     assert_equal(1, all_connection_count)
277     assert_equal(1, idle_connection_count)
278   end
279  
280   def test_connections_are_pooled_once_used
281     ActiveRecord::Base.transaction {@first = current_local_connection}
282     ActiveRecord::Base.transaction {@second = current_local_connection}
283     assert_same(@first, @second)
284   end
285  
286   def test_connections_are_local_to_current_thread
287     ActiveRecord::Base.connection.pretend_to_do_something do
288       @first = current_local_connection
289       assert_equal(1, all_connection_count)
290       assert_equal(0, idle_connection_count)
291
292       with_new_local_connection do   
293         @second = current_local_connection
294         assert_not_same @first, @second
295         assert_equal(2, all_connection_count)
296         assert_equal(0, idle_connection_count)
297       end
298
299       assert_equal(1, idle_connection_count)
300     end
301    
302     assert_equal(2, idle_connection_count)
303   end
304  
305   def test_connection_failures_invalidate_connection
306     ActiveRecord::Base.transaction {@first = current_local_connection}
307     assert_raise(ActiveRecord::ConnectionFailed) {ActiveRecord::Base.connection.raise_connection_failed}
308     assert_equal(0, idle_connection_count)
309     assert_equal(0, all_connection_count)
310     ActiveRecord::Base.transaction {@second = current_local_connection}
311     assert_not_same(@first, @second, "Original should no longer be available")
312   end
313  
314   def test_rebalance_respects_min_idle
315     @pooled_config[:min_idle] = 10
316     ActiveRecord::Base.establish_connection(@pooled_config)
317     ActiveRecord::Base.connection.rebalance_pool
318     assert_equal(10, idle_connection_count)
319   end
320  
321   def test_rebalance_respects_max_idle
322     @pooled_config[:max_idle] = 5
323     ActiveRecord::Base.establish_connection(@pooled_config)
324    
325     with_new_local_connection do
326       with_new_local_connection do
327         with_new_local_connection do
328           with_new_local_connection do
329             with_new_local_connection do
330               with_new_local_connection do
331                 with_new_local_connection do
332                   with_new_local_connection do
333                     assert_equal(8, all_connection_count)
334                   end
335                 end
336               end
337             end
338           end
339         end
340       end
341     end
342    
343     assert_equal(8, idle_connection_count)
344     ActiveRecord::Base.connection.rebalance_pool
345     assert_equal(5, idle_connection_count)
346   end
347  
348   def test_refresh_culls_inactive_connections
349     with_new_local_connection do
350       # 1 inactive connection
351       current_local_connection.active = false
352       with_new_local_connection do
353         with_new_local_connection do
354           with_new_local_connection do
355             # 2 inactive connections
356             current_local_connection.active = false
357             with_new_local_connection do
358               with_new_local_connection do
359                 with_new_local_connection do
360                   # 3 inactive connections
361                   current_local_connection.active = false
362                   with_new_local_connection do
363                     # 4 inactive connections
364                     current_local_connection.active = false
365                     assert_equal(8, all_connection_count)
366                   end
367                 end
368               end
369             end
370           end
371         end
372       end
373     end
374    
375     assert_equal(8, idle_connection_count)
376     ActiveRecord::Base.connection.refresh_pool
377     assert_equal(4, idle_connection_count)
378   end
379
380   def test_max_total_setting
381     @pooled_config[:max_count] = 5
382     ActiveRecord::Base.establish_connection(@pooled_config)
383     threads = []
384     last_exception = nil
385    
386     test = self
387     completed = 0
388    
389     110.times do
390       Thread.new do
391         begin
392           ActiveRecord::Base.connection.pretend_to_do_something do
393             100.times {Thread.pass}
394           end
395         rescue Exception => e
396           last_exception = e
397         ensure
398           completed = completed + 1
399         end
400       end
401     end
402
403     # ensure most threads have completed before continuing
404
405     while completed < 100
406       Thread.pass
407     end
408    
409     # join last few remaining threads
410    
411     Thread.list.each {|t|
412       t.join unless t == Thread.current
413     }
414    
415     # check no exceptions were caught
416    
417     assert last_exception.nil?, last_exception.to_s
418    
419     # check no more than 5 connections were used, and that all 5 are now currently idle
420    
421     assert_equal 5, all_connection_count
422     assert_equal 5, idle_connection_count
423   end
424  
425   def test_reaper_thread_refreshes_connections
426     @pooled_config[:prevent_reaper_thread]= false
427     ActiveRecord::Base.establish_connection(@pooled_config)
428     with_new_local_connection do
429       # 1 inactive connection
430       current_local_connection.active = false
431       with_new_local_connection do
432         with_new_local_connection do
433           with_new_local_connection do
434             # 2 inactive connections
435             current_local_connection.active = false
436             with_new_local_connection do
437               with_new_local_connection do
438                 with_new_local_connection do
439                   # 3 inactive connections
440                   current_local_connection.active = false
441                   with_new_local_connection do
442                     # 4 inactive connections
443                     current_local_connection.active = false
444                     assert_equal(8, all_connection_count)
445                   end
446                 end
447               end
448             end
449           end
450         end
451       end
452     end
453    
454     ActiveRecord::Base.connection.reaper_thread[:continue] = false
455     ActiveRecord::Base.connection.reaper_thread.wakeup
456     ActiveRecord::Base.connection.reaper_thread.join
457     assert_equal(4, idle_connection_count)
458   end
459  
460   def test_reaper_thread_rebalances_connections
461     @pooled_config[:max_idle] = 5
462     @pooled_config[:prevent_reaper_thread]= false
463     ActiveRecord::Base.establish_connection(@pooled_config)
464     with_new_local_connection do
465       with_new_local_connection do
466         with_new_local_connection do
467           with_new_local_connection do
468             with_new_local_connection do
469               with_new_local_connection do
470                 with_new_local_connection do
471                   with_new_local_connection do
472                     assert_equal(8, all_connection_count)
473                   end
474                 end
475               end
476             end
477           end
478         end
479       end
480     end
481
482     ActiveRecord::Base.connection.reaper_thread[:continue] = false
483     ActiveRecord::Base.connection.reaper_thread.wakeup
484     ActiveRecord::Base.connection.reaper_thread.join
485     assert_equal(5, idle_connection_count)   
486   end
487
488   private
489     def with_new_local_connection(&block)
490       Thread.new do
491         ActiveRecord::Base.connection.pretend_to_do_something do   
492           yield
493         end
494       end.join
495     end
496  
497     def current_local_connection
498       ActiveRecord::Base.connection.current_local_connection
499     end
500    
501     def live_connection_count
502       ActiveRecord::ConnectionAdapters::MockAdapter.live_connection_count
503     end
504    
505     def idle_connection_count
506       ActiveRecord::Base.connection.idle_connections.size
507     end
508    
509     def all_connection_count
510       ActiveRecord::Base.connection.all_connections.size
511     end
512 end