hot_tub 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/hot_tub/pool.rb CHANGED
@@ -2,6 +2,8 @@ module HotTub
2
2
  class Pool
3
3
  include HotTub::KnownClients
4
4
  include HotTub::Reaper::Mixin
5
+
6
+ attr_accessor :name
5
7
  attr_reader :current_size, :last_activity
6
8
 
7
9
  # Thread-safe lazy connection pool
@@ -38,48 +40,58 @@ module HotTub
38
40
  # http.start
39
41
  # http
40
42
  # }
41
- # pool.run { |clnt| puts clnt.head('/').code }
43
+ # pool.run { |clnt| s clnt.head('/').code }
42
44
  #
43
45
  # begin
44
- # pool.run { |clnt| puts clnt.head('/').code }
46
+ # pool.run { |clnt| s clnt.head('/').code }
45
47
  # rescue HotTub::Pool::Timeout => e
46
48
  # puts "Waited too long for a client: {e}"
47
49
  # end
48
50
  #
49
51
  #
50
52
  # === OPTIONS
53
+ # [:name]
54
+ # A string representing the name of your pool used for logging.
51
55
  #
52
56
  # [:size]
53
57
  # Default is 5. An integer that sets the size of the pool. Could be describe as minimum size the pool should
54
58
  # grow to.
59
+ #
55
60
  # [:max_size]
56
61
  # Default is 0. An integer that represents the maximum number of connections allowed when :non_blocking is true.
57
62
  # If set to 0, which is the default, there is no limit; connections will continue to open until load subsides
58
63
  # long enough for reaping to occur.
64
+ #
59
65
  # [:wait_timeout]
60
66
  # Default is 10 seconds. An integer that represents the timeout when waiting for a client from the pool
61
67
  # in seconds. After said time a HotTub::Pool::Timeout exception will be thrown
62
- # [:reap_timeout]
63
- # Default is 600 seconds. An integer that represents the timeout for reaping the pool in seconds.
68
+ #
64
69
  # [:close]
65
70
  # Default is nil. Can be a symbol representing an method to call on a client to close the client or a lambda
66
71
  # that accepts the client as a parameter that will close a client. The close option is performed on clients
67
72
  # on reaping and shutdown after the client has been removed from the pool. When nil, as is the default, no
68
73
  # action is performed.
74
+ #
69
75
  # [:clean]
70
76
  # Default is nil. Can be a symbol representing an method to call on a client to clean the client or a lambda
71
77
  # that accepts the client as a parameter that will clean a client. When nil, as is the default, no action is
72
78
  # performed.
73
- # [:reap]
79
+ #
80
+ # [:reap?]
74
81
  # Default is nil. Can be a symbol representing an method to call on a client that returns a boolean marking
75
82
  # a client for reaping, or a lambda that accepts the client as a parameter that returns a boolean boolean
76
83
  # marking a client for reaping. When nil, as is the default, no action is performed.
77
- # [:no_reaper]
78
- # Default is nil. A boolean like value that if true prevents the reaper from initializing
79
84
  #
80
- def initialize(opts={},&new_client)
85
+ # [:reaper]
86
+ # If set to false prevents, a HotTub::Reaper from initializing and all reaping will occur when the clients
87
+ # are returned to the pool, blocking the current thread.
88
+ #
89
+ # [:reap_timeout]
90
+ # Default is 600 seconds. An integer that represents the timeout for reaping the pool in seconds.
91
+ #
92
+ def initialize(opts={},&client_block)
81
93
  raise ArgumentError, 'a block that initializes a new client is required' unless block_given?
82
-
94
+ @name = (opts[:name] || self.class.name)
83
95
  @size = (opts[:size] || 5) # in seconds
84
96
  @wait_timeout = (opts[:wait_timeout] || 10) # in seconds
85
97
  @reap_timeout = (opts[:reap_timeout] || 600) # the interval to reap connections in seconds
@@ -87,38 +99,39 @@ module HotTub
87
99
 
88
100
  @close_client = opts[:close] # => lambda {|clnt| clnt.close} or :close
89
101
  @clean_client = opts[:clean] # => lambda {|clnt| clnt.clean} or :clean
