hot_tub 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 74ee741ed84ebe1bfdc105cea7cd6ea36d110069
4
- data.tar.gz: 213604cfc56cffe9fa593f4a4fe30bd139dafdcd
3
+ metadata.gz: b7eacafcd3cdc2be1e1f0b27ed6c6c4d351e8858
4
+ data.tar.gz: 6b0d4c6305a8a5779b5f736e9c1c489328b8f8dd
5
5
  SHA512:
6
- metadata.gz: 847dc6a4bd53ed60cddcd7103f341ec3b2f8e1c2fad9f380fd614ee7291cc850d0eb56fe44d8ad85e8f209f4566547fbd4b13225fadb9c8ba9f6159dbbd2056d
7
- data.tar.gz: a82fd572a976426711af212b2d5123432883abadea80f75940097b44c1cc7755cefca6659f151c37fb569f5ec1a04fccb51143fc137aa34490b85ea418665e00
6
+ metadata.gz: 339f265c1f55963cc9981ba58cd466ac6af16ccf15ef72af5e3c703f6a316cb405c3f8d63d91cf8633cdd92c2d5529e34e27f27c4e2def3b321a457bb75dbb42
7
+ data.tar.gz: eebf1efe482e2a84bdd6ddd1d6523d46657f43d684922b14e7850b0eb9d88d94733c09278bf7389f6267209cb354fc8b1be70d744546e49a0875b51e99bb693c
@@ -2,9 +2,10 @@ sudo: false
2
2
  cache: bundler
3
3
  language: ruby
4
4
  rvm:
5
+ - 2.3.0
5
6
  - 2.2.4
6
7
  - 2.1.8
7
8
  - 2.0.0-p648
8
9
  - ruby-head
9
10
  - rbx-2
10
- - jruby-head
11
+ - jruby
data/HISTORY.md CHANGED
@@ -5,6 +5,13 @@ Head
5
5
  =======
6
6
  - None yet.
7
7
 
8
+ 1.1.0
9
+ =======
10
+ - Close orphan clients outside of synchonize
11
+ - Freeze alarm message
12
+ - Detect dead resources and reap
13
+ - Detect fork
14
+
8
15
  1.0.0
9
16
  =======
10
17
  - Allow setting a default client for HotTub::Sessions
data/README.md CHANGED
@@ -140,35 +140,8 @@ a lambda that accepts the client as an argument or symbol representing a method
140
140
 
141
141
  ## Forking
142
142
 
143
- HotTub's `#reset!` methods close all idle connections, prevents connections in use from returning
144
- to the pool and attempts to close orphaned connections as they attempt to return.
143
+ HotTub::Pool automatically detects forks and drains the pool, so no additional "after fork" code is required.
145
144
 
146
- # Puma
147
- on_worker_boot do
148
-
149
- # If you let HotTub manage all your connections
150
- HotTub.reset!
151
-
152
- # If you have your own HotTub::Sessions
153
- MY_SESSIONS.reset!
154
-
155
- # If you have any one-off pools
156
- MY_POOL.reset!
157
-
158
- end
159
-
160
- # Unicorn
161
- before_fork do |server, worker|
162
-
163
- # If you let HotTub manage all your connections
164
- HotTub.reset!
165
-
166
- # If you have your own HotTub::Sessions
167
- MY_SESSIONS.reset!
168
-
169
- # If you have any one-off pools
170
- MY_POOL.reset!
171
- end
172
145
 
173
146
 
174
147
  ## Contributing to HotTub
@@ -14,7 +14,7 @@ Gem::Specification.new do |s|
14
14
 
15
15
  s.rubyforge_project = "hot_tub"
16
16
 
17
- s.add_development_dependency "rspec"
17
+ s.add_development_dependency "rspec", "~> 3.0"
18
18
  s.add_development_dependency "rspec-autotest"
19
19
  s.add_development_dependency "autotest"
20
20
  s.add_development_dependency "sinatra"
@@ -87,10 +87,13 @@ module HotTub
87
87
  # [:reap_timeout]
88
88
  # Default is 600 seconds. An integer that represents the timeout for reaping the pool in seconds.
89
89
  #
90
+ # [:detect_fork]
91
+ # Set to false to disable fork detection
92
+ #
90
93
  def initialize(opts={},&client_block)
91
94
  raise ArgumentError, 'a block that initializes a new client is required' unless block_given?
