r18_connection_pool 2.2.2a

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,161 @@
1
+ require 'connection_pool/version'
2
+ require '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}
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
+
52
+ @available = TimedStack.new(@size, &block)
53
+ @key = :"current-#{@available.object_id}"
54
+ @key_count = :"current-#{@available.object_id}-count"
55
+ end
56
+
57
+ if Thread.respond_to?(:handle_interrupt)
58
+
59
+ # MRI
60
+ def with(options = {})
61
+ Thread.handle_interrupt(Exception => :never) do
62
+ conn = checkout(options)
63
+ begin
64
+ Thread.handle_interrupt(Exception => :immediate) do
65
+ yield conn
66
+ end
67
+ ensure
68
+ checkin
69
+ end
70
+ end
71
+ end
72
+
73
+ else
74
+
75
+ # jruby 1.7.x
76
+ def with(options = {})
77
+ conn = checkout(options)
78
+ begin
79
+ yield conn
80
+ ensure
81
+ checkin
82
+ end
83
+ end
84
+
85
+ end
86
+
87
+ def checkout(options = {})
88
+ if ::Thread.current[@key]
89
+ ::Thread.current[@key_count]+= 1
90
+ ::Thread.current[@key]
91
+ else
92
+ ::Thread.current[@key_count]= 1
93
+ ::Thread.current[@key]= @available.pop(options[:timeout] || @timeout)
94
+ end
95
+ end
96
+
97
+ def checkin
98
+ if ::Thread.current[@key]
99
+ if ::Thread.current[@key_count] == 1
100
+ @available.push(::Thread.current[@key])
101
+ ::Thread.current[@key]= nil
102
+ else
103
+ ::Thread.current[@key_count]-= 1
104
+ end
105
+ else
106
+ raise ConnectionPool::Error, 'no connections are checked out'
107
+ end
108
+
109
+ nil
110
+ end
111
+
112
+ def shutdown(&block)
113
+ @available.shutdown(&block)
114
+ end
115
+
116
+ # Size of this connection pool
117
+ def size
118
+ @size
119
+ end
120
+
121
+ # Number of pool entries available for checkout at this instant.
122
+ def available
123
+ @available.length
124
+ end
125
+
126
+ private
127
+
128
+ class Wrapper # < ::BasicObject # MayamaTakeshi : to permit to use with ruby 1.8
129
+ METHODS = [:with, :pool_shutdown]
130
+
131
+ def initialize(options = {}, &block)
132
+ @pool = options.fetch(:pool) { ::ConnectionPool.new(options, &block) }
133
+ end
134
+
135
+ def with(&block)
136
+ @pool.with(&block)
137
+ end
138
+
139
+ def pool_shutdown(&block)
140
+ @pool.shutdown(&block)
141
+ end
142
+
143
+ def pool_size
144
+ @pool.size
145
+ end
146
+
147
+ def pool_available
148
+ @pool.available
149
+ end
150
+
151
+ def respond_to?(id, *args)
152
+ METHODS.include?(id) || with { |c| c.respond_to?(id, *args) }
153
+ end
154
+
155
+ def method_missing(name, *args, &block)
156
+ with do |connection|
157
+ connection.send(name, *args, &block)
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require "./lib/connection_pool/version"
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "r18_connection_pool"
6
+ s.version = ConnectionPool::VERSION
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = ["Mike Perham", "Damian Janowski", "Mayama Takeshi"]
9
+ s.email = ["mayamatakeshi@gmail.com"]
10
+ s.homepage = "https://github.com/MayamaTakeshi/connection_pool"
11
+ s.description = s.summary = %q{Generic connection pool for Ruby 1.8}
12
+
13
+ s.files = `git ls-files`.split("\n")
14
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16
+ s.require_paths = ["lib"]
17
+ s.license = "MIT"
18
+ s.add_development_dependency 'bundler'
19
+ s.add_development_dependency 'minitest', '>= 5.0.0'
20
+ s.add_development_dependency 'rake'
21
+ 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,516 @@
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_nested_checkout
349
+ recorder = Recorder.new
350
+ pool = ConnectionPool.new(size: 1) { recorder }
351
+ pool.with do |r_outer|
352
+ @other = Thread.new do |t|
353
+ pool.with do |r_other|
354
+ r_other.do_work('other')
355
+ end
356
+ end
357
+
358
+ pool.with do |r_inner|
359
+ r_inner.do_work('inner')
360
+ end
361
+
362
+ Thread.pass
363
+
364
+ r_outer.do_work('outer')
365
+ end
366
+
367
+ @other.join
368
+
369
+ assert_equal ['inner', 'outer', 'other'], recorder.calls
370
+ end
371
+
372
+ def test_shutdown_is_executed_for_all_connections
373
+ recorders = []
374
+
375
+ pool = ConnectionPool.new(size: 3) do
376
+ Recorder.new.tap { |r| recorders << r }
377
+ end
378
+
379
+ threads = use_pool pool, 3
380
+
381
+ pool.shutdown do |recorder|
382
+ recorder.do_work("shutdown")
383
+ end
384
+
385
+ kill_threads(threads)
386
+
387
+ assert_equal [["shutdown"]] * 3, recorders.map { |r| r.calls }
388
+ end
389
+
390
+ def test_raises_error_after_shutting_down
391
+ pool = ConnectionPool.new(size: 1) { true }
392
+
393
+ pool.shutdown { }
394
+
395
+ assert_raises ConnectionPool::PoolShuttingDownError do
396
+ pool.checkout
397
+ end
398
+ end
399
+
400
+ def test_runs_shutdown_block_asynchronously_if_connection_was_in_use
401
+ recorders = []
402
+
403
+ pool = ConnectionPool.new(size: 3) do
404
+ Recorder.new.tap { |r| recorders << r }
405
+ end
406
+
407
+ threads = use_pool pool, 2
408
+
409
+ pool.checkout
410
+
411
+ pool.shutdown do |recorder|
412
+ recorder.do_work("shutdown")
413
+ end
414
+
415
+ kill_threads(threads)
416
+
417
+ assert_equal [["shutdown"], ["shutdown"], []], recorders.map { |r| r.calls }
418
+
419
+ pool.checkin
420
+
421
+ assert_equal [["shutdown"], ["shutdown"], ["shutdown"]], recorders.map { |r| r.calls }
422
+ end
423
+
424
+ def test_raises_an_error_if_shutdown_is_called_without_a_block
425
+ pool = ConnectionPool.new(size: 1) { }
426
+
427
+ assert_raises ArgumentError do
428
+ pool.shutdown
429
+ end
430
+ end
431
+
432
+ def test_shutdown_is_executed_for_all_connections_in_wrapped_pool
433
+ recorders = []
434
+
435
+ wrapper = ConnectionPool::Wrapper.new(size: 3) do
436
+ Recorder.new.tap { |r| recorders << r }
437
+ end
438
+
439
+ threads = use_pool wrapper, 3
440
+
441
+ wrapper.pool_shutdown do |recorder|
442
+ recorder.do_work("shutdown")
443
+ end
444
+
445
+ kill_threads(threads)
446
+
447
+ assert_equal [["shutdown"]] * 3, recorders.map { |r| r.calls }
448
+ end
449
+
450
+ def test_wrapper_method_missing
451
+ wrapper = ConnectionPool::Wrapper.new { NetworkConnection.new }
452
+
453
+ assert_equal 1, wrapper.fast
454
+ end
455
+
456
+ def test_wrapper_respond_to_eh
457
+ wrapper = ConnectionPool::Wrapper.new { NetworkConnection.new }
458
+
459
+ assert_respond_to wrapper, :with
460
+
461
+ assert_respond_to wrapper, :fast
462
+ refute_respond_to wrapper, :"nonexistent method"
463
+ end
464
+
465
+ def test_wrapper_with
466
+ wrapper = ConnectionPool::Wrapper.new(timeout: 0, size: 1) { Object.new }
467
+
468
+ wrapper.with do
469
+ assert_raises Timeout::Error do
470
+ Thread.new do
471
+ wrapper.with { flunk 'connection checked out :(' }
472
+ end.join
473
+ end
474
+ end
475
+
476
+ assert Thread.new { wrapper.with { } }.join
477
+ end
478
+
479
+ class ConnWithEval
480
+ def eval(arg)
481
+ "eval'ed #{arg}"
482
+ end
483
+ end
484
+
485
+ def test_wrapper_kernel_methods
486
+ wrapper = ConnectionPool::Wrapper.new(timeout: 0, size: 1) { ConnWithEval.new }
487
+
488
+ assert_equal "eval'ed 1", wrapper.eval(1)
489
+ end
490
+
491
+ def test_wrapper_with_connection_pool
492
+ recorder = Recorder.new
493
+ pool = ConnectionPool.new(size: 1) { recorder }
494
+ wrapper = ConnectionPool::Wrapper.new(pool: pool)
495
+
496
+ pool.with { |r| r.do_work('with') }
497
+ wrapper.do_work('wrapped')
498
+
499
+ assert_equal ['with', 'wrapped'], recorder.calls
500
+ end
501
+
502
+ def test_stats_without_active_connection
503
+ pool = ConnectionPool.new(size: 2) { NetworkConnection.new }
504
+
505
+ assert_equal(2, pool.size)
506
+ assert_equal(2, pool.available)
507
+ end
508
+
509
+ def test_stats_with_active_connection
510
+ pool = ConnectionPool.new(size: 2) { NetworkConnection.new }
511
+
512
+ pool.with do
513
+ assert_equal(1, pool.available)
514
+ end
515
+ end
516
+ end