connection_pool 1.0.0 → 2.5.5

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 24c74a1caa5e04827c47155b75e19805bcda4c329e28793309dfa95b5881c4bd
4
+ data.tar.gz: 23aadf2da494be8c3314039700606cd4e01b58d6cae1f45e3b7cdef57a98e7bd
5
+ SHA512:
6
+ metadata.gz: 0f2385ddf4619ebc1bf9e2f202024b330c170ae2e4e7e63480be0f556242140af75eb3d272f6d262b2fc859625f0861c560af6a58c20bea3d69b005e2d248472
7
+ data.tar.gz: 0ac5103c69aa2e8226d2fa872714f9bd611b24ea171e91ce3d7a3c9be62b9887e24916cf4b6b1e5bc8c407349f87c3ea855c4659dbf5f3f137cd45738301d1a6
data/Changes.md CHANGED
@@ -1,3 +1,153 @@
1
+ # connection_pool Changelog
2
+
3
+ 2.5.5
4
+ ------
5
+
6
+ - Support `ConnectionPool::TimedStack#pop(exception: false)` [#207]
7
+ to avoid using exceptions as control flow.
8
+
9
+ 2.5.4
10
+ ------
11
+
12
+ - Add ability to remove a broken connection from the pool [#204, womblep]
13
+
14
+ 2.5.3
15
+ ------
16
+
17
+ - Fix TruffleRuby/JRuby crash [#201]
18
+
19
+ 2.5.2
20
+ ------
21
+
22
+ - Rollback inadvertant change to `auto_reload_after_fork` default. [#200]
23
+
24
+ 2.5.1
25
+ ------
26
+
27
+ - Pass options to TimedStack in `checkout` [#195]
28
+ - Optimize connection lookup [#196]
29
+ - Fixes for use with Ractors
30
+
31
+ 2.5.0
32
+ ------
33
+
34
+ - Reap idle connections [#187]
35
+ ```ruby
36
+ idle_timeout = 60
37
+ pool = ConnectionPool.new ...
38
+ pool.reap(idle_timeout, &:close)
39
+ ```
40
+ - `ConnectionPool#idle` returns the count of connections not in use [#187]
41
+
42
+ 2.4.1
43
+ ------
44
+
45
+ - New `auto_reload_after_fork` config option to disable auto-drop [#177, shayonj]
46
+
47
+ 2.4.0
48
+ ------
49
+
50
+ - Automatically drop all connections after fork [#166]
51
+
52
+ 2.3.0
53
+ ------
54
+
55
+ - Minimum Ruby version is now 2.5.0
56
+ - Add pool size to TimeoutError message
57
+
58
+ 2.2.5
59
+ ------
60
+
61
+ - Fix argument forwarding on Ruby 2.7 [#149]
62
+
63
+ 2.2.4
64
+ ------
65
+
66
+ - Add `reload` to close all connections, recreating them afterwards [Andrew Marshall, #140]
67
+ - Add `then` as a way to use a pool or a bare connection with the same code path [#138]
68
+
69
+ 2.2.3
70
+ ------
71
+
72
+ - Pool now throws `ConnectionPool::TimeoutError` on timeout. [#130]
73
+ - Use monotonic clock present in all modern Rubies [Tero Tasanen, #109]
74
+ - Remove code hacks necessary for JRuby 1.7
75
+ - Expose wrapped pool from ConnectionPool::Wrapper [Thomas Lecavelier, #113]
76
+
77
+ 2.2.2
78
+ ------
79
+
80
+ - Add pool `size` and `available` accessors for metrics and monitoring
81
+ purposes [#97, robholland]
82
+
83
+ 2.2.1
84
+ ------
85
+
86
+ - Allow CP::Wrapper to use an existing pool [#87, etiennebarrie]
87
+ - Use monotonic time for more accurate timeouts [#84, jdantonio]
88
+
89
+ 2.2.0
90
+ ------
91
+
92
+ - Rollback `Timeout` handling introduced in 2.1.1 and 2.1.2. It seems
93
+ impossible to safely work around the issue. Please never, ever use
94
+ `Timeout.timeout` in your code or you will see rare but mysterious bugs. [#75]
95
+
96
+ 2.1.3
97
+ ------
98
+
99
+ - Don't increment created count until connection is successfully
100
+ created. [mylesmegyesi, #73]
101
+
102
+ 2.1.2
103
+ ------
104
+
105
+ - The connection\_pool will now close any connections which respond to
106
+ `close` (Dalli) or `disconnect!` (Redis). This ensures discarded connections
107
+ from the fix in 2.1.1 are torn down ASAP and don't linger open.
108
+
109
+
110
+ 2.1.1
111
+ ------
112
+
113
+ - Work around a subtle race condition with code which uses `Timeout.timeout` and
114
+ checks out a connection within the timeout block. This might cause
115
+ connections to get into a bad state and raise very odd errors. [tamird, #67]
116
+
117
+
118
+ 2.1.0
119
+ ------
120
+
121
+ - Refactoring to better support connection pool subclasses [drbrain,
122
+ #55]
123
+ - `with` should return value of the last expression [#59]
124
+
125
+
126
+ 2.0.0
127
+ -----
128
+
129
+ - The connection pool is now lazy. Connections are created as needed
130
+ and retained until the pool is shut down. [drbrain, #52]
131
+
132
+ 1.2.0
133
+ -----
134
+
135
+ - Add `with(options)` and `checkout(options)`. [mattcamuto]
136
+ Allows the caller to override the pool timeout.
137
+ ```ruby
138
+ @pool.with(:timeout => 2) do |conn|
139
+ end
140
+ ```
141
+
142
+ 1.1.0
143
+ -----
144
+
145
+ - New `#shutdown` method (simao)
146
+
147
+ This method accepts a block and calls the block for each
148
+ connection in the pool. After calling this method, trying to get a
149
+ connection from the pool raises `PoolShuttingDownError`.
150
+
1
151
  1.0.0
2
152
  -----
3
153
 
data/README.md CHANGED
@@ -1,60 +1,69 @@
1
- connection_pool
2
- ======================
1
+ connection\_pool
2
+ =================
3
+ [![Build Status](https://github.com/mperham/connection_pool/actions/workflows/ci.yml/badge.svg)](https://github.com/mperham/connection_pool/actions/workflows/ci.yml)
3
4
 
4
5
  Generic connection pooling for Ruby.
5
6
 
6
- MongoDB has its own connection pool. ActiveRecord has its own connection pool. This is a generic connection pool that can be used with anything, e.g. Redis, Dalli and other Ruby network clients.
7
-
8
-
9
- Install
10
- ------------
11
-
12
- gem install connection_pool
13
-
14
-
15
- Notes
16
- ------------
17
-
18
- - Connections are eager created when the pool is created.
19
- - There is no provision for repairing or checking the health of a
20
- connection; connections should be self-repairing. This is
21
- true of the dalli and redis clients.
22
-
7
+ MongoDB has its own connection pool.
8
+ ActiveRecord has its own connection pool.
9
+ This is a generic connection pool that can be used with anything, e.g. Redis, Dalli and other Ruby network clients.
23
10
 
24
11
  Usage
25
- ------------
12
+ -----
26
13
 
27
14
  Create a pool of objects to share amongst the fibers or threads in your Ruby application:
28
15
 
29
16
  ``` ruby
30
- @memcached = ConnectionPool.new(:size => 5, :timeout => 5) { Dalli::Client.new }
17
+ $memcached = ConnectionPool.new(size: 5, timeout: 5) { Dalli::Client.new }
31
18
  ```
32
19
 
33
20
  Then use the pool in your application:
34
21
 
35
22
  ``` ruby
36
- @memcached.with do |dalli|
37
- dalli.get('some-count')
23
+ $memcached.with do |conn|
24
+ conn.get('some-count')
38
25
  end
39
26
  ```
40
27
 
41
28
  If all the objects in the connection pool are in use, `with` will block
42
- until one becomes available. If no object is available within `:timeout` seconds,
43
- `with` will raise a `Timeout::Error`.
29
+ until one becomes available.
30
+ If no object is available within `:timeout` seconds,
31
+ `with` will raise a `ConnectionPool::TimeoutError` (a subclass of `Timeout::Error`).
44
32
 
45
- You can use `ConnectionPool::Wrapper` to wrap a single global connection, making
46
- it easier to port your connection code over time:
33
+ You can also use `ConnectionPool#then` to support _both_ a
34
+ connection pool and a raw client.
35
+
36
+ ```ruby
37
+ # Compatible with a raw Redis::Client, and ConnectionPool Redis
38
+ $redis.then { |r| r.set 'foo' 'bar' }
39
+ ```
40
+
41
+ Optionally, you can specify a timeout override using the with-block semantics:
47
42
 
48
43
  ``` ruby
49
- $redis = ConnectionPool::Wrapper.new(:size => 5, :timeout => 3) { Redis.connect }
44
+ $memcached.with(timeout: 2.0) do |conn|
45
+ conn.get('some-count')
46
+ end
47
+ ```
48
+
49
+ This will only modify the resource-get timeout for this particular
50
+ invocation.
51
+ This is useful if you want to fail-fast on certain non-critical
52
+ sections when a resource is not available, or conversely if you are comfortable blocking longer on a particular resource.
53
+ This is not implemented in the `ConnectionPool::Wrapper` class.
54
+
55
+ ## Migrating to a Connection Pool
56
+
57
+ You can use `ConnectionPool::Wrapper` to wrap a single global connection, making it easier to migrate existing connection code over time:
58
+
59
+ ``` ruby
60
+ $redis = ConnectionPool::Wrapper.new(size: 5, timeout: 3) { Redis.new }
50
61
  $redis.sadd('foo', 1)
51
62
  $redis.smembers('foo')
52
63
  ```
53
64
 
54
- The Wrapper uses `method_missing` to checkout a connection, run the
55
- requested method and then immediately check the connection back into the
56
- pool. It's **not** high-performance so you'll want to port your
57
- performance sensitive code to use `with` as soon as possible.
65
+ The wrapper uses `method_missing` to checkout a connection, run the requested method and then immediately check the connection back into the pool.
66
+ It's **not** high-performance so you'll want to port your performance sensitive code to use `with` as soon as possible.
58
67
 
59
68
  ``` ruby
60
69
  $redis.with do |conn|
@@ -63,10 +72,117 @@ $redis.with do |conn|
63
72
  end
64
73
  ```
65
74
 
66
- Once you've ported your entire system to use `with`, you can simply
67
- remove ::Wrapper and use a simple, fast ConnectionPool.
75
+ Once you've ported your entire system to use `with`, you can simply remove `Wrapper` and use the simpler and faster `ConnectionPool`.
76
+
77
+
78
+ ## Shutdown
79
+
80
+ You can shut down a ConnectionPool instance once it should no longer be used.
81
+ Further checkout attempts will immediately raise an error but existing checkouts will work.
82
+
83
+ ```ruby
84
+ cp = ConnectionPool.new { Redis.new }
85
+ cp.shutdown { |c| c.close }
86
+ ```
87
+
88
+ Shutting down a connection pool will block until all connections are checked in and closed.
89
+ **Note that shutting down is completely optional**; Ruby's garbage collector will reclaim unreferenced pools under normal circumstances.
90
+
91
+ ## Reload
92
+
93
+ You can reload a ConnectionPool instance in the case it is desired to close all connections to the pool and, unlike `shutdown`, afterwards recreate connections so the pool may continue to be used.
94
+ Reloading may be useful after forking the process.
95
+
96
+ ```ruby
97
+ cp = ConnectionPool.new { Redis.new }
98
+ cp.reload { |conn| conn.quit }
99
+ cp.with { |conn| conn.get('some-count') }
100
+ ```
101
+
102
+ Like `shutdown`, this will block until all connections are checked in and closed.
103
+
104
+ ## Reap
105
+
106
+ You can reap idle connections in the ConnectionPool instance to close connections that were created but have not been used for a certain amount of time. This can be useful to run periodically in a separate thread especially if keeping the connection open is resource intensive.
107
+
108
+ You can specify how many seconds the connections have to be idle for them to be reaped.
109
+ Defaults to 60 seconds.
110
+
111
+ ```ruby
112
+ cp = ConnectionPool.new { Redis.new }
113
+ cp.reap(300) { |conn| conn.close } # Reaps connections that have been idle for 300 seconds (5 minutes).
114
+ ```
115
+
116
+ ### Reaper Thread
117
+
118
+ You can start your own reaper thread to reap idle connections in the ConnectionPool instance on a regular interval.
119
+
120
+ ```ruby
121
+ cp = ConnectionPool.new { Redis.new }
122
+
123
+ # Start a reaper thread to reap connections that have been idle for 300 seconds (5 minutes).
124
+ Thread.new do
125
+ loop do
126
+ cp.reap(300) { |conn| conn.close }
127
+ sleep 300
128
+ end
129
+ end
130
+ ```
131
+
132
+ ## Discarding Connections
133
+
134
+ You can discard connections in the ConnectionPool instance to remove connections that are broken and can't be restarted.
135
+
136
+ NOTE: the connection is not closed. It will just be removed from the pool so it won't be selected again.
137
+
138
+ It can only be done inside the block passed to `with` or `with_timeout`.
139
+
140
+ Takes an optional block that will be executed with the connection.
141
+
142
+ ```ruby
143
+ pool.with do |conn|
144
+ begin
145
+ conn.execute("SELECT 1")
146
+ rescue SomeConnectionError
147
+ pool.discard_current_connection # remove the connection from the pool
148
+ raise
149
+ end
150
+ end
151
+ ```
152
+
153
+ ## Current State
154
+
155
+ There are several methods that return information about a pool.
156
+
157
+ ```ruby
158
+ cp = ConnectionPool.new(size: 10) { Redis.new }
159
+ cp.size # => 10
160
+ cp.available # => 10
161
+ cp.idle # => 0
162
+
163
+ cp.with do |conn|
164
+ cp.size # => 10
165
+ cp.available # => 9
166
+ cp.idle # => 0
167
+ end
168
+
169
+ cp.idle # => 1
170
+ ```
171
+
172
+ Notes
173
+ -----
174
+
175
+ - Connections are lazily created as needed.
176
+ - There is no provision for repairing or checking the health of a connection;
177
+ connections should be self-repairing. This is true of the Dalli and Redis
178
+ clients.
179
+ - **WARNING**: Don't ever use `Timeout.timeout` in your Ruby code or you will see
180
+ occasional silent corruption and mysterious errors. The Timeout API is unsafe
181
+ and cannot be used correctly, ever. Use proper socket timeout options as
182
+ exposed by Net::HTTP, Redis, Dalli, etc.
183
+
68
184
 
69
185
  Author
70
- --------------
186
+ ------
71
187
 
72
- Mike Perham, [@mperham](https://twitter.com/mperham), <http://mikeperham.com>
188
+ Mike Perham, [@getajobmike](https://twitter.com/getajobmike), <https://www.mikeperham.com>
@@ -1,17 +1,24 @@
1
- # -*- encoding: utf-8 -*-
2
1
  require "./lib/connection_pool/version"
3
2
 
4
3
  Gem::Specification.new do |s|
5
- s.name = "connection_pool"
6
- s.version = ConnectionPool::VERSION
7
- s.platform = Gem::Platform::RUBY
8
- s.authors = ["Mike Perham"]
9
- s.email = ["mperham@gmail.com"]
10
- s.homepage = "https://github.com/mperham/connection_pool"
11
- s.description = s.summary = %q{Generic connection pool for Ruby}
4
+ s.name = "connection_pool"
5
+ s.version = ConnectionPool::VERSION
6
+ s.platform = Gem::Platform::RUBY
7
+ s.authors = ["Mike Perham", "Damian Janowski"]
8
+ s.email = ["mperham@gmail.com", "damian@educabilia.com"]
9
+ s.homepage = "https://github.com/mperham/connection_pool"
10
+ s.description = s.summary = "Generic connection pool for Ruby"
12
11
 
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) }
12
+ s.files = ["Changes.md", "LICENSE", "README.md", "connection_pool.gemspec",
13
+ "lib/connection_pool.rb", "lib/connection_pool/timed_stack.rb",
14
+ "lib/connection_pool/version.rb", "lib/connection_pool/wrapper.rb"]
15
+ s.executables = []
16
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
+ s.required_ruby_version = ">= 2.5.0"
22
+
23
+ s.metadata = {"changelog_uri" => "https://github.com/mperham/connection_pool/blob/main/Changes.md", "rubygems_mfa_required" => "true"}
17
24
  end
@@ -1,42 +1,237 @@
1
- require 'thread'
2
- require 'timeout'
3
-
1
+ ##
2
+ # The TimedStack manages a pool of homogeneous connections (or any resource
3
+ # you wish to manage). Connections are created lazily up to a given maximum
4
+ # number.
5
+ #
6
+ # Examples:
7
+ #
8
+ # ts = TimedStack.new(1) { MyConnection.new }
9
+ #
10
+ # # fetch a connection
11
+ # conn = ts.pop
12
+ #
13
+ # # return a connection
14
+ # ts.push conn
15
+ #
16
+ # conn = ts.pop
17
+ # ts.pop timeout: 5
18
+ # #=> raises ConnectionPool::TimeoutError after 5 seconds
4
19
  class ConnectionPool::TimedStack
5
- def initialize(size = 0)
6
- @que = Array.new(size) { yield }
7
- @mutex = Mutex.new
8
- @resource = ConditionVariable.new
20
+ attr_reader :max
21
+
22
+ ##
23
+ # Creates a new pool with +size+ connections that are created from the given
24
+ # +block+.
25
+ def initialize(size = 0, &block)
26
+ @create_block = block
27
+ @created = 0
28
+ @que = []
29
+ @max = size
30
+ @mutex = Thread::Mutex.new
31
+ @resource = Thread::ConditionVariable.new
32
+ @shutdown_block = nil
9
33
  end
10
34
 
11
- def push(obj)
35
+ ##
36
+ # Returns +obj+ to the stack. +options+ is ignored in TimedStack but may be
37
+ # used by subclasses that extend TimedStack.
38
+ def push(obj, options = {})
12
39
  @mutex.synchronize do
13
- @que.push obj
40
+ if @shutdown_block
41
+ @created -= 1 unless @created == 0
42
+ @shutdown_block.call(obj)
43
+ else
44
+ store_connection obj, options
45
+ end
46
+
14
47
  @resource.broadcast
15
48
  end
16
49
  end
17
50
  alias_method :<<, :push
18
51
 
19
- def pop(timeout=0.5)
20
- deadline = Time.now + timeout
52
+ ##
53
+ # Retrieves a connection from the stack. If a connection is available it is
54
+ # immediately returned. If no connection is available within the given
55
+ # timeout a ConnectionPool::TimeoutError is raised.
56
+ #
57
+ # @option options [Float] :timeout (0.5) Wait this many seconds for an available entry
58
+ # @option options [Class] :exception (ConnectionPool::TimeoutError) Exception class to raise
59
+ # if an entry was not available within the timeout period. Use `exception: false` to return nil.
60
+ #
61
+ # The +timeout+ argument will be removed in 3.0.
62
+ # Other options may be used by subclasses that extend TimedStack.
63
+ def pop(timeout = 0.5, options = {})
64
+ options, timeout = timeout, 0.5 if Hash === timeout
65
+ timeout = options.fetch :timeout, timeout
66
+
67
+ deadline = current_time + timeout
21
68
  @mutex.synchronize do
22
69
  loop do
23
- return @que.pop unless @que.empty?
24
- to_wait = deadline - Time.now
25
- raise Timeout::Error, "Waited #{timeout} sec" if to_wait <= 0
70
+ raise ConnectionPool::PoolShuttingDownError if @shutdown_block
71
+ if (conn = try_fetch_connection(options))
72
+ return conn
73
+ end
74
+
75
+ connection = try_create(options)
76
+ return connection if connection
77
+
78
+ to_wait = deadline - current_time
79
+ if to_wait <= 0
80
+ exc = options.fetch(:exception, ConnectionPool::TimeoutError)
81
+ if exc
82
+ raise ConnectionPool::TimeoutError, "Waited #{timeout} sec, #{length}/#{@max} available"
83
+ else
84
+ return nil
85
+ end
86
+ end
26
87
  @resource.wait(@mutex, to_wait)
27
88
  end
28
89
  end
29
90
  end
30
91
 
31
- def empty?
32
- @que.empty?
92
+ ##
93
+ # Shuts down the TimedStack by passing each connection to +block+ and then
94
+ # removing it from the pool. Attempting to checkout a connection after
95
+ # shutdown will raise +ConnectionPool::PoolShuttingDownError+ unless
96
+ # +:reload+ is +true+.
97
+ def shutdown(reload: false, &block)
98
+ raise ArgumentError, "shutdown must receive a block" unless block
99
+
100
+ @mutex.synchronize do
101
+ @shutdown_block = block
102
+ @resource.broadcast
103
+
104
+ shutdown_connections
105
+ @shutdown_block = nil if reload
106
+ end
107
+ end
108
+
109
+ ##
110
+ # Reaps connections that were checked in more than +idle_seconds+ ago.
111
+ def reap(idle_seconds, &block)
112
+ raise ArgumentError, "reap must receive a block" unless block
113
+ raise ArgumentError, "idle_seconds must be a number" unless idle_seconds.is_a?(Numeric)
114
+ raise ConnectionPool::PoolShuttingDownError if @shutdown_block
115
+
116
+ idle.times do
117
+ conn =
118
+ @mutex.synchronize do
119
+ raise ConnectionPool::PoolShuttingDownError if @shutdown_block
120
+
121
+ reserve_idle_connection(idle_seconds)
122
+ end
123
+ break unless conn
124
+
125
+ block.call(conn)
126
+ end
33
127
  end
34
128
 
35
- def clear
36
- @que.clear
129
+ ##
130
+ # Returns +true+ if there are no available connections.
131
+ def empty?
132
+ (@created - @que.length) >= @max
37
133
  end
38
134
 
135
+ ##
136
+ # The number of connections available on the stack.
39
137
  def length
138
+ @max - @created + @que.length
139
+ end
140
+
141
+ ##
142
+ # The number of connections created and available on the stack.
143
+ def idle
40
144
  @que.length
41
145
  end
146
+
147
+ ##
148
+ # Reduce the created count
149
+ def decrement_created
150
+ @created -= 1 unless @created == 0
151
+ end
152
+
153
+ private
154
+
155
+ def current_time
156
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
157
+ end
158
+
159
+ ##
160
+ # This is an extension point for TimedStack and is called with a mutex.
161
+ #
162
+ # This method must returns a connection from the stack if one exists. Allows
163
+ # subclasses with expensive match/search algorithms to avoid double-handling
164
+ # their stack.
165
+ def try_fetch_connection(options = nil)
166
+ connection_stored?(options) && fetch_connection(options)
167
+ end
168
+
169
+ ##
170
+ # This is an extension point for TimedStack and is called with a mutex.
171
+ #
172
+ # This method must returns true if a connection is available on the stack.
173
+ def connection_stored?(options = nil)
174
+ !@que.empty?
175
+ end
176
+
177
+ ##
178
+ # This is an extension point for TimedStack and is called with a mutex.
179
+ #
180
+ # This method must return a connection from the stack.
181
+ def fetch_connection(options = nil)
182
+ @que.pop&.first
183
+ end
184
+
185
+ ##
186
+ # This is an extension point for TimedStack and is called with a mutex.
187
+ #
188
+ # This method must shut down all connections on the stack.
189
+ def shutdown_connections(options = nil)
190
+ while (conn = try_fetch_connection(options))
191
+ @created -= 1 unless @created == 0
192
+ @shutdown_block.call(conn)
193
+ end
194
+ end
195
+
196
+ ##
197
+ # This is an extension point for TimedStack and is called with a mutex.
198
+ #
199
+ # This method returns the oldest idle connection if it has been idle for more than idle_seconds.
200
+ # This requires that the stack is kept in order of checked in time (oldest first).
201
+ def reserve_idle_connection(idle_seconds)
202
+ return unless idle_connections?(idle_seconds)
203
+
204
+ @created -= 1 unless @created == 0
205
+
206
+ @que.shift.first
207
+ end
208
+
209
+ ##
210
+ # This is an extension point for TimedStack and is called with a mutex.
211
+ #
212
+ # Returns true if the first connection in the stack has been idle for more than idle_seconds
213
+ def idle_connections?(idle_seconds)
214
+ connection_stored? && (current_time - @que.first.last > idle_seconds)
215
+ end
216
+
217
+ ##
218
+ # This is an extension point for TimedStack and is called with a mutex.
219
+ #
220
+ # This method must return +obj+ to the stack.
221
+ def store_connection(obj, options = nil)
222
+ @que.push [obj, current_time]
223
+ end
224
+
225
+ ##
226
+ # This is an extension point for TimedStack and is called with a mutex.
227
+ #
228
+ # This method must create a connection if and only if the total number of
229
+ # connections allowed has not been met.
230
+ def try_create(options = nil)
231
+ unless @created == @max
232
+ object = @create_block.call
233
+ @created += 1
234
+ object
235
+ end
236
+ end
42
237
  end
@@ -1,3 +1,3 @@
1
1
  class ConnectionPool
2
- VERSION = "1.0.0"
2
+ VERSION = "2.5.5"
3
3
  end
@@ -0,0 +1,56 @@
1
+ class ConnectionPool
2
+ class Wrapper < ::BasicObject
3
+ METHODS = [:with, :pool_shutdown, :wrapped_pool]
4
+
5
+ def initialize(options = {}, &block)
6
+ @pool = options.fetch(:pool) { ::ConnectionPool.new(options, &block) }
7
+ end
8
+
9
+ def wrapped_pool
10
+ @pool
11
+ end
12
+
13
+ def with(&block)
14
+ @pool.with(&block)
15
+ end
16
+
17
+ def pool_shutdown(&block)
18
+ @pool.shutdown(&block)
19
+ end
20
+
21
+ def pool_size
22
+ @pool.size
23
+ end
24
+
25
+ def pool_available
26
+ @pool.available
27
+ end
28
+
29
+ def respond_to?(id, *args)
30
+ METHODS.include?(id) || with { |c| c.respond_to?(id, *args) }
31
+ end
32
+
33
+ # rubocop:disable Style/MissingRespondToMissing
34
+ if ::RUBY_VERSION >= "3.0.0"
35
+ def method_missing(name, *args, **kwargs, &block)
36
+ with do |connection|
37
+ connection.send(name, *args, **kwargs, &block)
38
+ end
39
+ end
40
+ elsif ::RUBY_VERSION >= "2.7.0"
41
+ ruby2_keywords def method_missing(name, *args, &block)
42
+ with do |connection|
43
+ connection.send(name, *args, &block)
44
+ end
45
+ end
46
+ else
47
+ def method_missing(name, *args, &block)
48
+ with do |connection|
49
+ connection.send(name, *args, &block)
50
+ end
51
+ end
52
+ end
53
+ # rubocop:enable Style/MethodMissingSuper
54
+ # rubocop:enable Style/MissingRespondToMissing
55
+ end
56
+ end
@@ -1,16 +1,30 @@
1
- require_relative 'connection_pool/version'
1
+ require "timeout"
2
+ require_relative "connection_pool/version"
2
3
 
3
- # Generic connection pool class for e.g. sharing a limited number of network connections
4
- # among many threads. Note: Connections are eager created.
4
+ class ConnectionPool
5
+ class Error < ::RuntimeError; end
6
+
7
+ class PoolShuttingDownError < ::ConnectionPool::Error; end
8
+
9
+ class TimeoutError < ::Timeout::Error; end
10
+ end
11
+
12
+ # Generic connection pool class for sharing a limited number of objects or network connections
13
+ # among many threads. Note: pool elements are lazily created.
5
14
  #
6
15
  # Example usage with block (faster):
7
16
  #
8
17
  # @pool = ConnectionPool.new { Redis.new }
9
- #
10
18
  # @pool.with do |redis|
11
19
  # redis.lpop('my-list') if redis.llen('my-list') > 0
12
20
  # end
13
21
  #
22
+ # Using optional timeout override (for that single invocation)
23
+ #
24
+ # @pool.with(timeout: 2.0) do |redis|
25
+ # redis.lpop('my-list') if redis.llen('my-list') > 0
26
+ # end
27
+ #
14
28
  # Example usage replacing an existing connection (slower):
15
29
  #
16
30
  # $redis = ConnectionPool.wrap { Redis.new }
@@ -22,80 +36,198 @@ require_relative 'connection_pool/version'
22
36
  # Accepts the following options:
23
37
  # - :size - number of connections to pool, defaults to 5
24
38
  # - :timeout - amount of time to wait for a connection if none currently available, defaults to 5 seconds
39
+ # - :auto_reload_after_fork - automatically drop all connections after fork, defaults to true
25
40
  #
26
41
  class ConnectionPool
27
- DEFAULTS = {size: 5, timeout: 5}
42
+ DEFAULTS = {size: 5, timeout: 5, auto_reload_after_fork: true}.freeze
28
43
 
29
44
  def self.wrap(options, &block)
30
45
  Wrapper.new(options, &block)
31
46
  end
32
47
 
48
+ if Process.respond_to?(:fork)
49
+ INSTANCES = ObjectSpace::WeakMap.new
50
+ private_constant :INSTANCES
51
+
52
+ def self.after_fork
53
+ INSTANCES.values.each do |pool|
54
+ next unless pool.auto_reload_after_fork
55
+
56
+ # We're on after fork, so we know all other threads are dead.
57
+ # All we need to do is to ensure the main thread doesn't have a
58
+ # checked out connection
59
+ pool.checkin(force: true)
60
+ pool.reload do |connection|
61
+ # Unfortunately we don't know what method to call to close the connection,
62
+ # so we try the most common one.
63
+ connection.close if connection.respond_to?(:close)
64
+ end
65
+ end
66
+ nil
67
+ end
68
+
69
+ if ::Process.respond_to?(:_fork) # MRI 3.1+
70
+ module ForkTracker
71
+ def _fork
72
+ pid = super
73
+ if pid == 0
74
+ ConnectionPool.after_fork
75
+ end
76
+ pid
77
+ end
78
+ end
79
+ Process.singleton_class.prepend(ForkTracker)
80
+ end
81
+ else
82
+ INSTANCES = nil
83
+ private_constant :INSTANCES
84
+
85
+ def self.after_fork
86
+ # noop
87
+ end
88
+ end
89
+
33
90
  def initialize(options = {}, &block)
34
- raise ArgumentError, 'Connection pool requires a block' unless block
91
+ raise ArgumentError, "Connection pool requires a block" unless block
35
92
 
36
93
  options = DEFAULTS.merge(options)
37
94
 
38
- @size = options.fetch(:size)
95
+ @size = Integer(options.fetch(:size))
39
96
  @timeout = options.fetch(:timeout)
97
+ @auto_reload_after_fork = options.fetch(:auto_reload_after_fork)
40
98
 
41
99
  @available = TimedStack.new(@size, &block)
42
- @key = :"current-#{@available.object_id}"
100
+ @key = :"pool-#{@available.object_id}"
101
+ @key_count = :"pool-#{@available.object_id}-count"
102
+ @discard_key = :"pool-#{@available.object_id}-discard"
103
+ INSTANCES[self] = self if @auto_reload_after_fork && INSTANCES
43
104
  end
44
105
 
45
- def with
46
- conn = checkout
47
- begin
48
- yield conn
49
- ensure
50
- checkin
106
+ def with(options = {})
107
+ # We need to manage exception handling manually here in order
108
+ # to work correctly with `Timeout.timeout` and `Thread#raise`.
109
+ # Otherwise an interrupted Thread can leak connections.
110
+ Thread.handle_interrupt(Exception => :never) do
111
+ conn = checkout(options)
112
+ begin
113
+ Thread.handle_interrupt(Exception => :immediate) do
114
+ yield conn
115
+ end
116
+ ensure
117
+ checkin
118
+ end
51
119
  end
52
120
  end
121
+ alias_method :then, :with
122
+
123
+ ##
124
+ # Marks the current thread's checked-out connection for discard.
125
+ #
126
+ # When a connection is marked for discard, it will not be returned to the pool
127
+ # when checked in. Instead, the connection will be discarded.
128
+ # This is useful when a connection has become invalid or corrupted
129
+ # and should not be reused.
130
+ #
131
+ # Takes an optional block that will be called with the connection to be discarded.
132
+ # The block should perform any necessary clean-up on the connection.
133
+ #
134
+ # @yield [conn]
135
+ # @yieldparam conn [Object] The connection to be discarded.
136
+ # @yieldreturn [void]
137
+ #
138
+ #
139
+ # Note: This only affects the connection currently checked out by the calling thread.
140
+ # The connection will be discarded when +checkin+ is called.
141
+ #
142
+ # @return [void]
143
+ #
144
+ # @example
145
+ # pool.with do |conn|
146
+ # begin
147
+ # conn.execute("SELECT 1")
148
+ # rescue SomeConnectionError
149
+ # pool.discard_current_connection # Mark connection as bad
150
+ # raise
151
+ # end
152
+ # end
153
+ def discard_current_connection(&block)
154
+ ::Thread.current[@discard_key] = block || proc { |conn| conn }
155
+ end
53
156
 
54
- def checkout
55
- stack = ::Thread.current[@key] ||= []
56
-
57
- if stack.empty?
58
- conn = @available.pop(@timeout)
157
+ def checkout(options = {})
158
+ if ::Thread.current[@key]
159
+ ::Thread.current[@key_count] += 1
160
+ ::Thread.current[@key]
59
161
  else
60
- conn = stack.last
162
+ ::Thread.current[@key_count] = 1
163
+ ::Thread.current[@key] = @available.pop(options[:timeout] || @timeout, options)
61
164
  end
62
-
63
- stack.push conn
64
- conn
65
165
  end
66
166
 
67
- def checkin
68
- stack = ::Thread.current[@key]
69
- conn = stack.pop
70
- if stack.empty?
71
- @available << conn
167
+ def checkin(force: false)
168
+ if ::Thread.current[@key]
169
+ if ::Thread.current[@key_count] == 1 || force
170
+ if ::Thread.current[@discard_key]
171
+ begin
172
+ @available.decrement_created
173
+ ::Thread.current[@discard_key].call(::Thread.current[@key])
174
+ rescue
175
+ nil
176
+ ensure
177
+ ::Thread.current[@discard_key] = nil
178
+ end
179
+ else
180
+ @available.push(::Thread.current[@key])
181
+ end
182
+ ::Thread.current[@key] = nil
183
+ ::Thread.current[@key_count] = nil
184
+ else
185
+ ::Thread.current[@key_count] -= 1
186
+ end
187
+ elsif !force
188
+ raise ConnectionPool::Error, "no connections are checked out"
72
189
  end
190
+
73
191
  nil
74
192
  end
75
193
 
76
- class Wrapper < ::BasicObject
77
- METHODS = [:with]
194
+ ##
195
+ # Shuts down the ConnectionPool by passing each connection to +block+ and
196
+ # then removing it from the pool. Attempting to checkout a connection after
197
+ # shutdown will raise +ConnectionPool::PoolShuttingDownError+.
198
+ def shutdown(&block)
199
+ @available.shutdown(&block)
200
+ end
78
201
 
79
- def initialize(options = {}, &block)
80
- @pool = ::ConnectionPool.new(options, &block)
81
- end
202
+ ##
203
+ # Reloads the ConnectionPool by passing each connection to +block+ and then
204
+ # removing it the pool. Subsequent checkouts will create new connections as
205
+ # needed.
206
+ def reload(&block)
207
+ @available.shutdown(reload: true, &block)
208
+ end
82
209
 
83
- def with
84
- yield @pool.checkout
85
- ensure
86
- @pool.checkin
87
- end
210
+ ## Reaps idle connections that have been idle for over +idle_seconds+.
211
+ # +idle_seconds+ defaults to 60.
212
+ def reap(idle_seconds = 60, &block)
213
+ @available.reap(idle_seconds, &block)
214
+ end
88
215
 
89
- def respond_to?(id, *args)
90
- METHODS.include?(id) || @pool.with { |c| c.respond_to?(id, *args) }
91
- end
216
+ # Size of this connection pool
217
+ attr_reader :size
218
+ # Automatically drop all connections after fork
219
+ attr_reader :auto_reload_after_fork
92
220
 
93
- def method_missing(name, *args, &block)
94
- @pool.with do |connection|
95
- connection.send(name, *args, &block)
96
- end
97
- end
221
+ # Number of pool entries available for checkout at this instant.
222
+ def available
223
+ @available.length
224
+ end
225
+
226
+ # Number of pool entries created and idle in the pool.
227
+ def idle
228
+ @available.idle
98
229
  end
99
230
  end
100
231
 
101
- require_relative 'connection_pool/timed_stack'
232
+ require_relative "connection_pool/timed_stack"
233
+ require_relative "connection_pool/wrapper"
metadata CHANGED
@@ -1,59 +1,94 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: connection_pool
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
5
- prerelease:
4
+ version: 2.5.5
6
5
  platform: ruby
7
6
  authors:
8
7
  - Mike Perham
9
- autorequire:
8
+ - Damian Janowski
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2012-12-19 00:00:00.000000000 Z
13
- dependencies: []
11
+ date: 1980-01-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 5.0.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 5.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
14
55
  description: Generic connection pool for Ruby
15
56
  email:
16
57
  - mperham@gmail.com
58
+ - damian@educabilia.com
17
59
  executables: []
18
60
  extensions: []
19
61
  extra_rdoc_files: []
20
62
  files:
21
- - .gitignore
22
63
  - Changes.md
23
- - Gemfile
24
64
  - LICENSE
25
65
  - README.md
26
- - Rakefile
27
66
  - connection_pool.gemspec
28
67
  - lib/connection_pool.rb
29
68
  - lib/connection_pool/timed_stack.rb
30
69
  - lib/connection_pool/version.rb
31
- - test/helper.rb
32
- - test/test_connection_pool.rb
70
+ - lib/connection_pool/wrapper.rb
33
71
  homepage: https://github.com/mperham/connection_pool
34
- licenses: []
35
- post_install_message:
72
+ licenses:
73
+ - MIT
74
+ metadata:
75
+ changelog_uri: https://github.com/mperham/connection_pool/blob/main/Changes.md
76
+ rubygems_mfa_required: 'true'
36
77
  rdoc_options: []
37
78
  require_paths:
38
79
  - lib
39
80
  required_ruby_version: !ruby/object:Gem::Requirement
40
- none: false
41
81
  requirements:
42
- - - ! '>='
82
+ - - ">="
43
83
  - !ruby/object:Gem::Version
44
- version: '0'
84
+ version: 2.5.0
45
85
  required_rubygems_version: !ruby/object:Gem::Requirement
46
- none: false
47
86
  requirements:
48
- - - ! '>='
87
+ - - ">="
49
88
  - !ruby/object:Gem::Version
50
89
  version: '0'
51
90
  requirements: []
52
- rubyforge_project:
53
- rubygems_version: 1.8.24
54
- signing_key:
55
- specification_version: 3
91
+ rubygems_version: 3.6.9
92
+ specification_version: 4
56
93
  summary: Generic connection pool for Ruby
57
- test_files:
58
- - test/helper.rb
59
- - test/test_connection_pool.rb
94
+ test_files: []
data/.gitignore DELETED
@@ -1,4 +0,0 @@
1
- *.gem
2
- .bundle
3
- Gemfile.lock
4
- pkg/*
data/Gemfile DELETED
@@ -1,7 +0,0 @@
1
- source "http://rubygems.org"
2
-
3
- # Specify your gem's dependencies in connection_pool.gemspec
4
- gemspec
5
-
6
- gem 'rake'
7
- gem 'minitest'
data/Rakefile DELETED
@@ -1,14 +0,0 @@
1
- begin
2
- require 'bundler'
3
- Bundler::GemHelper.install_tasks
4
- rescue LoadError
5
- end
6
-
7
- require 'rake/testtask'
8
- Rake::TestTask.new(:test) do |test|
9
- test.libs << 'test'
10
- test.warning = true
11
- test.pattern = 'test/**/test_*.rb'
12
- end
13
-
14
- task :default => :test
data/test/helper.rb DELETED
@@ -1,19 +0,0 @@
1
- require 'rubygems'
2
- require 'minitest/pride'
3
- require 'minitest/autorun'
4
-
5
- puts RUBY_DESCRIPTION
6
-
7
- class MiniTest::Unit::TestCase
8
-
9
- def async_test(time=0.5)
10
- q = TimedQueue.new
11
- yield Proc.new { q << nil }
12
- q.timed_pop(time)
13
- end
14
-
15
- end
16
-
17
- $VERBOSE = 1
18
-
19
- require_relative '../lib/connection_pool'
@@ -1,150 +0,0 @@
1
- Thread.abort_on_exception = true
2
- require 'helper'
3
-
4
- class TestConnectionPool < MiniTest::Unit::TestCase
5
-
6
- class NetworkConnection
7
- def initialize
8
- @x = 0
9
- end
10
-
11
- def do_something
12
- @x += 1
13
- sleep 0.05
14
- @x
15
- end
16
-
17
- def fast
18
- @x += 1
19
- end
20
-
21
- def do_something_with_block
22
- @x += yield
23
- sleep 0.05
24
- @x
25
- end
26
-
27
- def respond_to?(method_id, *args)
28
- method_id == :do_magic || super(method_id, *args)
29
- end
30
- end
31
-
32
- def test_basic_multithreaded_usage
33
- pool = ConnectionPool.new(:size => 5) { NetworkConnection.new }
34
- threads = []
35
- 15.times do
36
- threads << Thread.new do
37
- pool.with do |net|
38
- net.do_something
39
- end
40
- end
41
- end
42
-
43
- a = Time.now
44
- result = threads.map(&:value)
45
- b = Time.now
46
- assert_operator((b - a), :>, 0.125)
47
- assert_equal([1,2,3].cycle(5).sort, result.sort)
48
- end
49
-
50
- def test_timeout
51
- pool = ConnectionPool.new(:timeout => 0.05, :size => 1) { NetworkConnection.new }
52
- Thread.new do
53
- pool.with do |net|
54
- net.do_something
55
- sleep 0.1
56
- end
57
- end
58
- sleep 0.05
59
- assert_raises Timeout::Error do
60
- pool.with { |net| net.do_something }
61
- end
62
-
63
- sleep 0.05
64
- pool.with do |conn|
65
- refute_nil conn
66
- end
67
- end
68
-
69
- def test_passthru
70
- pool = ConnectionPool.wrap(:timeout => 0.1, :size => 1) { NetworkConnection.new }
71
- assert_equal 1, pool.do_something
72
- assert_equal 2, pool.do_something
73
- assert_equal 5, pool.do_something_with_block { 3 }
74
- assert_equal 6, pool.with { |net| net.fast }
75
- end
76
-
77
- def test_passthru_respond_to
78
- pool = ConnectionPool.wrap(:timeout => 0.1, :size => 1) { NetworkConnection.new }
79
- assert pool.respond_to?(:with)
80
- assert pool.respond_to?(:do_something)
81
- assert pool.respond_to?(:do_magic)
82
- refute pool.respond_to?(:do_lots_of_magic)
83
- end
84
-
85
- def test_return_value
86
- pool = ConnectionPool.new(:timeout => 0.1, :size => 1) { NetworkConnection.new }
87
- result = pool.with do |net|
88
- net.fast
89
- end
90
- assert_equal 1, result
91
- end
92
-
93
- def test_heavy_threading
94
- pool = ConnectionPool.new(:timeout => 0.5, :size => 3) { NetworkConnection.new }
95
- 20.times do
96
- Thread.new do
97
- pool.with do |net|
98
- sleep 0.05
99
- end
100
- end
101
- end
102
- sleep 0.5
103
- end
104
-
105
- def test_reuses_objects_when_pool_not_saturated
106
- pool = ConnectionPool.new(:size => 5) { NetworkConnection.new }
107
-
108
- ids = 10.times.map do
109
- pool.with { |c| c.object_id }
110
- end
111
-
112
- assert_equal 1, ids.uniq.size
113
- end
114
-
115
- class Recorder
116
- def initialize
117
- @calls = []
118
- end
119
-
120
- attr_reader :calls
121
-
122
- def do_work(label)
123
- @calls << label
124
- end
125
- end
126
-
127
- def test_nested_checkout
128
- recorder = Recorder.new
129
- pool = ConnectionPool.new(:size => 1) { recorder }
130
- pool.with do |r_outer|
131
- @other = Thread.new do |t|
132
- pool.with do |r_other|
133
- r_other.do_work('other')
134
- end
135
- end
136
-
137
- pool.with do |r_inner|
138
- r_inner.do_work('inner')
139
- end
140
-
141
- sleep 0.1
142
-
143
- r_outer.do_work('outer')
144
- end
145
-
146
- @other.join
147
-
148
- assert_equal ['inner', 'outer', 'other'], recorder.calls
149
- end
150
- end