92
95
  @name = (opts[:name] || self.class.name)
93
- @size = (opts[:size] || 5) # in seconds
96
+ @size = (opts[:size] || 5)
94
97
  @wait_timeout = (opts[:wait_timeout] || 10) # in seconds
95
98
  @reap_timeout = (opts[:reap_timeout] || 600) # the interval to reap connections in seconds
96
99
  @max_size = (opts[:max_size] || 0) # maximum size of pool when non-blocking, 0 means no limit
@@ -102,7 +105,7 @@ module HotTub
102
105
 
103
106
  @_pool = [] # stores available clients
104
107
  @_pool.taint
105
- @_out = [] # stores all checked out clients
108
+ @_out = {} # stores all checked out clients
106
109
  @_out.taint
107
110
 
108
111
  @mutex = Mutex.new
@@ -116,12 +119,15 @@ module HotTub
116
119
 
117
120
  @never_block = (@max_size == 0)
118
121
 
122
+ @pid = Process.pid unless opts[:detect_fork] == false
123
+
119
124
  at_exit {shutdown!} unless @sessions_key
120
125
  end
121
126
 
122
127
  # Preform an operations with a client/connection.
123
128
  # Requires a block that receives the client.
124
129
  def run
130
+ drain! if forked?
125
131
  clnt = pop
126
132
  yield clnt
127
133
  ensure
@@ -133,10 +139,15 @@ module HotTub
133
139
  def clean!
134
140
  HotTub.logger.info "[HotTub] Cleaning pool #{@name}!" if HotTub.logger
135
141
  @mutex.synchronize do
136
- @_pool.each do |clnt|
137
- clean_client(clnt)
142
+ begin
143
+ @_pool.each do |clnt|
144
+ clean_client(clnt)
145
+ end
146
+ ensure
147
+ @cond.signal
138
148
  end
139
149
  end
150
+ nil
140
151
  end
141
152
 
142
153
  # Drain the pool of all clients currently checked into the pool.
@@ -153,47 +164,22 @@ module HotTub
153
164
  ensure
154
165
  @_out.clear
155
166
  @_pool.clear
156
- @cond.broadcast
157
- end
158
- end
159
- end
160
- alias :close! :drain!
161
-
162
- # Reset the pool.
163
- # or if shutdown allow threads to quickly finish their work
164
- # Clients from the previous pool will not return to pool.
165
- def reset!
166
- HotTub.logger.info "[HotTub] Resetting pool #{@name}!" if HotTub.logger
167
- @mutex.synchronize do
168
- begin
169
- while clnt = @_pool.pop
170
- close_client(clnt)
171
- end
172
- ensure
173
- @_out.clear
174
- @_pool.clear
167
+ @pid = Process.pid
175
168
  @cond.broadcast
176
169
  end
177
170
  end
178
171
  nil
179
172
  end
173
+ alias :close! :drain!
174
+ alias :reset! :drain!
180
175
 
181
176
  # Kills the reaper and drains the pool.
182
177
  def shutdown!
183
178
  HotTub.logger.info "[HotTub] Shutting down pool #{@name}!" if HotTub.logger
184
179
  @shutdown = true
185
180
  kill_reaper if @reaper
186
- @mutex.synchronize do
187
- begin
188
- while clnt = @_pool.pop
189
- close_client(clnt)
190
- end
191
- ensure
192
- @_out.clear
193
- @_pool.clear
194
- @cond.broadcast
195
- end
196
- end
181
+ drain!
182
+ @shutdown = false
197
183
  nil
198
184
  end
199
185
 
@@ -202,21 +188,33 @@ module HotTub
202
188
  # reaping is a low priority action
203
189
  def reap!
204
190
  HotTub.logger.info "[HotTub] Reaping pool #{@name}!" if HotTub.log_trace?
205
- reaped = nil
206
191
  while !@shutdown
192
+ reaped = nil
207
193
  @mutex.synchronize do
208
- if _reap?
209
- reaped = @_pool.shift
210
- else
211
- reaped = nil
194
+ begin
195
+ if _reap?
196
+ if _dead_clients?
197
+ reaped = @_out.select { |clnt, thrd| !thrd.alive? }.keys
198
+ @_out.delete_if { |k,v| reaped.include? k }
199
+ else
200
+ reaped = [@_pool.shift]
201
+ end
202
+ else
203
+ reaped = nil
204
+ end
205
+ ensure
206
+ @cond.signal
212
207
  end
