ezpool 1.0.0

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,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