hot_tub 1.0.0 → 1.1.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 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.