ezpool 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,37 @@
1
+ require_relative 'connection_wrapper'
2
+ require_relative 'errors'
3
+
4
+
5
+ class EzPool::ConnectionManager
6
+ def initialize(connect_with, disconnect_with = nil)
7
+ @connect_with = connect_with
8
+ @disconnect_with = disconnect_with
9
+ end
10
+
11
+ def connect
12
+ if @connect_with.nil?
13
+ raise EzPool::ConnectCallableNeverConfigured.new()
14
+ end
15
+ @connect_with.call
16
+ end
17
+
18
+ def disconnect(conn)
19
+ if !@disconnect_with.nil?
20
+ @disconnect_with.call(conn)
21
+ end
22
+ end
23
+
24
+ def connect_with(&block)
25
+ @connect_with = block
26
+ end
27
+
28
+ def disconnect_with(&block)
29
+ @disconnect_with = block
30
+ end
31
+
32
+ ##
33
+ # Create a new wrapped connection
34
+ def create_new
35
+ EzPool::ConnectionWrapper.new(connect, self)
36
+ end
37
+ end
@@ -0,0 +1,20 @@
1
+ require_relative 'monotonic_time'
2
+
3
+ class EzPool::ConnectionWrapper
4
+ attr_reader :raw_conn
5
+
6
+ def initialize(conn, connection_manager)
7
+ @raw_conn = conn
8
+ @created_at = EzPool.monotonic_time
9
+ @manager = connection_manager
10
+ end
11
+
12
+ # Shut down the connection. Can no longer be used after this!
13
+ def shutdown!
14
+ @manager.disconnect(@raw_conn)
15
+ end
16
+
17
+ def age
18
+ EzPool.monotonic_time - @created_at
19
+ end
20
+ end
@@ -0,0 +1,8 @@
1
+ class EzPool::Error < RuntimeError
2
+ end
3
+
4
+ class EzPool::CheckedInUnCheckedOutConnectionError < EzPool::Error
5
+ end
6
+
7
+ class EzPool::ConnectCallableNeverConfigured < EzPool::Error
8
+ end
@@ -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 EzPool
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,207 @@
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 EzPool::PoolShuttingDownError < RuntimeError; end
10
+
11
+
12
+ ##
13
+ # The TimedStack manages a pool of homogeneous connections (or any resource
14
+ # you wish to manage). Connections are created lazily up to a given maximum
15
+ # number.
16
+
17
+ # Examples:
18
+ #
19
+ # ts = TimedStack.new(1) { MyConnection.new }
20
+ #
21
+ # # fetch a connection
22
+ # conn = ts.pop
23
+ #
24
+ # # return a connection
25
+ # ts.push conn
26
+ #
27
+ # conn = ts.pop
28
+ # ts.pop timeout: 5
29
+ # #=> raises Timeout::Error after 5 seconds
30
+
31
+ class EzPool::TimedStack
32
+
33
+ ##
34
+ # Creates a new pool with +size+ connections that are created by
35
+ # constructing the given +connection_wrapper+ class
36
+
37
+ def initialize(connection_manager, size = 0)
38
+ @created = 0
39
+ @que = []
40
+ @max = size
41
+ @mutex = Mutex.new
42
+ @resource = ConditionVariable.new
43
+ @connection_manager = connection_manager
44
+ @shutting_down = false
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(wrapper, options = {})
52
+ @mutex.synchronize do
53
+ if @shutting_down
54
+ wrapper.shutdown!
55
+ else
56
+ store_connection wrapper, 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 = EzPool.monotonic_time + timeout
78
+ @mutex.synchronize do
79
+ loop do
80
+ raise EzPool::PoolShuttingDownError if @shutting_down
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 - EzPool.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
+ # Mark a connection as abandoned so that it cannot be used again.
95
+ # Will call the pre-configured shutdown proc, if provided.
96
+ #
97
+ def abandon(connection_wrapper)
98
+ @mutex.synchronize do
99
+ connection_wrapper.shutdown!
100
+ @created -= 1
101
+ end
102
+ end
103
+
104
+ ##
105
+ # Shuts down the TimedStack which prevents connections from being checked
106
+ # out. Calls the shutdown program specified in the EzPool
107
+ # initializer
108
+
109
+ def shutdown()
110
+ @mutex.synchronize do
111
+ @shutting_down = true
112
+ @resource.broadcast
113
+
114
+ shutdown_connections
115
+ end
116
+ end
117
+
118
+ ##
119
+ # Returns +true+ if there are no available connections.
120
+
121
+ def empty?
122
+ (@created - @que.length) >= @max
123
+ end
124
+
125
+ ##
126
+ # The number of connections available on the stack.
127
+
128
+ def length
129
+ @max - @created + @que.length
130
+ end
131
+
132
+ ##
133
+ # Pre-create all possible connections
134
+ def fill
135
+ while add_one
136
+ end
137
+ end
138
+
139
+ ##
140
+ # Add one connection to the queue
141
+ #
142
+ # Returns true iff a connection was successfully created
143
+ def add_one
144
+ connection = try_create
145
+ if connection.nil?
146
+ false
147
+ else
148
+ push(connection)
149
+ true
150
+ end
151
+ end
152
+
153
+ private
154
+
155
+ ##
156
+ # This is an extension point for TimedStack and is called with a mutex.
157
+ #
158
+ # This method must returns true if a connection is available on the stack.
159
+
160
+ def connection_stored?(options = nil)
161
+ !@que.empty?
162
+ end
163
+
164
+ ##
165
+ # This is an extension point for TimedStack and is called with a mutex.
166
+ #
167
+ # This method must return a connection from the stack.
168
+
169
+ def fetch_connection(options = nil)
170
+ @que.pop
171
+ end
172
+
173
+ ##
174
+ # This is an extension point for TimedStack and is called with a mutex.
175
+ #
176
+ # This method must shut down all connections on the stack.
177
+
178
+ def shutdown_connections(options = nil)
179
+ while connection_stored?(options)
180
+ conn = fetch_connection(options)
181
+ conn.shutdown!
182
+ end
183
+ end
184
+
185
+ ##
186
+ # This is an extension point for TimedStack and is called with a mutex.
187
+ #
188
+ # This method must return +obj+ to the stack.
189
+
190
+ def store_connection(obj, options = nil)
191
+ @que.push obj
192
+ end
193
+
194
+ ##
195
+ # This is an extension point for TimedStack and is called with a mutex.
196
+ #
197
+ # This method must create a connection if and only if the total number of
198
+ # connections allowed has not been met.
199
+
200
+ def try_create(options = nil)
201
+ unless @created == @max
202
+ object = @connection_manager.create_new()
203
+ @created += 1
204
+ object
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,3 @@
1
+ class EzPool
2
+ VERSION = "1.0.0"
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/ezpool'
@@ -0,0 +1,519 @@
1
+ require_relative 'helper'
2
+
3
+ class TestEzPool < 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 = EzPool.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 = EzPool.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 = EzPool.new(
109
+ timeout: 0,
110
+ size: 1,
111
+ connect_with: lambda { Object.new }
112
+ )
113
+
114
+ pool.with do
115
+ assert_raises Timeout::Error do
116
+ Thread.new { pool.checkout }.join
117
+ end
118
+ end
119
+
120
+ assert Thread.new { pool.checkout }.join
121
+ end
122
+
123
+ def test_with_timeout
124
+ pool = EzPool.new(
125
+ timeout: 0,
126
+ size: 1,
127
+ connect_with: lambda { Object.new }
128
+ )
129
+
130
+ assert_raises Timeout::Error do
131
+ Timeout.timeout(0.01) do
132
+ pool.with do |obj|
133
+ assert_equal 0, pool.instance_variable_get(:@available).instance_variable_get(:@que).size
134
+ sleep 0.015
135
+ end
136
+ end
137
+ end
138
+ assert_equal 1, pool.instance_variable_get(:@available).instance_variable_get(:@que).size
139
+ end
140
+
141
+ def test_checkout_ignores_timeout
142
+ skip("Thread.handle_interrupt not available") unless Thread.respond_to?(:handle_interrupt)
143
+
144
+ pool = EzPool.new(
145
+ timeout: 0,
146
+ size: 1,
147
+ connect_with: lambda { Object.new }
148
+ )
149
+
150
+ def pool.checkout(options)
151
+ sleep 0.015
152
+ super
153
+ end
154
+
155
+ did_something = false
156
+ assert_raises Timeout::Error do
157
+ Timeout.timeout(0.01) do
158
+ pool.with do |obj|
159
+ did_something = true
160
+ # Timeout::Error will be triggered by any non-trivial Ruby code
161
+ # executed here since it couldn't be raised during checkout.
162
+ # It looks like setting the local variable above does not trigger
163
+ # the Timeout check in MRI 2.2.1.
164
+ obj.tap { obj.hash }
165
+ end
166
+ end
167
+ end
168
+ assert did_something
169
+ assert_equal 1, pool.instance_variable_get(:@available).instance_variable_get(:@que).size
170
+ end
171
+
172
+ def test_explicit_return
173
+ pool = EzPool.new(timeout: 0, size: 1)
174
+ pool.connect_with do
175
+ mock = Minitest::Mock.new
176
+ def mock.disconnect!
177
+ raise "should not disconnect upon explicit return"
178
+ end
179
+ mock
180
+ end
181
+
182
+ pool.with do |conn|
183
+ return true
184
+ end
185
+ end
186
+
187
+ def test_with_timeout_override
188
+ pool = EzPool.new(timeout: 0, size: 1) { NetworkConnection.new }
189
+
190
+ t = Thread.new do
191
+ pool.with do |net|
192
+ net.do_something
193
+ sleep 0.01
194
+ end
195
+ end
196
+
197
+ Thread.pass while t.status == 'run'
198
+
199
+ assert_raises Timeout::Error do
200
+ pool.with { |net| net.do_something }
201
+ end
202
+
203
+ pool.with(timeout: 2 * NetworkConnection::SLEEP_TIME) do |conn|
204
+ refute_nil conn
205
+ end
206
+ end
207
+
208
+ def test_checkin
209
+ pool = EzPool.new(timeout: 0, size: 1) { NetworkConnection.new }
210
+ conn = pool.checkout
211
+
212
+ assert_raises Timeout::Error do
213
+ Thread.new { pool.checkout }.join
214
+ end
215
+
216
+ pool.checkin conn
217
+
218
+ assert_same conn, Thread.new { pool.checkout }.value
219
+ end
220
+
221
+ def test_returns_value
222
+ pool = EzPool.new(timeout: 0, size: 1) { Object.new }
223
+ assert_equal 1, pool.with {|o| 1 }
224
+ end
225
+
226
+ def test_checkin_garbage
227
+ pool = EzPool.new(timeout: 0, size: 1) { Object.new }
228
+
229
+ assert_raises EzPool::CheckedInUnCheckedOutConnectionError do
230
+ pool.checkin Object.new
231
+ end
232
+ end
233
+
234
+ def test_checkout
235
+ pool = EzPool.new(size: 2) { NetworkConnection.new }
236
+
237
+ conn = pool.checkout
238
+
239
+ assert_kind_of NetworkConnection, conn
240
+
241
+ refute_same conn, pool.checkout
242
+ end
243
+
244
+ def test_checkout_multithread
245
+ pool = EzPool.new(size: 2) { NetworkConnection.new }
246
+ conn = pool.checkout
247
+
248
+ t = Thread.new do
249
+ pool.checkout
250
+ end
251
+
252
+ refute_same conn, t.value
253
+ end
254
+
255
+ def test_checkout_timeout
256
+ pool = EzPool.new(timeout: 0, size: 0) { Object.new }
257
+
258
+ assert_raises Timeout::Error do
259
+ pool.checkout
260
+ end
261
+ end
262
+
263
+ def test_checkout_timeout_override
264
+ pool = EzPool.new(timeout: 0, size: 1) { NetworkConnection.new }
265
+
266
+ thread = Thread.new do
267
+ pool.with do |net|
268
+ net.do_something
269
+ sleep 0.01
270
+ end
271
+ end
272
+
273
+ Thread.pass while thread.status == 'run'
274
+
275
+ assert_raises Timeout::Error do
276
+ pool.checkout
277
+ end
278
+
279
+ assert pool.checkout timeout: 2 * NetworkConnection::SLEEP_TIME
280
+ end
281
+
282
+ def test_passthru
283
+ pool = EzPool.wrap(timeout: 2 * NetworkConnection::SLEEP_TIME, size: 1) { NetworkConnection.new }
284
+ assert_equal 1, pool.do_something
285
+ assert_equal 2, pool.do_something
286
+ assert_equal 5, pool.do_something_with_block { 3 }
287
+ assert_equal 6, pool.with { |net| net.fast }
288
+ end
289
+
290
+ def test_passthru_respond_to
291
+ pool = EzPool.wrap(timeout: 2 * NetworkConnection::SLEEP_TIME, size: 1) { NetworkConnection.new }
292
+ assert pool.respond_to?(:with)
293
+ assert pool.respond_to?(:do_something)
294
+ assert pool.respond_to?(:do_magic)
295
+ refute pool.respond_to?(:do_lots_of_magic)
296
+ end
297
+
298
+ def test_return_value
299
+ pool = EzPool.new(timeout: 2 * NetworkConnection::SLEEP_TIME, size: 1) { NetworkConnection.new }
300
+ result = pool.with do |net|
301
+ net.fast
302
+ end
303
+ assert_equal 1, result
304
+ end
305
+
306
+ def test_heavy_threading
307
+ pool = EzPool.new(timeout: 0.5, size: 3) { NetworkConnection.new }
308
+
309
+ threads = Array.new(20) do
310
+ Thread.new do
311
+ pool.with do |net|
312
+ sleep 0.01
313
+ end
314
+ end
315
+ end
316
+
317
+ threads.map { |thread| thread.join }
318
+ end
319
+
320
+ def test_reuses_objects_when_pool_not_saturated
321
+ pool = EzPool.new(size: 5) { NetworkConnection.new }
322
+
323
+ ids = 10.times.map do
324
+ pool.with { |c| c.object_id }
325
+ end
326
+
327
+ assert_equal 1, ids.uniq.size
328
+ end
329
+
330
+ def test_nested_checkout_fails
331
+ recorder = Recorder.new
332
+ pool = EzPool.new(size: 1) { recorder }
333
+ pool.with do |r_outer|
334
+ @other = Thread.new do |t|
335
+ pool.with do |r_other|
336
+ r_other.do_work('other')
337
+ end
338
+ end
339
+
340
+ Thread.pass
341
+
342
+ r_outer.do_work('outer')
343
+ end
344
+
345
+ @other.join
346
+
347
+ assert_equal ['outer', 'other'], recorder.calls
348
+ end
349
+
350
+ def test_shutdown_is_executed_for_all_connections
351
+ recorders = []
352
+
353
+ pool = EzPool.new(size: 3) do
354
+ Recorder.new.tap { |r| recorders << r }
355
+ end
356
+
357
+ threads = use_pool pool, 3
358
+
359
+ pool.disconnect_with do |recorder|
360
+ recorder.do_work("shutdown")
361
+ end
362
+
363
+ pool.shutdown
364
+
365
+ kill_threads(threads)
366
+
367
+ assert_equal [["shutdown"]] * 3, recorders.map { |r| r.calls }
368
+ end
369
+
370
+ def test_shutdown_works_as_argument_to_ezpool
371
+ recorders = []
372
+ pool = EzPool.new(
373
+ size: 3,
374
+ connect_with: lambda { Recorder.new.tap { |r| recorders << r } },
375
+ disconnect_with: lambda { |recorder| recorder.do_work("shutdown")}
376
+ )
377
+
378
+ threads = use_pool pool, 3
379
+
380
+ pool.shutdown
381
+
382
+ kill_threads(threads)
383
+
384
+ assert_equal [["shutdown"]] * 3, recorders.map { |r| r.calls }
385
+ end
386
+
387
+ def test_raises_error_after_shutting_down
388
+ pool = EzPool.new(size: 1) { true }
389
+
390
+ pool.shutdown
391
+
392
+ assert_raises EzPool::PoolShuttingDownError do
393
+ pool.checkout
394
+ end
395
+ end
396
+
397
+ def test_runs_shutdown_block_asynchronously_if_connection_was_in_use
398
+ recorders = []
399
+
400
+ pool = EzPool.new(
401
+ size: 3,
402
+ connect_with: lambda { Recorder.new.tap { |r| recorders << r } },
403
+ disconnect_with: lambda { |recorder| recorder.do_work("shutdown") }
404
+ )
405
+
406
+ threads = use_pool pool, 2
407
+
408
+ conn = pool.checkout
409
+
410
+ pool.shutdown
411
+
412
+ kill_threads(threads)
413
+
414
+ assert_equal [["shutdown"], ["shutdown"], []], recorders.map { |r| r.calls }
415
+
416
+ pool.checkin conn
417
+
418
+ assert_equal [["shutdown"], ["shutdown"], ["shutdown"]], recorders.map { |r| r.calls }
419
+ end
420
+
421
+ def test_max_age
422
+ recorders = []
423
+
424
+ pool = EzPool.new(
425
+ size: 3, max_age: 0.1,
426
+ connect_with: lambda { Recorder.new.tap { |r| recorders << r } },
427
+ disconnect_with: lambda { |conn| conn.do_work("shutdown") }
428
+ )
429
+
430
+ pool.with do |conn|
431
+ sleep(0.2)
432
+ end
433
+
434
+ pool.with do |conn|
435
+ sleep(0.2)
436
+ end
437
+
438
+ assert_equal [["shutdown"], ["shutdown"]], recorders.map { |r| r.calls }
439
+ end
440
+
441
+ def test_connect_with
442
+ conn_cls = Struct.new("Conn")
443
+
444
+ pool = EzPool.new(size: 1, connect_with: proc { conn_cls.new })
445
+
446
+ pool.with do |conn|
447
+ assert_instance_of(conn_cls, conn)
448
+ end
449
+ end
450
+
451
+ def test_shutdown_is_executed_for_all_connections_in_wrapped_pool
452
+ recorders = []
453
+
454
+ wrapper = EzPool::Wrapper.new(
455
+ size: 3,
456
+ connect_with: lambda { Recorder.new.tap { |r| recorders << r } },
457
+ disconnect_with: lambda { |recorder| recorder.do_work("shutdown") }
458
+ )
459
+
460
+ threads = use_pool wrapper, 3
461
+
462
+ wrapper.pool_shutdown
463
+
464
+ kill_threads(threads)
465
+
466
+ assert_equal [["shutdown"]] * 3, recorders.map { |r| r.calls }
467
+ end
468
+
469
+ def test_wrapper_method_missing
470
+ wrapper = EzPool::Wrapper.new { NetworkConnection.new }
471
+ assert_equal 1, wrapper.fast
472
+ end
473
+
474
+ def test_wrapper_respond_to_eh
475
+ wrapper = EzPool::Wrapper.new { NetworkConnection.new }
476
+
477
+ assert_respond_to wrapper, :with
478
+
479
+ assert_respond_to wrapper, :fast
480
+ refute_respond_to wrapper, :"nonexistent method"
481
+ end
482
+
483
+ def test_wrapper_with
484
+ wrapper = EzPool::Wrapper.new(timeout: 0, size: 1) { Object.new }
485
+
486
+ wrapper.with do
487
+ assert_raises Timeout::Error do
488
+ Thread.new do
489
+ wrapper.with { flunk 'connection checked out :(' }
490
+ end.join
491
+ end
492
+ end
493
+
494
+ assert Thread.new { wrapper.with { } }.join
495
+ end
496
+
497
+ class ConnWithEval
498
+ def eval(arg)
499
+ "eval'ed #{arg}"
500
+ end
501
+ end
502
+
503
+ def test_wrapper_kernel_methods
504
+ wrapper = EzPool::Wrapper.new(timeout: 0, size: 1) { ConnWithEval.new }
505
+
506
+ assert_equal "eval'ed 1", wrapper.eval(1)
507
+ end
508
+
509
+ def test_wrapper_with_ezpool
510
+ recorder = Recorder.new
511
+ pool = EzPool.new(size: 1) { recorder }
512
+ wrapper = EzPool::Wrapper.new(pool: pool)
513
+
514
+ pool.with { |r| r.do_work('with') }
515
+ wrapper.do_work('wrapped')
516
+
517
+ assert_equal ['with', 'wrapped'], recorder.calls
518
+ end
519
+ end