90
- @reap_client = opts[:reap] # => lambda {|clnt| clnt.reap?} or :reap? # should return boolean
91
- @new_client = new_client
102
+ @reap_client = opts[:reap?] # => lambda {|clnt| clnt.reap?} or :reap? # should return boolean
103
+ @client_block = client_block
92
104
 
93
- @_pool = [] # stores available clients
105
+ @_pool = [] # stores available clients
94
106
  @_pool.taint
95
- @_out = [] # stores all checked out clients
107
+ @_out = [] # stores all checked out clients
96
108
  @_out.taint
97
109
 
98
110
  @mutex = Mutex.new
99
111
  @cond = ConditionVariable.new
100
112
 
101
- @shutdown = false # Kills reaper when true
102
- @reaper = Reaper.spawn(self) unless opts[:no_reaper]
113
+ @shutdown = false
114
+ @blocking_reap = (opts[:reaper] == false && !opts[:sessions])
115
+ @reaper = Reaper.spawn(self) unless (opts[:sessions] || (opts[:reaper] == false))
116
+
117
+ @never_block = (@max_size == 0)
103
118
 
104
- at_exit {shutdown!}
119
+ at_exit {shutdown!} unless opts[:sessions]
105
120
  end
106
121
 
107
- # Hand off to client.run
122
+ # Preform an operations with a client/connection.
123
+ # Requires a block that receives the client.
108
124
  def run
109
- if block_given?
110
- clnt = client
111
- return yield clnt if clnt
112
- else
113
- raise ArgumentError, 'Run requires a block.'
114
- end
125
+ clnt = pop
126
+ yield clnt
115
127
  ensure
116
- push(clnt) if clnt
128
+ push(clnt)
117
129
  end
118
130
 
119
131
  # Clean all clients currently checked into the pool.
120
132
  # Its possible clients may be returned to the pool after cleaning
121
133
  def clean!
134
+ HotTub.logger.info "[HotTub] Cleaning pool #{@name}!" if HotTub.logger
122
135
  @mutex.synchronize do
123
136
  @_pool.each do |clnt|
124
137
  clean_client(clnt)
@@ -131,12 +144,15 @@ module HotTub
131
144
  # or if shutdown allow threads to quickly finish their work
132
145
  # Its possible clients may be returned to the pool after cleaning
133
146
  def drain!
147
+ HotTub.logger.info "[HotTub] Draining pool #{@name}!" if HotTub.logger
134
148
  @mutex.synchronize do
135
149
  begin
136
- while clnt = (@_pool.pop || @_out.pop)
150
+ while clnt = @_pool.pop
137
151
  close_client(clnt)
138
152
  end
139
153
  ensure
154
+ @_out.clear
155
+ @_pool.clear
140
156
  @cond.broadcast
141
157
  end
142
158
  end
@@ -147,9 +163,10 @@ module HotTub
147
163
  # or if shutdown allow threads to quickly finish their work
148
164
  # Clients from the previous pool will not return to pool.
149
165
  def reset!
166
+ HotTub.logger.info "[HotTub] Resetting pool #{@name}!" if HotTub.logger
150
167
  @mutex.synchronize do
151
168
  begin
152
- while clnt = (@_pool.pop || @_out.pop)
169
+ while clnt = @_pool.pop
153
170
  close_client(clnt)
154
171
  end
155
172
  if @reaper
@@ -157,6 +174,8 @@ module HotTub
157
174
  @reaper = Reaper.spawn(self)
158
175
  end
159
176
  ensure
177
+ @_out.clear
178
+ @_pool.clear
160
179
  @cond.broadcast
161
180
  end
162
181
  end
@@ -165,21 +184,36 @@ module HotTub
165
184
 
166
185
  # Kills the reaper and drains the pool.
167
186
  def shutdown!
187
+ HotTub.logger.info "[HotTub] Shutting down pool #{@name}!" if HotTub.logger
168
188
  @shutdown = true
169
- kill_reaper if @reaper
170
- ensure
171
- drain!
189
+ @mutex.synchronize do
190
+ begin
191
+ while clnt = @_pool.pop
192
+ close_client(clnt)
193
+ end
194
+ kill_reaper if @reaper
195
+ ensure
196
+ @_out.clear
197
+ @_pool.clear
198
+ @cond.broadcast
199
+ end
200
+ end
201
+ nil
172
202
  end
