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.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.travis.yml +11 -0
- data/Changes.md +131 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.md +165 -0
- data/Rakefile +6 -0
- data/ezpool.gemspec +21 -0
- data/lib/ezpool.rb +190 -0
- data/lib/ezpool/connection_manager.rb +37 -0
- data/lib/ezpool/connection_wrapper.rb +20 -0
- data/lib/ezpool/errors.rb +8 -0
- data/lib/ezpool/monotonic_time.rb +66 -0
- data/lib/ezpool/timed_stack.rb +207 -0
- data/lib/ezpool/version.rb +3 -0
- data/test/helper.rb +8 -0
- data/test/test_ezpool.rb +519 -0
- data/test/test_ezpool_timed_stack.rb +159 -0
- metadata +109 -0
@@ -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,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
|
data/test/helper.rb
ADDED
data/test/test_ezpool.rb
ADDED
@@ -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
|