connection_pool 2.4.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ea0776fcb09a3cc48ef4ca03774399e20b09e51039d0c47c1e4cb3bac621c52b
4
- data.tar.gz: b955d6b4e984259f20ae8cf6414f59692f9a51848424231363643e0c16dd2a3f
3
+ metadata.gz: 24c74a1caa5e04827c47155b75e19805bcda4c329e28793309dfa95b5881c4bd
4
+ data.tar.gz: 23aadf2da494be8c3314039700606cd4e01b58d6cae1f45e3b7cdef57a98e7bd
5
5
  SHA512:
6
- metadata.gz: bf57d8b5547502d91f5550ca6ea0be16905604c90e61efb6741e5ec3ce607c7a65f0b31e1673c96c60a06a2f64f5239cab6e94d3a50095fb822ea9b1c1bb2f0a
7
- data.tar.gz: 4b42aa5aa67b0e45bbbc8a9f29ca3a969efd8ade3b6dfca6cff082f526ec65a2a2e5c8fa17f512d33470b82535af8b675fc80903ccd75db26748e9845dd9a612
6
+ metadata.gz: 0f2385ddf4619ebc1bf9e2f202024b330c170ae2e4e7e63480be0f556242140af75eb3d272f6d262b2fc859625f0861c560af6a58c20bea3d69b005e2d248472
7
+ data.tar.gz: 0ac5103c69aa2e8226d2fa872714f9bd611b24ea171e91ce3d7a3c9be62b9887e24916cf4b6b1e5bc8c407349f87c3ea855c4659dbf5f3f137cd45738301d1a6
data/Changes.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # connection_pool Changelog
2
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
+
3
42
  2.4.1
4
43
  ------
5
44
 
data/README.md CHANGED
@@ -101,6 +101,55 @@ cp.with { |conn| conn.get('some-count') }
101
101
 
102
102
  Like `shutdown`, this will block until all connections are checked in and closed.
103
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
+
104
153
  ## Current State
105
154
 
106
155
  There are several methods that return information about a pool.
@@ -109,11 +158,15 @@ There are several methods that return information about a pool.
109
158
  cp = ConnectionPool.new(size: 10) { Redis.new }
110
159
  cp.size # => 10
111
160
  cp.available # => 10
161
+ cp.idle # => 0
112
162
 
113
163
  cp.with do |conn|
114
164
  cp.size # => 10
115
165
  cp.available # => 9
166
+ cp.idle # => 0
116
167
  end