213
208
  end
214
209
  if reaped
215
- close_client(reaped)
210
+ reaped.each do |clnt|
211
+ close_client(clnt)
212
+ end
216
213
  else
217
214
  break
218
215
  end
219
216
  end
217
+ nil
220
218
  end
221
219
 
222
220
  def current_size
@@ -232,9 +230,13 @@ module HotTub
232
230
  @max_size = max_size
233
231
  end
234
232
 
233
+ def forked?
234
+ (@pid && (@pid != Process.pid))
235
+ end
236
+
235
237
  private
236
238
 
237
- ALARM_MESSAGE = "Could not fetch a free client in time. Consider increasing your pool size."
239
+ ALARM_MESSAGE = "Could not fetch a free client in time. Consider increasing your pool size.".freeze
238
240
 
239
241
  def raise_alarm
240
242
  message = ALARM_MESSAGE
@@ -242,28 +244,36 @@ module HotTub
242
244
  raise Timeout, message
243
245
  end
244
246
 
247
+ def close_orphan(clnt)
248
+ HotTub.logger.info "[HotTub] An orphaned client attempted to return to #{@name}." if HotTub.log_trace?
249
+ close_client(clnt)
250
+ end
251
+
245
252
  # Safely add client back to pool, only if
246
253
  # that client is registered
247
254
  def push(clnt)
248
255
  if clnt
256
+ orphaned = false
249
257
  @mutex.synchronize do
250
258
  begin
251
259
  if !@shutdown && @_out.delete(clnt)
252
260
  @_pool << clnt
253
261
  else
254
- close_client(clnt)
255
- HotTub.logger.info "[HotTub] An orphaned client attempted to return to #{@name}." if HotTub.log_trace?
262
+ orphaned = true
256
263
  end
257
264
  ensure
258
265
  @cond.signal
259
266
  end
260
267
  end
268
+ close_orphan(clnt) if orphaned
261
269
  reap! if @blocking_reap
262
270
  end
263
271
  nil
264
272
  end
265
273
 
266
274
  # Safely pull client from pool, adding if allowed
275
+ # If a client is not available, check for dead
276
+ # resources and schedule reap if nesseccary
267
277
  def pop
268
278
  alarm = (Time.now + @wait_timeout)
269
279
  clnt = nil
@@ -271,13 +281,20 @@ module HotTub
271
281
  while !@shutdown
272
282
  raise_alarm if (Time.now > alarm)
273
283
  @mutex.synchronize do
274
- if clnt = @_pool.pop
275
- dirty = true
276
- @_out << clnt
277
- elsif clnt = _fetch_new(&@client_block)
278
- @_out << clnt
279
- else
280
- @cond.wait(@mutex,@wait_timeout)
284
+ begin
285
+ if clnt = @_pool.pop
286
+ dirty = true
287
+ else
288
+ clnt = _fetch_new(&@client_block)
289
+ end
290
+ ensure
291
+ if clnt
292
+ _checkout(clnt)
293
+ @cond.signal
294
+ else
295
+ @reaper.wakeup if @reaper && _dead_clients?
296
+ @cond.wait(@mutex,@wait_timeout)
297
+ end
281
298
  end
282
299
  end
283
300
  break if clnt
@@ -288,6 +305,10 @@ module HotTub
288
305
 
289
306
  ### START VOLATILE METHODS ###
290
307
 
308
+ def _checkout(clnt)
309
+ @_out[clnt] = Thread.current
310
+ end
311
+
291
312
  # Returns the total number of clients in the pool
292
313
  # and checked out. _total_current_size is volatile and
293
314
  # may be inaccurate if called outside @mutex.synchronize {}
@@ -316,7 +337,14 @@ module HotTub
316
337
  # volatile; and may be inaccurate if called outside
317
338
  # @mutex.synchronize {}
318
339
  def _reap?
319
- (!@shutdown && ((@_pool.length > @size) || reap_client?(@_pool[0])))
340
+ (!@shutdown && ((@_pool.length > @size) || _dead_clients? || reap_client?(@_pool[0])))
341
+ end
342
+
343
+ # Returns true if we have checked out clients whose resource is dead.
344
+ # _dead_clients? is volatile; and may be inaccurate if called outside
345
+ # @mutex.synchronize {}
346
+ def _dead_clients?
347
+ @_out.detect { |clnt, thrd| !thrd.alive? }
320
348
  end
