healthy_pools 2.2.3
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 +14 -0
- data/Changes.md +127 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.md +114 -0
- data/Rakefile +6 -0
- data/healthy_pools.gemspec +21 -0
- data/lib/connection_pool/monotonic_time.rb +66 -0
- data/lib/connection_pool/timed_stack.rb +201 -0
- data/lib/connection_pool/version.rb +3 -0
- data/lib/connection_pool.rb +162 -0
- data/test/helper.rb +8 -0
- data/test/test_connection_pool.rb +546 -0
- data/test/test_connection_pool_timed_stack.rb +149 -0
- metadata +101 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
require_relative 'connection_pool/version'
|
|
2
|
+
require_relative '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, health_check: nil}
|
|
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
|
+
@health_check = options[:health_check]
|
|
52
|
+
|
|
53
|
+
@available = TimedStack.new(@size, health_check: @health_check, &block)
|
|
54
|
+
@key = :"current-#{@available.object_id}"
|
|
55
|
+
@key_count = :"current-#{@available.object_id}-count"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
if Thread.respond_to?(:handle_interrupt)
|
|
59
|
+
|
|
60
|
+
# MRI
|
|
61
|
+
def with(options = {})
|
|
62
|
+
Thread.handle_interrupt(Exception => :never) do
|
|
63
|
+
conn = checkout(options)
|
|
64
|
+
begin
|
|
65
|
+
Thread.handle_interrupt(Exception => :immediate) do
|
|
66
|
+
yield conn
|
|
67
|
+
end
|
|
68
|
+
ensure
|
|
69
|
+
checkin
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
else
|
|
75
|
+
|
|
76
|
+
# jruby 1.7.x
|
|
77
|
+
def with(options = {})
|
|
78
|
+
conn = checkout(options)
|
|
79
|
+
begin
|
|
80
|
+
yield conn
|
|
81
|
+
ensure
|
|
82
|
+
checkin
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def checkout(options = {})
|
|
89
|
+
if ::Thread.current[@key]
|
|
90
|
+
::Thread.current[@key_count]+= 1
|
|
91
|
+
::Thread.current[@key]
|
|
92
|
+
else
|
|
93
|
+
::Thread.current[@key_count]= 1
|
|
94
|
+
::Thread.current[@key]= @available.pop(options[:timeout] || @timeout)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def checkin
|
|
99
|
+
if ::Thread.current[@key]
|
|
100
|
+
if ::Thread.current[@key_count] == 1
|
|
101
|
+
@available.push(::Thread.current[@key])
|
|
102
|
+
::Thread.current[@key]= nil
|
|
103
|
+
else
|
|
104
|
+
::Thread.current[@key_count]-= 1
|
|
105
|
+
end
|
|
106
|
+
else
|
|
107
|
+
raise ConnectionPool::Error, 'no connections are checked out'
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def shutdown(&block)
|
|
114
|
+
@available.shutdown(&block)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Size of this connection pool
|
|
118
|
+
def size
|
|
119
|
+
@size
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Number of pool entries available for checkout at this instant.
|
|
123
|
+
def available
|
|
124
|
+
@available.length
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
class Wrapper < ::BasicObject
|
|
130
|
+
METHODS = [:with, :pool_shutdown]
|
|
131
|
+
|
|
132
|
+
def initialize(options = {}, &block)
|
|
133
|
+
@pool = options.fetch(:pool) { ::ConnectionPool.new(options, &block) }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def with(&block)
|
|
137
|
+
@pool.with(&block)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def pool_shutdown(&block)
|
|
141
|
+
@pool.shutdown(&block)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def pool_size
|
|
145
|
+
@pool.size
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def pool_available
|
|
149
|
+
@pool.available
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def respond_to?(id, *args)
|
|
153
|
+
METHODS.include?(id) || with { |c| c.respond_to?(id, *args) }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def method_missing(name, *args, &block)
|
|
157
|
+
with do |connection|
|
|
158
|
+
connection.send(name, *args, &block)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
data/test/helper.rb
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
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_failed_health_check_removes_failed_connections
|
|
349
|
+
pool = ConnectionPool.new(size: 5, health_check: lambda {|x| false}) { NetworkConnection.new }
|
|
350
|
+
|
|
351
|
+
ids = 10.times.map do
|
|
352
|
+
pool.with { |c| c.object_id }
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
assert_equal 10, ids.uniq.size
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def test_exceptions_during_health_check_removes_connections
|
|
359
|
+
pool = ConnectionPool.new(size: 5, health_check: lambda {|x| raise "failed"}) { NetworkConnection.new }
|
|
360
|
+
|
|
361
|
+
ids = 10.times.map do
|
|
362
|
+
pool.with { |c| c.object_id }
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
assert_equal 10, ids.uniq.size
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def test_failed_health_check_does_not_remove_good_connections
|
|
369
|
+
pool = ConnectionPool.new(size: 5, health_check: lambda {|x| true}) { NetworkConnection.new }
|
|
370
|
+
|
|
371
|
+
ids = 10.times.map do
|
|
372
|
+
pool.with { |c| c.object_id }
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
assert_equal 1, ids.uniq.size
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def test_nested_checkout
|
|
379
|
+
recorder = Recorder.new
|
|
380
|
+
pool = ConnectionPool.new(size: 1) { recorder }
|
|
381
|
+
pool.with do |r_outer|
|
|
382
|
+
@other = Thread.new do |t|
|
|
383
|
+
pool.with do |r_other|
|
|
384
|
+
r_other.do_work('other')
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
pool.with do |r_inner|
|
|
389
|
+
r_inner.do_work('inner')
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
Thread.pass
|
|
393
|
+
|
|
394
|
+
r_outer.do_work('outer')
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
@other.join
|
|
398
|
+
|
|
399
|
+
assert_equal ['inner', 'outer', 'other'], recorder.calls
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def test_shutdown_is_executed_for_all_connections
|
|
403
|
+
recorders = []
|
|
404
|
+
|
|
405
|
+
pool = ConnectionPool.new(size: 3) do
|
|
406
|
+
Recorder.new.tap { |r| recorders << r }
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
threads = use_pool pool, 3
|
|
410
|
+
|
|
411
|
+
pool.shutdown do |recorder|
|
|
412
|
+
recorder.do_work("shutdown")
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
kill_threads(threads)
|
|
416
|
+
|
|
417
|
+
assert_equal [["shutdown"]] * 3, recorders.map { |r| r.calls }
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def test_raises_error_after_shutting_down
|
|
421
|
+
pool = ConnectionPool.new(size: 1) { true }
|
|
422
|
+
|
|
423
|
+
pool.shutdown { }
|
|
424
|
+
|
|
425
|
+
assert_raises ConnectionPool::PoolShuttingDownError do
|
|
426
|
+
pool.checkout
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def test_runs_shutdown_block_asynchronously_if_connection_was_in_use
|
|
431
|
+
recorders = []
|
|
432
|
+
|
|
433
|
+
pool = ConnectionPool.new(size: 3) do
|
|
434
|
+
Recorder.new.tap { |r| recorders << r }
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
threads = use_pool pool, 2
|
|
438
|
+
|
|
439
|
+
pool.checkout
|
|
440
|
+
|
|
441
|
+
pool.shutdown do |recorder|
|
|
442
|
+
recorder.do_work("shutdown")
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
kill_threads(threads)
|
|
446
|
+
|
|
447
|
+
assert_equal [["shutdown"], ["shutdown"], []], recorders.map { |r| r.calls }
|
|
448
|
+
|
|
449
|
+
pool.checkin
|
|
450
|
+
|
|
451
|
+
assert_equal [["shutdown"], ["shutdown"], ["shutdown"]], recorders.map { |r| r.calls }
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def test_raises_an_error_if_shutdown_is_called_without_a_block
|
|
455
|
+
pool = ConnectionPool.new(size: 1) { }
|
|
456
|
+
|
|
457
|
+
assert_raises ArgumentError do
|
|
458
|
+
pool.shutdown
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def test_shutdown_is_executed_for_all_connections_in_wrapped_pool
|
|
463
|
+
recorders = []
|
|
464
|
+
|
|
465
|
+
wrapper = ConnectionPool::Wrapper.new(size: 3) do
|
|
466
|
+
Recorder.new.tap { |r| recorders << r }
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
threads = use_pool wrapper, 3
|
|
470
|
+
|
|
471
|
+
wrapper.pool_shutdown do |recorder|
|
|
472
|
+
recorder.do_work("shutdown")
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
kill_threads(threads)
|
|
476
|
+
|
|
477
|
+
assert_equal [["shutdown"]] * 3, recorders.map { |r| r.calls }
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def test_wrapper_method_missing
|
|
481
|
+
wrapper = ConnectionPool::Wrapper.new { NetworkConnection.new }
|
|
482
|
+
|
|
483
|
+
assert_equal 1, wrapper.fast
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def test_wrapper_respond_to_eh
|
|
487
|
+
wrapper = ConnectionPool::Wrapper.new { NetworkConnection.new }
|
|
488
|
+
|
|
489
|
+
assert_respond_to wrapper, :with
|
|
490
|
+
|
|
491
|
+
assert_respond_to wrapper, :fast
|
|
492
|
+
refute_respond_to wrapper, :"nonexistent method"
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def test_wrapper_with
|
|
496
|
+
wrapper = ConnectionPool::Wrapper.new(timeout: 0, size: 1) { Object.new }
|
|
497
|
+
|
|
498
|
+
wrapper.with do
|
|
499
|
+
assert_raises Timeout::Error do
|
|
500
|
+
Thread.new do
|
|
501
|
+
wrapper.with { flunk 'connection checked out :(' }
|
|
502
|
+
end.join
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
assert Thread.new { wrapper.with { } }.join
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
class ConnWithEval
|
|
510
|
+
def eval(arg)
|
|
511
|
+
"eval'ed #{arg}"
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def test_wrapper_kernel_methods
|
|
516
|
+
wrapper = ConnectionPool::Wrapper.new(timeout: 0, size: 1) { ConnWithEval.new }
|
|
517
|
+
|
|
518
|
+
assert_equal "eval'ed 1", wrapper.eval(1)
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def test_wrapper_with_connection_pool
|
|
522
|
+
recorder = Recorder.new
|
|
523
|
+
pool = ConnectionPool.new(size: 1) { recorder }
|
|
524
|
+
wrapper = ConnectionPool::Wrapper.new(pool: pool)
|
|
525
|
+
|
|
526
|
+
pool.with { |r| r.do_work('with') }
|
|
527
|
+
wrapper.do_work('wrapped')
|
|
528
|
+
|
|
529
|
+
assert_equal ['with', 'wrapped'], recorder.calls
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def test_stats_without_active_connection
|
|
533
|
+
pool = ConnectionPool.new(size: 2) { NetworkConnection.new }
|
|
534
|
+
|
|
535
|
+
assert_equal(2, pool.size)
|
|
536
|
+
assert_equal(2, pool.available)
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def test_stats_with_active_connection
|
|
540
|
+
pool = ConnectionPool.new(size: 2) { NetworkConnection.new }
|
|
541
|
+
|
|
542
|
+
pool.with do
|
|
543
|
+
assert_equal(1, pool.available)
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
end
|