hot_tub 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,17 +1,16 @@
1
- source "http://rubygems.org"
1
+ source "https://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in http_hot_tub.gemspec
4
4
  gemspec
5
5
 
6
6
  group :development do
7
7
  platform :ruby do
8
+ gem 'debugger'
8
9
  gem 'eventmachine'
9
10
  gem 'em-http-request', '~> 1.0', :require => 'em-http'
10
11
  gem 'em-synchrony', '~> 1.0', :require => ['em-synchrony', 'em-synchrony/em-http']
11
- gem "excon"
12
12
  end
13
13
  platform :jruby do
14
14
  gem 'jruby-openssl'
15
- gem 'jruby-httpclient'
16
15
  end
17
16
  end
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2010 Joshua T. Mckinney
1
+ Copyright (c) 2010-2013 Joshua T. Mckinney
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # HotTub
2
- A simple thread-safe pooling gem to use with your preferred http library that support
3
- keep-alive.
2
+ A simple thread-safe connection pooling gem. Supports [HTTPClient](https://github.com/nahi/httpclient) (default) and
3
+ [EM-Http-Requests](https://github.com/igrigorik/em-http-request) via [EM-Synchrony](https://github.com/igrigorik/em-synchrony).
4
+ There are a couple Ruby connection pool libraries out there but HotTub differs from most in that its connections are lazy
5
+ (created only when necessary), accomidates libraries that do not clean their dirty connections automatically, and manages unexpected usage increases by opening new connections rather than just blocking or throwings exception (never_block), although never_block can be disabled.
4
6
 
5
7
  ## Installation
6
8
 
@@ -8,16 +10,86 @@ HotTub is available through [Rubygems](https://rubygems.org/gems/hot_tub) and ca
8
10
 
9
11
  $ gem install hot_tub
10
12
 
11
- ## Setup
13
+ ## Usage
14
+
15
+ ### Default (HTTPClient)
16
+
17
+ class MyClass
18
+ @@url = "http://test12345.com"
19
+ @@pool = HotTub::Pool.new({:size => 10})
20
+ def self.fetch_results(url,query={})
21
+ @@pool.run |connection|
22
+ connection.get(@@url,query).body
23
+ end
24
+ end
25
+ end
26
+ MyClass.fetch_results({:foo => "goo"}) # => "Some reponse"
27
+
28
+ ### EM-Http-Request
29
+
30
+ require "em-synchrony"
31
+ require "em-synchrony/em-http"
32
+ class EMClass
33
+ @@pool = HotTub::Pool.new(:size => 12) { EM::HttpRequest.new("http://somewebservice.com") }
34
+ def async_post_results(query = {})
35
+ @@pool.run do |connection|
36
+ connection.aget(:query => results, :keepalive => true)
37
+ end
38
+ end
39
+ end
40
+
41
+ EM.synchrony do {
42
+ EMClass.async_fetch_results({:foo => "goo"})
43
+ EM.stop
44
+ }
45
+
46
+ ### Other
47
+ You can use any libary you want. Close and clean can be defined at initialization with
48
+ lambdas, if they are not defined they are ignored.
49
+
12
50
  class MyClass
13
- @@pool = HotTub::Session.new(HotTub::ExconClient.new('https://google.com'),{:size => 2, :never_block => true})
51
+ @@url = "http://test12345.com"
52
+ @@pool = HotTub::Pool.new({:size => 10, :close => lambda {|clnt| clnt.close}}) { MyHttpLib.new }
53
+ def self.fetch_results(url,query={})
54
+ @@pool.run |connection|
55
+ connection.get(@@url,query).body
56
+ end
57
+ end
58
+ end
59
+
60
+ MyClass.fetch_results({:foo => "goo"}) # => "Some reponse"
14
61
 
15
- def self.fetch_results(query)
16
- @@pool.get(:query => query) # keepalive has be defaulted to true in the client
62
+ ## Sessions
63
+
64
+ HTTPClient has a built in thread-safe sessions feature that allows a single client to access multiple domains.
65
+ Not all clients have a sessions feature, Em-Http-Request clients are initialized to a single domain and while you
66
+ can change paths the client domain cannot change. HotTub::Session allows you create a session object that initializes
67
+ seperate pools for you various domains based on URI.
68
+
69
+ # Assuming EM is running
70
+ require 'hot_tub/clients/em_http_request_client'
71
+ class EMClass
72
+ @@sessons = HotTub::Sessions.new {|url| HotTub::EmHttpRequestClient.new(url,{:connect_timeout => 5}) }
73
+ def async_post_results(query = {})
74
+ @@sessons.run("http://somewebservice.com") do |connection|
75
+ puts connection.get(:query => results).response_header.status
76
+ end
77
+ @@sessons.run("https://someotherwebservice.com") do |connection|
78
+ puts connection.get(:query => results).response_header.status
79
+ end
17
80
  end
18
81
  end
19
82
 
20
- MyClass.fetch_results({:foo => "goo"})
83
+ ## Related
84
+
85
+ * [HTTPClient](https://github.com/nahi/httpclient)
86
+ * [EM-Http-Request](https://github.com/igrigorik/em-http-request)
87
+ * [EM-Synchrony](https://github.com/igrigorik/em-synchrony)
88
+
89
+ ## Other Pooling Gem
90
+
91
+ * [ConnectionPool](https://github.com/mperham/connection_pool)
92
+ * [EM-Synchrony](https://github.com/igrigorik/em-synchrony) has a connection pool feature
21
93
 
22
94
  ## Contributing to HotTub
23
95
 
data/http_hot_tub.gemspec CHANGED
@@ -8,14 +8,17 @@ Gem::Specification.new do |s|
8
8
  s.authors = ["Joshua Mckinney"]
9
9
  s.email = ["joshmckin@gmail.com"]
10
10
  s.homepage = "https://github.com/JoshMcKin/hot_tub"
11
- s.summary = %q{A very simple ruby pool gem}
12
- s.description = %q{A very simple ruby pool gem}
11
+ s.license = "MIT"
12
+ s.summary = %q{A simple thread-safe http connection pooling gem.}
13
+ s.description = %q{A simple thread-safe http connection pooling gem. Http client options include HTTPClient and EM-Http-Request}
13
14
 
14
- s.rubyforge_project = "hot_tub"
15
15
 
16
+ s.rubyforge_project = "hot_tub"
17
+ s.add_runtime_dependency "httpclient"
18
+ s.add_development_dependency "rspec"
19
+
16
20
  s.files = `git ls-files`.split("\n")
17
21
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
22
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
23
  s.require_paths = ["lib"]
20
- s.add_development_dependency "rspec"
21
24
  end
data/lib/hot_tub.rb CHANGED
@@ -2,20 +2,24 @@ require 'thread'
2
2
  require 'timeout'
3
3
  require 'logger'
4
4
  require "hot_tub/version"
5
+ require "hot_tub/pool"
5
6
  require "hot_tub/session"
6
- require "hot_tub/clients/client"
7
- require "hot_tub/clients/em_synchrony_client"
8
- require "hot_tub/clients/excon_client"
9
- require "hot_tub/clients/http_client_client" if RUBY_VERSION < '1.9' or (defined? RUBY_ENGINE and 'jruby' == RUBY_ENGINE)
10
7
 
11
8
  module HotTub
12
- @@logger = Logger.new(STDOUT)
9
+ @@logger = Logger.new(STDOUT)
13
10
  def self.logger
14
11
  @@logger
15
12
  end
16
-
13
+
17
14
  def self.logger=logger
18
15
  @@logger = logger
19
16
  end
20
- end
21
17
 
18
+ def self.em?
19
+ (defined?(EM) && EM::reactor_running?)
20
+ end
21
+
22
+ def self.jruby?
23
+ (defined?(JRUBY_VERSION))
24
+ end
25
+ end
@@ -0,0 +1,188 @@
1
+ require 'httpclient'
2
+ module HotTub
3
+ class Pool
4
+ attr_reader :current_size
5
+ KNOWN_CLIENTS = {
6
+ "HTTPClient" => {
7
+ :close => lambda { |clnt|
8
+ sessions = clnt.instance_variable_get(:@session_manager)
9
+ sessions.reset_all if sessions
10
+ }
11
+ },
12
+ 'EventMachine::HttpConnection' => {
13
+ :close => lambda { |clnt|
14
+ if clnt.conn
15
+ clnt.conn.close_connection
16
+ clnt.instance_variable_set(:@deferred, true)
17
+ end
18
+ },
19
+ :clean => lambda { |clnt|
20
+ if clnt.conn && clnt.conn.error?
21
+ HotTub.logger.info "Sanitizing connection : #{EventMachine::report_connection_error_status(clnt.conn.instance_variable_get(:@signature))}"
22
+ clnt.conn.close_connection
23
+ clnt.instance_variable_set(:@deferred, true)
24
+ end
25
+ clnt
26
+ }
27
+ }
28
+ }
29
+
30
+ # Generic lazy connection pool of HTTP clients
31
+ # The default client is HTTPClient.
32
+ # Clients must respond to :clean, :close, and :run
33
+ #
34
+ # == Example (HTTPClient)
35
+ # pool = HotTub::Pool.new(:size => 25)
36
+ # pool.run {|clnt| clnt.get('http://test.com').body }
37
+ #
38
+ # == Example with different client
39
+ # pool = HotTub::Pool.new { EM::HttpRequest.new("http://somewebservice.com") }
40
+ # pool.run {|clnt| clnt.get(:keepalive => true).body }
41
+ #
42
+ # HotTub::Pool defaults never_block to true, which means if run out of
43
+ # connections simply create a new client to continue operations.
44
+ # The pool size will remain consistent and extra connections will be closed
45
+ # as they are pushed back. If you would like to throw an exception rather than
46
+ # add new connections set :never_block to false; blocking_timeout defaults to 10 seconds.
47
+ #
48
+ # == Example without #never_block (will BlockingTimeout exception)
49
+ # pool = HotTub::Pool.new(:size => 1, :never_block => false, :blocking_timeout => 0.5)
50
+ #
51
+ # begin
52
+ # pool.run {|clnt| clnt.get('http://test.com').body }
53
+ # rescue HotTub::BlockingTimeout => e
54
+ # puts "Our pool ran out: {e}"
55
+ # end
56
+ #
57
+ def initialize(options={},&client_block)
58
+ @client_block = (block_given? ? client_block : lambda { HTTPClient.new })
59
+ @options = {
60
+ :size => 5,
61
+ :never_block => true,
62
+ :blocking_timeout => 10,
63
+ :close => nil,
64
+ :clean => nil
65
+ }.merge(options)
66
+ @pool = []
67
+ @current_size = 0
68
+ @clients = []
69
+ @mutex = (HotTub.em? ? EM::Synchrony::Thread::Mutex.new : Mutex.new)
70
+ @blocking_timeout = @options[:blocking_timeout]
71
+ @never_block = @options[:never_block]
72
+ @size = @options[:size]
73
+ end
74
+
75
+ # Hand off to client.run
76
+ def run(&block)
77
+ clnt = client
78
+ if block_given?
79
+ return block.call(clnt) if clnt
80
+ else
81
+ raise ArgumentError, 'Run requires a block.'
82
+ end
83
+ ensure
84
+ push(clnt) if clnt
85
+ end
86
+
87
+ # Calls close on all connections and reset the pools
88
+ def close_all
89
+ @mutex.synchronize do
90
+ while clnt = @clients.pop
91
+ begin
92
+ close_client(clnt)
93
+ rescue => e
94
+ HotTub.logger.error "There was an error close one of your HotTub::Pool connections: #{e}"
95
+ end
96
+ @pool.delete(clnt)
97
+ end
98
+ @current_size = 0
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ # Returns an instance of the client for this pool.
105
+ def client
106
+ clnt = nil
107
+ alarm = (Time.now + @blocking_timeout)
108
+ # block until we get an available client or raise Timeout::Error
109
+ while clnt.nil?
110
+ raise_alarm if alarm <= Time.now
111
+ clnt = pop
112
+ end
113
+ clean_client(clnt)
114
+ clnt
115
+ end
116
+
117
+ # Attempts to clean the provided client, checking the options first for a clean block
118
+ # then checking the known clients
119
+ def clean_client(clnt)
120
+ return @options[:clean].call(clnt) if @options[:clean] if @options[:clean].is_a?(Proc)
121
+ if settings = KNOWN_CLIENTS[clnt.class.name]
122
+ settings[:clean].call(clnt) if settings[:clean].is_a?(Proc)
123
+ end
124
+ end
125
+
126
+
127
+ # Attempts to close the provided client, checking the options first for a close block
128
+ # then checking the known clients
129
+ def close_client(clnt)
130
+ return @options[:close].call(clnt) if @options[:close] if @options[:close].is_a?(Proc)
131
+ if settings = KNOWN_CLIENTS[clnt.class.name]
132
+ settings[:close].call(clnt) if settings[:close].is_a?(Proc)
133
+ end
134
+ end
135
+
136
+ def raise_alarm
137
+ message = "Could not fetch a free client in time. Consider increasing your pool size for #{@client.class.name}."
138
+ HotTub.logger.error message
139
+ raise BlockingTimeout, message
140
+ end
141
+
142
+ # Safely add client back to pool
143
+ def push(clnt)
144
+ @mutex.synchronize do
145
+ if @pool.length < @size
146
+ @pool << clnt
147
+ else
148
+ @clients.delete(clnt)
149
+ close_client(clnt)
150
+ end
151
+ end
152
+ nil # make sure never return the pool
153
+ end
154
+
155
+ # Safely pull client from pool, adding if allowed
156
+ def pop
157
+ @mutex.synchronize do
158
+ add if add?
159
+ clnt = @pool.pop
160
+ if (clnt.nil? && @never_block)
161
+ HotTub.logger.info "Adding never_block client for #{@client.class.name}."
162
+ clnt = new_client
163
+ end
164
+ clnt
165
+ end
166
+ end
167
+
168
+ # create a new client from base client
169
+ def new_client
170
+ clnt = @client_block.call
171
+ @clients << clnt
172
+ clnt
173
+ end
174
+
175
+ # Only want to add a client if the pool is empty in keeping with
176
+ # a lazy model.
177
+ def add?
178
+ (@pool.length == 0 && @current_size <= @size)
179
+ end
180
+
181
+ def add
182
+ HotTub.logger.info "Adding HotTub client: #{@client.class.name} to pool"
183
+ @current_size += 1
184
+ @pool << new_client
185
+ end
186
+ end
187
+ class BlockingTimeout < StandardError;end
188
+ end
@@ -1,129 +1,70 @@
1
+ require 'uri'
1
2
  module HotTub
2
3
  class Session
3
-
4
- # OPTIONS
5
- # * :size - number of clients/connections for each pool
6
- # * :inactivity_timeout - number of seconds to wait before disconnecting, setting to 0 means the connection will not be closed
7
- # * :pool_timeout - the amount of seconds to block waiting for an available client,
8
- # because this is blocking it should be an extremely short amount of
9
- # time default to 0.5 seconds, if you need more consider enlarging your pool
10
- # instead of raising this number
11
- # :never_block - if set to true, a client will always be returned,
12
- # but the pool size will never grow past that :size option, extra clients are closed
13
- def initialize(client,options={})
14
- @options = {
15
- :size => 5,
16
- :never_block => false,
17
- :blocking_timeout => 0.5
18
- }.merge(options || {})
19
- @pool = []
20
- @pool_data = {:current_size => 0}
21
- @client = client
22
- @mutex = (@client.respond_to?(:mutex) ? @client.mutex : Mutex.new)
23
- end
24
-
25
- # The synchronized pool for all our clients.
4
+
5
+ # A HotTub::Session is a synchronized hash used to separate HotTub::Pools by their domain.
6
+ # EmHttpRequest clients are initialized to a specific domain, so we sometimes need a way to
7
+ # manage multiple pools like when a process need to connect to various AWS resources. Sessions
8
+ # are unnecessary for HTTPClient because the client has its own threads safe sessions object.
9
+ # Example:
26
10
  #
27
- def pool(client=nil)
28
- @mutex.synchronize do
29
- if client
30
- return_client(client)
31
- else
32
- add_client if add_client?
33
- end
34
- @pool
35
- end
36
- end
37
-
38
- # Run the block on the retrieved client. Good for ensure the same client
39
- # is used for multiple requests. For HTTP requests make sure you request has
40
- # keep-alive properly set for your client
41
- # EX:
42
- # @pool = HotTub.new(HotTub::ExconClient.new("https://some_web_site.com"))
43
- # results = []
44
- # @pool.run do |client|
45
- # results.push (client.get(:query => {:foo => "bar"}))
46
- # results.push (client.get(:query => {:bar => "foo"})) # reuse client
11
+ # sessions = HotTub::Session.new(:client_options => {:connect_timeout => 10})
12
+ #
13
+ # sessions.run("https://wwww.yahoo.com") do |conn|
14
+ # p conn.head.response_header.status
47
15
  # end
48
16
  #
49
- def run(&block)
50
- client = fetch
51
- if block_given?
52
- block.call(client)
53
- else
54
- raise ArgumentError, 'Run requires a block.'
55
- end
56
- ensure
57
- pool(client) if client
58
- end
59
-
60
-
61
- # Let pool instance respond to client methods. For HTTP request make sure you
62
- # requests has keep-alive properly set for your client
63
- # EX:
64
- # @pool = HotTub.new(HotTub::ExconClient.new("https://some_web_site.com"))
65
- # r1 = @pool.get(:query => {:foo => "bar"})
66
- # r2 = @pool.get(:query => {:bar => "foo"}) # uses a different client
17
+ # sessions.run("https://wwww.google.com") do |conn|
18
+ # p conn.head.response_header.status
19
+ # end
67
20
  #
68
- def method_missing(method, *args, &blk)
69
- client = fetch
70
- client.send(method,*args,&blk)
71
- ensure
72
- pool(client) if client
73
- end
74
-
75
- private
76
-
77
- def add_client?
78
- (@pool.empty? && (@pool_data[:current_size] < @options[:size]))
79
- end
80
-
81
- def new_client
82
- @client.dup
83
- end
84
-
85
- def add_client
86
- HotTub.logger.info "Adding HotTub client: #{@client.class.name} to pool"
87
- @pool_data[:current_size] += 1
88
- @pool << new_client
89
- @pool
90
- end
91
-
92
- # return a client to the pool
93
- def return_client(client)
94
- if @pool.length < @options[:size]
95
- @pool << client
96
- else
97
- HotTub.logger.info "Closed extra client for #{@client.class.name}."
98
- client.close # Too hot in the hot tub...
99
- end
21
+ # Other client classes
22
+ # If you have your own client class you can use sessions but your client class must initialize similar to
23
+ # EmHttpRequest, accepting a URI and options see: hot_tub/clients/em_http_request_client.rb
24
+ # Example Custom Client:
25
+ #
26
+ # sessions = HotTub::Session.new(:client_class => MyClient, :client_options => {:connect_timeout => 10})
27
+ #
28
+ # sessions.run("https://wwww.yahoo.com") do |conn|
29
+ # p conn.head.response_header.status # => create pool for "https://wwww.yahoo.com"
30
+ # end
31
+ #
32
+ # sessions.run("https://wwww.google.com") do |conn|
33
+ # p conn.head.response_header.status # => create separate pool for "https://wwww.google.com"
34
+ # end
35
+ def initialize(options={},&client_block)
36
+ raise ArgumentError, "HotTub::Sessions requre a block on initialization that accepts a single argument" unless block_given?
37
+ @client_block = client_block
38
+ @options = {
39
+ :size => 5,
40
+ :never_block => true,
41
+ :blocking_timeout => 10,
42
+ :client_class => nil, # EmHttpRequestClient
43
+ :client_options => {}
44
+ }.merge(options || {})
45
+ @sessions = Hash.new
46
+ @mutex = (HotTub.em? ? EM::Synchrony::Thread::Mutex.new : Mutex.new)
100
47
  end
101
-
102
- # Fetches an available client from the pool.
103
- # Hot tubs are not always clean... Make sure we have a good client. The client
104
- # should respond to clean, which checks to make sure the client is still
105
- # viable, and reset if necessary.
106
- def fetch
107
- client = nil
108
- alarm = (Time.now + @options[:blocking_timeout])
109
- # block until we get an available client or raise Timeout::Error
110
- while client.nil?
111
- raise_alarm if alarm <= Time.now
112
- client = pool.shift
113
- if client.nil? && (@options[:never_block])
114
- HotTub.logger.info "Adding never_block client for #{@client.class.name}."
115
- client = new_client
116
- client.mark_temporary
48
+
49
+ # Synchronize access to our key hash
50
+ # expects a url string or URI
51
+ def sessions(url)
52
+ @mutex.synchronize do
53
+ if url.is_a?(String)
54
+ uri = URI(url)
55
+ elsif url.is_a?(URI)
56
+ uri = url
57
+ else
58
+ raise ArgumentError, "you must pass a string or a URI object"
117
59
  end
60
+ @sessions["#{uri.scheme}-#{uri.host}"] ||= HotTub::Pool.new(@options) { @client_block.call(url) }
118
61
  end
119
- client.clean
120
- client
121
62
  end
122
-
123
- def raise_alarm
124
- message = "Could not fetch a free client in time. Consider increasing your pool size for #{@client.class.name}."
125
- HotTub.logger.error message
126
- raise Timeout::Error, message
63
+
64
+ # Hand off to pool.run
65
+ def run(url,&block)
66
+ pool = sessions(url)
67
+ pool.run(&block) if pool
127
68
  end
128
69
  end
129
- end
70
+ end