173
203
 
174
204
  # Remove and close extra clients
175
205
  # Releases mutex each iteration because
176
206
  # reaping is a low priority action
177
207
  def reap!
178
- loop do
179
- break if @shutdown
180
- reaped = nil
208
+ HotTub.logger.info "[HotTub] Reaping pool #{@name}!" if HotTub.log_trace?
209
+ reaped = nil
210
+ while !@shutdown
181
211
  @mutex.synchronize do
182
- reaped = @_pool.shift if _reap?
212
+ if _reap?
213
+ reaped = @_pool.shift
214
+ else
215
+ reaped = nil
216
+ end
183
217
  end
184
218
  if reaped
185
219
  close_client(reaped)
@@ -189,32 +223,20 @@ module HotTub
189
223
  end
190
224
  end
191
225
 
192
- def never_block?
193
- (@max_size == 0)
194
- end
195
-
196
226
  def current_size
197
227
  @mutex.synchronize do
198
228
  _total_current_size
199
229
  end
200
230
  end
201
231
 
202
- private
203
-
204
- # Returns an instance of the client for this pool.
205
- def client
206
- clnt = pop
207
- clean_client(clnt) if clnt
208
- clnt
232
+ # We must reset our @never_block cache
233
+ # when we set max_size after initialization
234
+ def max_size=max_size
235
+ @never_block = (max_size == 0)
236
+ @max_size = max_size
209
237
  end
210
238
 
211
- def alarm_time
212
- (Time.now + @wait_timeout)
213
- end
214
-
215
- def raise_alarm?(time)
216
- (time <= Time.now)
217
- end
239
+ private
218
240
 
219
241
  ALARM_MESSAGE = "Could not fetch a free client in time. Consider increasing your pool size."
220
242
 
@@ -225,41 +247,46 @@ module HotTub
225
247
  end
226
248
 
227
249
  # Safely add client back to pool, only if
228
- # that clnt is registered
250
+ # that client is registered
229
251
  def push(clnt)
230
252
  if clnt
231
253
  @mutex.synchronize do
232
254
  begin
233
- if @_out.delete(clnt)
234
- unless @shutdown
235
- @_pool << clnt
236
- end
255
+ if !@shutdown && @_out.delete(clnt)
256
+ @_pool << clnt
257
+ else
258
+ close_client(clnt)
259
+ HotTub.logger.info "[HotTub] An orphaned client attempted to return to #{@name}." if HotTub.log_trace?
237
260
  end
238
261
  ensure
239
262
  @cond.signal
240
263
  end
241
264
  end
242
- close_client(clnt) if @shutdown
243
- reap! unless @reaper
265
+ reap! if @blocking_reap
244
266
  end
245
267
  nil
246
268
  end
247
269
 
248
270
  # Safely pull client from pool, adding if allowed
249
271
  def pop
272
+ alarm = (Time.now + @wait_timeout)
250
273
  clnt = nil
251
- alarm = alarm_time
252
- while clnt.nil?
253
- break if @shutdown
254
- raise_alarm if raise_alarm?(alarm)
274
+ dirty = false
275
+ while !@shutdown
276
+ raise_alarm if (Time.now > alarm)
255
277
  @mutex.synchronize do
256
- if clnt = (@_pool.pop || _fetch_new)
278
+ if clnt = @_pool.pop
279
+ dirty = true
280
+ @_out << clnt
281
+ elsif clnt = _fetch_new(&@client_block)
257
282
  @_out << clnt
258
283
  else
259
284
  @cond.wait(@mutex,@wait_timeout)
260
285
  end
261
286
  end
287
+ break if clnt
262
288
  end
289
+ clean_client(clnt) if dirty && clnt
263
290
  clnt
264
291
  end
265
292
 
@@ -272,28 +299,14 @@ module HotTub
272
299
  (@_pool.length + @_out.length)
273
300
  end
274
301
 
