connection_pool 2.2.2

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,66 @@
1
+ # Global monotonic clock from Concurrent Ruby 1.0.
2
+ # Copyright (c) Jerry D'Antonio -- released under the MIT license.
3
+ # Slightly modified; used with permission.
4
+ # https://github.com/ruby-concurrency/concurrent-ruby
5
+
6
+ require 'thread'
7
+
8
+ class ConnectionPool
9
+
10
+ class_definition = Class.new do
11
+
12
+ if defined?(Process::CLOCK_MONOTONIC)
13
+
14
+ # @!visibility private
15
+ def get_time
16
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
17
+ end
18
+
19
+ elsif defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
20
+
21
+ # @!visibility private
22
+ def get_time
23
+ java.lang.System.nanoTime() / 1_000_000_000.0
24
+ end
25
+
26
+ else
27
+
28
+ # @!visibility private
29
+ def initialize
30
+ @mutex = Mutex.new
31
+ @last_time = Time.now.to_f
32
+ end
33
+
34
+ # @!visibility private
35
+ def get_time
36
+ @mutex.synchronize do
37
+ now = Time.now.to_f
38
+ if @last_time < now
39
+ @last_time = now
40
+ else # clock has moved back in time
41
+ @last_time += 0.000_001
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ ##
49
+ # Clock that cannot be set and represents monotonic time since
50
+ # some unspecified starting point.
51
+ #
52
+ # @!visibility private
53
+ GLOBAL_MONOTONIC_CLOCK = class_definition.new
54
+ private_constant :GLOBAL_MONOTONIC_CLOCK
55
+
56
+ class << self
57
+ ##
58
+ # Returns the current time a tracked by the application monotonic clock.
59
+ #
60
+ # @return [Float] The current monotonic time when `since` not given else
61
+ # the elapsed monotonic time between `since` and the current time
62
+ def monotonic_time
63
+ GLOBAL_MONOTONIC_CLOCK.get_time
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,176 @@
1
+ require 'thread'
2
+ require 'timeout'
3
+ require_relative 'monotonic_time'
4
+
5
+ ##
6
+ # Raised when you attempt to retrieve a connection from a pool that has been
7
+ # shut down.
8
+
9
+ class ConnectionPool::PoolShuttingDownError < RuntimeError; end
10
+
11
+ ##
12
+ # The TimedStack manages a pool of homogeneous connections (or any resource
13
+ # you wish to manage). Connections are created lazily up to a given maximum
14
+ # number.
15
+
16
+ # Examples:
17
+ #
18
+ # ts = TimedStack.new(1) { MyConnection.new }
19
+ #
20
+ # # fetch a connection
21
+ # conn = ts.pop
22
+ #
23
+ # # return a connection
24
+ # ts.push conn
25
+ #
26
+ # conn = ts.pop
27
+ # ts.pop timeout: 5
28
+ # #=> raises Timeout::Error after 5 seconds
29
+
30
+ class ConnectionPool::TimedStack
31
+ attr_reader :max
32
+
33
+ ##
34
+ # Creates a new pool with +size+ connections that are created from the given
35
+ # +block+.
36
+
37
+ def initialize(size = 0, &block)
38
+ @create_block = block
39
+ @created = 0
40
+ @que = []
41
+ @max = size
42
+ @mutex = Mutex.new
43
+ @resource = ConditionVariable.new
44
+ @shutdown_block = nil
45
+ end
46
+
47
+ ##
48
+ # Returns +obj+ to the stack. +options+ is ignored in TimedStack but may be
49
+ # used by subclasses that extend TimedStack.
50
+
51
+ def push(obj, options = {})
52
+ @mutex.synchronize do
53
+ if @shutdown_block
54
+ @shutdown_block.call(obj)
55
+ else
56
+ store_connection obj, options
57
+ end
58
+
59
+ @resource.broadcast
60
+ end
61
+ end
62
+ alias_method :<<, :push
63
+
64
+ ##
65
+ # Retrieves a connection from the stack. If a connection is available it is
66
+ # immediately returned. If no connection is available within the given
67
+ # timeout a Timeout::Error is raised.
68
+ #
69
+ # +:timeout+ is the only checked entry in +options+ and is preferred over
70
+ # the +timeout+ argument (which will be removed in a future release). Other
71
+ # options may be used by subclasses that extend TimedStack.
72
+
73
+ def pop(timeout = 0.5, options = {})
74
+ options, timeout = timeout, 0.5 if Hash === timeout
75
+ timeout = options.fetch :timeout, timeout
76
+
77
+ deadline = ConnectionPool.monotonic_time + timeout
78
+ @mutex.synchronize do
79
+ loop do
80
+ raise ConnectionPool::PoolShuttingDownError if @shutdown_block
81
+ return fetch_connection(options) if connection_stored?(options)
82
+
83
+ connection = try_create(options)
84
+ return connection if connection
85
+
86
+ to_wait = deadline - ConnectionPool.monotonic_time
87
+ raise Timeout::Error, "Waited #{timeout} sec" if to_wait <= 0
88
+ @resource.wait(@mutex, to_wait)
89
+ end
90
+ end
91
+ end
92
+
93
+ ##
94
+ # Shuts down the TimedStack which prevents connections from being checked
95
+ # out. The +block+ is called once for each connection on the stack.
96
+
97
+ def shutdown(&block)
98
+ raise ArgumentError, "shutdown must receive a block" unless block_given?
99
+
100
+ @mutex.synchronize do
101
+ @shutdown_block = block
102
+ @resource.broadcast
103
+
104
+ shutdown_connections
105
+ end
106
+ end
107
+
108
+ ##
109
+ # Returns +true+ if there are no available connections.
110
+
111
+ def empty?
112
+ (@created - @que.length) >= @max
113
+ end
114
+
115
+ ##
116
+ # The number of connections available on the stack.
117
+
118
+ def length
119
+ @max - @created + @que.length
120
+ end
121
+
122
+ private
123
+
124
+ ##
125
+ # This is an extension point for TimedStack and is called with a mutex.
126
+ #
127
+ # This method must returns true if a connection is available on the stack.
128
+
129
+ def connection_stored?(options = nil)
130
+ !@que.empty?
131
+ end
132
+
133
+ ##
134
+ # This is an extension point for TimedStack and is called with a mutex.
135
+ #
136
+ # This method must return a connection from the stack.
137
+
138
+ def fetch_connection(options = nil)
139
+ @que.pop
140
+ end
141
+
142
+ ##
143
+ # This is an extension point for TimedStack and is called with a mutex.
144
+ #
145
+ # This method must shut down all connections on the stack.
146
+
147
+ def shutdown_connections(options = nil)
148
+ while connection_stored?(options)
149
+ conn = fetch_connection(options)
150
+ @shutdown_block.call(conn)
151
+ end
152
+ end
153
+
154
+ ##
155
+ # This is an extension point for TimedStack and is called with a mutex.
156
+ #
157
+ # This method must return +obj+ to the stack.
158
+
159
+ def store_connection(obj, options = nil)
160
+ @que.push obj
161
+ end
162
+
163
+ ##
164
+ # This is an extension point for TimedStack and is called with a mutex.
165
+ #
166
+ # This method must create a connection if and only if the total number of
167
+ # connections allowed has not been met.
168
+
169
+ def try_create(options = nil)
170
+ unless @created == @max
171
+ object = @create_block.call
172
+ @created += 1
173
+ object
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,3 @@
1
+ class ConnectionPool
2
+ VERSION = "2.2.2"
3
+ end
@@ -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