321
349
 
322
350
  ### END VOLATILE METHODS ###
@@ -1,3 +1,3 @@
1
1
  module HotTub
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
@@ -1,104 +1,107 @@
1
1
  require 'spec_helper'
2
2
 
3
+
3
4
  describe HotTub do
4
- context "blocking (size equals max_size)" do
5
- let(:pool) do
6
- HotTub::Pool.new(:size => 4, :max_size => 4) {
7
- uri = URI.parse(HotTub::Server.url)
8
- http = Net::HTTP.new(uri.host, uri.port)
9
- http.use_ssl = false
10
- http.start
11
- http
12
- }
13
- end
5
+ unless HotTub.jruby?
6
+ context "blocking (size equals max_size)" do
7
+ let(:pool) do
8
+ HotTub::Pool.new(:size => 4, :max_size => 4) {
9
+ uri = URI.parse(HotTub::Server.url)
10
+ http = Net::HTTP.new(uri.host, uri.port)
11
+ http.use_ssl = false
12
+ http.start
13
+ http
14
+ }
15
+ end
14
16
 
15
- let(:threads) { [] }
17
+ let(:threads) { [] }
16
18
 
17
- before(:each) do
18
- 20.times do
19
- net_http_thread_work(pool, 10, threads)
19
+ before(:each) do
20
+ 20.times do
21
+ net_http_thread_work(pool, 10, threads)
22
+ end
20
23
  end
21
- end
22
24
 
23
- it { expect(pool.current_size).to eql(4) }
25
+ it { expect(pool.current_size).to eql(4) }
24
26
 
25
- it "should work" do
26
- results = threads.collect{ |t| t[:status]}
27
- expect(results.length).to eql(200)
28
- expect(results.uniq).to eql(['200'])
29
- end
27
+ it "should work" do
28
+ results = threads.collect{ |t| t[:status]}
29
+ expect(results.length).to eql(200)
30
+ expect(results.uniq).to eql(['200'])
31
+ end
30
32
 
31
- it "should shutdown" do
32
- pool.shutdown!
33
- expect(pool.current_size).to eql(0)
33
+ it "should shutdown" do
34
+ pool.shutdown!
35
+ expect(pool.current_size).to eql(0)
36
+ end
34
37
  end
35
- end
36
38
 
37
- context "with larger max" do
38
- let(:pool) do
39
- HotTub::Pool.new(:size => 4, :max_size => 8) {
40
- uri = URI.parse(HotTub::Server.url)
41
- http = Net::HTTP.new(uri.host, uri.port)
42
- http.use_ssl = false
43
- http.start
44
- http
45
- }
46
- end
39
+ context "with larger max" do
40
+ let(:pool) do
41
+ HotTub::Pool.new(:size => 4, :max_size => 8) {
42
+ uri = URI.parse(HotTub::Server.url)
43
+ http = Net::HTTP.new(uri.host, uri.port)
44
+ http.use_ssl = false
45
+ http.start
46
+ http
47
+ }
48
+ end
47
49
 
48
- let(:threads) { [] }
50
+ let(:threads) { [] }
49
51
 
50
- before(:each) do
51
- 20.times do
52
- net_http_thread_work(pool, 10, threads)
52
+ before(:each) do
53
+ 20.times do
54
+ net_http_thread_work(pool, 10, threads)
55
+ end
53
56
  end
54
- end
55
57
 
56
- it { expect(pool.current_size).to be >= 4 }
57
- it { expect(pool.current_size).to be <= 8 }
58
- it "should work" do
59
- results = threads.collect{ |t| t[:status]}
60
- expect(results.length).to eql(200)
61
- expect(results.uniq).to eql(['200'])
58
+ it { expect(pool.current_size).to be >= 4 }
59
+ it { expect(pool.current_size).to be <= 8 }
60
+ it "should work" do
61
+ results = threads.collect{ |t| t[:status]}
62
+ expect(results.length).to eql(200)
63
+ expect(results.uniq).to eql(['200'])
64
+ end
62
65
  end
63
- end
64
66
 