275
- # Return true if we have reached our limit set by the :size option
276
- # _less_than_size? is volatile; and may be inaccurate
277
- # if called outside @mutex.synchronize {}
278
- def _less_than_size?
279
- (_total_current_size < @size)
280
- end
281
-
282
- # Return true if we have reached our limit set by the :max_size option
283
- # _less_than_max? is volatile; and may be inaccurate
284
- # if called outside @mutex.synchronize {}
285
- def _less_than_max?
286
- (_total_current_size < @max_size)
287
- end
288
-
289
- # Adds a new client to the pool if its allowed
302
+ # Returns a new client if its allowed.
290
303
  # _add is volatile; and may cause threading issues
291
304
  # if called outside @mutex.synchronize {}
292
305
  def _fetch_new
293
- return nil unless (@_pool.empty? && (never_block? || _less_than_size?|| _less_than_max?))
294
- nc = @new_client.call
295
- HotTub.logger.info "Adding HotTub client: #{nc.class.name} to pool" if HotTub.logger
296
- nc
306
+ if (@never_block || (_total_current_size < @max_size))
307
+ HotTub.logger.info "[HotTub] Adding client: #{nc.class.name} to #{@name}." if HotTub.log_trace?
308
+ yield
309
+ end
297
310
  end
298
311
 
299
312
  # Returns true if we have clients in the pool, the pool
@@ -302,7 +315,7 @@ module HotTub
302
315
  # volatile; and may be inaccurate if called outside
303
316
  # @mutex.synchronize {}
304
317
  def _reap?
305
- ( !@_pool.empty? && !@shutdown && ((@_pool.length > @size) || reap_client?(@_pool[0])))
318
+ (!@shutdown && ((@_pool.length > @size) || reap_client?(@_pool[0])))
306
319
  end
307
320
 
308
321
  ### END VOLATILE METHODS ###
@@ -15,7 +15,7 @@ module HotTub
15
15
  break if obj.shutdown
16
16
  sleep(obj.reap_timeout || 600)
17
17
  rescue Exception => e
18
- HotTub.logger.error "HotTub::Reaper for #{obj.class.name} terminated with exception: #{e.message}" if HotTub.logger
18
+ HotTub.logger.error "[HotTub] Reaper for #{obj.class.name} terminated with exception: #{e.message}" if HotTub.logger
19
19
  HotTub.logger.error e.backtrace.map {|line| " #{line}"} if HotTub.logger
20
20
  break
21
21
  end
@@ -28,7 +28,19 @@ module HotTub
28
28
 
29
29
  # Mixin to dry up Reaper usage
30
30
  module Mixin
31
- attr_reader :reap_timeout, :reaper, :shutdown
31
+ attr_reader :reap_timeout, :shutdown, :reaper
32
+
33
+ # Setting reaper kills the current reaper.
34
+ # If the values is truthy a new HotTub::Reaper
35
+ # is created.
36
+ def reaper=reaper
37
+ kill_reaper
38
+ if reaper
39
+ @reaper = HotTub::Reaper.new(self)
40
+ else
41
+ @reaper = false
42
+ end
43
+ end
32
44
 
33
45
  def reap!
34
46
  raise NoMethodError.new('#reap! must be redefined in your class')
@@ -3,128 +3,161 @@ module HotTub
3
3
  class Sessions
4
4
  include HotTub::KnownClients
5
5
  include HotTub::Reaper::Mixin
6
+ attr_accessor :name
6
7
 
7
- # HotTub::Session is a ThreadSafe::Cache where URLs are mapped HotTub::Pools.
8
- #
8
+ # HotTub::Sessions simplifies managing multiple Pools in a single object
9
+ # and using a single Reaper.
9
10
  #
10
11
  # == Example:
11
- # You can initialize a HotTub::Pool with each client by passing :with_pool as true and any pool options
12
- # sessions = HotTub::Sessions.new(:size => 12) {
13
- # uri = URI.parse("http://somewebservice.com")
14
- # http = Net::HTTP.new(uri.host, uri.port)
15
- # http.use_ssl = false
16
- # http.start
17
- # http
18
- # }
19
12
  #
