hot_tub 0.2.6 → 0.3.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 +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 [](https://travis-ci.org/JoshMcKin/hot_tub) [](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
|