65
- context "sized without max" do
66
- let(:pool) do
67
- HotTub::Pool.new(:size => 4) {
68
- uri = URI.parse(HotTub::Server.url)
69
- http = Net::HTTP.new(uri.host, uri.port)
70
- http.use_ssl = false
71
- http.start
72
- http
73
- }
74
- end
67
+ context "sized without max" do
68
+ let(:pool) do
69
+ HotTub::Pool.new(:size => 4) {
70
+ uri = URI.parse(HotTub::Server.url)
71
+ http = Net::HTTP.new(uri.host, uri.port)
72
+ http.use_ssl = false
73
+ http.start
74
+ http
75
+ }
76
+ end
75
77
 
76
- let(:threads) { [] }
78
+ let(:threads) { [] }
77
79
 
78
- before(:each) do
79
- 20.times do
80
- net_http_thread_work(pool, 10, threads)
80
+ before(:each) do
81
+ 20.times do
82
+ net_http_thread_work(pool, 10, threads)
83
+ end
81
84
  end
82
- end
83
85
 
84
- it { expect(pool.current_size).to be > 4 }
86
+ it { expect(pool.current_size).to be > 4 }
85
87
 
86
- it "should work" do
87
- results = threads.collect{ |t| t[:status]}
88
- expect(results.length).to eql(200)
89
- expect(results.uniq).to eql(['200'])
88
+ it "should work" do
89
+ results = threads.collect{ |t| t[:status]}
90
+ expect(results.length).to eql(200)
91
+ expect(results.uniq).to eql(['200'])
92
+ end
90
93
  end
91
94
  end
92
- end
93
95
 
94
- def net_http_thread_work(pool,thread_count=0, threads=[])
95
- thread_count.times.each do
96
- threads << Thread.new do
97
- uri = URI.parse(HotTub::Server.url)
98
- pool.run{|connection| Thread.current[:status] = connection.get(uri.path).code }
96
+ def net_http_thread_work(pool,thread_count=0, threads=[])
97
+ thread_count.times.each do
98
+ threads << Thread.new do
99
+ uri = URI.parse(HotTub::Server.url)
100
+ pool.run{|connection| Thread.current[:status] = connection.get(uri.path).code }
101
+ end
102
+ end
103
+ threads.each do |t|
104
+ t.join
99
105
  end
100
- end
101
- threads.each do |t|
102
- t.join
103
106
  end
104
107
  end
@@ -169,10 +169,22 @@ describe HotTub::Pool do
169
169
  expect(pool.current_size).to eql(1)
170
170
  expect(old_client).to be_closed
171
171
  end
172
+
173
+ context "when client is lost with dead thread" do
174
+ it "should close dead client" do
175
+ pool = HotTub::Pool.new({ :size => 1, :close => :close }) { MocClient.new }
176
+ thread = Thread.new {}
177
+ thread.join
178
+ client = MocClient.new
179
+ pool.instance_variable_set(:@_out, {client => thread})
180
+ pool.reap!
181
+ expect(client).to be_closed
182
+ end
183
+ end
172
184
  end
173
185
 
174
186
  context 'private methods' do
175
- let(:pool) { HotTub::Pool.new(:close => :close) { MocClient.new} }
187
+ let(:pool) { HotTub::Pool.new( :close => :close) { MocClient.new} }
176
188
 
177
189
  describe '#pop' do
178
190
  context "is allowed" do
@@ -180,6 +192,16 @@ describe HotTub::Pool do
180
192
  expect(pool.send(:pop)).to be_a(MocClient)
181
193
  end
182
194
  end
195
+ context "has dead client" do
196
+ it "should return new client" do
197
+ pool.max_size = 1
198
+ thread = Thread.new {}
199
+ thread.join
200
+ client = MocClient.new
201
+ pool.instance_variable_set(:@_out, {client => thread})
202
+ expect(pool.send(:pop)).to be_a(MocClient)
203
+ end
204
+ end
183
205
  end
184
206
 
185
207
  describe '#push' do
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hot_tub
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Mckinney
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-12-24 00:00:00.000000000 Z
11
+ date: 2016-05-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: '3.0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: '3.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rspec-autotest
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -152,7 +152,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
152
152
  version: '0'
153
153
  requirements: []
154
154
  rubyforge_project: hot_tub
155
- rubygems_version: 2.4.8
155
+ rubygems_version: 2.5.1
156
156
  signing_key:
157
157
  specification_version: 4
158
158
  summary: Flexible connection pooling for Ruby.