20
- # sessions.run("http://wwww.yahoo.com") do |conn|
13
+ # url = "http://somewebservice.com"
14
+ # url2 = "http://somewebservice2.com"
15
+ #
16
+ # sessions = HotTub::Sessions
17
+ # sessions.add(url,{:size => 12}) {
18
+ # uri = URI.parse(url)
19
+ # http = Net::HTTP.new(uri.host, uri.port)
20
+ # http.use_ssl = false
21
+ # http.start
22
+ # http
23
+ # }
24
+ # sessions.add(url2,{:size => 5}) {
25
+ # Excon.new(url2)
26
+ # }
27
+ #
28
+ # sessions.run(url) do |conn|
21
29
  # p conn.head('/').code
22
30
  # end
23
31
  #
24
- # sessions.run("https://wwww.google.com") do |conn|
32
+ # sessions.run(url2) do |conn|
25
33
  # p conn.head('/').code
26
34
  # end
27
35
  #
28
36
  # === OPTIONS
29
- # [:close]
30
- # Default is nil. Can be a symbol representing an method to call on a client to close the client or a lambda
31
- # that accepts the client as a parameter that will close a client. The close option is performed on clients
32
- # on reaping and shutdown after the client has been removed from the pool. When nil, as is the default, no
33
- # action is performed.
34
- # [:clean]
35
- # Default is nil. Can be a symbol representing an method to call on a client to clean the client or a lambda
36
- # that accepts the client as a parameter that will clean a client. When nil, as is the default, no action is
37
- # performed.
38
- # [:reap]
39
- # Default is nil. Can be a symbol representing an method to call on a client that returns a boolean marking
40
- # a client for reaping, or a lambda that accepts the client as a parameter that returns a boolean boolean
41
- # marking a client for reaping. When nil, as is the default, no action is performed.
42
- # [:no_reaper]
43
- # Default is nil. A boolean like value that if true prevents the reaper from initializing
37
+ # [:name]
38
+ # A string representing the name of your sessions used for logging.
44
39
  #
45
- def initialize(opts={},&new_client)
46
- raise ArgumentError, "HotTub::Sessions require a block on initialization that accepts a single argument" unless block_given?
47
- @close_client = opts[:close] # => lambda {|clnt| clnt.close}
48
- @clean_client = opts[:clean] # => lambda {|clnt| clnt.clean}
49
- @reap_client = opts[:reap] # => lambda {|clnt| clnt.reap?} # should return boolean
50
- @new_client = new_client # => { |url| MyClient.new(url) } # block that accepts a url param
51
- @sessions = ThreadSafe::Cache.new
40
+ # [:reaper]
41
+ # If set to false prevents a HotTub::Reaper from initializing.
42
+ #
43
+ # [:reap_timeout]
44
+ # Default is 600 seconds. An integer that represents the timeout for reaping the pool in seconds.
45
+ #
46
+ def initialize(opts={})
47
+ @name = (opts[:name] || self.class.name)
48
+ @reaper = opts[:reaper]
49
+ @reap_timeout = (opts[:reap_timeout] || 600)
50
+
51
+ @_sessions = {}
52
+ @mutex = Mutex.new
52
53
  @shutdown = false
53
- @reap_timeout = (opts[:reap_timeout] || 600) # the interval to reap connections in seconds
54
- @reaper = Reaper.spawn(self) unless opts[:no_reaper]
55
- @pool_options = {:no_reaper => true}.merge(opts)
56
- at_exit {drain!}
54
+
55
+ at_exit {shutdown!}
56
+ end
57
+
58
+ # Adds a new HotTub::Pool for the given key unless
59
+ # one already exists.
60
+ def add(key, pool_options={}, &client_block)
61
+ raise ArgumentError, 'a block that initializes a new client is required.' unless block_given?
62
+ pool = nil
63
+ return pool if pool = @_sessions[key]
64
+ pool_options[:sessions] = true
65
+ pool_options[:name] = "#{@name} - #{key}"
66
+ @mutex.synchronize do
67
+ @reaper ||= Reaper.spawn(self) if @reaper.nil?
68
+ pool = @_sessions[key] ||= HotTub::Pool.new(pool_options, &client_block) unless @shutdown
69
+ end
70
+ pool
71
+ end
72
+
73
+ # Deletes and shutdowns the pool if its found.
74
+ def delete(key)
75
+ deleted = false
76
+ pool = nil
77
+ @mutex.synchronize do
78
+ pool = @_sessions.delete(key)
79
+ end
80
+ if pool
81
+ pool.reset!
82
+ deleted = true
83
+ HotTub.logger.info "[HotTub] #{key} was deleted from #{@name}." if HotTub.logger
84
+ end
85
+ deleted
57
86
  end
