healthy_pools 2.2.3

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,162 @@
1
+ require_relative 'connection_pool/version'
2
+ require_relative 'connection_pool/timed_stack'
3
+
4
+
5
+ # Generic connection pool class for e.g. sharing a limited number of network connections
6
+ # among many threads. Note: Connections are lazily created.
7
+ #
8
+ # Example usage with block (faster):
9
+ #
10
+ # @pool = ConnectionPool.new { Redis.new }
11
+ #
12
+ # @pool.with do |redis|
13
+ # redis.lpop('my-list') if redis.llen('my-list') > 0
14
+ # end
15
+ #
16
+ # Using optional timeout override (for that single invocation)
17
+ #
18
+ # @pool.with(timeout: 2.0) do |redis|
19
+ # redis.lpop('my-list') if redis.llen('my-list') > 0
20
+ # end
21
+ #
22
+ # Example usage replacing an existing connection (slower):
23
+ #
24
+ # $redis = ConnectionPool.wrap { Redis.new }
25
+ #
26
+ # def do_work
27
+ # $redis.lpop('my-list') if $redis.llen('my-list') > 0
28
+ # end
29
+ #
30
+ # Accepts the following options:
31
+ # - :size - number of connections to pool, defaults to 5
32
+ # - :timeout - amount of time to wait for a connection if none currently available, defaults to 5 seconds
33
+ #
34
+ class ConnectionPool
35
+ DEFAULTS = {size: 5, timeout: 5, health_check: nil}
36
+
37
+ class Error < RuntimeError
38
+ end
39
+
40
+ def self.wrap(options, &block)
41
+ Wrapper.new(options, &block)
42
+ end
43
+
44
+ def initialize(options = {}, &block)
45
+ raise ArgumentError, 'Connection pool requires a block' unless block
46
+
47
+ options = DEFAULTS.merge(options)
48
+
49
+ @size = options.fetch(:size)
50
+ @timeout = options.fetch(:timeout)
51
+ @health_check = options[:health_check]
52
+
53
+ @available = TimedStack.new(@size, health_check: @health_check, &block)
54
+ @key = :"current-#{@available.object_id}"
55
+ @key_count = :"current-#{@available.object_id}-count"
56
+ end
57
+
58
+ if Thread.respond_to?(:handle_interrupt)
59
+
60
+ # MRI
61
+ def with(options = {})
62
+ Thread.handle_interrupt(Exception => :never) do
63
+ conn = checkout(options)
64
+ begin
65
+ Thread.handle_interrupt(Exception => :immediate) do
66
+ yield conn
67
+ end
68
+ ensure
69
+ checkin
70
+ end
71
+ end
72
+ end
73
+
74
+ else
75
+
76
+ # jruby 1.7.x
77
+ def with(options = {})
78
+ conn = checkout(options)
79
+ begin
80
+ yield conn
81
+ ensure
82
+ checkin
83
+ end
84
+ end
85
+
86
+ end
87
+
88
+ def checkout(options = {})
89
+ if ::Thread.current[@key]
90
+ ::Thread.current[@key_count]+= 1
91
+ ::Thread.current[@key]
92
+ else
93
+ ::Thread.current[@key_count]= 1
94
+ ::Thread.current[@key]= @available.pop(options[:timeout] || @timeout)
95
+ end
96
+ end
97
+
98
+ def checkin
99
+ if ::Thread.current[@key]
100
+ if ::Thread.current[@key_count] == 1
101
+ @available.push(::Thread.current[@key])
102
+ ::Thread.current[@key]= nil
103
+ else
104
+ ::Thread.current[@key_count]-= 1
105
+ end
106
+ else
107
+ raise ConnectionPool::Error, 'no connections are checked out'
108
+ end
109
+
110
+ nil
111
+ end
112
+
113
+ def shutdown(&block)
114
+ @available.shutdown(&block)
115
+ end
116
+
117
+ # Size of this connection pool
118
+ def size
119
+ @size
120
+ end
121
+
122
+ # Number of pool entries available for checkout at this instant.
123
+ def available
124
+ @available.length
125
+ end
126
+
127
+ private
128
+
129
+ class Wrapper < ::BasicObject
130
+ METHODS = [:with, :pool_shutdown]
131
+
132
+ def initialize(options = {}, &block)
133
+ @pool = options.fetch(:pool) { ::ConnectionPool.new(options, &block) }
134
+ end
135
+
136
+ def with(&block)
137
+ @pool.with(&block)
138
+ end
139
+
140
+ def pool_shutdown(&block)
141
+ @pool.shutdown(&block)
142
+ end
143
+
144
+ def pool_size
145
+ @pool.size
146
+ end
147
+
148
+ def pool_available
149
+ @pool.available
150
+ end
151
+
152
+ def respond_to?(id, *args)
153
+ METHODS.include?(id) || with { |c| c.respond_to?(id, *args) }
154
+ end
155
+
156
+ def method_missing(name, *args, &block)
157
+ with do |connection|
158
+ connection.send(name, *args, &block)
159
+ end
160
+ end
161
+ end
162
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,8 @@
1
+ gem 'minitest'
2
+
3
+ require 'minitest/pride'
4
+ require 'minitest/autorun'
5
+
6
+ $VERBOSE = 1
7
+
8
+ require_relative '../lib/connection_pool'
@@ -0,0 +1,546 @@
1
+ require_relative 'helper'
2
+
3
+ class TestConnectionPool < Minitest::Test
4
+
5
+ class NetworkConnection
6
+ SLEEP_TIME = 0.1
7
+
8
+ def initialize
9
+ @x = 0
10
+ end
11
+
12
+ def do_something
13
+ @x += 1
14
+ sleep SLEEP_TIME
15
+ @x
16
+ end
17
+
18
+ def fast
19
+ @x += 1
20
+ end
21
+
22
+ def do_something_with_block
23
+ @x += yield
24
+ sleep SLEEP_TIME
25
+ @x
26
+ end
27
+
28
+ def respond_to?(method_id, *args)
29
+ method_id == :do_magic || super(method_id, *args)
30
+ end
31
+ end
32
+
33
+ class Recorder
34
+ def initialize
35
+ @calls = []
36
+ end
37
+
38
+ attr_reader :calls
39
+
40
+ def do_work(label)
41
+ @calls << label
42
+ end
43
+ end
44
+
45
+ def use_pool(pool, size)
46
+ Array.new(size) do
47
+ Thread.new do
48
+ pool.with do sleep end
49
+ end
50
+ end.each do |thread|
51
+ Thread.pass until thread.status == 'sleep'
52
+ end
53
+ end
54
+
55
+ def kill_threads(threads)
56
+ threads.each do |thread|
57
+ thread.kill
58
+ thread.join
59
+ end
60
+ end
61
+
62
+ def test_basic_multithreaded_usage
63
+ pool_size = 5
64
+ pool = ConnectionPool.new(size: pool_size) { NetworkConnection.new }
65
+
66
+ start = Time.new
67
+
68
+ generations = 3
69
+
70
+ result = Array.new(pool_size * generations) do
71
+ Thread.new do
72
+ pool.with do |net|
73
+ net.do_something
74
+ end
75
+ end
76
+ end.map(&:value)
77
+
78
+ finish = Time.new
79
+
80
+ assert_equal((1..generations).cycle(pool_size).sort, result.sort)
81
+
82
+ assert_operator(finish - start, :>, generations * NetworkConnection::SLEEP_TIME)
83
+ end
84
+
85
+ def test_timeout
86
+ pool = ConnectionPool.new(timeout: 0, size: 1) { NetworkConnection.new }
87
+ thread = Thread.new do
88
+ pool.with do |net|
89
+ net.do_something
90
+ sleep 0.01
91
+ end
92
+ end
93
+
94
+ Thread.pass while thread.status == 'run'
95
+
96
+ assert_raises Timeout::Error do
97
+ pool.with { |net| net.do_something }
98
+ end
99
+
100
+ thread.join
101
+
102
+ pool.with do |conn|
103
+ refute_nil conn
104
+ end
105
+ end
106
+
107
+ def test_with
108
+ pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new }
109
+
110
+ pool.with do
111
+ assert_raises Timeout::Error do
112
+ Thread.new { pool.checkout }.join
113
+ end
114
+ end
115
+
116
+ assert Thread.new { pool.checkout }.join
117
+ end
118
+
119
+ def test_with_timeout
120
+ pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new }
121
+
122
+ assert_raises Timeout::Error do
123
+ Timeout.timeout(0.01) do
124
+ pool.with do |obj|
125
+ assert_equal 0, pool.instance_variable_get(:@available).instance_variable_get(:@que).size
126
+ sleep 0.015
127
+ end
128
+ end
129
+ end
130
+ assert_equal 1, pool.instance_variable_get(:@available).instance_variable_get(:@que).size
131
+ end
132
+
133
+ def test_checkout_ignores_timeout
134
+ skip("Thread.handle_interrupt not available") unless Thread.respond_to?(:handle_interrupt)
135
+
136
+ pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new }
137
+ def pool.checkout(options)
138
+ sleep 0.015
139
+ super
140
+ end
141
+
142
+ did_something = false
143
+ assert_raises Timeout::Error do
144
+ Timeout.timeout(0.01) do
145
+ pool.with do |obj|
146
+ did_something = true
147
+ # Timeout::Error will be triggered by any non-trivial Ruby code
148
+ # executed here since it couldn't be raised during checkout.
149
+ # It looks like setting the local variable above does not trigger
150
+ # the Timeout check in MRI 2.2.1.
151
+ obj.tap { obj.hash }
152
+ end
153
+ end
154
+ end
155
+ assert did_something
156
+ assert_equal 1, pool.instance_variable_get(:@available).instance_variable_get(:@que).size
157
+ end
158
+
159
+ def test_explicit_return
160
+ pool = ConnectionPool.new(timeout: 0, size: 1) do
161
+ mock = Minitest::Mock.new
162
+ def mock.disconnect!
163
+ raise "should not disconnect upon explicit return"
164
+ end
165
+ mock
166
+ end
167
+
168
+ pool.with do |conn|
169
+ return true
170
+ end
171
+ end
172
+
173
+ def test_with_timeout_override
174
+ pool = ConnectionPool.new(timeout: 0, size: 1) { NetworkConnection.new }
175
+
176
+ t = Thread.new do
177
+ pool.with do |net|
178
+ net.do_something
179
+ sleep 0.01
180
+ end
181
+ end
182
+
183
+ Thread.pass while t.status == 'run'
184
+
185
+ assert_raises Timeout::Error do
186
+ pool.with { |net| net.do_something }
187
+ end
188
+
189
+ pool.with(timeout: 2 * NetworkConnection::SLEEP_TIME) do |conn|
190
+ refute_nil conn
191
+ end
192
+ end
193
+
194
+ def test_checkin
195
+ pool = ConnectionPool.new(timeout: 0, size: 1) { NetworkConnection.new }
196
+ conn = pool.checkout
197
+
198
+ assert_raises Timeout::Error do
199
+ Thread.new { pool.checkout }.join
200
+ end
201
+
202
+ pool.checkin
203
+
204
+ assert_same conn, Thread.new { pool.checkout }.value
205
+ end
206
+
207
+ def test_returns_value
208
+ pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new }
209
+ assert_equal 1, pool.with {|o| 1 }
210
+ end
211
+
212
+ def test_checkin_never_checkout
213
+ pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new }
214
+
215
+ e = assert_raises ConnectionPool::Error do
216
+ pool.checkin
217
+ end
218
+
219
+ assert_equal 'no connections are checked out', e.message
220
+ end
221
+
222
+ def test_checkin_no_current_checkout
223
+ pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new }
224
+
225
+ pool.checkout
226
+ pool.checkin
227
+
228
+ assert_raises ConnectionPool::Error do
229
+ pool.checkin
230
+ end
231
+ end
232
+
233
+ def test_checkin_twice
234
+ pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new }
235
+
236
+ pool.checkout
237
+ pool.checkout
238
+
239
+ pool.checkin
240
+
241
+ assert_raises Timeout::Error do
242
+ Thread.new do
243
+ pool.checkout
244
+ end.join
245
+ end
246
+
247
+ pool.checkin
248
+
249
+ assert Thread.new { pool.checkout }.join
250
+ end
251
+
252
+ def test_checkout
253
+ pool = ConnectionPool.new(size: 1) { NetworkConnection.new }
254
+
255
+ conn = pool.checkout
256
+
257
+ assert_kind_of NetworkConnection, conn
258
+
259
+ assert_same conn, pool.checkout
260
+ end
261
+
262
+ def test_checkout_multithread
263
+ pool = ConnectionPool.new(size: 2) { NetworkConnection.new }
264
+ conn = pool.checkout
265
+
266
+ t = Thread.new do
267
+ pool.checkout
268
+ end
269
+
270
+ refute_same conn, t.value
271
+ end
272
+
273
+ def test_checkout_timeout
274
+ pool = ConnectionPool.new(timeout: 0, size: 0) { Object.new }
275
+
276
+ assert_raises Timeout::Error do
277
+ pool.checkout
278
+ end
279
+ end
280
+
281
+ def test_checkout_timeout_override
282
+ pool = ConnectionPool.new(timeout: 0, size: 1) { NetworkConnection.new }
283
+
284
+ thread = Thread.new do
285
+ pool.with do |net|
286
+ net.do_something
287
+ sleep 0.01
288
+ end
289
+ end
290
+
291
+ Thread.pass while thread.status == 'run'
292
+
293
+ assert_raises Timeout::Error do
294
+ pool.checkout
295
+ end
296
+
297
+ assert pool.checkout timeout: 2 * NetworkConnection::SLEEP_TIME
298
+ end
299
+
300
+ def test_passthru
301
+ pool = ConnectionPool.wrap(timeout: 2 * NetworkConnection::SLEEP_TIME, size: 1) { NetworkConnection.new }
302
+ assert_equal 1, pool.do_something
303
+ assert_equal 2, pool.do_something
304
+ assert_equal 5, pool.do_something_with_block { 3 }
305
+ assert_equal 6, pool.with { |net| net.fast }
306
+ end
307
+
308
+ def test_passthru_respond_to
309
+ pool = ConnectionPool.wrap(timeout: 2 * NetworkConnection::SLEEP_TIME, size: 1) { NetworkConnection.new }
310
+ assert pool.respond_to?(:with)
311
+ assert pool.respond_to?(:do_something)
312
+ assert pool.respond_to?(:do_magic)
313
+ refute pool.respond_to?(:do_lots_of_magic)
314
+ end
315
+
316
+ def test_return_value
317
+ pool = ConnectionPool.new(timeout: 2 * NetworkConnection::SLEEP_TIME, size: 1) { NetworkConnection.new }
318
+ result = pool.with do |net|
319
+ net.fast
320
+ end
321
+ assert_equal 1, result
322
+ end
323
+
324
+ def test_heavy_threading
325
+ pool = ConnectionPool.new(timeout: 0.5, size: 3) { NetworkConnection.new }
326
+
327
+ threads = Array.new(20) do
328
+ Thread.new do
329
+ pool.with do |net|
330
+ sleep 0.01
331
+ end
332
+ end
333
+ end
334
+
335
+ threads.map { |thread| thread.join }
336
+ end
337
+
338
+ def test_reuses_objects_when_pool_not_saturated
339
+ pool = ConnectionPool.new(size: 5) { NetworkConnection.new }
340
+
341
+ ids = 10.times.map do
342
+ pool.with { |c| c.object_id }
343
+ end
344
+
345
+ assert_equal 1, ids.uniq.size
346
+ end
347
+
348
+ def test_failed_health_check_removes_failed_connections
349
+ pool = ConnectionPool.new(size: 5, health_check: lambda {|x| false}) { NetworkConnection.new }
350
+
351
+ ids = 10.times.map do
352
+ pool.with { |c| c.object_id }
353
+ end
354
+
355
+ assert_equal 10, ids.uniq.size
356
+ end
357
+
358
+ def test_exceptions_during_health_check_removes_connections
359
+ pool = ConnectionPool.new(size: 5, health_check: lambda {|x| raise "failed"}) { NetworkConnection.new }
360
+
361
+ ids = 10.times.map do
362
+ pool.with { |c| c.object_id }
363
+ end
364
+
365
+ assert_equal 10, ids.uniq.size
366
+ end
367
+
368
+ def test_failed_health_check_does_not_remove_good_connections
369
+ pool = ConnectionPool.new(size: 5, health_check: lambda {|x| true}) { NetworkConnection.new }
370
+
371
+ ids = 10.times.map do
372
+ pool.with { |c| c.object_id }
373
+ end
374
+
375
+ assert_equal 1, ids.uniq.size
376
+ end
377
+
378
+ def test_nested_checkout
379
+ recorder = Recorder.new
380
+ pool = ConnectionPool.new(size: 1) { recorder }
381
+ pool.with do |r_outer|
382
+ @other = Thread.new do |t|
383
+ pool.with do |r_other|
384
+ r_other.do_work('other')
385
+ end
386
+ end
387
+
388
+ pool.with do |r_inner|
389
+ r_inner.do_work('inner')
390
+ end
391
+
392
+ Thread.pass
393
+
394
+ r_outer.do_work('outer')
395
+ end
396
+
397
+ @other.join
398
+
399
+ assert_equal ['inner', 'outer', 'other'], recorder.calls
400
+ end
401
+
402
+ def test_shutdown_is_executed_for_all_connections
403
+ recorders = []
404
+
405
+ pool = ConnectionPool.new(size: 3) do
406
+ Recorder.new.tap { |r| recorders << r }
407
+ end
408
+
409
+ threads = use_pool pool, 3
410
+
411
+ pool.shutdown do |recorder|
412
+ recorder.do_work("shutdown")
413
+ end
414
+
415
+ kill_threads(threads)
416
+
417
+ assert_equal [["shutdown"]] * 3, recorders.map { |r| r.calls }
418
+ end
419
+
420
+ def test_raises_error_after_shutting_down
421
+ pool = ConnectionPool.new(size: 1) { true }
422
+
423
+ pool.shutdown { }
424
+
425
+ assert_raises ConnectionPool::PoolShuttingDownError do
426
+ pool.checkout
427
+ end
428
+ end
429
+
430
+ def test_runs_shutdown_block_asynchronously_if_connection_was_in_use
431
+ recorders = []
432
+
433
+ pool = ConnectionPool.new(size: 3) do
434
+ Recorder.new.tap { |r| recorders << r }
435
+ end
436
+
437
+ threads = use_pool pool, 2
438
+
439
+ pool.checkout
440
+
441
+ pool.shutdown do |recorder|
442
+ recorder.do_work("shutdown")
443
+ end
444
+
445
+ kill_threads(threads)
446
+
447
+ assert_equal [["shutdown"], ["shutdown"], []], recorders.map { |r| r.calls }
448
+
449
+ pool.checkin
450
+
451
+ assert_equal [["shutdown"], ["shutdown"], ["shutdown"]], recorders.map { |r| r.calls }
452
+ end
453
+
454
+ def test_raises_an_error_if_shutdown_is_called_without_a_block
455
+ pool = ConnectionPool.new(size: 1) { }
456
+
457
+ assert_raises ArgumentError do
458
+ pool.shutdown
459
+ end
460
+ end
461
+
462
+ def test_shutdown_is_executed_for_all_connections_in_wrapped_pool
463
+ recorders = []
464
+
465
+ wrapper = ConnectionPool::Wrapper.new(size: 3) do
466
+ Recorder.new.tap { |r| recorders << r }
467
+ end
468
+
469
+ threads = use_pool wrapper, 3
470
+
471
+ wrapper.pool_shutdown do |recorder|
472
+ recorder.do_work("shutdown")
473
+ end
474
+
475
+ kill_threads(threads)
476
+
477
+ assert_equal [["shutdown"]] * 3, recorders.map { |r| r.calls }
478
+ end
479
+
480
+ def test_wrapper_method_missing
481
+ wrapper = ConnectionPool::Wrapper.new { NetworkConnection.new }
482
+
483
+ assert_equal 1, wrapper.fast
484
+ end
485
+
486
+ def test_wrapper_respond_to_eh
487
+ wrapper = ConnectionPool::Wrapper.new { NetworkConnection.new }
488
+
489
+ assert_respond_to wrapper, :with
490
+
491
+ assert_respond_to wrapper, :fast
492
+ refute_respond_to wrapper, :"nonexistent method"
493
+ end
494
+
495
+ def test_wrapper_with
496
+ wrapper = ConnectionPool::Wrapper.new(timeout: 0, size: 1) { Object.new }
497
+
498
+ wrapper.with do
499
+ assert_raises Timeout::Error do
500
+ Thread.new do
501
+ wrapper.with { flunk 'connection checked out :(' }
502
+ end.join
503
+ end
504
+ end
505
+
506
+ assert Thread.new { wrapper.with { } }.join
507
+ end
508
+
509
+ class ConnWithEval
510
+ def eval(arg)
511
+ "eval'ed #{arg}"
512
+ end
513
+ end
514
+
515
+ def test_wrapper_kernel_methods
516
+ wrapper = ConnectionPool::Wrapper.new(timeout: 0, size: 1) { ConnWithEval.new }
517
+
518
+ assert_equal "eval'ed 1", wrapper.eval(1)
519
+ end
520
+
521
+ def test_wrapper_with_connection_pool
522
+ recorder = Recorder.new
523
+ pool = ConnectionPool.new(size: 1) { recorder }
524
+ wrapper = ConnectionPool::Wrapper.new(pool: pool)
525
+
526
+ pool.with { |r| r.do_work('with') }
527
+ wrapper.do_work('wrapped')
528
+
529
+ assert_equal ['with', 'wrapped'], recorder.calls
530
+ end
531
+
532
+ def test_stats_without_active_connection
533
+ pool = ConnectionPool.new(size: 2) { NetworkConnection.new }
534
+
535
+ assert_equal(2, pool.size)
536
+ assert_equal(2, pool.available)
537
+ end
538
+
539
+ def test_stats_with_active_connection
540
+ pool = ConnectionPool.new(size: 2) { NetworkConnection.new }
541
+
542
+ pool.with do
543
+ assert_equal(1, pool.available)
544
+ end
545
+ end
546
+ end