ezpool 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a26d4a215e4a1e76ad3bfcdc570986a20e42f878
4
+ data.tar.gz: 5069ba506dcb424d68b7beb1d7c9843beb409fa9
5
+ SHA512:
6
+ metadata.gz: 8d3538e8663f049aef366a5665e69d69f9ee3444b256fa41f0fa4c846370675bd6759f4c3a3353a5e07aa1aaa56e8cc21b2d92519d977c645b655d4d11761d4a
7
+ data.tar.gz: 67fdd2120df11acd5ce1856c33ab0a52c3abb8c354955f519b3daca7182a67093323a4501af947dbb56ef85dad1b764f5acbb77ad878e356fdfc229432113da3
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
@@ -0,0 +1,11 @@
1
+ ---
2
+ sudo: false
3
+ cache: bundler
4
+ language: ruby
5
+ rvm:
6
+ - 1.9.3
7
+ - 2.0.0
8
+ - 2.1.6
9
+ - 2.2.2
10
+ - 2.3.3
11
+ - jruby
@@ -0,0 +1,131 @@
1
+ ezpool ChangeLog
2
+ ================
3
+
4
+ 1.0.0
5
+ -----
6
+ - Initial release of fork under the new name `ezpool`
7
+ - connect / disconnect methods are now arguments to the `EzPool` constructor
8
+ - Adds a new `max_age` parameter to gracefully recycle connections
9
+ - Removes re-entrant/recursive connection handling
10
+
11
+
12
+ Previous versions (pre-fork):
13
+
14
+
15
+ connection\_pool changelog
16
+ ===========================
17
+
18
+ 2.2.1
19
+ ------
20
+
21
+ - Allow CP::Wrapper to use an existing pool [#87, etiennebarrie]
22
+ - Use monotonic time for more accurate timeouts [#84, jdantonio]
23
+
24
+ 2.2.0
25
+ ------
26
+
27
+ - Rollback `Timeout` handling introduced in 2.1.1 and 2.1.2. It seems
28
+ impossible to safely work around the issue. Please never, ever use
29
+ `Timeout.timeout` in your code or you will see rare but mysterious bugs. [#75]
30
+
31
+ 2.1.3
32
+ ------
33
+
34
+ - Don't increment created count until connection is successfully
35
+ created. [mylesmegyesi, #73]
36
+
37
+ 2.1.2
38
+ ------
39
+
40
+ - The connection\_pool will now close any connections which respond to
41
+ `close` (Dalli) or `disconnect!` (Redis). This ensures discarded connections
42
+ from the fix in 2.1.1 are torn down ASAP and don't linger open.
43
+
44
+
45
+ 2.1.1
46
+ ------
47
+
48
+ - Work around a subtle race condition with code which uses `Timeout.timeout` and
49
+ checks out a connection within the timeout block. This might cause
50
+ connections to get into a bad state and raise very odd errors. [tamird, #67]
51
+
52
+
53
+ 2.1.0
54
+ ------
55
+
56
+ - Refactoring to better support connection pool subclasses [drbrain,
57
+ #55]
58
+ - `with` should return value of the last expression [#59]
59
+
60
+
61
+ 2.0.0
62
+ -----
63
+
64
+ - The connection pool is now lazy. Connections are created as needed
65
+ and retained until the pool is shut down. [drbrain, #52]
66
+
67
+ 1.2.0
68
+ -----
69
+
70
+ - Add `with(options)` and `checkout(options)`. [mattcamuto]
71
+ Allows the caller to override the pool timeout.
72
+ ```ruby
73
+ @pool.with(:timeout => 2) do |conn|
74
+ end
75
+ ```
76
+
77
+ 1.1.0
78
+ -----
79
+
80
+ - New `#shutdown` method (simao)
81
+
82
+ This method accepts a block and calls the block for each
83
+ connection in the pool. After calling this method, trying to get a
84
+ connection from the pool raises `PoolShuttingDownError`.
85
+
86
+ 1.0.0
87
+ -----
88
+
89
+ - `#with_connection` is now gone in favor of `#with`.
90
+
91
+ - We no longer pollute the top level namespace with our internal
92
+ `TimedStack` class.
93
+
94
+ 0.9.3
95
+ --------
96
+
97
+ - `#with_connection` is now deprecated in favor of `#with`.
98
+
99
+ A warning will be issued in the 0.9 series and the method will be
100
+ removed in 1.0.
101
+
102
+ - We now reuse objects when possible.
103
+
104
+ This means that under no contention, the same object will be checked
105
+ out from the pool after subsequent calls to `ConnectionPool#with`.
106
+
107
+ This change should have no impact on end user performance. If
108
+ anything, it should be an improvement, depending on what objects you
109
+ are pooling.
110
+
111
+ 0.9.2
112
+ --------
113
+
114
+ - Fix reentrant checkout leading to early checkin.
115
+
116
+ 0.9.1
117
+ --------
118
+
119
+ - Fix invalid superclass in version.rb
120
+
121
+ 0.9.0
122
+ --------
123
+
124
+ - Move method\_missing magic into ConnectionPool::Wrapper (djanowski)
125
+ - Remove BasicObject superclass (djanowski)
126
+
127
+ 0.1.0
128
+ --------
129
+
130
+ - More precise timeouts and better error message
131
+ - ConnectionPool now subclasses BasicObject so `method_missing` is more effective.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec(development_group: :runtime)
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Mike Perham
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,165 @@
1
+ ezpool
2
+ =================
3
+ [![Build Status](https://travis-ci.org/EasyPost/ezpool.svg?branch=master)](https://travis-ci.org/EasyPost/ezpool)
4
+
5
+ Generic connection pooling for Ruby. Originally forked from
6
+ [connection_pool](https://github.com/mperham/connection_pool), but with moderately different semantics.
7
+
8
+ MongoDB has its own connection pool. ActiveRecord has its own connection pool.
9
+ This is a generic connection pool that can be used with anything, e.g. Redis,
10
+ Dalli and other Ruby network clients.
11
+
12
+
13
+ Usage
14
+ -----
15
+
16
+ Create a pool of objects to share amongst the fibers or threads in your Ruby
17
+ application:
18
+
19
+ ```ruby
20
+ $memcached = EzPool.new(
21
+ size: 5,
22
+ timeout: 5,
23
+ connect: proc { Dalli::Client.new }
24
+ )
25
+ ```
26
+
27
+ You can also pass your connection function as a block:
28
+
29
+ ```ruby
30
+ $memcached = EzPool.new(size: 5, timeout: 5) { Dalli::Client.new }
31
+ ```
32
+
33
+ Or you can configure it later:
34
+
35
+ ```ruby
36
+ $memcached = EzPool.new(size: 5, timeout: 5)
37
+ $memcached.connect_with { Dalli::Client.new }
38
+ ```
39
+
40
+ Then use the pool in your application:
41
+
42
+ ``` ruby
43
+ $memcached.with do |conn|
44
+ conn.get('some-count')
45
+ end
46
+ ```
47
+
48
+ If all the objects in the connection pool are in use, `with` will block
49
+ until one becomes available. If no object is available within `:timeout` seconds,
50
+ `with` will raise a `Timeout::Error`.
51
+
52
+ Optionally, you can specify a timeout override using the with-block semantics:
53
+
54
+ ``` ruby
55
+ $memcached.with(timeout: 2.0) do |conn|
56
+ conn.get('some-count')
57
+ end
58
+ ```
59
+
60
+ This will only modify the resource-get timeout for this particular
61
+ invocation. This is useful if you want to fail-fast on certain non critical
62
+ sections when a resource is not available, or conversely if you are comfortable
63
+ blocking longer on a particular resource. This is not implemented in the below
64
+ `EzPool::Wrapper` class.
65
+
66
+ Note that you can also explicitly check-in/check-out connections using the `#checkin`
67
+ and `#checkout` methods; however, there are no safety nets here! Once you check out a connection,
68
+ nobody else may use it until you check it back in, and if you leak it, it's gone for good;
69
+ we don't do anything clever like override the finalizer so that we can detect when
70
+ the connection goes out of scope and return it to the pool. Caveat emptor!
71
+
72
+ ## Migrating to a Connection Pool
73
+
74
+ You can use `EzPool::Wrapper` to wrap a single global connection,
75
+ making it easier to migrate existing connection code over time:
76
+
77
+ ``` ruby
78
+ $redis = EzPool::Wrapper.new(size: 5, timeout: 3) { Redis.connect }
79
+ $redis.sadd('foo', 1)
80
+ $redis.smembers('foo')
81
+ ```
82
+
83
+ The wrapper uses `method_missing` to checkout a connection, run the requested
84
+ method and then immediately check the connection back into the pool. It's
85
+ **not** high-performance so you'll want to port your performance sensitive code
86
+ to use `with` as soon as possible.
87
+
88
+ ``` ruby
89
+ $redis.with do |conn|
90
+ conn.sadd('foo', 1)
91
+ conn.smembers('foo')
92
+ end
93
+ ```
94
+
95
+ And, of course, if there's any kind of load balancing or distribution on
96
+ the other end of the connection, there is no guarantee that two subsequent calls
97
+ to a `Wrapper`-wrapped object will go to the same database.
98
+
99
+ Once you've ported your entire system to use `with`, you can simply remove
100
+ `Wrapper` and use the simpler and faster `EzPool`.
101
+
102
+ ## Thread-safety / Connection Multiplexing
103
+
104
+ `EzPool`s are thread-safe and re-entrant in that the pool itself can be shared between different
105
+ threads and it is guaranteed that the same connection will never be returned from overlapping calls
106
+ to `#checkout` / `#with`. Note that this is achieved through the judicious use of mutexes; this code
107
+ is not appropriate for systems with hard real-time requiremnts. Then again, neither is Ruby.
108
+
109
+ The original `connection_pool` library had special functionality so that if you checked out
110
+ a connection from a thread which already had a checked out connection, you would be guaranteed
111
+ to get the same connection back again. This logic prevents a number of valable use cases,
112
+ so no longer exists. Each overlapping call to `pool.checkout` / `pool.with` will get a different
113
+ connection.
114
+
115
+ In particular, this feature could be abused to assume that adjacent calls to `Wrap`ed connections
116
+ from the same thread would go to the same database connection and perhaps the same session/transaction.
117
+ Don't do that. If you care about transactions or database sessions, you need to be explicitly checking
118
+ out and passing around connections.
119
+
120
+
121
+ ## Shutdown
122
+
123
+ You can shut down a EzPool instance once it should no longer be used.
124
+ Further checkout attempts will immediately raise an error but existing checkouts
125
+ will work.
126
+
127
+ ```ruby
128
+ cp = EzPool.new(
129
+ connect: lambda { Redis.new },
130
+ disconnect: lambda { |conn| conn.quit }
131
+ )
132
+ cp.shutdown
133
+ ```
134
+
135
+ Shutting down a connection pool will block until all connections are checked in and closed.
136
+ **Note that shutting down is completely optional**; Ruby's garbage collector will reclaim
137
+ unreferenced pools under normal circumstances.
138
+
139
+ ## Connection recycling
140
+
141
+ If you specify the `max_age` parameter to a connection pool, it will attempt to gracefully recycle
142
+ connections once they reach a certain age. This can be beneficial for, e.g., load balancers where you
143
+ want client applications to eventually pick up new databases coming into service without having to
144
+ explicitly restart the client processes.
145
+
146
+
147
+ Notes
148
+ -----
149
+
150
+ - Connections are lazily created as needed.
151
+ - There is no provision for repairing or checking the health of a connection;
152
+ connections should be self-repairing. This is true of the Dalli and Redis
153
+ clients.
154
+ - **WARNING**: Don't ever use `Timeout.timeout` in your Ruby code or you will see
155
+ occasional silent corruption and mysterious errors. The Timeout API is unsafe
156
+ and cannot be used correctly, ever. Use proper socket timeout options as
157
+ exposed by Net::HTTP, Redis, Dalli, etc.
158
+
159
+
160
+ Author
161
+ ------
162
+
163
+ Originally by Mike Perham, [@mperham](https://twitter.com/mperham), <http://mikeperham.com>
164
+
165
+ Forked and modified by engineers at [EasyPost](https://www.easypost.com).
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rake/testtask'
4
+ Rake::TestTask.new
5
+
6
+ task :default => :test
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require "./lib/ezpool/version"
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "ezpool"
6
+ s.version = EzPool::VERSION
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = ["Mike Perham", "Damian Janowski", "James Brown"]
9
+ s.email = ["oss@easypost.com"]
10
+ s.homepage = "https://github.com/EasyPost/ezpool"
11
+ s.description = s.summary = %q{More featureful generic connection pool for Ruby}
12
+
13
+ s.files = `git ls-files`.split("\n")
14
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16
+ s.require_paths = ["lib"]
17
+ s.license = "MIT"
18
+ s.add_development_dependency 'bundler'
19
+ s.add_development_dependency 'minitest', '>= 5.0.0'
20
+ s.add_development_dependency 'rake'
21
+ end
@@ -0,0 +1,190 @@
1
+ require_relative 'ezpool/version'
2
+ require_relative 'ezpool/timed_stack'
3
+ require_relative 'ezpool/errors'
4
+ require_relative 'ezpool/connection_manager'
5
+
6
+
7
+ # Generic connection pool class for e.g. sharing a limited number of network connections
8
+ # among many threads. Note: Connections are lazily created.
9
+ #
10
+ # Example usage with block (faster):
11
+ #
12
+ # @pool = EzPool.new { Redis.new }
13
+ #
14
+ # @pool.with do |redis|
15
+ # redis.lpop('my-list') if redis.llen('my-list') > 0
16
+ # end
17
+ #
18
+ # Using optional timeout override (for that single invocation)
19
+ #
20
+ # @pool.with(timeout: 2.0) do |redis|
21
+ # redis.lpop('my-list') if redis.llen('my-list') > 0
22
+ # end
23
+ #
24
+ # Example usage replacing an existing connection (slower):
25
+ #
26
+ # $redis = EzPool.wrap { Redis.new }
27
+ #
28
+ # def do_work
29
+ # $redis.lpop('my-list') if $redis.llen('my-list') > 0
30
+ # end
31
+ #
32
+ # Note that there's no way to pass a disconnection function to this
33
+ # usage, nor any way to guarantee that subsequent calls will go to the
34
+ # same connection (if your connection has any concept of sessions, this
35
+ # may be important). We strongly recommend against using wrapped
36
+ # connections in production environments.
37
+ #
38
+ # Accepts the following options:
39
+ # - :size - number of connections to pool, defaults to 5
40
+ # - :timeout - amount of time to wait for a connection if none currently available, defaults to 5 seconds
41
+ # - :max_age - maximum number of seconds that a connection may be alive for (will recycle on checkin/checkout)
42
+ # - :connect_with - callable for creating a connection
43
+ # - :disconnect-_with - callable for shutting down a connection
44
+ #
45
+ class EzPool
46
+ DEFAULTS = {size: 5, timeout: 1, max_age: Float::INFINITY}
47
+
48
+ def self.wrap(options, &block)
49
+ if block_given?
50
+ options[:connect_with] = block
51
+ end
52
+ Wrapper.new(options)
53
+ end
54
+
55
+ def initialize(options = {}, &block)
56
+ options = DEFAULTS.merge(options)
57
+
58
+ @size = options.fetch(:size)
59
+ @timeout = options.fetch(:timeout)
60
+ @max_age = options.fetch(:max_age).to_f
61
+
62
+ if @max_age <= 0
63
+ raise ArgumentError.new(":max_age must be > 0")
64
+ end
65
+
66
+ if block_given?
67
+ if options.include?(:connect_with)
68
+ raise ArgumentError.new("Block passed to EzPool *and* :connect_with in options")
69
+ else
70
+ options[:connect_with] = block
71
+ end
72
+ end
73
+
74
+ @manager = EzPool::ConnectionManager.new(options[:connect_with], options[:disconnect_with])
75
+
76
+ @available = TimedStack.new(@manager, @size)
77
+ @key = :"current-#{@available.object_id}"
78
+
79
+ @checked_out_connections = Hash.new
80
+ @mutex = Mutex.new
81
+ end
82
+
83
+ def connect_with(&block)
84
+ @manager.connect_with(&block)
85
+ end
86
+
87
+ def disconnect_with(&block)
88
+ @manager.disconnect_with(&block)
89
+ end
90
+
91
+ if Thread.respond_to?(:handle_interrupt)
92
+ # MRI
93
+ def with(options = {})
94
+ Thread.handle_interrupt(Exception => :never) do
95
+ conn = checkout(options)
96
+ begin
97
+ Thread.handle_interrupt(Exception => :immediate) do
98
+ yield conn
99
+ end
100
+ ensure
101
+ checkin conn
102
+ end
103
+ end
104
+ end
105
+ else
106
+ # non-MRI
107
+ def with(options = {})
108
+ conn = checkout(options)
109
+ begin
110
+ yield conn
111
+ ensure
112
+ checkin conn
113
+ end
114
+ end
115
+ end
116
+
117
+ def checkout(options = {})
118
+ conn_wrapper = nil
119
+ while conn_wrapper.nil? do
120
+ timeout = options[:timeout] || @timeout
121
+ conn_wrapper = @available.pop(timeout: timeout)
122
+ if expired? conn_wrapper
123
+ @available.abandon(conn_wrapper)
124
+ conn_wrapper = nil
125
+ end
126
+ end
127
+
128
+ @mutex.synchronize do
129
+ @checked_out_connections[conn_wrapper.raw_conn.object_id] = conn_wrapper
130
+ end
131
+ conn_wrapper.raw_conn
132
+ end
133
+
134
+ def checkin(conn)
135
+ conn_wrapper = @mutex.synchronize do
136
+ @checked_out_connections.delete(conn.object_id)
137
+ end
138
+ if conn_wrapper.nil?
139
+ raise EzPool::CheckedInUnCheckedOutConnectionError
140
+ end
141
+ if expired? conn_wrapper
142
+ @available.abandon(conn_wrapper)
143
+ else
144
+ @available.push(conn_wrapper)
145
+ end
146
+ nil
147
+ end
148
+
149
+ def shutdown
150
+ if block_given?
151
+ raise ArgumentError.new("shutdown no longer accepts a block; call #disconnect_with to set the disconnect method, or pass the disconnect: option to the EzPool initializer")
152
+ end
153
+ @available.shutdown
154
+ end
155
+
156
+ private
157
+ def expired?(connection_wrapper)
158
+ if @max_age.finite?
159
+ connection_wrapper.age > @max_age
160
+ else
161
+ false
162
+ end
163
+ end
164
+
165
+ class Wrapper < ::BasicObject
166
+ METHODS = [:with, :pool_shutdown]
167
+
168
+ def initialize(options = {}, &block)
169
+ @pool = options.fetch(:pool) { ::EzPool.new(options, &block) }
170
+ end
171
+
172
+ def with(&block)
173
+ @pool.with(&block)
174
+ end
175
+
176
+ def pool_shutdown(&block)
177
+ @pool.shutdown(&block)
178
+ end
179
+
180
+ def respond_to?(id, *args)
181
+ METHODS.include?(id) || with { |c| c.respond_to?(id, *args) }
182
+ end
183
+
184
+ def method_missing(name, *args, &block)
185
+ with do |connection|
186
+ connection.send(name, *args, &block)
187
+ end
188
+ end
189
+ end
190
+ end