58
87
 
59
- # Safely initializes sessions
60
- # expects a url string or URI
61
- def session(url)
62
- key = to_key(url)
63
- return @sessions.get(key) if @sessions.get(key)
64
- @sessions.compute_if_absent(key) {
65
- HotTub::Pool.new(@pool_options) { @new_client.call(url) }
66
- }
67
- @sessions.get(key)
88
+ def fetch(key)
89
+ pool = @_sessions[key]
90
+ raise MissingSession, "A session could not be found for #{key.inspect} #{@name}" unless pool
91
+ pool
68
92
  end
69
- alias :sessions :session
70
93
 
71
- def run(url,&block)
72
- session = sessions(url)
73
- session.run(&block) if session
94
+ alias :[] :fetch
95
+
96
+ def run(url, &run_block)
97
+ pool = fetch(url)
98
+ pool.run &run_block
74
99
  end
75
100
 
76
101
  def clean!
77
- @sessions.each_pair do |key,pool|
78
- pool.clean!
102
+ HotTub.logger.info "[HotTub] Cleaning #{@name}!" if HotTub.logger
103
+ @mutex.synchronize do
104
+ @_sessions.each_value do |pool|
105
+ break if @shutdown
106
+ pool.clean!
107
+ end
79
108
  end
80
- @sessions
109
+ nil
81
110
  end
82
111
 
83
112
  def drain!
84
- @sessions.each_pair do |key,pool|
85
- pool.drain!
113
+ HotTub.logger.info "[HotTub] Draining #{@name}!" if HotTub.logger
114
+ @mutex.synchronize do
115
+ @_sessions.each_value do |pool|
116
+ break if @shutdown
117
+ pool.drain!
118
+ end
86
119
  end
87
- @sessions
120
+ nil
88
121
  end
89
122
 
90
123
  def reset!
91
- @sessions.each_pair do |key,pool|
92
- pool.reset!
124
+ HotTub.logger.info "[HotTub] Resetting #{@name}!" if HotTub.logger
125
+ @mutex.synchronize do
126
+ @_sessions.each_value do |pool|
127
+ break if @shutdown
128
+ pool.reset!
129
+ end
93
130
  end
94
- @sessions.clear
95
- @sessions = ThreadSafe::Cache.new
96
- @sessions
131
+ nil
97
132
  end
98
133
 
99
134
  def shutdown!
100
135
  @shutdown = true
136
+ HotTub.logger.info "[HotTub] Shutting down #{@name}!" if HotTub.logger
101
137
  begin
102
138
  kill_reaper
103
139
  ensure
104
- drain!
105
- @sessions = nil
140
+ @mutex.synchronize do
141
+ @_sessions.each_value do |pool|
142
+ pool.shutdown!
143
+ end
144
+ end
106
145
  end
107
146
  nil
108
147
  end
109
148
 
110
149
  # Remove and close extra clients
111
150
  def reap!
112
- @sessions.each_pair do |key,pool|
113
- pool.reap!
151
+ HotTub.logger.info "[HotTub] Reaping #{@name}!" if HotTub.log_trace?
152
+ @mutex.synchronize do
153
+ @_sessions.each_value do |pool|
154
+ break if @shutdown
155
+ pool.reap!
156
+ end
114
157
  end
158
+ nil
115
159
  end
116
160
 
117
- private
118
-
119
- def to_key(url)
120
- if url.is_a?(String)
121
- uri = URI(url)
122
- elsif url.is_a?(URI)
123
- uri = url
124
- else
125
- raise ArgumentError, "you must pass a string or a URI object"
126
- end
127
- "#{uri.scheme}://#{uri.host}:#{uri.port}"
128
- end
161
+ MissingSession = Class.new(Exception)
129
162
  end
130
163
  end
@@ -1,3 +1,3 @@
1
1
  module HotTub
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end