ezpool 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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