168
+
169
+ cp.idle # => 1
117
170
  ```
118
171
 
119
172
  Notes
@@ -1,8 +1,8 @@
1
1
  ##
2
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
3
+ # you wish to manage). Connections are created lazily up to a given maximum
4
4
  # number.
5
-
5
+ #
6
6
  # Examples:
7
7
  #
8
8
  # ts = TimedStack.new(1) { MyConnection.new }
@@ -16,14 +16,12 @@
16
16
  # conn = ts.pop
17
17
  # ts.pop timeout: 5
18
18
  # #=> raises ConnectionPool::TimeoutError after 5 seconds
19
-
20
19
  class ConnectionPool::TimedStack
21
20
  attr_reader :max
22
21
 
23
22
  ##
24
23
  # Creates a new pool with +size+ connections that are created from the given
25
24
  # +block+.
26
-
27
25
  def initialize(size = 0, &block)
28
26
  @create_block = block
29
27
  @created = 0
@@ -35,12 +33,12 @@ class ConnectionPool::TimedStack
35
33
  end
36
34
 
37
35
  ##
38
- # Returns +obj+ to the stack. +options+ is ignored in TimedStack but may be
36
+ # Returns +obj+ to the stack. +options+ is ignored in TimedStack but may be
39
37
  # used by subclasses that extend TimedStack.
40
-
41
38
  def push(obj, options = {})
42
39
  @mutex.synchronize do
43
40
  if @shutdown_block
41
+ @created -= 1 unless @created == 0
44
42
  @shutdown_block.call(obj)
45
43
  else
46
44
  store_connection obj, options
@@ -52,14 +50,16 @@ class ConnectionPool::TimedStack
52
50
  alias_method :<<, :push
53
51
 
54
52
  ##
55
- # Retrieves a connection from the stack. If a connection is available it is
56
- # immediately returned. If no connection is available within the given
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
57
55
  # timeout a ConnectionPool::TimeoutError is raised.
58
56
  #
59
- # +:timeout+ is the only checked entry in +options+ and is preferred over
60
- # the +timeout+ argument (which will be removed in a future release). Other
61
- # options may be used by subclasses that extend TimedStack.
62
-
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
63
  def pop(timeout = 0.5, options = {})
64
64
  options, timeout = timeout, 0.5 if Hash === timeout
65
65
  timeout = options.fetch :timeout, timeout
@@ -68,13 +68,22 @@ class ConnectionPool::TimedStack
68
68
  @mutex.synchronize do
69
69
  loop do
70
70
  raise ConnectionPool::PoolShuttingDownError if @shutdown_block
71
- return fetch_connection(options) if connection_stored?(options)
71
+ if (conn = try_fetch_connection(options))
72
+ return conn
73
+ end
72
74
 
73
75
  connection = try_create(options)
74
76
  return connection if connection
75
77
 
76
78
  to_wait = deadline - current_time
77
- raise ConnectionPool::TimeoutError, "Waited #{timeout} sec, #{length}/#{@max} available" if to_wait <= 0
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
78
87
  @resource.wait(@mutex, to_wait)
79
88
  end
80
89
  end
@@ -85,7 +94,6 @@ class ConnectionPool::TimedStack
85
94
  # removing it from the pool. Attempting to checkout a connection after
86
95
  # shutdown will raise +ConnectionPool::PoolShuttingDownError+ unless
87
96
  # +:reload+ is +true+.
88
-
89
97
  def shutdown(reload: false, &block)
90
98
  raise ArgumentError, "shutdown must receive a block" unless block
91
99
 
@@ -99,19 +107,49 @@ class ConnectionPool::TimedStack
99
107
  end
100
108
 
101
109
  ##
102
- # Returns +true+ if there are no available connections.
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
127
+ end
103
128
 
129
+ ##
130
+ # Returns +true+ if there are no available connections.
104
131
  def empty?
105
132
  (@created - @que.length) >= @max
106
133
  end
107
134
 
108
135
  ##
109
136
  # The number of connections available on the stack.
110
-
111
137
  def length
112
138
  @max - @created + @que.length
113
139
  end
114
140
 
141
+ ##
142
+ # The number of connections created and available on the stack.
143
+ def idle
144
+ @que.length
145
+ end
146
+
147
+ ##
148
+ # Reduce the created count
149
+ def decrement_created
150
+ @created -= 1 unless @created == 0
151
+ end
152
+
115
153
  private
116
154
 
117
155
  def current_time
@@ -121,8 +159,17 @@ class ConnectionPool::TimedStack
121
159
  ##
122
160
  # This is an extension point for TimedStack and is called with a mutex.
123
161
  #
124
- # This method must returns true if a connection is available on the stack.
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
125
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.
126
173
  def connection_stored?(options = nil)
127
174
  !@que.empty?
128
175
  end
@@ -131,31 +178,48 @@ class ConnectionPool::TimedStack
131
178
  # This is an extension point for TimedStack and is called with a mutex.
132
179
  #
133
180
  # This method must return a connection from the stack.
134
-
135
181
  def fetch_connection(options = nil)
136
- @que.pop
182
+ @que.pop&.first
137
183
  end
138
184
 
139
185
  ##
140
186
  # This is an extension point for TimedStack and is called with a mutex.
141
187
  #
142
188
  # This method must shut down all connections on the stack.
143
-
144
189
  def shutdown_connections(options = nil)
145
- while connection_stored?(options)
146
- conn = fetch_connection(options)
190
+ while (conn = try_fetch_connection(options))
191
+ @created -= 1 unless @created == 0
147
192
  @shutdown_block.call(conn)
148
193
  end
149
- @created = 0
150
194
  end
151
195
 
152
196
  ##
153
197
  # This is an extension point for TimedStack and is called with a mutex.
154
198
  #
155
- # This method must return +obj+ to the stack.
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
156
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.
157
221
  def store_connection(obj, options = nil)
158
- @que.push obj
222
+ @que.push [obj, current_time]
159
223
  end
160
224
 
161
225
  ##
@@ -163,7 +227,6 @@ class ConnectionPool::TimedStack
163
227
  #
164
228
  # This method must create a connection if and only if the total number of
165
229
  # connections allowed has not been met.
166
-
167
230
  def try_create(options = nil)
168
231
  unless @created == @max
169
232
  object = @create_block.call
@@ -1,3 +1,3 @@
1
1
  class ConnectionPool
2
- VERSION = "2.4.1"
2
+ VERSION = "2.5.5"
3
3
  end
@@ -39,7 +39,7 @@ end
39
39
  # - :auto_reload_after_fork - automatically drop all connections after fork, defaults to true
40
40
  #
41
41
  class ConnectionPool
42
- DEFAULTS = {size: 5, timeout: 5, auto_reload_after_fork: true}
42
+ DEFAULTS = {size: 5, timeout: 5, auto_reload_after_fork: true}.freeze
43
43
 
44
44
  def self.wrap(options, &block)
45
45
  Wrapper.new(options, &block)
@@ -99,10 +99,14 @@ class ConnectionPool
99
99
  @available = TimedStack.new(@size, &block)
100
100
  @key = :"pool-#{@available.object_id}"
101
101
  @key_count = :"pool-#{@available.object_id}-count"
102
- INSTANCES[self] = self if INSTANCES
102
+ @discard_key = :"pool-#{@available.object_id}-discard"
103
+ INSTANCES[self] = self if @auto_reload_after_fork && INSTANCES
103
104
  end
104
105
 
105
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.
106
110
  Thread.handle_interrupt(Exception => :never) do
107
111
  conn = checkout(options)
108
112
  begin
@@ -116,20 +120,65 @@ class ConnectionPool
116
120
  end
117
121
  alias_method :then, :with
118
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
156
+
119
157
  def checkout(options = {})
120
158
  if ::Thread.current[@key]
121
159
  ::Thread.current[@key_count] += 1
122
160
  ::Thread.current[@key]
123
161
  else
124
162
  ::Thread.current[@key_count] = 1
125
- ::Thread.current[@key] = @available.pop(options[:timeout] || @timeout)
163
+ ::Thread.current[@key] = @available.pop(options[:timeout] || @timeout, options)
126
164
  end
127
165
  end
128
166
 
129
167
  def checkin(force: false)
130
168
  if ::Thread.current[@key]
131
169
  if ::Thread.current[@key_count] == 1 || force
132
- @available.push(::Thread.current[@key])
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
133
182
  ::Thread.current[@key] = nil
134
183
  ::Thread.current[@key_count] = nil
135
184
  else
@@ -146,7 +195,6 @@ class ConnectionPool
146
195
  # Shuts down the ConnectionPool by passing each connection to +block+ and
147
196
  # then removing it from the pool. Attempting to checkout a connection after
148
197
  # shutdown will raise +ConnectionPool::PoolShuttingDownError+.
149
-
150
198
  def shutdown(&block)
151
199
  @available.shutdown(&block)
152
200
  end
@@ -155,11 +203,16 @@ class ConnectionPool
155
203
  # Reloads the ConnectionPool by passing each connection to +block+ and then
156
204
  # removing it the pool. Subsequent checkouts will create new connections as
157
205
  # needed.
158
-
159
206
  def reload(&block)
160
207
  @available.shutdown(reload: true, &block)
161
208
  end
162
209
 
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
215
+
163
216
  # Size of this connection pool
164
217
  attr_reader :size
165
218
  # Automatically drop all connections after fork
@@ -169,6 +222,11 @@ class ConnectionPool
169
222
  def available
170
223
  @available.length
171
224
  end
225
+
226
+ # Number of pool entries created and idle in the pool.
227
+ def idle
228
+ @available.idle
229
+ end
172
230
  end
173
231
 
174
232
  require_relative "connection_pool/timed_stack"
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: connection_pool
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.1
4
+ version: 2.5.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Perham
8
8
  - Damian Janowski
9
- autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2023-05-19 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: bundler
@@ -75,7 +74,6 @@ licenses:
75
74
  metadata:
76
75
  changelog_uri: https://github.com/mperham/connection_pool/blob/main/Changes.md
77
76
  rubygems_mfa_required: 'true'
78
- post_install_message:
79
77
  rdoc_options: []
80
78
  require_paths:
81
79
  - lib
@@ -90,8 +88,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
90
88
  - !ruby/object:Gem::Version
91
89
  version: '0'
92
90
  requirements: []
93
- rubygems_version: 3.4.7
94
- signing_key:
91
+ rubygems_version: 3.6.9
95
92
  specification_version: 4
96
93
  summary: Generic connection pool for Ruby
97
94
  test_files: []