hot_tub 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml CHANGED
@@ -6,3 +6,6 @@ rvm:
6
6
  - rbx-19mode
7
7
  - ruby-head
8
8
  - jruby-head
9
+ matrix:
10
+ allow_failures:
11
+ - rvm: 1.9.2
data/Gemfile CHANGED
@@ -5,7 +5,6 @@ gemspec
5
5
  gem 'rake'
6
6
 
7
7
  group :development do
8
- gem 'excon'
9
8
  platform :ruby do
10
9
  gem 'eventmachine'
11
10
  gem 'em-http-request', '~> 1.0', :require => 'em-http'
data/README.md CHANGED
@@ -1,8 +1,23 @@
1
1
  # HotTub [![Build Status](https://travis-ci.org/JoshMcKin/hot_tub.png?branch=master)](https://travis-ci.org/JoshMcKin/hot_tub)
2
- A simple thread-safe connection pooling gem. Out-of-the-box support for [Excon](https://github.com/geemus/excon) and
2
+ A simple thread-safe connection pool and sessions gem. Out-of-the-box support for [Excon](https://github.com/geemus/excon) and
3
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 throwing exceptions (never_block), although never_block can be disabled.
4
+ There are a couple Ruby connection pooling libraries out there but HotTub differs from most with all its features.
5
+
6
+ ## Features
7
+
8
+ ### HotTub::Pool
9
+ * Thread safe
10
+ * Lazy clients/connections (created only when necessary)
11
+ * Can be used with any client library
12
+ * Support for cleaning dirty resources
13
+ * Set to expand pool under load that is eventually reaped back down to set size (never_block), can be disabled
14
+ * Attempts to close clients/connections at_exit
15
+
16
+ ### HotTub::Sessions
17
+ * Thread safe
18
+ * The same api as HotTub::Pool
19
+ * Can be used with HotTub::Pool or any client library
20
+ * Attempts to close clients/connections at_exit
6
21
 
7
22
  ## Installation
8
23
 
@@ -10,105 +25,93 @@ HotTub is available through [Rubygems](https://rubygems.org/gems/hot_tub) and ca
10
25
 
11
26
  $ gem install hot_tub
12
27
 
13
- ## Usage
28
+ ## Rails setup
14
29
 
15
- ### Excon
30
+ Add hot_tub to your gemfile:
31
+
32
+ gem 'hot_tub'
16
33
 
17
- require 'excon'
18
- class MyClass
19
- @@url = "http://test12345.com"
20
- @@pool = HotTub::Pool.new({:size => 10}) { Excon.new("http://somewebservice.com") }
21
- def self.fetch_results(url,query={})
22
- @@pool.run |connection|
23
- connection.get(:query => query).body
24
- end
25
- end
26
- end
27
- MyClass.fetch_results({:foo => "goo"}) # => "Some reponse"
34
+ Run bundle:
35
+
36
+ bundle install
28
37
 
29
- ### EM-Http-Request
38
+ Configure Logger by creating a hot_tub.rb initializer and adding the following:
39
+
40
+ HotTub.logger = Rails.logger
30
41
 
31
- require "em-synchrony"
32
- require "em-synchrony/em-http"
33
- class EMClass
34
- @@pool = HotTub::Pool.new(:size => 12) { EM::HttpRequest.new("http://somewebservice.com") }
35
- def async_post_results(query = {})
36
- @@pool.run do |connection|
37
- connection.aget(:query => results, :keepalive => true)
38
- end
39
- end
40
- end
42
+ # Usage
43
+
44
+ ## HotTub::Pool
41
45
 
46
+ ### EM-Http-Request
47
+ require 'hot_tub'
48
+ require 'em-synchrony'
49
+ require 'em-synchrony/em-http'
42
50
  EM.synchrony do {
43
- EMClass.async_fetch_results({:foo => "goo"})
51
+ pool = HotTub::Pool.new(:size => 12) { EM::HttpRequest.new("http://somewebservice.com") }
52
+ pool.run { |clnt| clnt.aget(:query => results, :keepalive => true) }
44
53
  EM.stop
45
54
  }
46
55
 
47
56
  ### Other
48
- You can use any libary you want. Close and clean can be defined at initialization with
49
- lambdas, if they are not defined they are ignored.
57
+ You can use any library you want with HotTub::Pool, regardless of that clients thread-safety status.
58
+ Close and clean can be defined at initialization with lambdas, if they are not defined they are ignored.
50
59
 
60
+ url = "http://test12345.com"
61
+ pool = HotTub::Pool.new({:size => 10, :close => lambda {|clnt| clnt.close}}) { MyHttpLib.new }
62
+ pool.run { |clnt| clnt.get(@@url,query).body }
63
+
64
+ ## HotTub::Sessions Usage
65
+ HotTub::Sessions are a synchronized hash of clients/pools and are implemented similar HotTub::Pool.
66
+ For example, Excon is thread safe but you set a single url at the client level so sessions
67
+ are handy if you need to access multiple urls but would prefer a single object.
68
+
69
+ require 'hot_tub'
51
70
  require 'excon'
52
- class MyClass
53
- @@url = "http://test12345.com"
54
- @@pool = HotTub::Pool.new({:size => 10, :close => lambda {|clnt| clnt.close}}) { MyHttpLib.new }
55
- def self.fetch_results(url,query={})
56
- @@pool.run |connection|
57
- connection.get(@@url,query).body
58
- end
59
- end
71
+ # Our client block must accept the url argument
72
+ sessions = HotTub::Sessions.new {|url| Excon.new(url) }
73
+
74
+ sessions.run("http://somewebservice.com") do |clnt|
75
+ puts clnt.get(:query => {:some => 'stuff'}).response_header.status
76
+ end
77
+ sessions.run("https://someotherwebservice.com") do |clnt|
78
+ puts clnt.get(:query => {:other => 'stuff'}).response_header.status
79
+ end
80
+ sessions.run("https://127.0.0.1:5252") do |clnt|
81
+ puts clnt.get.response_header.status
60
82
  end
61
83
 
62
- MyClass.fetch_results({:foo => "goo"}) # => "Some reponse"
84
+ ### HotTub::Sessions with HotTub::Pool
85
+ Suppose you have a client that is not thread safe, you can use HotTub::Pool with HotTub::Sessions to get what you need.
86
+
87
+ require 'hot_tub'
88
+ require "em-synchrony"
89
+ require "em-synchrony/em-http"
90
+ # Our client block must accept the url argument
63
91
 
64
- ## Sessions with Pool
65
- Not all clients have a sessions feature, Excon and 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 your various domains based on URI. Options are passed to the pool when each pool is initialized.
92
+ EM.synchrony do {
93
+ sessions = HotTub::Sessions.new {|url| HotTub::Pool.new(:size => 12) { EM::HttpRequest.new(url, :inactivity_timeout => 0) }}
68
94
 
69
- require 'excon'
70
- class MyClass
71
- # Our client block must accept the url argument
72
- @@sessons = HotTub::Sessions.new(:size => 10) {|url| { Excon.new(url) }
73
- def async_post_results(query = {})
74
- @@sessons.run("http://somewebservice.com") do |connection|
75
- puts connection.run(: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
95
+ sessions.run("http://somewebservice.com") do |clnt|
96
+ puts clnt.get(:query => results).response_header.status
80
97
  end
81
- end
82
-
83
- ## Sessions without Pool
84
- If you have a client that is thread safe but does not support sessions you can implement sessions similarly. Excon
85
- is thread safe but you are using a single client, so you may loose preformance with multithreading.
86
-
87
- require 'excon'
88
- class MyClass
89
- # Our client block must accept the url argument
90
- @@sessons = HotTub::Sessions.new(:with_pool => false) {|url| Excon.new(url) }
91
- def async_post_results(query = {})
92
- @@sessons.run("http://somewebservice.com") do |connection|
93
- puts connection.get(:query => results).response_header.status
94
- end
95
- @@sessons.run("https://someotherwebservice.com") do |connection|
96
- puts connection.get(:query => results).response_header.status
97
- end
98
+ sessions.run("https://someotherwebservice.com") do |clnt|
99
+ puts clnt.get(:query => results).response_header.status
98
100
  end
99
- end
101
+ EM.stop
102
+ }
100
103
 
101
104
  ## Related
102
105
 
103
106
  * [EM-Http-Request](https://github.com/igrigorik/em-http-request)
104
107
  * [EM-Synchrony](https://github.com/igrigorik/em-synchrony)
105
- * [Excon](https://github.com/geemus/excon)
106
- * [HTTPClient](https://github.com/nahi/httpclient) A thread safe http client that supports sessions all by itself.
108
+ * [Excon](https://github.com/geemus/excon) Thread safe with its own built in pool.
109
+ * [HTTPClient](https://github.com/nahi/httpclient) A thread safe http client that supports pools and sessions all by itself.
107
110
 
108
111
  ## Other Pooling Gem
109
112
 
110
113
  * [ConnectionPool](https://github.com/mperham/connection_pool)
111
- * [EM-Synchrony](https://github.com/igrigorik/em-synchrony) has a connection pool feature
114
+ * [EM-Synchrony](https://github.com/igrigorik/em-synchrony) has a pool feature
112
115
 
113
116
  ## Contributing to HotTub
114
117
 
data/hot_tub.gemspec CHANGED
@@ -13,7 +13,11 @@ Gem::Specification.new do |s|
13
13
  s.description = %q{A simple thread-safe http connection pooling gem. Http client options include HTTPClient and EM-Http-Request}
14
14
 
15
15
  s.rubyforge_project = "hot_tub"
16
+
16
17
  s.add_development_dependency "rspec"
18
+ s.add_development_dependency "sinatra"
19
+ s.add_development_dependency "puma"
20
+ s.add_development_dependency "excon"
17
21
 
18
22
  s.files = `git ls-files`.split("\n")
19
23
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
@@ -0,0 +1,47 @@
1
+ module HotTub
2
+ module KnownClients
3
+ KNOWN_CLIENTS = {
4
+ "Excon::Connection" => {
5
+ :close => lambda { |clnt| clnt.reset }
6
+ },
7
+ 'EventMachine::HttpConnection' => {
8
+ :close => lambda { |clnt|
9
+ if clnt.conn
10
+ clnt.conn.close_connection
11
+ clnt.instance_variable_set(:@deferred, true)
12
+ 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
+ }
22
+ }
23
+ }
24
+ attr_accessor :options
25
+ # Attempts to clean the provided client, checking the options first for a clean block
26
+ # then checking the known clients
27
+ def clean_client(clnt)
28
+ return @options[:clean].call(clnt) if @options && @options[:clean] && @options[:clean].is_a?(Proc)
29
+ if settings = KNOWN_CLIENTS[clnt.class.name]
30
+ settings[:clean].call(clnt) if settings[:clean].is_a?(Proc)
31
+ end
32
+ end
33
+
34
+ # Attempts to close the provided client, checking the options first for a close block
35
+ # then checking the known clients
36
+ def close_client(clnt)
37
+ return @options[:close].call(clnt) if @options && @options[:close] && @options[:close].is_a?(Proc)
38
+ if settings = KNOWN_CLIENTS[clnt.class.name]
39
+ begin
40
+ settings[:close].call(clnt) if settings[:close].is_a?(Proc)
41
+ rescue => e
42
+ HotTub.logger.error "There was an error close one of your #{self.class.name} connections: #{e}"
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
data/lib/hot_tub/pool.rb CHANGED
@@ -1,49 +1,25 @@
1
1
  module HotTub
2
2
  class Pool
3
- attr_reader :current_size
4
- KNOWN_CLIENTS = {
5
- "Excon::Connection" => {
6
- :close => lambda { |clnt| clnt.reset }
7
- },
8
- 'EventMachine::HttpConnection' => {
9
- :close => lambda { |clnt|
10
- if clnt.conn
11
- clnt.conn.close_connection
12
- clnt.instance_variable_set(:@deferred, true)
13
- end
14
- },
15
- :clean => lambda { |clnt|
16
- if clnt.conn && clnt.conn.error?
17
- HotTub.logger.info "Sanitizing connection : #{EventMachine::report_connection_error_status(clnt.conn.instance_variable_get(:@signature))}"
18
- clnt.conn.close_connection
19
- clnt.instance_variable_set(:@deferred, true)
20
- end
21
- clnt
22
- }
23
- }
24
- }
25
-
3
+ include HotTub::KnownClients
4
+ attr_reader :current_size, :fetching_client, :last_activity
5
+
26
6
  # Thread-safe lazy connection pool
27
7
  #
28
- # == Example Excon
29
- # pool = HotTub::Pool.new(:size => 25) { Excon.new('http://test.com') }
30
- # pool.run {|clnt| clnt.get.body }
31
- #
32
8
  # == Example EM-Http-Request
33
9
  # pool = HotTub::Pool.new { EM::HttpRequest.new("http://somewebservice.com") }
34
10
  # pool.run {|clnt| clnt.get(:keepalive => true).body }
35
11
  #
36
- # HotTub::Pool defaults never_block to true, which means if run out of
12
+ # HotTub::Pool defaults never_block to true, which means if we run out of
37
13
  # connections simply create a new client to continue operations.
38
- # The pool size will remain consistent and extra connections will be closed
39
- # as they are pushed back. If you would like to throw an exception rather than
40
- # add new connections set :never_block to false; blocking_timeout defaults to 10 seconds.
14
+ # The pool will grow and extra connections will be resued until activity dies down.
15
+ # If you would like to block and possibly throw an exception rather than temporarily
16
+ # grow the set :size, set :never_block to false; blocking_timeout defaults to 10 seconds.
41
17
  #
42
18
  # == Example without #never_block (will BlockingTimeout exception)
43
- # pool = HotTub::Pool.new(:size => 1, :never_block => false, :blocking_timeout => 0.5) { Excon.new('http://test.com') }
19
+ # pool = HotTub::Pool.new(:size => 1, :never_block => false, :blocking_timeout => 0.5) { EM::HttpRequest.new("http://somewebservice.com") }
44
20
  #
45
21
  # begin
46
- # pool.run {|clnt| clnt.get.body }
22
+ # pool.run {|clnt| clnt.get(:keepalive => true).body }
47
23
  # rescue HotTub::BlockingTimeout => e
48
24
  # puts "Our pool ran out: {e}"
49
25
  # end
@@ -61,8 +37,9 @@ module HotTub
61
37
  }.merge(options)
62
38
  @pool = []
63
39
  @current_size = 0
64
- @clients = []
65
- @mutex = (HotTub.em? ? EM::Synchrony::Thread::Mutex.new : Mutex.new)
40
+ @mutex = (HotTub.em_synchrony? ? EM::Synchrony::Thread::Mutex.new : Mutex.new)
41
+ @last_activity = Time.now
42
+ @fetching_client = false
66
43
  end
67
44
 
68
45
  # Hand off to client.run
@@ -80,13 +57,12 @@ module HotTub
80
57
  # Calls close on all connections and reset the pools
81
58
  def close_all
82
59
  @mutex.synchronize do
83
- while clnt = @clients.pop
60
+ while clnt = @pool.pop
84
61
  begin
85
62
  close_client(clnt)
86
63
  rescue => e
87
64
  HotTub.logger.error "There was an error close one of your HotTub::Pool connections: #{e}"
88
65
  end
89
- @pool.delete(clnt)
90
66
  end
91
67
  @current_size = 0
92
68
  end
@@ -107,26 +83,8 @@ module HotTub
107
83
  clnt
108
84
  end
109
85
 
110
- # Attempts to clean the provided client, checking the options first for a clean block
111
- # then checking the known clients
112
- def clean_client(clnt)
113
- return @options[:clean].call(clnt) if @options[:clean] if @options[:clean].is_a?(Proc)
114
- if settings = KNOWN_CLIENTS[clnt.class.name]
115
- settings[:clean].call(clnt) if settings[:clean].is_a?(Proc)
116
- end
117
- end
118
-
119
- # Attempts to close the provided client, checking the options first for a close block
120
- # then checking the known clients
121
- def close_client(clnt)
122
- return @options[:close].call(clnt) if @options[:close] if @options[:close].is_a?(Proc)
123
- if settings = KNOWN_CLIENTS[clnt.class.name]
124
- settings[:close].call(clnt) if settings[:close].is_a?(Proc)
125
- end
126
- end
127
-
128
86
  def raise_alarm
129
- message = "Could not fetch a free client in time. Consider increasing your pool size for #{@client.class.name}."
87
+ message = "Could not fetch a free client in time. Consider increasing your pool size."
130
88
  HotTub.logger.error message
131
89
  raise BlockingTimeout, message
132
90
  end
@@ -134,47 +92,59 @@ module HotTub
134
92
  # Safely add client back to pool
135
93
  def push(clnt)
136
94
  @mutex.synchronize do
137
- if @pool.length < @options[:size]
138
- @pool << clnt
139
- else
140
- @clients.delete(clnt)
141
- close_client(clnt)
142
- end
95
+ @pool << clnt
143
96
  end
144
97
  nil # make sure never return the pool
145
98
  end
146
99
 
147
100
  # Safely pull client from pool, adding if allowed
148
101
  def pop
102
+ @fetching_client = true # kill reap_pool
149
103
  @mutex.synchronize do
150
104
  add if add?
151
- clnt = @pool.pop
105
+ clnt = @pool.pop # get warm connection
152
106
  if (clnt.nil? && @options[:never_block])
153
- HotTub.logger.info "Adding never_block client for #{@client.class.name}."
154
- clnt = new_client
107
+ add
108
+ clnt = @pool.pop
155
109
  end
110
+ @fetching_client = false
156
111
  clnt
157
112
  end
113
+ ensure
114
+ reap_pool if reap_pool?
158
115
  end
159
116
 
160
117
  # create a new client from base client
161
118
  def new_client
162
- clnt = @client_block.call
163
- @clients << clnt
164
- clnt
119
+ @client_block.call
165
120
  end
166
121
 
167
122
  # Only want to add a client if the pool is empty in keeping with
168
123
  # a lazy model.
169
124
  def add?
170
- (@pool.length == 0 && @current_size <= @options[:size])
125
+ (@pool.length == 0 && (@options[:size] > @current_size))
171
126
  end
172
127
 
173
128
  def add
174
129
  HotTub.logger.info "Adding HotTub client: #{@client.class.name} to pool"
130
+ @last_activity = Time.now
175
131
  @current_size += 1
176
132
  @pool << new_client
177
133
  end
134
+
135
+ def reap_pool?
136
+ (!@fetching_client && (@current_size > @options[:size]) && ((@last_activity + (600)) < Time.now))
137
+ end
138
+
139
+ # Remove extra connections from front of pool
140
+ def reap_pool
141
+ @mutex.synchronize do
142
+ if reap_pool? && clnt = @pool.shift
143
+ @current_size -= 1
144
+ close_client(clnt)
145
+ end
146
+ end
147
+ end
178
148
  end
179
149
  class BlockingTimeout < StandardError;end
180
150
  end
@@ -1,42 +1,42 @@
1
1
  require 'uri'
2
2
  module HotTub
3
3
  class Session
4
-
4
+ include HotTub::KnownClients
5
5
  # A HotTub::Session is a synchronized hash used to separate pools/clients by their domain.
6
6
  # Excon and 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.
7
+ # manage multiple pools like when a process need to connect to various AWS resources. You can use any client
8
+ # you choose, but make sure you client is threadsafe.
8
9
  # Example:
9
10
  #
10
- # sessions = HotTub::Session.new(:client_options => {:connect_timeout => 10}) { |url| Excon.new(url) }
11
+ # sessions = HotTub::Session.new { |url| Excon.new(url) }
11
12
  #
12
13
  # sessions.run("http://wwww.yahoo.com") do |conn|
13
- # p conn.head.response_header.status
14
+ # p conn.head.status
14
15
  # end
15
16
  #
16
17
  # sessions.run("https://wwww.google.com") do |conn|
17
- # p conn.head.response_header.status
18
+ # p conn.head.status
18
19
  # end
19
20
  #
20
- # Other client classes
21
- # If you have your own client class you can use sessions but your client class must initialize similar to
22
- # EmHttpRequest, accepting a URI and options see: hot_tub/clients/em_http_request_client.rb
23
- # Example Custom Client:
21
+ # Example with Pool:
24
22
  #
25
- # sessions = HotTub::Session.new({:never_block => false}) { |url| Excon.new(url) }
23
+ # sessions = HotTub::Pool.new(:size => 12) { EM::HttpRequest.new("http://somewebservice.com") }
26
24
  #
27
- # sessions.run("https://wwww.yahoo.com") do |conn|
28
- # p conn.head.response_header.status # => create pool for "https://wwww.yahoo.com"
25
+ # sessions.run("http://wwww.yahoo.com") do |conn|
26
+ # p conn.head.response_header.status
29
27
  # end
30
28
  #
31
29
  # sessions.run("https://wwww.google.com") do |conn|
32
- # p conn.head.response_header.status # => create separate pool for "https://wwww.google.com"
30
+ # p conn.head.response_header.status
33
31
  # end
32
+ #
34
33
  def initialize(options={},&client_block)
35
34
  raise ArgumentError, "HotTub::Sessions requre a block on initialization that accepts a single argument" unless block_given?
36
- @options = {:with_pool => true}.merge(options)
35
+ at_exit { close_all } # close connections at exit
36
+ @options = options || {}
37
37
  @client_block = client_block
38
38
  @sessions = Hash.new
39
- @mutex = (HotTub.em? ? EM::Synchrony::Thread::Mutex.new : Mutex.new)
39
+ @mutex = (HotTub.em_synchrony? ? EM::Synchrony::Thread::Mutex.new : Mutex.new)
40
40
  end
41
41
 
42
42
  # Synchronizes initialization of our sessions
@@ -45,20 +45,35 @@ module HotTub
45
45
  key = to_key(url)
46
46
  return @sessions[key] if @sessions[key]
47
47
  @mutex.synchronize do
48
- if @options[:with_pool]
49
- @sessions[key] ||= HotTub::Pool.new(@options) { @client_block.call(url) }
50
- else
51
- @sessions[key] ||= @client_block.call(url)
52
- end
48
+ @sessions[key] = @client_block.call(url) if @sessions[key].nil?
49
+ @sessions[key]
53
50
  end
54
51
  end
55
52
 
56
53
  def run(url,&block)
57
54
  session = sessions(url)
58
- return session.run(&block) if @options[:with_pool]
55
+ return session.run(&block) if session.is_a?(HotTub::Pool)
59
56
  block.call(sessions(url))
60
57
  end
61
58
 
59
+ # Calls close on all pools/clients in sessions
60
+ def close_all
61
+ @sessions.each do |key,clnt|
62
+ if clnt.is_a?(HotTub::Pool)
63
+ clnt.close_all
64
+ else
65
+ begin
66
+ close_client(clnt)
67
+ rescue => e
68
+ HotTub.logger.error "There was an error close one of your HotTub::Session clients: #{e}"
69
+ end
70
+ end
71
+ @mutex.synchronize do
72
+ @sessions[key] = nil
73
+ end
74
+ end
75
+ end
76
+
62
77
  private
63
78
 
64
79
  def to_key(url)
@@ -1,3 +1,3 @@
1
1
  module HotTub
2
- VERSION = "0.2.0"
2
+ VERSION = "0.2.1"
3
3
  end
data/lib/hot_tub.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'thread'
2
- require 'timeout'
3
2
  require 'logger'
4
3
  require "hot_tub/version"
4
+ require "hot_tub/known_clients"
5
5
  require "hot_tub/pool"
6
6
  require "hot_tub/session"
7
7
 
@@ -19,6 +19,10 @@ module HotTub
19
19
  (defined?(EM) && EM::reactor_running?)
20
20
  end
21
21
 
22
+ def self.em_synchrony?
23
+ (self.em? && defined?(EM::Synchrony))
24
+ end
25
+
22
26
  def self.jruby?
23
27
  (defined?(JRUBY_VERSION))
24
28
  end
@@ -0,0 +1,33 @@
1
+ class MocClient
2
+ def initialize(url=nil,options={})
3
+ @close = false
4
+ @clean = false
5
+ end
6
+
7
+ # Perform an IO
8
+ def get
9
+ return `sleep #{self.class.sleep_time}; echo "that was slow IO"`
10
+ end
11
+
12
+ def close
13
+ @close = true
14
+ end
15
+
16
+ def closed?
17
+ @close == true
18
+ end
19
+
20
+ def clean
21
+ @clean = true
22
+ end
23
+
24
+ def cleaned?
25
+ @clean == true
26
+ end
27
+
28
+ class << self
29
+ def sleep_time
30
+ 0.2
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,38 @@
1
+ require 'sinatra'
2
+ require 'puma'
3
+ module HotTub
4
+ class Server < Sinatra::Base
5
+
6
+ def self.run
7
+ @events = Puma::Events.new STDOUT, STDERR
8
+ @server = Puma::Server.new HotTub::Server.new, @events
9
+ @server.min_threads = 10
10
+ @server.max_threads = 100
11
+ @server.add_tcp_listener '127.0.0.1', 9595
12
+ @server.run
13
+ end
14
+
15
+ set :server, 'puma'
16
+ set :port, 9595
17
+
18
+ get '/data/:amount' do |amount|
19
+ (('x' * amount.to_i ) << Random.new.rand(0..999999).to_s)
20
+ end
21
+
22
+ def self.teardown
23
+ @server.stop(true) if @server
24
+ end
25
+
26
+ def self.size
27
+ 10_000
28
+ end
29
+
30
+ def self.path
31
+ '/data/' << size.to_s
32
+ end
33
+
34
+ def self.url
35
+ 'http://127.0.0.1:9595' << path
36
+ end
37
+ end
38
+ end
data/spec/pool_spec.rb CHANGED
@@ -87,10 +87,8 @@ describe HotTub::Pool do
87
87
  end
88
88
  it "should reset pool" do
89
89
  @pool.current_size.should eql(5)
90
- @pool.instance_variable_get(:@clients).length.should eql(5)
91
90
  @pool.instance_variable_get(:@pool).length.should eql(5)
92
91
  @pool.close_all
93
- @pool.instance_variable_get(:@clients).length.should eql(0)
94
92
  @pool.instance_variable_get(:@pool).length.should eql(0)
95
93
  @pool.current_size.should eql(0)
96
94
  end
@@ -136,9 +134,57 @@ describe HotTub::Pool do
136
134
  end
137
135
  end
138
136
 
137
+ context ':never_block' do
138
+ context 'is true' do
139
+ it "should add connections to pool as necessary" do
140
+ pool = HotTub::Pool.new({:size => 1}) { MocClient.new }
141
+ threads = []
142
+ 5.times.each do
143
+ threads << Thread.new do
144
+ pool.run{|connection| connection.get }
145
+ end
146
+ end
147
+ sleep(0.01)
148
+ threads.each do |t|
149
+ t.join
150
+ end
151
+ (pool.current_size > 1).should be_true
152
+ end
153
+ end
154
+ context 'is false' do
155
+ it "should not add connections to pool beyond specified size" do
156
+ pool = HotTub::Pool.new({:size => 1, :never_block => false, :blocking_timeout => 10}) { MocClient.new }
157
+ threads = []
158
+ 3.times.each do
159
+ threads << Thread.new do
160
+ pool.run{|connection| connection.get }
161
+ end
162
+ end
163
+ sleep(0.01)
164
+ threads.each do |t|
165
+ t.join
166
+ end
167
+ pool.current_size.should eql(1)
168
+ end
169
+ end
170
+ end
171
+
172
+ describe '#reap_pool' do
173
+ context 'current_size is greater than :size' do
174
+ it "should remove a connection from the pool" do
175
+ pool = HotTub::Pool.new({:size => 1}) { MocClient.new }
176
+ pool.instance_variable_set(:@last_activity,(Time.now - 601))
177
+ pool.instance_variable_set(:@pool, [MocClient.new,MocClient.new])
178
+ pool.instance_variable_set(:@current_size,2)
179
+ pool.send(:reap_pool)
180
+ pool.current_size.should eql(1)
181
+ pool.instance_variable_get(:@pool).length.should eql(1)
182
+ end
183
+ end
184
+ end
185
+
139
186
  context 'thread safety' do
140
187
  it "should work" do
141
- url = "https://www.google.com/"
142
188
  pool = HotTub::Pool.new({:size => 10}) { MocClient.new }
143
189
  failed = false
144
190
  lambda {
@@ -153,7 +199,7 @@ describe HotTub::Pool do
153
199
  t.join
154
200
  end
155
201
  }.should_not raise_error
156
- pool.instance_variable_get(:@pool).length.should eql(10) # make sure work got done
202
+ (pool.instance_variable_get(:@pool).length >= 10).should be_true # make sure work got done
157
203
  end
158
204
  end
159
205
 
@@ -169,9 +215,9 @@ describe HotTub::Pool do
169
215
  end
170
216
  end
171
217
 
172
- context 'Excon' do
218
+ context 'Excon' do # Excon has its own pool, but just need to test with a real non-EM library
173
219
  before(:each) do
174
- @pool = HotTub::Pool.new(:size => 10) { Excon.new('https://www.google.com')}
220
+ @pool = HotTub::Pool.new(:size => 10) { Excon.new(HotTub::Server.url)}
175
221
  end
176
222
  it "should work" do
177
223
  result = nil
@@ -180,11 +226,23 @@ describe HotTub::Pool do
180
226
  end
181
227
  context 'threads' do
182
228
  it "should work" do
183
- url = "https://www.google.com/"
184
229
  failed = false
185
230
  threads = []
186
231
  lambda {
187
- 20.times.each do
232
+ 15.times.each do
233
+ threads << Thread.new do
234
+ @pool.run{|connection| Thread.current[:status] = connection.head.status }
235
+ end
236
+ end
237
+ sleep(0.01)
238
+ threads.each do |t|
239
+ t.join
240
+ end
241
+ }.should_not raise_error
242
+ # Reuse and run reaper
243
+ @pool.instance_variable_set(:@last_activity,(Time.now - 601))
244
+ lambda {
245
+ 10.times.each do
188
246
  threads << Thread.new do
189
247
  @pool.run{|connection| Thread.current[:status] = connection.head.status }
190
248
  end
@@ -194,9 +252,9 @@ describe HotTub::Pool do
194
252
  t.join
195
253
  end
196
254
  }.should_not raise_error
197
- @pool.instance_variable_get(:@pool).length.should eql(10) # make sure work got done
255
+ (@pool.instance_variable_get(:@pool).length == 10).should be_true # make sure work got done
198
256
  results = threads.collect{ |t| t[:status]}
199
- results.length.should eql(20) # make sure all threads are present
257
+ results.length.should eql(25) # make sure all threads are present
200
258
  results.uniq.should eql([200]) # make sure all returned status 200
201
259
  end
202
260
  end
@@ -205,7 +263,7 @@ describe HotTub::Pool do
205
263
  unless HotTub.jruby?
206
264
  context 'EM:HTTPRequest' do
207
265
  before(:each) do
208
- @url = "https://www.google.com"
266
+ @url = HotTub::Server.url
209
267
  end
210
268
 
211
269
  it "should work" do
@@ -216,6 +274,7 @@ describe HotTub::Pool do
216
274
  c.run { |conn| status << conn.ahead(:keepalive => true).response_header.status}
217
275
  c.run { |conn| status << conn.head(:keepalive => true).response_header.status}
218
276
  status.should eql([200,0,200])
277
+ c.close_all
219
278
  EM.stop
220
279
  end
221
280
  end
@@ -223,14 +282,15 @@ describe HotTub::Pool do
223
282
  context 'fibers' do
224
283
  it "should work" do
225
284
  EM.synchrony do
226
- url = "https://www.google.com/"
227
285
  pool = HotTub::Pool.new({:size => 5}) {EM::HttpRequest.new(@url)}
228
286
  failed = false
229
287
  fibers = []
230
288
  lambda {
231
289
  10.times.each do
232
290
  fibers << Fiber.new do
233
- pool.run{|connection| failed = true unless connection.head(:keepalive => true).response_header.status == 200}
291
+ pool.run{|connection|
292
+ s = connection.head(:keepalive => true).response_header.status
293
+ failed = true unless s == 200}
234
294
  end
235
295
  end
236
296
  fibers.each do |f|
@@ -248,8 +308,9 @@ describe HotTub::Pool do
248
308
  end
249
309
  end
250
310
  }.should_not raise_error
251
- pool.instance_variable_get(:@pool).length.should eql(5) #make sure work got done
311
+ (pool.instance_variable_get(:@pool).length >= 5).should be_true #make sure work got done
252
312
  failed.should be_false # Make sure our requests worked
313
+ pool.close_all
253
314
  EM.stop
254
315
  end
255
316
  end
data/spec/session_spec.rb CHANGED
@@ -2,6 +2,10 @@ require 'spec_helper'
2
2
  require 'hot_tub/session'
3
3
  require 'uri'
4
4
  require 'time'
5
+ unless HotTub.jruby?
6
+ require "em-synchrony"
7
+ require "em-synchrony/em-http"
8
+ end
5
9
  describe HotTub::Session do
6
10
 
7
11
  context 'initialized without a block' do
@@ -16,14 +20,6 @@ describe HotTub::Session do
16
20
  @sessions = HotTub::Session.new { |url| MocClient.new(url) }
17
21
  end
18
22
 
19
- context 'default options' do
20
- describe ':with_pool' do
21
- it "with_pool should be true" do
22
- @sessions.instance_variable_get(:@options)[:with_pool].should be_true
23
- end
24
- end
25
- end
26
-
27
23
  describe '#to_url' do
28
24
  context "passed URL string" do
29
25
  it "should return key with URI scheme-domain" do
@@ -48,32 +44,23 @@ describe HotTub::Session do
48
44
  end
49
45
 
50
46
  describe '#sessions' do
51
-
52
- context ':with_pool is true (default)' do
53
- it "should add a new pool for the url" do
54
- @sessions.sessions(@url)
55
- sessions = @sessions.instance_variable_get(:@sessions)
56
- sessions.length.should eql(1)
57
- sessions.first[1].should be_a(HotTub::Pool)
58
- end
59
-
60
- it "should pass options to pool" do
61
- with_pool_options = HotTub::Session.new(:size => 13, :never_block => false) { |url| MocClient.new(url) }
47
+ context 'HotTub::Pool as client' do
48
+ it "should add a new client for the url" do
49
+ with_pool_options = HotTub::Session.new { |url| HotTub::Pool.new(:size => 13) { MocClient.new(url) } }
62
50
  with_pool_options.sessions(@url)
63
51
  sessions = with_pool_options.instance_variable_get(:@sessions)
64
- pool_options = sessions.first[1].instance_variable_get(:@options)
65
- pool_options[:size].should eql(13)
66
- pool_options[:never_block].should be_false
52
+ sessions.length.should eql(1)
53
+ sessions.first[1].should be_a(HotTub::Pool)
67
54
  end
68
55
  end
69
56
 
70
- context ':with_pool is false' do
57
+ context 'other clients' do
71
58
  it "should add a new client for the url" do
72
- no_pool = HotTub::Session.new(:with_pool => false) { |url| MocClient.new(url) }
59
+ no_pool = HotTub::Session.new { |url| Excon.new(url) }
73
60
  no_pool.sessions(@url)
74
61
  sessions = no_pool.instance_variable_get(:@sessions)
75
62
  sessions.length.should eql(1)
76
- sessions.first[1].should be_a(MocClient)
63
+ sessions.first[1].should be_a(Excon::Connection)
77
64
  end
78
65
  end
79
66
 
@@ -81,14 +68,14 @@ describe HotTub::Session do
81
68
  it "should set key with URI scheme-domain" do
82
69
  @sessions.sessions(@url)
83
70
  sessions = @sessions.instance_variable_get(:@sessions)
84
- sessions["#{@uri.scheme}-#{@uri.host}"].should be_a(HotTub::Pool)
71
+ sessions["#{@uri.scheme}-#{@uri.host}"].should be_a(MocClient)
85
72
  end
86
73
  end
87
74
  context "passed URI" do
88
75
  it "should set key with URI scheme-domain" do
89
76
  @sessions.sessions(@uri)
90
77
  sessions = @sessions.instance_variable_get(:@sessions)
91
- sessions["#{@uri.scheme}-#{@uri.host}"].should be_a(HotTub::Pool)
78
+ sessions["#{@uri.scheme}-#{@uri.host}"].should be_a(MocClient)
92
79
  end
93
80
  end
94
81
  end
@@ -105,11 +92,11 @@ describe HotTub::Session do
105
92
  end
106
93
  end
107
94
 
108
- context 'thread safety' do
95
+ context 'threads' do
109
96
  it "should work" do
110
- url = "https://www.somewebsite.com/"
111
- url2 = "http://www.someotherwebsit.com/"
112
- session = HotTub::Session.new { |url| MocClient.new(url)}
97
+ url = HotTub::Server.url
98
+ url2 = "http://www.yahoo.com/"
99
+ session = HotTub::Session.new { |url| Excon.new(url)}
113
100
  failed = false
114
101
  start_time = Time.now
115
102
  stop_time = nil
@@ -136,5 +123,45 @@ describe HotTub::Session do
136
123
  session.instance_variable_get(:@sessions).keys.length.should eql(2) # make sure sessions were created
137
124
  end
138
125
  end
126
+
127
+ unless HotTub.jruby?
128
+ context 'fibers' do
129
+ it "should work" do
130
+ EM.synchrony do
131
+ sessions = HotTub::Session.new {|url| HotTub::Pool.new({:size => 5}) {EM::HttpRequest.new(url)}}
132
+ failed = false
133
+ fibers = []
134
+ lambda {
135
+ 10.times.each do
136
+ fibers << Fiber.new do
137
+ sessions.run(@url) {|connection|
138
+ s = connection.head(:keepalive => true).response_header.status
139
+ failed = true unless s == 200}
140
+ end
141
+ end
142
+ fibers.each do |f|
143
+ f.resume
144
+ end
145
+ loop do
146
+ done = true
147
+ fibers.each do |f|
148
+ done = false if f.alive?
149
+ end
150
+ if done
151
+ break
152
+ else
153
+ EM::Synchrony.sleep(0.01)
154
+ end
155
+ end
156
+ }.should_not raise_error
157
+ sessions.instance_variable_get(:@sessions).keys.length.should eql(1)
158
+ (sessions.sessions(@url).instance_variable_get(:@pool).length >= 5).should be_true #make sure work got done
159
+ failed.should be_false # Make sure our requests worked
160
+ sessions.close_all
161
+ EM.stop
162
+ end
163
+ end
164
+ end
165
+ end
139
166
  end
140
167
  end
data/spec/spec_helper.rb CHANGED
@@ -3,45 +3,20 @@ require 'rspec'
3
3
  require 'bundler/setup'
4
4
  require 'logger'
5
5
  require 'excon'
6
+ require 'helpers/moc_client'
7
+ require 'helpers/server'
8
+ require 'net/https'
9
+
6
10
  # Requires supporting files with custom matchers and macros, etc,
7
11
  # in ./support/ and its subdirectories.
8
12
  #Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
9
13
  HotTub.logger.level = Logger::ERROR
10
14
 
11
15
  RSpec.configure do |config|
12
-
13
- end
14
-
15
- class MocClient
16
- def initialize(url=nil,options={})
17
- @close = false
18
- @clean = false
19
- end
20
-
21
- # Perform an IO
22
- def get
23
- return `sleep #{self.class.sleep_time}; echo "that was slow IO"`
24
- end
25
-
26
- def close
27
- @close = true
28
- end
29
-
30
- def closed?
31
- @close == true
32
- end
33
-
34
- def clean
35
- @clean = true
36
- end
37
-
38
- def cleaned?
39
- @clean == true
40
- end
41
-
42
- class << self
43
- def sleep_time
44
- 0.5
45
- end
46
- end
47
- end
16
+ config.before(:suite) do
17
+ HotTub::Server.run
18
+ end
19
+ config.after(:suite) do
20
+ HotTub::Server.teardown
21
+ end
22
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hot_tub
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-04-03 00:00:00.000000000 Z
12
+ date: 2013-04-04 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
@@ -27,6 +27,54 @@ dependencies:
27
27
  - - ! '>='
28
28
  - !ruby/object:Gem::Version
29
29
  version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: sinatra
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: puma
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: excon
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
30
78
  description: A simple thread-safe http connection pooling gem. Http client options
31
79
  include HTTPClient and EM-Http-Request
32
80
  email:
@@ -44,9 +92,12 @@ files:
44
92
  - Rakefile
45
93
  - hot_tub.gemspec
46
94
  - lib/hot_tub.rb
95
+ - lib/hot_tub/known_clients.rb
47
96
  - lib/hot_tub/pool.rb
48
97
  - lib/hot_tub/session.rb
49
98
  - lib/hot_tub/version.rb
99
+ - spec/helpers/moc_client.rb
100
+ - spec/helpers/server.rb
50
101
  - spec/pool_spec.rb
51
102
  - spec/session_spec.rb
52
103
  - spec/spec_helper.rb
@@ -65,7 +116,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
65
116
  version: '0'
66
117
  segments:
67
118
  - 0
68
- hash: 1359205392366777656
119
+ hash: -1949508601036359576
69
120
  required_rubygems_version: !ruby/object:Gem::Requirement
70
121
  none: false
71
122
  requirements:
@@ -74,7 +125,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
74
125
  version: '0'
75
126
  segments:
76
127
  - 0
77
- hash: 1359205392366777656
128
+ hash: -1949508601036359576
78
129
  requirements: []
79
130
  rubyforge_project: hot_tub
80
131
  rubygems_version: 1.8.25
@@ -82,6 +133,8 @@ signing_key:
82
133
  specification_version: 3
83
134
  summary: A simple thread-safe http connection pooling gem.
84
135
  test_files:
136
+ - spec/helpers/moc_client.rb
137
+ - spec/helpers/server.rb
85
138
  - spec/pool_spec.rb
86
139
  - spec/session_spec.rb
87
140
  - spec/spec_helper.rb