connection_pool 2.5.3 → 3.0.2
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 +4 -4
- data/Changes.md +35 -0
- data/README.md +49 -34
- data/connection_pool.gemspec +16 -5
- data/lib/connection_pool/fork.rb +40 -0
- data/lib/connection_pool/timed_stack.rb +49 -34
- data/lib/connection_pool/version.rb +1 -1
- data/lib/connection_pool/wrapper.rb +15 -28
- data/lib/connection_pool.rb +72 -71
- metadata +12 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a064d41333b8b92fcb23617701011f1b34e6348b324048ca16e5cb758d31f05f
|
|
4
|
+
data.tar.gz: 7ca1cc56ff7d020f2b7f2cee01b2008502faadf563dad010c2b8eee4a0b83dcd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1c7554d540f6aefcd356154246a1cdcab5e4ddea745e9a580261fba0635e6b711a9e0781691503f20aca7faaf982e2ba8f5f48cc71ff2a4f67053039e947ba3d
|
|
7
|
+
data.tar.gz: 431dcf74c39f6a9db1503290a227f5780c4b03e392ef25709245e63d0ca46a2f0f6cc0792a5241e615eafd92663aa38c1165aae99f6dffb0783f5b442846582f
|
data/Changes.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# connection_pool Changelog
|
|
2
2
|
|
|
3
|
+
3.0.2
|
|
4
|
+
------
|
|
5
|
+
|
|
6
|
+
- Support :name keyword for backwards compatibility [#210]
|
|
7
|
+
|
|
8
|
+
3.0.1
|
|
9
|
+
------
|
|
10
|
+
|
|
11
|
+
- Add missing `fork.rb` to gemspec.
|
|
12
|
+
|
|
13
|
+
3.0.0
|
|
14
|
+
------
|
|
15
|
+
|
|
16
|
+
- **BREAKING CHANGES** `ConnectionPool` and `ConnectionPool::TimedStack` now
|
|
17
|
+
use keyword arguments rather than positional arguments everywhere. Expected impact is minimal as most people use the `with` API, which is unchanged.
|
|
18
|
+
```ruby
|
|
19
|
+
pool = ConnectionPool.new(size: 5, timeout: 5)
|
|
20
|
+
pool.checkout(1) # 2.x
|
|
21
|
+
pool.reap(30) # 2.x
|
|
22
|
+
pool.checkout(timeout: 1) # 3.x
|
|
23
|
+
pool.reap(idle_seconds: 30) # 3.x
|
|
24
|
+
```
|
|
25
|
+
- Dropped support for Ruby <3.2.0
|
|
26
|
+
|
|
27
|
+
2.5.5
|
|
28
|
+
------
|
|
29
|
+
|
|
30
|
+
- Support `ConnectionPool::TimedStack#pop(exception: false)` [#207]
|
|
31
|
+
to avoid using exceptions as control flow.
|
|
32
|
+
|
|
33
|
+
2.5.4
|
|
34
|
+
------
|
|
35
|
+
|
|
36
|
+
- Add ability to remove a broken connection from the pool [#204, womblep]
|
|
37
|
+
|
|
3
38
|
2.5.3
|
|
4
39
|
------
|
|
5
40
|
|
data/README.md
CHANGED
|
@@ -38,7 +38,7 @@ connection pool and a raw client.
|
|
|
38
38
|
$redis.then { |r| r.set 'foo' 'bar' }
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
Optionally, you can specify a timeout override
|
|
41
|
+
Optionally, you can specify a timeout override:
|
|
42
42
|
|
|
43
43
|
``` ruby
|
|
44
44
|
$memcached.with(timeout: 2.0) do |conn|
|
|
@@ -46,11 +46,9 @@ $memcached.with(timeout: 2.0) do |conn|
|
|
|
46
46
|
end
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
This will only modify the
|
|
50
|
-
invocation.
|
|
49
|
+
This will only modify the timeout for this particular invocation.
|
|
51
50
|
This is useful if you want to fail-fast on certain non-critical
|
|
52
51
|
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
52
|
|
|
55
53
|
## Migrating to a Connection Pool
|
|
56
54
|
|
|
@@ -72,7 +70,7 @@ $redis.with do |conn|
|
|
|
72
70
|
end
|
|
73
71
|
```
|
|
74
72
|
|
|
75
|
-
Once you've ported your entire system to use `with`, you can
|
|
73
|
+
Once you've ported your entire system to use `with`, you can remove `::Wrapper` and use `ConnectionPool` directly.
|
|
76
74
|
|
|
77
75
|
|
|
78
76
|
## Shutdown
|
|
@@ -90,41 +88,50 @@ Shutting down a connection pool will block until all connections are checked in
|
|
|
90
88
|
|
|
91
89
|
## Reload
|
|
92
90
|
|
|
93
|
-
You can reload a ConnectionPool instance
|
|
94
|
-
|
|
91
|
+
You can reload a ConnectionPool instance if it is necessary to close all existing connections and continue to use the pool.
|
|
92
|
+
ConnectionPool will automatically reload if the process is forked.
|
|
93
|
+
Use `auto_reload_after_fork: false` if you don't want this behavior.
|
|
95
94
|
|
|
96
95
|
```ruby
|
|
97
|
-
cp = ConnectionPool.new { Redis.new }
|
|
98
|
-
cp.reload { |conn| conn.quit }
|
|
96
|
+
cp = ConnectionPool.new(auto_reload_after_fork: false) { Redis.new }
|
|
97
|
+
cp.reload { |conn| conn.quit } # reload manually
|
|
99
98
|
cp.with { |conn| conn.get('some-count') }
|
|
100
99
|
```
|
|
101
100
|
|
|
102
|
-
Like `shutdown`,
|
|
101
|
+
Like `shutdown`, `reload` will block until all connections are checked in and closed.
|
|
103
102
|
|
|
104
103
|
## Reap
|
|
105
104
|
|
|
106
|
-
You can reap
|
|
105
|
+
You can call `reap` periodically on the ConnectionPool instance to close connections that were created but have not been used for a certain amount of time. This can be useful in environments where connections are expensive.
|
|
107
106
|
|
|
108
|
-
You can specify how many seconds the connections have to be idle for them to be reaped.
|
|
109
|
-
Defaults to 60 seconds.
|
|
107
|
+
You can specify how many seconds the connections have to be idle for them to be reaped, defaulting to 60 seconds.
|
|
110
108
|
|
|
111
109
|
```ruby
|
|
112
110
|
cp = ConnectionPool.new { Redis.new }
|
|
113
|
-
|
|
111
|
+
|
|
112
|
+
# Start a reaper thread to reap connections that have been
|
|
113
|
+
# idle more than 300 seconds (5 minutes)
|
|
114
|
+
Thread.new do
|
|
115
|
+
loop do
|
|
116
|
+
cp.reap(idle_seconds: 300, &:close)
|
|
117
|
+
sleep 30
|
|
118
|
+
end
|
|
119
|
+
end
|
|
114
120
|
```
|
|
115
121
|
|
|
116
|
-
|
|
122
|
+
## Discarding Connections
|
|
117
123
|
|
|
118
|
-
You can
|
|
124
|
+
You can discard connections in the ConnectionPool instance to remove connections that are broken and can't be repaired.
|
|
125
|
+
It can only be done inside the block passed to `with`.
|
|
126
|
+
Takes an optional block that will be executed with the connection.
|
|
119
127
|
|
|
120
128
|
```ruby
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
sleep 300
|
|
129
|
+
pool.with do |conn|
|
|
130
|
+
begin
|
|
131
|
+
conn.execute("SELECT 1")
|
|
132
|
+
rescue SomeConnectionError
|
|
133
|
+
pool.discard_current_connection(&:close) # remove the connection from the pool
|
|
134
|
+
raise
|
|
128
135
|
end
|
|
129
136
|
end
|
|
130
137
|
```
|
|
@@ -148,20 +155,28 @@ end
|
|
|
148
155
|
cp.idle # => 1
|
|
149
156
|
```
|
|
150
157
|
|
|
151
|
-
|
|
152
|
-
|
|
158
|
+
## Upgrading from ConnectionPool 2
|
|
159
|
+
|
|
160
|
+
* Support for Ruby <3.2 has been removed.
|
|
161
|
+
* ConnectionPool's APIs now consistently use keyword arguments everywhere.
|
|
162
|
+
Positional arguments must be converted to keywords:
|
|
163
|
+
```ruby
|
|
164
|
+
pool = ConnectionPool.new(size: 5, timeout: 5)
|
|
165
|
+
pool.checkout(1) # 2.x
|
|
166
|
+
pool.reap(30) # 2.x
|
|
167
|
+
pool.checkout(timeout: 1) # 3.x
|
|
168
|
+
pool.reap(idle_seconds: 30) # 3.x
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Notes
|
|
153
172
|
|
|
154
173
|
- Connections are lazily created as needed.
|
|
155
|
-
-
|
|
156
|
-
connections should be self-repairing. This is true of the Dalli and Redis
|
|
157
|
-
clients.
|
|
158
|
-
- **WARNING**: Don't ever use `Timeout.timeout` in your Ruby code or you will see
|
|
174
|
+
- **WARNING**: Avoid `Timeout.timeout` in your Ruby code or you can see
|
|
159
175
|
occasional silent corruption and mysterious errors. The Timeout API is unsafe
|
|
160
|
-
and
|
|
161
|
-
|
|
176
|
+
and dangerous to use. Use proper socket timeout options as exposed by
|
|
177
|
+
Net::HTTP, Redis, Dalli, etc.
|
|
162
178
|
|
|
163
179
|
|
|
164
|
-
Author
|
|
165
|
-
------
|
|
180
|
+
## Author
|
|
166
181
|
|
|
167
|
-
Mike Perham, [@getajobmike](https://
|
|
182
|
+
Mike Perham, [@getajobmike](https://ruby.social/@getajobmike), <https://www.mikeperham.com>
|
data/connection_pool.gemspec
CHANGED
|
@@ -10,15 +10,26 @@ Gem::Specification.new do |s|
|
|
|
10
10
|
s.description = s.summary = "Generic connection pool for Ruby"
|
|
11
11
|
|
|
12
12
|
s.files = ["Changes.md", "LICENSE", "README.md", "connection_pool.gemspec",
|
|
13
|
-
"lib/connection_pool.rb",
|
|
14
|
-
"lib/connection_pool/
|
|
13
|
+
"lib/connection_pool.rb",
|
|
14
|
+
"lib/connection_pool/timed_stack.rb",
|
|
15
|
+
"lib/connection_pool/version.rb",
|
|
16
|
+
"lib/connection_pool/fork.rb",
|
|
17
|
+
"lib/connection_pool/wrapper.rb"]
|
|
15
18
|
s.executables = []
|
|
16
19
|
s.require_paths = ["lib"]
|
|
17
20
|
s.license = "MIT"
|
|
21
|
+
|
|
22
|
+
s.required_ruby_version = ">= 3.2.0"
|
|
18
23
|
s.add_development_dependency "bundler"
|
|
19
|
-
s.add_development_dependency "
|
|
24
|
+
s.add_development_dependency "maxitest"
|
|
20
25
|
s.add_development_dependency "rake"
|
|
21
|
-
s.required_ruby_version = ">= 2.5.0"
|
|
22
26
|
|
|
23
|
-
s.metadata = {
|
|
27
|
+
s.metadata = {
|
|
28
|
+
"bug_tracker_uri" => "https://github.com/mperham/connection_pool/issues",
|
|
29
|
+
"documentation_uri" => "https://github.com/mperham/connection_pool/wiki",
|
|
30
|
+
"changelog_uri" => "https://github.com/mperham/connection_pool/blob/main/Changes.md",
|
|
31
|
+
"source_code_uri" => "https://github.com/mperham/connection_pool",
|
|
32
|
+
"homepage_uri" => "https://github.com/mperham/connection_pool",
|
|
33
|
+
"rubygems_mfa_required" => "true"
|
|
34
|
+
}
|
|
24
35
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
class ConnectionPool
|
|
2
|
+
if Process.respond_to?(:fork)
|
|
3
|
+
INSTANCES = ObjectSpace::WeakMap.new
|
|
4
|
+
private_constant :INSTANCES
|
|
5
|
+
|
|
6
|
+
def self.after_fork
|
|
7
|
+
INSTANCES.each_value do |pool|
|
|
8
|
+
# We're in after_fork, so we know all other threads are dead.
|
|
9
|
+
# All we need to do is ensure the main thread doesn't have a
|
|
10
|
+
# checked out connection
|
|
11
|
+
pool.checkin(force: true)
|
|
12
|
+
pool.reload do |connection|
|
|
13
|
+
# Unfortunately we don't know what method to call to close the connection,
|
|
14
|
+
# so we try the most common one.
|
|
15
|
+
connection.close if connection.respond_to?(:close)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
module ForkTracker
|
|
22
|
+
def _fork
|
|
23
|
+
pid = super
|
|
24
|
+
if pid == 0
|
|
25
|
+
ConnectionPool.after_fork
|
|
26
|
+
end
|
|
27
|
+
pid
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
Process.singleton_class.prepend(ForkTracker)
|
|
31
|
+
else
|
|
32
|
+
# JRuby, et al
|
|
33
|
+
INSTANCES = nil
|
|
34
|
+
private_constant :INSTANCES
|
|
35
|
+
|
|
36
|
+
def self.after_fork
|
|
37
|
+
# noop
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
#
|
|
6
6
|
# Examples:
|
|
7
7
|
#
|
|
8
|
-
# ts = TimedStack.new(1) { MyConnection.new }
|
|
8
|
+
# ts = TimedStack.new(size: 1) { MyConnection.new }
|
|
9
9
|
#
|
|
10
10
|
# # fetch a connection
|
|
11
11
|
# conn = ts.pop
|
|
@@ -22,7 +22,7 @@ class ConnectionPool::TimedStack
|
|
|
22
22
|
##
|
|
23
23
|
# Creates a new pool with +size+ connections that are created from the given
|
|
24
24
|
# +block+.
|
|
25
|
-
def initialize(size
|
|
25
|
+
def initialize(size: 0, &block)
|
|
26
26
|
@create_block = block
|
|
27
27
|
@created = 0
|
|
28
28
|
@que = []
|
|
@@ -33,15 +33,15 @@ class ConnectionPool::TimedStack
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
##
|
|
36
|
-
# Returns +obj+ to the stack.
|
|
36
|
+
# Returns +obj+ to the stack. Additional kwargs are ignored in TimedStack but may be
|
|
37
37
|
# used by subclasses that extend TimedStack.
|
|
38
|
-
def push(obj,
|
|
38
|
+
def push(obj, **)
|
|
39
39
|
@mutex.synchronize do
|
|
40
40
|
if @shutdown_block
|
|
41
41
|
@created -= 1 unless @created == 0
|
|
42
42
|
@shutdown_block.call(obj)
|
|
43
43
|
else
|
|
44
|
-
store_connection obj,
|
|
44
|
+
store_connection obj, **
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
@resource.broadcast
|
|
@@ -54,26 +54,31 @@ class ConnectionPool::TimedStack
|
|
|
54
54
|
# immediately returned. If no connection is available within the given
|
|
55
55
|
# timeout a ConnectionPool::TimeoutError is raised.
|
|
56
56
|
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
+
# Other options may be used by subclasses that extend TimedStack.
|
|
62
|
+
def pop(timeout: 0.5, exception: ConnectionPool::TimeoutError, **)
|
|
64
63
|
deadline = current_time + timeout
|
|
65
64
|
@mutex.synchronize do
|
|
66
65
|
loop do
|
|
67
66
|
raise ConnectionPool::PoolShuttingDownError if @shutdown_block
|
|
68
|
-
if (conn = try_fetch_connection(
|
|
67
|
+
if (conn = try_fetch_connection(**))
|
|
69
68
|
return conn
|
|
70
69
|
end
|
|
71
70
|
|
|
72
|
-
connection = try_create(
|
|
71
|
+
connection = try_create(**)
|
|
73
72
|
return connection if connection
|
|
74
73
|
|
|
75
74
|
to_wait = deadline - current_time
|
|
76
|
-
|
|
75
|
+
if to_wait <= 0
|
|
76
|
+
if exception
|
|
77
|
+
raise exception, "Waited #{timeout} sec, #{length}/#{@max} available"
|
|
78
|
+
else
|
|
79
|
+
return nil
|
|
80
|
+
end
|
|
81
|
+
end
|
|
77
82
|
@resource.wait(@mutex, to_wait)
|
|
78
83
|
end
|
|
79
84
|
end
|
|
@@ -98,21 +103,20 @@ class ConnectionPool::TimedStack
|
|
|
98
103
|
|
|
99
104
|
##
|
|
100
105
|
# Reaps connections that were checked in more than +idle_seconds+ ago.
|
|
101
|
-
def reap(idle_seconds
|
|
102
|
-
raise ArgumentError, "reap must receive a block" unless
|
|
106
|
+
def reap(idle_seconds:)
|
|
107
|
+
raise ArgumentError, "reap must receive a block" unless block_given?
|
|
103
108
|
raise ArgumentError, "idle_seconds must be a number" unless idle_seconds.is_a?(Numeric)
|
|
104
109
|
raise ConnectionPool::PoolShuttingDownError if @shutdown_block
|
|
105
110
|
|
|
106
|
-
idle
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
end
|
|
111
|
+
count = idle
|
|
112
|
+
count.times do
|
|
113
|
+
conn = @mutex.synchronize do
|
|
114
|
+
raise ConnectionPool::PoolShuttingDownError if @shutdown_block
|
|
115
|
+
reserve_idle_connection(idle_seconds)
|
|
116
|
+
end
|
|
113
117
|
break unless conn
|
|
114
118
|
|
|
115
|
-
|
|
119
|
+
yield conn
|
|
116
120
|
end
|
|
117
121
|
end
|
|
118
122
|
|
|
@@ -134,6 +138,12 @@ class ConnectionPool::TimedStack
|
|
|
134
138
|
@que.length
|
|
135
139
|
end
|
|
136
140
|
|
|
141
|
+
##
|
|
142
|
+
# Reduce the created count
|
|
143
|
+
def decrement_created
|
|
144
|
+
@created -= 1 unless @created == 0
|
|
145
|
+
end
|
|
146
|
+
|
|
137
147
|
private
|
|
138
148
|
|
|
139
149
|
def current_time
|
|
@@ -146,15 +156,15 @@ class ConnectionPool::TimedStack
|
|
|
146
156
|
# This method must returns a connection from the stack if one exists. Allows
|
|
147
157
|
# subclasses with expensive match/search algorithms to avoid double-handling
|
|
148
158
|
# their stack.
|
|
149
|
-
def try_fetch_connection(
|
|
150
|
-
connection_stored?(
|
|
159
|
+
def try_fetch_connection(**)
|
|
160
|
+
connection_stored?(**) && fetch_connection(**)
|
|
151
161
|
end
|
|
152
162
|
|
|
153
163
|
##
|
|
154
164
|
# This is an extension point for TimedStack and is called with a mutex.
|
|
155
165
|
#
|
|
156
166
|
# This method must returns true if a connection is available on the stack.
|
|
157
|
-
def connection_stored?(
|
|
167
|
+
def connection_stored?(**)
|
|
158
168
|
!@que.empty?
|
|
159
169
|
end
|
|
160
170
|
|
|
@@ -162,7 +172,7 @@ class ConnectionPool::TimedStack
|
|
|
162
172
|
# This is an extension point for TimedStack and is called with a mutex.
|
|
163
173
|
#
|
|
164
174
|
# This method must return a connection from the stack.
|
|
165
|
-
def fetch_connection(
|
|
175
|
+
def fetch_connection(**)
|
|
166
176
|
@que.pop&.first
|
|
167
177
|
end
|
|
168
178
|
|
|
@@ -170,8 +180,8 @@ class ConnectionPool::TimedStack
|
|
|
170
180
|
# This is an extension point for TimedStack and is called with a mutex.
|
|
171
181
|
#
|
|
172
182
|
# This method must shut down all connections on the stack.
|
|
173
|
-
def shutdown_connections(
|
|
174
|
-
while (conn = try_fetch_connection(
|
|
183
|
+
def shutdown_connections(**)
|
|
184
|
+
while (conn = try_fetch_connection(**))
|
|
175
185
|
@created -= 1 unless @created == 0
|
|
176
186
|
@shutdown_block.call(conn)
|
|
177
187
|
end
|
|
@@ -187,6 +197,8 @@ class ConnectionPool::TimedStack
|
|
|
187
197
|
|
|
188
198
|
@created -= 1 unless @created == 0
|
|
189
199
|
|
|
200
|
+
# Most active elements are at the tail of the array.
|
|
201
|
+
# Most idle will be at the head so `shift` rather than `pop`.
|
|
190
202
|
@que.shift.first
|
|
191
203
|
end
|
|
192
204
|
|
|
@@ -195,14 +207,17 @@ class ConnectionPool::TimedStack
|
|
|
195
207
|
#
|
|
196
208
|
# Returns true if the first connection in the stack has been idle for more than idle_seconds
|
|
197
209
|
def idle_connections?(idle_seconds)
|
|
198
|
-
connection_stored?
|
|
210
|
+
return unless connection_stored?
|
|
211
|
+
# Most idle will be at the head so `first`
|
|
212
|
+
age = (current_time - @que.first.last)
|
|
213
|
+
age > idle_seconds
|
|
199
214
|
end
|
|
200
215
|
|
|
201
216
|
##
|
|
202
217
|
# This is an extension point for TimedStack and is called with a mutex.
|
|
203
218
|
#
|
|
204
219
|
# This method must return +obj+ to the stack.
|
|
205
|
-
def store_connection(obj,
|
|
220
|
+
def store_connection(obj, **)
|
|
206
221
|
@que.push [obj, current_time]
|
|
207
222
|
end
|
|
208
223
|
|
|
@@ -211,7 +226,7 @@ class ConnectionPool::TimedStack
|
|
|
211
226
|
#
|
|
212
227
|
# This method must create a connection if and only if the total number of
|
|
213
228
|
# connections allowed has not been met.
|
|
214
|
-
def try_create(
|
|
229
|
+
def try_create(**)
|
|
215
230
|
unless @created == @max
|
|
216
231
|
object = @create_block.call
|
|
217
232
|
@created += 1
|
|
@@ -2,20 +2,20 @@ class ConnectionPool
|
|
|
2
2
|
class Wrapper < ::BasicObject
|
|
3
3
|
METHODS = [:with, :pool_shutdown, :wrapped_pool]
|
|
4
4
|
|
|
5
|
-
def initialize(options
|
|
6
|
-
@pool = options.fetch(:pool) { ::ConnectionPool.new(options, &
|
|
5
|
+
def initialize(**options, &)
|
|
6
|
+
@pool = options.fetch(:pool) { ::ConnectionPool.new(**options, &) }
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def wrapped_pool
|
|
10
10
|
@pool
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
def with(&
|
|
14
|
-
@pool.with(&
|
|
13
|
+
def with(**, &)
|
|
14
|
+
@pool.with(**, &)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
def pool_shutdown(&
|
|
18
|
-
@pool.shutdown(&
|
|
17
|
+
def pool_shutdown(&)
|
|
18
|
+
@pool.shutdown(&)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def pool_size
|
|
@@ -26,31 +26,18 @@ class ConnectionPool
|
|
|
26
26
|
@pool.available
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def respond_to?(id,
|
|
30
|
-
METHODS.include?(id) || with { |c| c.respond_to?(id,
|
|
29
|
+
def respond_to?(id, *, **)
|
|
30
|
+
METHODS.include?(id) || with { |c| c.respond_to?(id, *, **) }
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
33
|
+
def respond_to_missing?(id, *, **)
|
|
34
|
+
with { |c| c.respond_to?(id, *, **) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def method_missing(name, *, **, &)
|
|
38
|
+
with do |connection|
|
|
39
|
+
connection.send(name, *, **, &)
|
|
51
40
|
end
|
|
52
41
|
end
|
|
53
|
-
# rubocop:enable Style/MethodMissingSuper
|
|
54
|
-
# rubocop:enable Style/MissingRespondToMissing
|
|
55
42
|
end
|
|
56
43
|
end
|
data/lib/connection_pool.rb
CHANGED
|
@@ -39,72 +39,30 @@ end
|
|
|
39
39
|
# - :auto_reload_after_fork - automatically drop all connections after fork, defaults to true
|
|
40
40
|
#
|
|
41
41
|
class ConnectionPool
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def self.wrap(options, &block)
|
|
45
|
-
Wrapper.new(options, &block)
|
|
46
|
-
end
|
|
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
|
|
42
|
+
def self.wrap(**, &)
|
|
43
|
+
Wrapper.new(**, &)
|
|
88
44
|
end
|
|
89
45
|
|
|
90
|
-
|
|
91
|
-
raise ArgumentError, "Connection pool requires a block" unless block
|
|
92
|
-
|
|
93
|
-
options = DEFAULTS.merge(options)
|
|
46
|
+
attr_reader :size
|
|
94
47
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
@auto_reload_after_fork = options.fetch(:auto_reload_after_fork)
|
|
48
|
+
def initialize(timeout: 5, size: 5, auto_reload_after_fork: true, name: nil, &)
|
|
49
|
+
raise ArgumentError, "Connection pool requires a block" unless block_given?
|
|
98
50
|
|
|
99
|
-
@
|
|
51
|
+
@size = Integer(size)
|
|
52
|
+
@timeout = Float(timeout)
|
|
53
|
+
@available = TimedStack.new(size: @size, &)
|
|
100
54
|
@key = :"pool-#{@available.object_id}"
|
|
101
55
|
@key_count = :"pool-#{@available.object_id}-count"
|
|
102
|
-
|
|
56
|
+
@discard_key = :"pool-#{@available.object_id}-discard"
|
|
57
|
+
INSTANCES[self] = self if auto_reload_after_fork && INSTANCES
|
|
103
58
|
end
|
|
104
59
|
|
|
105
|
-
def with(
|
|
60
|
+
def with(**)
|
|
61
|
+
# We need to manage exception handling manually here in order
|
|
62
|
+
# to work correctly with `Timeout.timeout` and `Thread#raise`.
|
|
63
|
+
# Otherwise an interrupted Thread can leak connections.
|
|
106
64
|
Thread.handle_interrupt(Exception => :never) do
|
|
107
|
-
conn = checkout(
|
|
65
|
+
conn = checkout(**)
|
|
108
66
|
begin
|
|
109
67
|
Thread.handle_interrupt(Exception => :immediate) do
|
|
110
68
|
yield conn
|
|
@@ -116,20 +74,67 @@ class ConnectionPool
|
|
|
116
74
|
end
|
|
117
75
|
alias_method :then, :with
|
|
118
76
|
|
|
119
|
-
|
|
77
|
+
##
|
|
78
|
+
# Marks the current thread's checked-out connection for discard.
|
|
79
|
+
#
|
|
80
|
+
# When a connection is marked for discard, it will not be returned to the pool
|
|
81
|
+
# when checked in. Instead, the connection will be discarded.
|
|
82
|
+
# This is useful when a connection has become invalid or corrupted
|
|
83
|
+
# and should not be reused.
|
|
84
|
+
#
|
|
85
|
+
# Takes an optional block that will be called with the connection to be discarded.
|
|
86
|
+
# The block should perform any necessary clean-up on the connection.
|
|
87
|
+
#
|
|
88
|
+
# @yield [conn]
|
|
89
|
+
# @yieldparam conn [Object] The connection to be discarded.
|
|
90
|
+
# @yieldreturn [void]
|
|
91
|
+
#
|
|
92
|
+
#
|
|
93
|
+
# Note: This only affects the connection currently checked out by the calling thread.
|
|
94
|
+
# The connection will be discarded when +checkin+ is called.
|
|
95
|
+
#
|
|
96
|
+
# @return [void]
|
|
97
|
+
#
|
|
98
|
+
# @example
|
|
99
|
+
# pool.with do |conn|
|
|
100
|
+
# begin
|
|
101
|
+
# conn.execute("SELECT 1")
|
|
102
|
+
# rescue SomeConnectionError
|
|
103
|
+
# pool.discard_current_connection # Mark connection as bad
|
|
104
|
+
# raise
|
|
105
|
+
# end
|
|
106
|
+
# end
|
|
107
|
+
def discard_current_connection(&block)
|
|
108
|
+
::Thread.current[@discard_key] = block || proc { |conn| conn }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def checkout(timeout: @timeout, **)
|
|
120
112
|
if ::Thread.current[@key]
|
|
121
113
|
::Thread.current[@key_count] += 1
|
|
122
114
|
::Thread.current[@key]
|
|
123
115
|
else
|
|
116
|
+
conn = @available.pop(timeout:, **)
|
|
117
|
+
::Thread.current[@key] = conn
|
|
124
118
|
::Thread.current[@key_count] = 1
|
|
125
|
-
|
|
119
|
+
conn
|
|
126
120
|
end
|
|
127
121
|
end
|
|
128
122
|
|
|
129
123
|
def checkin(force: false)
|
|
130
124
|
if ::Thread.current[@key]
|
|
131
125
|
if ::Thread.current[@key_count] == 1 || force
|
|
132
|
-
|
|
126
|
+
if ::Thread.current[@discard_key]
|
|
127
|
+
begin
|
|
128
|
+
@available.decrement_created
|
|
129
|
+
::Thread.current[@discard_key].call(::Thread.current[@key])
|
|
130
|
+
rescue
|
|
131
|
+
nil
|
|
132
|
+
ensure
|
|
133
|
+
::Thread.current[@discard_key] = nil
|
|
134
|
+
end
|
|
135
|
+
else
|
|
136
|
+
@available.push(::Thread.current[@key])
|
|
137
|
+
end
|
|
133
138
|
::Thread.current[@key] = nil
|
|
134
139
|
::Thread.current[@key_count] = nil
|
|
135
140
|
else
|
|
@@ -146,29 +151,24 @@ class ConnectionPool
|
|
|
146
151
|
# Shuts down the ConnectionPool by passing each connection to +block+ and
|
|
147
152
|
# then removing it from the pool. Attempting to checkout a connection after
|
|
148
153
|
# shutdown will raise +ConnectionPool::PoolShuttingDownError+.
|
|
149
|
-
def shutdown(&
|
|
150
|
-
@available.shutdown(&
|
|
154
|
+
def shutdown(&)
|
|
155
|
+
@available.shutdown(&)
|
|
151
156
|
end
|
|
152
157
|
|
|
153
158
|
##
|
|
154
159
|
# Reloads the ConnectionPool by passing each connection to +block+ and then
|
|
155
160
|
# removing it the pool. Subsequent checkouts will create new connections as
|
|
156
161
|
# needed.
|
|
157
|
-
def reload(&
|
|
158
|
-
@available.shutdown(reload: true, &
|
|
162
|
+
def reload(&)
|
|
163
|
+
@available.shutdown(reload: true, &)
|
|
159
164
|
end
|
|
160
165
|
|
|
161
166
|
## Reaps idle connections that have been idle for over +idle_seconds+.
|
|
162
167
|
# +idle_seconds+ defaults to 60.
|
|
163
|
-
def reap(idle_seconds
|
|
164
|
-
@available.reap(idle_seconds
|
|
168
|
+
def reap(idle_seconds: 60, &)
|
|
169
|
+
@available.reap(idle_seconds:, &)
|
|
165
170
|
end
|
|
166
171
|
|
|
167
|
-
# Size of this connection pool
|
|
168
|
-
attr_reader :size
|
|
169
|
-
# Automatically drop all connections after fork
|
|
170
|
-
attr_reader :auto_reload_after_fork
|
|
171
|
-
|
|
172
172
|
# Number of pool entries available for checkout at this instant.
|
|
173
173
|
def available
|
|
174
174
|
@available.length
|
|
@@ -182,3 +182,4 @@ end
|
|
|
182
182
|
|
|
183
183
|
require_relative "connection_pool/timed_stack"
|
|
184
184
|
require_relative "connection_pool/wrapper"
|
|
185
|
+
require_relative "connection_pool/fork"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: connection_pool
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Perham
|
|
8
8
|
- Damian Janowski
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -25,19 +25,19 @@ dependencies:
|
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '0'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
|
-
name:
|
|
28
|
+
name: maxitest
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
31
|
- - ">="
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version:
|
|
33
|
+
version: '0'
|
|
34
34
|
type: :development
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - ">="
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version:
|
|
40
|
+
version: '0'
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: rake
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -65,6 +65,7 @@ files:
|
|
|
65
65
|
- README.md
|
|
66
66
|
- connection_pool.gemspec
|
|
67
67
|
- lib/connection_pool.rb
|
|
68
|
+
- lib/connection_pool/fork.rb
|
|
68
69
|
- lib/connection_pool/timed_stack.rb
|
|
69
70
|
- lib/connection_pool/version.rb
|
|
70
71
|
- lib/connection_pool/wrapper.rb
|
|
@@ -72,7 +73,11 @@ homepage: https://github.com/mperham/connection_pool
|
|
|
72
73
|
licenses:
|
|
73
74
|
- MIT
|
|
74
75
|
metadata:
|
|
76
|
+
bug_tracker_uri: https://github.com/mperham/connection_pool/issues
|
|
77
|
+
documentation_uri: https://github.com/mperham/connection_pool/wiki
|
|
75
78
|
changelog_uri: https://github.com/mperham/connection_pool/blob/main/Changes.md
|
|
79
|
+
source_code_uri: https://github.com/mperham/connection_pool
|
|
80
|
+
homepage_uri: https://github.com/mperham/connection_pool
|
|
76
81
|
rubygems_mfa_required: 'true'
|
|
77
82
|
rdoc_options: []
|
|
78
83
|
require_paths:
|
|
@@ -81,14 +86,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
81
86
|
requirements:
|
|
82
87
|
- - ">="
|
|
83
88
|
- !ruby/object:Gem::Version
|
|
84
|
-
version: 2.
|
|
89
|
+
version: 3.2.0
|
|
85
90
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
86
91
|
requirements:
|
|
87
92
|
- - ">="
|
|
88
93
|
- !ruby/object:Gem::Version
|
|
89
94
|
version: '0'
|
|
90
95
|
requirements: []
|
|
91
|
-
rubygems_version: 3.6.
|
|
96
|
+
rubygems_version: 3.6.9
|
|
92
97
|
specification_version: 4
|
|
93
98
|
summary: Generic connection pool for Ruby
|
|
94
99
|
test_files: []
|