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 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
@@ -1,13 +1,13 @@
1
1
  language: ruby
2
2
  rvm:
3
+ - 2.1.1
4
+ - 2.1.0
3
5
  - 2.0.0
4
6
  - 1.9.3
5
- - 1.9.2
6
- - jruby-19mode
7
- - rbx-19mode
7
+ - rbx-2
8
+ - rbx
8
9
  - ruby-head
9
10
  - jruby-head
10
11
  matrix:
11
12
  allow_failures:
12
- - rvm: jruby-head
13
- - rvm: ruby-head
13
+ - rvm: rbx
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
- HEAD
4
+ Head
5
5
  =======
6
6
 
7
- - Rename unsafe methods with leading "_"
8
- - Add register to pool to track all connections
9
- - EM.add_shutdown_hook for EM connections
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
@@ -1,4 +1,4 @@
1
- Copyright (c) 2010-2013 Joshua T. Mckinney
1
+ Copyright (c) 2010-2014 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,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 simple thread-safe connection pool and sessions gem. Out-of-the-box support for [Excon](https://github.com/geemus/excon) and
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
- * Thread safe / Fiber safe (with EM::HttpRequest + EM::Synchrony)
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 on shutdown
15
-
16
- ### HotTub::Session
17
- * Thread safe / Fiber safe (with EM::HttpRequest + EM::Synchrony)
18
- * The same api as HotTub::Pool
19
- * Can be used with HotTub::Pool or any client library
20
- * Attempts to close clients/connections on shutdown
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 a hot_tub.rb initializer and adding the following:
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::Pool
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 'em-synchrony'
53
- require 'em-synchrony/em-http'
54
- EM.synchrony do {
55
- pool = HotTub::Pool.new(:size => 12) { EM::HttpRequest.new("http://somewebservice.com") }
56
- # Make sure we set :keepalive as true
57
- pool.run { |clnt| clnt.aget(:query => results, :keepalive => true) }
58
- EM.stop
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. Close and clean can be defined at initialization
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
- pool = HotTub::Pool.new({:size => 10, :close => lambda {|clnt| clnt.close}}) { MyHttpLib.new }
67
- pool.run { |clnt| clnt.get(url,query).body }
68
-
69
- ## HotTub::Session
70
- HotTub::Sessions are a synchronized hash of clients/pools and are implemented similar HotTub::Pool.
71
- For example, Excon is thread safe but you set a single url at the client level so sessions
72
- are handy if you need to access multiple urls but would prefer a single object.
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::Session.new {|url| Excon.new(url) }
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
- ## Other Pooling Gem
142
+ ## Dependencies
118
143
 
119
- * [ConnectionPool](https://github.com/mperham/connection_pool)
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 simple thread-safe http connection pooling gem.}
13
- s.description = %q{A simple thread-safe http connection pooling gem. Out-of-the-box support for Excon and EM-Http-Request}
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/session"
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.hot_at_exit with_em=false, &blk
35
- if with_em
36
- EM.add_shutdown_hook &blk
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
- at_exit &blk
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
- 'EventMachine::HttpConnection' => {
7
+ "Net::HTTP" => {
8
8
  :close => lambda { |clnt|
9
- if clnt.conn
10
- clnt.conn.close_connection
11
- clnt.instance_variable_set(:@deferred, true)
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
- 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)
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
- 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
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
- attr_reader :current_size, :fetching_client, :last_activity
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 EM-Http-Request
9
- # pool = HotTub::Pool.new { EM::HttpRequest.new("http://somewebservice.com") }
10
- # pool.run {|clnt| clnt.get(:keepalive => true).body }
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
- # connections simply create a new client to continue operations.
14
- # The pool will grow and extra connections will be resued until activity dies down.
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; blocking_timeout defaults to 10 seconds.
31
+ # grow the set :size, set :never_block to false; wait_timeout defaults to 10 seconds.
17
32
  #
18
- # == Example without #never_block (will BlockingTimeout exception)
19
- # pool = HotTub::Pool.new(:size => 1, :never_block => false, :blocking_timeout => 0.5) { EM::HttpRequest.new("http://somewebservice.com") }
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.get(:keepalive => true).body }
23
- # rescue HotTub::BlockingTimeout => e
24
- # puts "Our pool ran out: {e}"
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
- def initialize(options={},&client_block)
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
- @client_block = client_block
30
- @options = {
31
- :size => 5,
32
- :never_block => true, # Return new client if we run out
33
- :blocking_timeout => 10, # in seconds
34
- :close => nil, # => lambda {|clnt| clnt.close}
35
- :clean => nil # => lambda {|clnt| clnt.clean}
36
- }.merge(options)
37
- @pool = [] # stores available connection
38
- @register = [] # stores all connections at all times
39
- @current_size = 0
40
- @pool_mutex = (em_client? ? EM::Synchrony::Thread::Mutex.new : Mutex.new)
41
- @last_activity = Time.now
42
- @fetching_client = false
43
- HotTub.hot_at_exit( em_client? ) {close_all}
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
- # Calls close on all connections and reset the pools
59
- def close_all
60
- @pool_mutex.synchronize do
61
- while clnt = @register.pop
62
- @pool.delete(clnt)
63
- begin
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
- private
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
- def em_client?
76
- begin
77
- (HotTub.em_synchrony? && @client_block.call.is_a?(EventMachine::HttpConnection))
78
- rescue
79
- false
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 = nil
86
- alarm = (Time.now + @options[:blocking_timeout])
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 BlockingTimeout, message
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
- @pool_mutex.synchronize do
106
- if @register.include?(clnt)
107
- @pool << clnt
108
- else
109
- close_client(clnt)
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 # make sure never return the pool
212
+ nil
113
213
  end
114
214
 
115
215
  # Safely pull client from pool, adding if allowed
116
216
  def pop
117
- @fetching_client = true # kill reap_pool
118
- @pool_mutex.synchronize do
119
- _add if add?
120
- clnt = @pool.pop # get warm connection
121
- if (clnt.nil? && @options[:never_block])
122
- _add
123
- clnt = @pool.pop
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
- ensure
129
- reap_pool if reap_pool?
230
+ clnt
130
231
  end
131
232
 
132
- # create a new client from base client
133
- def new_client
134
- @client_block.call
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
- # Only want to add a client if the pool is empty in keeping with
138
- # a lazy model.
139
- def add?
140
- (@pool.length == 0 && (@options[:size] > @current_size))
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
- def reap_pool?
144
- (!@fetching_client && (@current_size > @options[:size]) && ((@last_activity + (600)) < Time.now))
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
- # Remove extra connections from front of pool
148
- def reap_pool
149
- @pool_mutex.synchronize do
150
- if reap_pool? && clnt = @pool.shift
151
- @register.delete(clnt)
152
- @current_size -= 1
153
- close_client(clnt)
154
- end
155
- end
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
- # _add is volatile; and may cause theading issues
159
- # if called outside @pool_mutex.synchronize {}
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
- @last_activity = Time.now
162
- @current_size += 1
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
- # end volatile
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
- class BlockingTimeout < StandardError;end
307
+
171
308
  end