hot_tub 0.2.6 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.travis.yml +5 -5
- data/Gemfile +0 -4
- data/HISTORY.md +12 -4
- data/LICENSE.txt +1 -1
- data/README.md +94 -70
- data/hot_tub.gemspec +4 -2
- data/lib/hot_tub.rb +8 -13
- data/lib/hot_tub/known_clients.rb +41 -23
- data/lib/hot_tub/pool.rb +235 -98
- data/lib/hot_tub/reaper.rb +37 -0
- data/lib/hot_tub/sessions.rb +157 -0
- data/lib/hot_tub/version.rb +1 -1
- data/spec/helpers/moc_client.rb +10 -6
- data/spec/helpers/moc_pool.rb +23 -0
- data/spec/hot_tub_spec.rb +20 -0
- data/spec/pool_spec.rb +189 -178
- data/spec/reaper_mixin_spec.rb +29 -0
- data/spec/reaper_spec.rb +28 -0
- data/spec/sessions_spec.rb +223 -0
- data/spec/spec_helper.rb +1 -0
- metadata +34 -27
- data/lib/hot_tub/session.rb +0 -103
- data/spec/session_spec.rb +0 -213
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
YThjNDQ4Y2Q0YTg2NGNmY2RhMTVhZjg1NzNiMmM1NjZjNmE2NDUyNw==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
ZjAwZWMwNWJkNzE4ZDhlYTg2MzIyYjdlMGM1YTIxY2JkZWQ1ZGY3ZA==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
YjIyMTkxZDZkNDc2Nzc2OGVlMGI5YTgzODAxZWYwZGIzOWIxNzRkOGQ5NTM4
|
10
|
+
ODQ2ZTBkMThmZmU4YjIxNTg3YWM1OGNlZWRmYTExOTFlY2JkOTYyMzM0MThm
|
11
|
+
M2U4ODI2MmE0NWI3ZmFiZmEwMzU0ZTdlZTY2NjIyMTMyMDdjMzM=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
M2QyMzI2OWY0MDBlZWYzYWYwYmVmNmIzMGMyZWVhOWZmYzk4NDEyNGYzYWIz
|
14
|
+
OWZkNmQwMDFmOTFhOTEzNDgwYTVmYzBlMTFjNjk0MTg4YzVmNzk2NjlkY2E3
|
15
|
+
YmNiOTUyYWE2OTNhYzFiY2EwYTc4MzhlZDg2OGYwMWU5NGE2Y2M=
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
@@ -3,13 +3,9 @@ source "https://rubygems.org"
|
|
3
3
|
# Specify your gem's dependencies in http_hot_tub.gemspec
|
4
4
|
gemspec
|
5
5
|
gem 'rake'
|
6
|
-
|
7
6
|
group :development do
|
8
7
|
platform :ruby do
|
9
8
|
gem 'coveralls', :require => false
|
10
|
-
gem 'eventmachine'
|
11
|
-
gem 'em-http-request', '~> 1.0', :require => 'em-http'
|
12
|
-
gem 'em-synchrony', '~> 1.0', :require => ['em-synchrony', 'em-synchrony/em-http']
|
13
9
|
end
|
14
10
|
platform :jruby do
|
15
11
|
gem 'jruby-openssl'
|
data/HISTORY.md
CHANGED
@@ -1,9 +1,17 @@
|
|
1
1
|
HotTub Changelog
|
2
2
|
=====================
|
3
3
|
|
4
|
-
|
4
|
+
Head
|
5
5
|
=======
|
6
6
|
|
7
|
-
-
|
8
|
-
|
9
|
-
|
7
|
+
- Nothing yet
|
8
|
+
|
9
|
+
0.3.0
|
10
|
+
=======
|
11
|
+
|
12
|
+
- Drop EM support, will move to separate gem
|
13
|
+
- Simplify API with HotTub.new
|
14
|
+
- Use ThreadSafe::Cache for sessions
|
15
|
+
- Better documentation and lots of clean up
|
16
|
+
- HotTub::Reaper for reaping in separate thread
|
17
|
+
- HotTub::Pool now raises HotTub::Pool::Timeout instead of HotTub::BlockingTimeout
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -1,25 +1,23 @@
|
|
1
1
|
# HotTub [![Build Status](https://travis-ci.org/JoshMcKin/hot_tub.png?branch=master)](https://travis-ci.org/JoshMcKin/hot_tub) [![Coverage Status](https://coveralls.io/repos/JoshMcKin/hot_tub/badge.png?branch=master)](https://coveralls.io/r/JoshMcKin/hot_tub)
|
2
2
|
|
3
|
-
A
|
4
|
-
[EM-Http-Requests](https://github.com/igrigorik/em-http-request) via [EM-Synchrony](https://github.com/igrigorik/em-synchrony).
|
3
|
+
A dynamic thread-safe pooling gem, when you need more than a standard static pool.
|
5
4
|
|
6
5
|
## Features
|
7
6
|
|
8
7
|
### HotTub::Pool
|
9
|
-
|
10
|
-
|
11
|
-
*
|
12
|
-
*
|
13
|
-
*
|
14
|
-
*
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
## Requirements
|
8
|
+
A thread safe, lazy pool.
|
9
|
+
|
10
|
+
* Thread safe
|
11
|
+
* Lazy, pool starts off at 0 and grows as necessary.
|
12
|
+
* Non-Blocking, can be configured to always return a client if your pool runs out under load. Overflow clients are returned to the pool for reuse. Once load dies, the pool is reaped down to size.
|
13
|
+
* Can be used with any client library instance.
|
14
|
+
* Support for cleaning dirty resources, no one likes a dirty `HotTub`
|
15
|
+
* Support for closing resources on shutdown
|
16
|
+
|
17
|
+
### HotTub::Sessions
|
18
|
+
A [ThreadSafe::Cache](https://github.com/headius/thread_safe) where URLs are mapped to a pool or client instance.
|
19
|
+
|
20
|
+
### Requirements
|
23
21
|
HotTub is tested on MRI, JRUBY and Rubinius
|
24
22
|
* Ruby >= 1.9
|
25
23
|
|
@@ -39,85 +37,111 @@ Run bundle:
|
|
39
37
|
|
40
38
|
bundle install
|
41
39
|
|
42
|
-
Configure Logger by creating
|
40
|
+
Configure Logger by creating `config\initializers\hot_tub.rb` and adding the following:
|
43
41
|
|
44
42
|
HotTub.logger = Rails.logger
|
45
43
|
|
46
44
|
# Usage
|
47
45
|
|
48
|
-
## HotTub
|
46
|
+
## HotTub
|
47
|
+
For convenience you can initialize a new HotTub::Pool by calling HotTub.new or HotTub::Pool.new directly.
|
48
|
+
Returns an instance of HotTub::Pool.
|
49
|
+
|
50
|
+
### Redis
|
51
|
+
# We don't want too many connections so we set our :max_size. Under load our pool
|
52
|
+
# can grow to 30 connections. Once load dies down our pool can be reaped back down to 5
|
53
|
+
pool = HotTub::Pool.new(:size => 5, :max_size => 30, :reap_timeout => 60) { Redis.new }
|
54
|
+
pool.set('hot', 'stuff')
|
55
|
+
pool.get('hot')
|
56
|
+
# => 'stuff'
|
57
|
+
|
58
|
+
### Net::HTTP
|
59
|
+
|
60
|
+
require 'hot_tub'
|
61
|
+
require 'net/http'
|
62
|
+
|
63
|
+
pool = HotTub.new(:size => 10) {
|
64
|
+
uri = URI.parse("http://somewebservice.com")
|
65
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
66
|
+
http.use_ssl = false
|
67
|
+
http.start
|
68
|
+
http
|
69
|
+
}
|
70
|
+
pool.run {|clnt| puts clnt.head('/').code }
|
71
|
+
|
72
|
+
### HotTub Options
|
73
|
+
**size**: Default is 5. An integer that sets the size of the pool. Could be describe as minimum size the pool should grow to.
|
74
|
+
|
75
|
+
**max_size**: Default is 0. An integer that represents the maximum number of connections allowed when :non_blocking is true. If set to 0, which is the default, there is no limit; connections will continue to open until load subsides long enough for reaping to occur.
|
76
|
+
|
77
|
+
**wait_timeout**: Default is 10 seconds. An integer that represents the timeout when waiting for a client from the pool in seconds. After said time a HotTub::Pool::Timeout exception will be thrown
|
78
|
+
|
79
|
+
**reap_timeout**: Default is 600 seconds. An integer that represents the timeout for reaping the pool in seconds.
|
80
|
+
|
81
|
+
**close_out**: Default is false. A boolean value that if true force close_client to be called on checkout clients when #drain! is called
|
82
|
+
|
83
|
+
**close**: Default is nil. Can be a symbol representing an method to call on a client to close the client or a lambda that accepts the client as a parameter that will close a client. The close option is performed on clients on reaping and shutdown after the client has been removed from the pool. When nil, as is the default, no action is performed.
|
84
|
+
|
85
|
+
**clean**: Default is nil. Can be a symbol representing an method to call on a client to clean the client or a lambda that accepts the client as a parameter that will clean a client. When nil, as is the default, no action is performed.
|
86
|
+
|
87
|
+
**reap**: Default is nil. Can be a symbol representing an method to call on a client that returns a boolean marking a client for reaping, or a lambda that accepts the client as a parameter that returns a boolean marking a client for reaping. When nil, as is the default, no action is performed.
|
88
|
+
|
89
|
+
**no_reaper**: Default is nil. A boolean like value that if true prevents the reaper from initializing
|
90
|
+
|
91
|
+
**sessions**: Default is false. Returns an instance of `HotTub::Sessions.new` that wraps clients in `HotTub::Pool.new`
|
92
|
+
|
93
|
+
### With sessions
|
94
|
+
Available via `HotTub.new(:sessions => true)` or `HotTub::Sessions.new`
|
49
95
|
|
50
|
-
### EM-Http-Request
|
51
96
|
require 'hot_tub'
|
52
|
-
require '
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
97
|
+
require 'net/http'
|
98
|
+
|
99
|
+
# We must pass any pool options in our options hash, and our client block
|
100
|
+
# must accept the a single argument which is normally the url
|
101
|
+
|
102
|
+
hot_tub = HotTub.new(:size => 12, :sessions => true) { |url|
|
103
|
+
uri = URI.parse(url)
|
104
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
105
|
+
http.use_ssl = false
|
106
|
+
http.start
|
107
|
+
http
|
59
108
|
}
|
109
|
+
hot_tub.run("http://somewebservice.com") do |clnt|
|
110
|
+
puts clnt.head('/').code
|
111
|
+
end
|
112
|
+
hot_tub.run("https://someotherwebservice.com") do |clnt|
|
113
|
+
puts clnt.head('/').code
|
114
|
+
end
|
60
115
|
|
61
116
|
### Other
|
62
|
-
You can use any library you want with HotTub::Pool
|
63
|
-
with lambdas, if they are not defined they are ignored.
|
117
|
+
You can use any library you want with `HotTub::Pool`.
|
64
118
|
|
65
119
|
url = "http://test12345.com"
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
##
|
70
|
-
|
71
|
-
|
72
|
-
|
120
|
+
hot_tub = HotTub.new({:size => 10, :close => lambda {|clnt| clnt.close}, :clean => :clean, :reap => :reap?}) { MyHttpLib.new }
|
121
|
+
hot_tub.run { |clnt| clnt.get(url,query).body }
|
122
|
+
|
123
|
+
## Sessions only
|
124
|
+
Returns a `HotTub::Sessions` instance.
|
125
|
+
|
126
|
+
[Excon](https://github.com/geemus/excon) is thread safe but you set a single url at the client level so sessions
|
127
|
+
are handy if you need to access multiple URLs from a single instances
|
73
128
|
|
74
129
|
require 'hot_tub'
|
75
130
|
require 'excon'
|
76
131
|
# Our client block must accept the url argument
|
77
|
-
sessions = HotTub::
|
132
|
+
sessions = HotTub::Sessions.new {|url| Excon.new(url) }
|
78
133
|
|
79
134
|
sessions.run("http://somewebservice.com") do |clnt|
|
80
135
|
puts clnt.get(:query => {:some => 'stuff'}).response_header.status
|
81
136
|
end
|
137
|
+
|
82
138
|
sessions.run("https://someotherwebservice.com") do |clnt|
|
83
139
|
puts clnt.get(:query => {:other => 'stuff'}).response_header.status
|
84
140
|
end
|
85
|
-
sessions.run("https://127.0.0.1:5252") do |clnt|
|
86
|
-
puts clnt.get.response_header.status
|
87
|
-
end
|
88
|
-
|
89
|
-
### HotTub::Session with HotTub::Pool
|
90
|
-
Suppose you have a client that lacks pooling and session features you can use HotTub::Pool with HotTub::Sessions to get what you need.
|
91
|
-
|
92
|
-
require 'hot_tub'
|
93
|
-
require "em-synchrony"
|
94
|
-
require "em-synchrony/em-http"
|
95
|
-
|
96
|
-
# We must tell HotTub::Session to use HotTub::Pool, pass any pool options in our
|
97
|
-
# options has, and our client block must accept the url argument
|
98
|
-
EM.synchrony do {
|
99
|
-
sessions = HotTub::Session.new(:with_pool => true, :size => 12) {|url| EM::HttpRequest.new(url, :inactivity_timeout => 0) }
|
100
|
-
|
101
|
-
sessions.run("http://somewebservice.com") do |clnt|
|
102
|
-
puts clnt.get(:query => results, :keepalive => true).response_header.status
|
103
|
-
end
|
104
|
-
sessions.run("https://someotherwebservice.com") do |clnt|
|
105
|
-
puts clnt.get(:query => results, :keepalive => true).response_header.status
|
106
|
-
end
|
107
|
-
EM.stop
|
108
|
-
}
|
109
|
-
|
110
|
-
## Related
|
111
|
-
|
112
|
-
* [EM-Http-Request](https://github.com/igrigorik/em-http-request)
|
113
|
-
* [EM-Synchrony](https://github.com/igrigorik/em-synchrony)
|
114
|
-
* [Excon](https://github.com/geemus/excon) Thread safe with its own built in pool.
|
115
|
-
* [HTTPClient](https://github.com/nahi/httpclient) A thread safe http client that supports pools and sessions all by itself.
|
116
141
|
|
117
|
-
##
|
142
|
+
## Dependencies
|
118
143
|
|
119
|
-
* [
|
120
|
-
* [EM-Synchrony](https://github.com/igrigorik/em-synchrony) has a pool feature
|
144
|
+
* [ThreadSafe](https://github.com/headius/thread_safe)
|
121
145
|
|
122
146
|
## Contributing to HotTub
|
123
147
|
|
data/hot_tub.gemspec
CHANGED
@@ -9,10 +9,12 @@ Gem::Specification.new do |s|
|
|
9
9
|
s.email = ["joshmckin@gmail.com"]
|
10
10
|
s.homepage = "https://github.com/JoshMcKin/hot_tub"
|
11
11
|
s.license = "MIT"
|
12
|
-
s.summary = %q{A
|
13
|
-
s.description = %q{A
|
12
|
+
s.summary = %q{A dynamic thread-safe pooling gem.}
|
13
|
+
s.description = %q{A dynamic thread-safe pooling gem, when you need more than a standard static pool.}
|
14
14
|
|
15
15
|
s.rubyforge_project = "hot_tub"
|
16
|
+
|
17
|
+
s.add_runtime_dependency "thread_safe"
|
16
18
|
|
17
19
|
s.add_development_dependency "rspec"
|
18
20
|
s.add_development_dependency "sinatra"
|
data/lib/hot_tub.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
require 'thread'
|
2
|
+
require 'thread_safe'
|
2
3
|
require 'logger'
|
3
4
|
require "hot_tub/version"
|
4
5
|
require "hot_tub/known_clients"
|
6
|
+
require "hot_tub/reaper"
|
5
7
|
require "hot_tub/pool"
|
6
|
-
require "hot_tub/
|
8
|
+
require "hot_tub/sessions"
|
7
9
|
|
8
10
|
module HotTub
|
9
11
|
@@logger = Logger.new(STDOUT)
|
@@ -15,14 +17,6 @@ module HotTub
|
|
15
17
|
@@logger = logger
|
16
18
|
end
|
17
19
|
|
18
|
-
def self.em?
|
19
|
-
(defined?(EM))
|
20
|
-
end
|
21
|
-
|
22
|
-
def self.em_synchrony?
|
23
|
-
(defined?(EM::Synchrony))
|
24
|
-
end
|
25
|
-
|
26
20
|
def self.jruby?
|
27
21
|
(defined?(JRUBY_VERSION))
|
28
22
|
end
|
@@ -31,11 +25,12 @@ module HotTub
|
|
31
25
|
defined?(RUBY_ENGINE) and RUBY_ENGINE == 'rbx'
|
32
26
|
end
|
33
27
|
|
34
|
-
def self.
|
35
|
-
if
|
36
|
-
|
28
|
+
def self.new(opts={},&client_block)
|
29
|
+
if opts[:sessions] == true
|
30
|
+
opts[:with_pool] = true
|
31
|
+
Sessions.new(opts,&client_block)
|
37
32
|
else
|
38
|
-
|
33
|
+
Pool.new(opts,&client_block)
|
39
34
|
end
|
40
35
|
end
|
41
36
|
end
|
@@ -4,44 +4,62 @@ module HotTub
|
|
4
4
|
"Excon::Connection" => {
|
5
5
|
:close => lambda { |clnt| clnt.reset }
|
6
6
|
},
|
7
|
-
|
7
|
+
"Net::HTTP" => {
|
8
8
|
:close => lambda { |clnt|
|
9
|
-
|
10
|
-
clnt.
|
11
|
-
|
9
|
+
begin
|
10
|
+
clnt.finish
|
11
|
+
rescue IOError
|
12
|
+
nil
|
12
13
|
end
|
13
|
-
},
|
14
|
-
:clean => lambda { |clnt|
|
15
|
-
if clnt.conn && clnt.conn.error?
|
16
|
-
HotTub.logger.info "Sanitizing connection : #{EventMachine::report_connection_error_status(clnt.conn.instance_variable_get(:@signature))}"
|
17
|
-
clnt.conn.close_connection
|
18
|
-
clnt.instance_variable_set(:@deferred, true)
|
19
|
-
end
|
20
|
-
clnt
|
21
14
|
}
|
22
15
|
}
|
23
16
|
}
|
24
|
-
attr_accessor :options
|
25
17
|
# Attempts to clean the provided client, checking the options first for a clean block
|
26
18
|
# then checking the known clients
|
27
19
|
def clean_client(clnt)
|
28
|
-
|
29
|
-
|
30
|
-
|
20
|
+
begin
|
21
|
+
action = (@clean_client || known_client_action(clnt,:clean))
|
22
|
+
preform_client_action(clnt,action) if action
|
23
|
+
rescue => e
|
24
|
+
HotTub.logger.error "There was an error cleaning one of your #{self.class.name} clients: #{e}"
|
31
25
|
end
|
32
26
|
end
|
33
27
|
|
34
28
|
# Attempts to close the provided client, checking the options first for a close block
|
35
29
|
# then checking the known clients
|
36
30
|
def close_client(clnt)
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
31
|
+
begin
|
32
|
+
action = (@close_client || known_client_action(clnt,:close))
|
33
|
+
preform_client_action(clnt,action) if action
|
34
|
+
rescue => e
|
35
|
+
HotTub.logger.error "There was an error closing one of your #{self.class.name} clients: #{e}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Attempts to determine if a client should be reaped, block should return a boolean
|
40
|
+
def reap_client?(clnt)
|
41
|
+
begin
|
42
|
+
action = (@reap_client || known_client_action(clnt,:reap))
|
43
|
+
return preform_client_action(clnt,action) if action
|
44
|
+
rescue => e
|
45
|
+
HotTub.logger.error "There was an error reaping one of your #{self.class.name} clients: #{e}"
|
46
|
+
end
|
47
|
+
return false
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def known_client_action(clnt,key)
|
53
|
+
(KNOWN_CLIENTS[clnt.class.name] && KNOWN_CLIENTS[clnt.class.name][key])
|
54
|
+
end
|
55
|
+
|
56
|
+
def preform_client_action(clnt,action)
|
57
|
+
if action.is_a?(Proc)
|
58
|
+
return action.call(clnt)
|
59
|
+
elsif action.is_a?(Symbol)
|
60
|
+
return clnt.send(action)
|
44
61
|
end
|
62
|
+
false
|
45
63
|
end
|
46
64
|
end
|
47
65
|
end
|
data/lib/hot_tub/pool.rb
CHANGED
@@ -1,52 +1,117 @@
|
|
1
1
|
module HotTub
|
2
2
|
class Pool
|
3
3
|
include HotTub::KnownClients
|
4
|
-
|
4
|
+
include HotTub::Reaper::Mixin
|
5
|
+
attr_reader :current_size, :last_activity
|
5
6
|
|
6
7
|
# Thread-safe lazy connection pool
|
7
8
|
#
|
8
|
-
# == Example
|
9
|
-
# pool = HotTub::Pool.new
|
10
|
-
#
|
9
|
+
# == Example Net::HTTP
|
10
|
+
# pool = HotTub::Pool.new(:size => 10) {
|
11
|
+
# uri = URI.parse("http://somewebservice.com")
|
12
|
+
# http = Net::HTTP.new(uri.host, uri.port)
|
13
|
+
# http.use_ssl = false
|
14
|
+
# http.start
|
15
|
+
# http
|
16
|
+
# }
|
17
|
+
# pool.run {|clnt| puts clnt.head('/').code }
|
18
|
+
#
|
19
|
+
# == Example Redis
|
20
|
+
# # We don't want too many connections so we set our :max_size Under load our pool
|
21
|
+
# # can grow to 30 connections. Once load dies down our pool can be reaped back down to 5
|
22
|
+
# pool = HotTub::Pool.new(:size => 5, :max_size => 30, :reap_timeout => 60) { Redis.new }
|
23
|
+
# pool.set('hot', 'stuff')
|
24
|
+
# pool.get('hot')
|
25
|
+
# # => 'stuff'
|
11
26
|
#
|
12
27
|
# HotTub::Pool defaults never_block to true, which means if we run out of
|
13
|
-
#
|
14
|
-
# The pool will grow and extra
|
28
|
+
# clients simply create a new client to continue operations.
|
29
|
+
# The pool will grow and extra clients will be reused until activity dies down.
|
15
30
|
# If you would like to block and possibly throw an exception rather than temporarily
|
16
|
-
# grow the set :size, set :never_block to false;
|
31
|
+
# grow the set :size, set :never_block to false; wait_timeout defaults to 10 seconds.
|
17
32
|
#
|
18
|
-
# == Example
|
19
|
-
# pool = HotTub::Pool.new(:size => 1, :
|
33
|
+
# == Example with set pool size (will throw HotTub::Pool::Timeout exception)
|
34
|
+
# pool = HotTub::Pool.new(:size => 1, :max_size => 1, :wait_timeout => 0.5) {
|
35
|
+
# uri = URI.parse("http://someslowwebservice.com")
|
36
|
+
# http = Net::HTTP.new(uri.host, uri.port)
|
37
|
+
# http.use_ssl = false
|
38
|
+
# http.start
|
39
|
+
# http
|
40
|
+
# }
|
41
|
+
# pool.run { |clnt| puts clnt.head('/').code }
|
20
42
|
#
|
21
43
|
# begin
|
22
|
-
# pool.run {|clnt| clnt.
|
23
|
-
# rescue HotTub::
|
24
|
-
# puts "
|
44
|
+
# pool.run { |clnt| puts clnt.head('/').code }
|
45
|
+
# rescue HotTub::Pool::Timeout => e
|
46
|
+
# puts "Waited too long for a client: {e}"
|
25
47
|
# end
|
26
48
|
#
|
27
|
-
|
49
|
+
#
|
50
|
+
# === OPTIONS
|
51
|
+
#
|
52
|
+
# [:size]
|
53
|
+
# Default is 5. An integer that sets the size of the pool. Could be describe as minimum size the pool should
|
54
|
+
# grow to.
|
55
|
+
# [:max_size]
|
56
|
+
# Default is 0. An integer that represents the maximum number of connections allowed when :non_blocking is true.
|
57
|
+
# If set to 0, which is the default, there is no limit; connections will continue to open until load subsides
|
58
|
+
# long enough for reaping to occur.
|
59
|
+
# [:wait_timeout]
|
60
|
+
# Default is 10 seconds. An integer that represents the timeout when waiting for a client from the pool
|
61
|
+
# 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.
|
64
|
+
# [:close_out]
|
65
|
+
# Default is nil. A boolean like value that if it can be interpreted as true force close_client to be called
|
66
|
+
# on checkout clients when #drain! is called
|
67
|
+
# [:close]
|
68
|
+
# Default is nil. Can be a symbol representing an method to call on a client to close the client or a lambda
|
69
|
+
# that accepts the client as a parameter that will close a client. The close option is performed on clients
|
70
|
+
# on reaping and shutdown after the client has been removed from the pool. When nil, as is the default, no
|
71
|
+
# action is performed.
|
72
|
+
# [:clean]
|
73
|
+
# Default is nil. Can be a symbol representing an method to call on a client to clean the client or a lambda
|
74
|
+
# that accepts the client as a parameter that will clean a client. When nil, as is the default, no action is
|
75
|
+
# performed.
|
76
|
+
# [:reap]
|
77
|
+
# Default is nil. Can be a symbol representing an method to call on a client that returns a boolean marking
|
78
|
+
# a client for reaping, or a lambda that accepts the client as a parameter that returns a boolean boolean
|
79
|
+
# marking a client for reaping. When nil, as is the default, no action is performed.
|
80
|
+
# [:no_reaper]
|
81
|
+
# Default is nil. A boolean like value that if true prevents the reaper from initializing
|
82
|
+
#
|
83
|
+
def initialize(opts={},&new_client)
|
28
84
|
raise ArgumentError, 'a block that initializes a new client is required' unless block_given?
|
29
|
-
|
30
|
-
@
|
31
|
-
|
32
|
-
|
33
|
-
:
|
34
|
-
|
35
|
-
|
36
|
-
}
|
37
|
-
@
|
38
|
-
@
|
39
|
-
@
|
40
|
-
|
41
|
-
@
|
42
|
-
@
|
43
|
-
|
85
|
+
|
86
|
+
@size = (opts[:size] || 5) # in seconds
|
87
|
+
@wait_timeout = (opts[:wait_timeout] || 10) # in seconds
|
88
|
+
@reap_timeout = (opts[:reap_timeout] || 600) # the interval to reap connections in seconds
|
89
|
+
@close_out = opts[:close_out] # if true on drain! call close_client block on checked out clients
|
90
|
+
@max_size = (opts[:max_size] || 0) # maximum size of pool when non-blocking, 0 means no limit
|
91
|
+
|
92
|
+
@close_client = opts[:close] # => lambda {|clnt| clnt.close} or :close
|
93
|
+
@clean_client = opts[:clean] # => lambda {|clnt| clnt.clean} or :clean
|
94
|
+
@reap_client = opts[:reap] # => lambda {|clnt| clnt.reap?} or :reap? # should return boolean
|
95
|
+
@new_client = new_client
|
96
|
+
|
97
|
+
@pool = [] # stores available clients
|
98
|
+
@pool.taint
|
99
|
+
@out = [] # stores all checked out clients
|
100
|
+
@out.taint
|
101
|
+
|
102
|
+
@mutex = Mutex.new
|
103
|
+
@cond = ConditionVariable.new
|
104
|
+
|
105
|
+
@shutdown = false # Kills reaper when true
|
106
|
+
@reaper = Reaper.spawn(self) unless opts[:no_reaper]
|
107
|
+
|
108
|
+
at_exit {shutdown!}
|
44
109
|
end
|
45
110
|
|
46
111
|
# Hand off to client.run
|
47
112
|
def run(&block)
|
48
|
-
clnt = client
|
49
113
|
if block_given?
|
114
|
+
clnt = client
|
50
115
|
return block.call(clnt) if clnt
|
51
116
|
else
|
52
117
|
raise ArgumentError, 'Run requires a block.'
|
@@ -55,117 +120,189 @@ module HotTub
|
|
55
120
|
push(clnt) if clnt
|
56
121
|
end
|
57
122
|
|
58
|
-
#
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
close_client(clnt)
|
65
|
-
rescue => e
|
66
|
-
HotTub.logger.error "There was an error close one of your HotTub::Pool connections: #{e}"
|
67
|
-
end
|
123
|
+
# Clean all clients currently checked into the pool.
|
124
|
+
# Its possible clients may be returned to the pool after cleaning
|
125
|
+
def clean!
|
126
|
+
@mutex.synchronize do
|
127
|
+
@pool.each do |clnt|
|
128
|
+
clean_client(clnt)
|
68
129
|
end
|
69
|
-
@current_size = 0
|
70
130
|
end
|
71
131
|
end
|
72
132
|
|
73
|
-
|
133
|
+
# Drain the pool of all clients currently checked into the pool.
|
134
|
+
# After draining, wake all sleeping threads to allow repopulating the pool
|
135
|
+
# or if shutdown allow threads to quickly finish their work
|
136
|
+
# Its possible clients may be returned to the pool after cleaning
|
137
|
+
def drain!
|
138
|
+
@mutex.synchronize do
|
139
|
+
while clnt = (@pool.pop || (@close_out && @out.pop))
|
140
|
+
close_client(clnt)
|
141
|
+
end
|
142
|
+
@cond.broadcast
|
143
|
+
end
|
144
|
+
end
|
145
|
+
alias :close! :drain!
|
146
|
+
alias :close_all! :drain!
|
147
|
+
|
148
|
+
# Kills the reaper and drains the pool.
|
149
|
+
def shutdown!
|
150
|
+
@shutdown = true
|
151
|
+
drain!
|
152
|
+
end
|
74
153
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
154
|
+
# Remove and close extra clients
|
155
|
+
# Releases mutex each iteration because
|
156
|
+
# reaping is low priority action
|
157
|
+
def reap!
|
158
|
+
start = Time.now
|
159
|
+
loop do
|
160
|
+
reaped = nil
|
161
|
+
@mutex.synchronize do
|
162
|
+
reaped = @pool.shift if _reap?
|
163
|
+
end
|
164
|
+
if reaped
|
165
|
+
close_client(reaped)
|
166
|
+
else
|
167
|
+
break
|
168
|
+
end
|
80
169
|
end
|
81
170
|
end
|
82
171
|
|
172
|
+
def never_block?
|
173
|
+
(@max_size == 0)
|
174
|
+
end
|
175
|
+
|
176
|
+
private
|
177
|
+
|
83
178
|
# Returns an instance of the client for this pool.
|
84
179
|
def client
|
85
|
-
clnt =
|
86
|
-
|
87
|
-
# block until we get an available client or raise Timeout::Error
|
88
|
-
while clnt.nil?
|
89
|
-
raise_alarm if alarm <= Time.now
|
90
|
-
clnt = pop
|
91
|
-
end
|
92
|
-
clean_client(clnt)
|
180
|
+
clnt = pop
|
181
|
+
clean_client(clnt) if clnt
|
93
182
|
clnt
|
94
183
|
end
|
95
184
|
|
185
|
+
def alarm_time
|
186
|
+
(Time.now + @wait_timeout)
|
187
|
+
end
|
188
|
+
|
189
|
+
def raise_alarm?(time)
|
190
|
+
(time <= Time.now)
|
191
|
+
end
|
192
|
+
|
96
193
|
def raise_alarm
|
97
194
|
message = "Could not fetch a free client in time. Consider increasing your pool size."
|
98
195
|
HotTub.logger.error message
|
99
|
-
raise
|
196
|
+
raise Timeout, message
|
100
197
|
end
|
101
198
|
|
102
199
|
# Safely add client back to pool, only if
|
103
200
|
# that clnt is registered
|
104
201
|
def push(clnt)
|
105
|
-
|
106
|
-
|
107
|
-
@
|
108
|
-
|
109
|
-
|
202
|
+
if clnt
|
203
|
+
@mutex.synchronize do
|
204
|
+
@out.delete(clnt)
|
205
|
+
unless @shutdown
|
206
|
+
@pool << clnt
|
207
|
+
@cond.signal
|
208
|
+
end
|
110
209
|
end
|
210
|
+
close_client(clnt) if @shutdown
|
111
211
|
end
|
112
|
-
nil
|
212
|
+
nil
|
113
213
|
end
|
114
214
|
|
115
215
|
# Safely pull client from pool, adding if allowed
|
116
216
|
def pop
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
if (
|
122
|
-
|
123
|
-
|
217
|
+
clnt = nil
|
218
|
+
alarm = alarm_time
|
219
|
+
while clnt.nil?
|
220
|
+
break if @shutdown
|
221
|
+
raise_alarm if raise_alarm?(alarm)
|
222
|
+
@mutex.synchronize do
|
223
|
+
if (_space? || _add)
|
224
|
+
@out << clnt = @pool.pop
|
225
|
+
else
|
226
|
+
@cond.wait(@mutex,@wait_timeout)
|
227
|
+
end
|
124
228
|
end
|
125
|
-
@fetching_client = false
|
126
|
-
clnt
|
127
229
|
end
|
128
|
-
|
129
|
-
reap_pool if reap_pool?
|
230
|
+
clnt
|
130
231
|
end
|
131
232
|
|
132
|
-
|
133
|
-
|
134
|
-
|
233
|
+
### START VOLATILE METHODS ###
|
234
|
+
|
235
|
+
# _empty? is volatile; and may cause be inaccurate
|
236
|
+
# if called outside @mutex.synchronize {}
|
237
|
+
def _empty?
|
238
|
+
@pool.empty?
|
135
239
|
end
|
136
240
|
|
137
|
-
#
|
138
|
-
#
|
139
|
-
def
|
140
|
-
|
241
|
+
# _space? is volatile; and may be inaccurate
|
242
|
+
# if called outside @mutex.synchronize {}
|
243
|
+
def _space?
|
244
|
+
!_empty?
|
141
245
|
end
|
142
246
|
|
143
|
-
|
144
|
-
|
247
|
+
# Returns the total number of clients in the pool
|
248
|
+
# and checked out. _total_current_size is volatile and
|
249
|
+
# may be inaccurate if called outside @mutex.synchronize {}
|
250
|
+
def _total_current_size
|
251
|
+
(@pool.length + @out.length)
|
145
252
|
end
|
146
253
|
|
147
|
-
#
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
254
|
+
# Return true if we have reached our limit set by the :size option
|
255
|
+
# _less_than_size? is volatile; and may be inaccurate
|
256
|
+
# if called outside @mutex.synchronize {}
|
257
|
+
def _less_than_size?
|
258
|
+
(_total_current_size < @size)
|
259
|
+
end
|
260
|
+
|
261
|
+
# Return true if we have reached our limit set by the :max_size option
|
262
|
+
# _less_than_max? is volatile; and may be inaccurate
|
263
|
+
# if called outside @mutex.synchronize {}
|
264
|
+
def _less_than_max?
|
265
|
+
(_total_current_size < @max_size)
|
266
|
+
end
|
267
|
+
|
268
|
+
# We only want to add a client if the pool is empty in keeping with
|
269
|
+
# a lazy model. If the pool is empty we can only add clients if
|
270
|
+
# never_block? is true or there is room to grow. _add? is volatile;
|
271
|
+
# and may be in accurate if called outside @mutex.synchronize {}
|
272
|
+
def _add?
|
273
|
+
(_empty? && (never_block? || _less_than_size?|| _less_than_max?))
|
156
274
|
end
|
157
275
|
|
158
|
-
#
|
159
|
-
#
|
276
|
+
# Adds a new client to the pool if its allowed
|
277
|
+
# _add is volatile; and may cause threading issues
|
278
|
+
# if called outside @mutex.synchronize {}
|
160
279
|
def _add
|
161
|
-
|
162
|
-
|
163
|
-
nc = new_client
|
280
|
+
return false unless _add?
|
281
|
+
nc = @new_client.call
|
164
282
|
HotTub.logger.info "Adding HotTub client: #{nc.class.name} to pool"
|
165
|
-
@register << nc
|
166
283
|
@pool << nc
|
284
|
+
true
|
167
285
|
end
|
168
|
-
|
286
|
+
|
287
|
+
# Returns true if we have clients in the pool, the pool
|
288
|
+
# is not shutting down, and there is overflow or the first
|
289
|
+
# client in the pool is ready for reaping. _reap_pool? is
|
290
|
+
# volatile; and may be inaccurate if called outside
|
291
|
+
# @mutex.synchronize {}
|
292
|
+
def _reap?
|
293
|
+
(_space? && !@shutdown && (_overflow? || reap_client?(@pool[0])))
|
294
|
+
end
|
295
|
+
|
296
|
+
# Returns true if the pool is greater than the :size option and the
|
297
|
+
# pool has been stagnant long enough to allow for reaping (we don't
|
298
|
+
# want to reap under load). _overflow_expired? is volatile; and may
|
299
|
+
# be inaccurate if called outside @mutex.synchronize {}
|
300
|
+
def _overflow?
|
301
|
+
(@pool.length > @size)
|
302
|
+
end
|
303
|
+
|
304
|
+
### END VOLATILE METHODS ###
|
305
|
+
Timeout = Class.new(Exception) # HotTub::Pool::Timeout
|
169
306
|
end
|
170
|
-
|
307
|
+
|
171
308
|
end
|