connection_pool 2.1.3 → 2.2.4
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 +5 -5
- data/.github/workflows/ci.yml +21 -0
- data/Changes.md +35 -0
- data/README.md +53 -14
- data/Rakefile +3 -3
- data/connection_pool.gemspec +14 -14
- data/lib/connection_pool.rb +58 -69
- data/lib/connection_pool/timed_stack.rb +17 -39
- data/lib/connection_pool/version.rb +1 -1
- data/lib/connection_pool/wrapper.rb +51 -0
- data/test/helper.rb +4 -4
- data/test/test_connection_pool.rb +210 -114
- data/test/test_connection_pool_timed_stack.rb +22 -21
- metadata +9 -9
- data/.travis.yml +0 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0af12e8d2f551b932ee863a1385ddc9a3f3140fff886cacaca25298a4c21d218
|
4
|
+
data.tar.gz: 16bd7b78d7b35337d35befe063ceeb2cb749dfc9e68cd373e7f76bd856f6cba3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: edb345021997307fe736408ad3e3cbebd2ce86fcbdc636fe2d4f03d61258ecea22285a4f91e854d43631533513d6500c9f6cf2094f52d6756a2d61ee2daba256
|
7
|
+
data.tar.gz: d58d7519b9d4be9bcf7b0628e2aa9d937df2d61d0d1a9423b43618685adf242810256b9decc22bca90cb3ee3eec88e8eabffb409aa9e49f28d56815f26056b81
|
@@ -0,0 +1,21 @@
|
|
1
|
+
name: CI
|
2
|
+
|
3
|
+
on: [push, pull_request]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
test:
|
7
|
+
runs-on: ubuntu-latest
|
8
|
+
strategy:
|
9
|
+
fail-fast: false
|
10
|
+
matrix:
|
11
|
+
ruby: ["2.4", "2.5", "2.6", "2.7", "3.0", "jruby", "truffleruby"]
|
12
|
+
steps:
|
13
|
+
- uses: actions/checkout@v2
|
14
|
+
- uses: ruby/setup-ruby@v1
|
15
|
+
with:
|
16
|
+
ruby-version: ${{matrix.ruby}}
|
17
|
+
bundler-cache: true
|
18
|
+
|
19
|
+
- name: Run tests
|
20
|
+
timeout-minutes: 5
|
21
|
+
run: ${{matrix.env}} bundle exec rake
|
data/Changes.md
CHANGED
@@ -1,3 +1,38 @@
|
|
1
|
+
# connection_pool Changelog
|
2
|
+
|
3
|
+
2.2.4
|
4
|
+
------
|
5
|
+
|
6
|
+
- Add `reload` to close all connections, recreating them afterwards [Andrew Marshall, #140]
|
7
|
+
- Add `then` as a way to use a pool or a bare connection with the same code path [#138]
|
8
|
+
|
9
|
+
2.2.3
|
10
|
+
------
|
11
|
+
|
12
|
+
- Pool now throws `ConnectionPool::TimeoutError` on timeout. [#130]
|
13
|
+
- Use monotonic clock present in all modern Rubies [Tero Tasanen, #109]
|
14
|
+
- Remove code hacks necessary for JRuby 1.7
|
15
|
+
- Expose wrapped pool from ConnectionPool::Wrapper [Thomas Lecavelier, #113]
|
16
|
+
|
17
|
+
2.2.2
|
18
|
+
------
|
19
|
+
|
20
|
+
- Add pool `size` and `available` accessors for metrics and monitoring
|
21
|
+
purposes [#97, robholland]
|
22
|
+
|
23
|
+
2.2.1
|
24
|
+
------
|
25
|
+
|
26
|
+
- Allow CP::Wrapper to use an existing pool [#87, etiennebarrie]
|
27
|
+
- Use monotonic time for more accurate timeouts [#84, jdantonio]
|
28
|
+
|
29
|
+
2.2.0
|
30
|
+
------
|
31
|
+
|
32
|
+
- Rollback `Timeout` handling introduced in 2.1.1 and 2.1.2. It seems
|
33
|
+
impossible to safely work around the issue. Please never, ever use
|
34
|
+
`Timeout.timeout` in your code or you will see rare but mysterious bugs. [#75]
|
35
|
+
|
1
36
|
2.1.3
|
2
37
|
------
|
3
38
|
|
data/README.md
CHANGED
@@ -31,6 +31,14 @@ If all the objects in the connection pool are in use, `with` will block
|
|
31
31
|
until one becomes available. If no object is available within `:timeout` seconds,
|
32
32
|
`with` will raise a `Timeout::Error`.
|
33
33
|
|
34
|
+
You can also use `ConnectionPool#then` to support _both_ a
|
35
|
+
connection pool and a raw client (requires Ruby 2.5+).
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
# Compatible with a raw Redis::Client, and ConnectionPool Redis
|
39
|
+
$redis.then { |r| r.set 'foo' 'bar' }
|
40
|
+
```
|
41
|
+
|
34
42
|
Optionally, you can specify a timeout override using the with-block semantics:
|
35
43
|
|
36
44
|
``` ruby
|
@@ -45,11 +53,13 @@ sections when a resource is not available, or conversely if you are comfortable
|
|
45
53
|
blocking longer on a particular resource. This is not implemented in the below
|
46
54
|
`ConnectionPool::Wrapper` class.
|
47
55
|
|
56
|
+
## Migrating to a Connection Pool
|
57
|
+
|
48
58
|
You can use `ConnectionPool::Wrapper` to wrap a single global connection,
|
49
|
-
making it easier to
|
59
|
+
making it easier to migrate existing connection code over time:
|
50
60
|
|
51
61
|
``` ruby
|
52
|
-
$redis = ConnectionPool::Wrapper.new(size: 5, timeout: 3) { Redis.
|
62
|
+
$redis = ConnectionPool::Wrapper.new(size: 5, timeout: 3) { Redis.new }
|
53
63
|
$redis.sadd('foo', 1)
|
54
64
|
$redis.smembers('foo')
|
55
65
|
```
|
@@ -69,38 +79,67 @@ end
|
|
69
79
|
Once you've ported your entire system to use `with`, you can simply remove
|
70
80
|
`Wrapper` and use the simpler and faster `ConnectionPool`.
|
71
81
|
|
82
|
+
|
83
|
+
## Shutdown
|
84
|
+
|
72
85
|
You can shut down a ConnectionPool instance once it should no longer be used.
|
73
86
|
Further checkout attempts will immediately raise an error but existing checkouts
|
74
87
|
will work.
|
75
88
|
|
76
89
|
```ruby
|
77
90
|
cp = ConnectionPool.new { Redis.new }
|
78
|
-
cp.shutdown { |conn| conn.
|
91
|
+
cp.shutdown { |conn| conn.quit }
|
79
92
|
```
|
80
93
|
|
81
94
|
Shutting down a connection pool will block until all connections are checked in and closed.
|
82
|
-
Note that shutting down is completely optional
|
95
|
+
**Note that shutting down is completely optional**; Ruby's garbage collector will reclaim
|
83
96
|
unreferenced pools under normal circumstances.
|
84
97
|
|
98
|
+
## Reload
|
99
|
+
|
100
|
+
You can reload a ConnectionPool instance in the case it is desired to close all
|
101
|
+
connections to the pool and, unlike `shutdown`, afterwards recreate connections
|
102
|
+
so the pool may continue to be used. Reloading may be useful after forking the
|
103
|
+
process.
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
cp = ConnectionPool.new { Redis.new }
|
107
|
+
cp.reload { |conn| conn.quit }
|
108
|
+
cp.with { |conn| conn.get('some-count') }
|
109
|
+
```
|
110
|
+
|
111
|
+
Like `shutdown`, this will block until all connections are checked in and
|
112
|
+
closed.
|
113
|
+
|
114
|
+
## Current State
|
115
|
+
|
116
|
+
There are several methods that return information about a pool.
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
cp = ConnectionPool.new(size: 10) { Redis.new }
|
120
|
+
cp.size # => 10
|
121
|
+
cp.available # => 10
|
122
|
+
|
123
|
+
cp.with do |conn|
|
124
|
+
cp.size # => 10
|
125
|
+
cp.available # => 9
|
126
|
+
end
|
127
|
+
```
|
85
128
|
|
86
129
|
Notes
|
87
130
|
-----
|
88
131
|
|
89
132
|
- Connections are lazily created as needed.
|
90
133
|
- There is no provision for repairing or checking the health of a connection;
|
91
|
-
connections should be self-repairing.
|
134
|
+
connections should be self-repairing. This is true of the Dalli and Redis
|
92
135
|
clients.
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
```
|
99
|
-
$ gem install connection_pool
|
100
|
-
```
|
136
|
+
- **WARNING**: Don't ever use `Timeout.timeout` in your Ruby code or you will see
|
137
|
+
occasional silent corruption and mysterious errors. The Timeout API is unsafe
|
138
|
+
and cannot be used correctly, ever. Use proper socket timeout options as
|
139
|
+
exposed by Net::HTTP, Redis, Dalli, etc.
|
101
140
|
|
102
141
|
|
103
142
|
Author
|
104
143
|
------
|
105
144
|
|
106
|
-
Mike Perham, [@
|
145
|
+
Mike Perham, [@getajobmike](https://twitter.com/getajobmike), <https://www.mikeperham.com>
|
data/Rakefile
CHANGED
data/connection_pool.gemspec
CHANGED
@@ -1,21 +1,21 @@
|
|
1
|
-
# -*- encoding: utf-8 -*-
|
2
1
|
require "./lib/connection_pool/version"
|
3
2
|
|
4
3
|
Gem::Specification.new do |s|
|
5
|
-
s.name
|
6
|
-
s.version
|
7
|
-
s.platform
|
8
|
-
s.authors
|
9
|
-
s.email
|
10
|
-
s.homepage
|
11
|
-
s.description = s.summary =
|
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
|
14
|
-
s.test_files
|
15
|
-
s.executables
|
12
|
+
s.files = `git ls-files`.split("\n")
|
13
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
16
15
|
s.require_paths = ["lib"]
|
17
16
|
s.license = "MIT"
|
18
|
-
s.add_development_dependency
|
19
|
-
s.add_development_dependency
|
20
|
-
s.add_development_dependency
|
17
|
+
s.add_development_dependency "bundler"
|
18
|
+
s.add_development_dependency "minitest", ">= 5.0.0"
|
19
|
+
s.add_development_dependency "rake"
|
20
|
+
s.required_ruby_version = ">= 2.2.0"
|
21
21
|
end
|
data/lib/connection_pool.rb
CHANGED
@@ -1,20 +1,25 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
require "timeout"
|
2
|
+
require "connection_pool/version"
|
3
3
|
|
4
|
-
|
5
|
-
|
4
|
+
class ConnectionPool
|
5
|
+
class Error < ::RuntimeError; end
|
6
|
+
class PoolShuttingDownError < ::ConnectionPool::Error; end
|
7
|
+
class TimeoutError < ::Timeout::Error; end
|
8
|
+
end
|
9
|
+
|
10
|
+
# Generic connection pool class for sharing a limited number of objects or network connections
|
11
|
+
# among many threads. Note: pool elements are lazily created.
|
6
12
|
#
|
7
13
|
# Example usage with block (faster):
|
8
14
|
#
|
9
15
|
# @pool = ConnectionPool.new { Redis.new }
|
10
|
-
#
|
11
16
|
# @pool.with do |redis|
|
12
17
|
# redis.lpop('my-list') if redis.llen('my-list') > 0
|
13
18
|
# end
|
14
19
|
#
|
15
20
|
# Using optional timeout override (for that single invocation)
|
16
21
|
#
|
17
|
-
# @pool.with(:
|
22
|
+
# @pool.with(timeout: 2.0) do |redis|
|
18
23
|
# redis.lpop('my-list') if redis.llen('my-list') > 0
|
19
24
|
# end
|
20
25
|
#
|
@@ -33,105 +38,89 @@ require_relative 'connection_pool/timed_stack'
|
|
33
38
|
class ConnectionPool
|
34
39
|
DEFAULTS = {size: 5, timeout: 5}
|
35
40
|
|
36
|
-
class Error < RuntimeError
|
37
|
-
end
|
38
|
-
|
39
41
|
def self.wrap(options, &block)
|
40
42
|
Wrapper.new(options, &block)
|
41
43
|
end
|
42
44
|
|
43
45
|
def initialize(options = {}, &block)
|
44
|
-
raise ArgumentError,
|
46
|
+
raise ArgumentError, "Connection pool requires a block" unless block
|
45
47
|
|
46
48
|
options = DEFAULTS.merge(options)
|
47
49
|
|
48
|
-
@size = options.fetch(:size)
|
50
|
+
@size = Integer(options.fetch(:size))
|
49
51
|
@timeout = options.fetch(:timeout)
|
50
52
|
|
51
53
|
@available = TimedStack.new(@size, &block)
|
52
|
-
@key = :"
|
54
|
+
@key = :"pool-#{@available.object_id}"
|
55
|
+
@key_count = :"pool-#{@available.object_id}-count"
|
53
56
|
end
|
54
57
|
|
55
58
|
def with(options = {})
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
success = true # means the connection wasn't interrupted
|
64
|
-
result
|
65
|
-
ensure
|
66
|
-
if success
|
67
|
-
# everything is roses, we can safely check the connection back in
|
59
|
+
Thread.handle_interrupt(Exception => :never) do
|
60
|
+
conn = checkout(options)
|
61
|
+
begin
|
62
|
+
Thread.handle_interrupt(Exception => :immediate) do
|
63
|
+
yield conn
|
64
|
+
end
|
65
|
+
ensure
|
68
66
|
checkin
|
69
|
-
else
|
70
|
-
@available.discard!(pop_connection)
|
71
67
|
end
|
72
68
|
end
|
73
69
|
end
|
70
|
+
alias then with
|
74
71
|
|
75
72
|
def checkout(options = {})
|
76
|
-
|
77
|
-
|
78
|
-
@
|
73
|
+
if ::Thread.current[@key]
|
74
|
+
::Thread.current[@key_count] += 1
|
75
|
+
::Thread.current[@key]
|
79
76
|
else
|
80
|
-
|
77
|
+
::Thread.current[@key_count] = 1
|
78
|
+
::Thread.current[@key] = @available.pop(options[:timeout] || @timeout)
|
81
79
|
end
|
82
|
-
|
83
|
-
stack.push conn
|
84
|
-
conn
|
85
80
|
end
|
86
81
|
|
87
82
|
def checkin
|
88
|
-
|
89
|
-
|
83
|
+
if ::Thread.current[@key]
|
84
|
+
if ::Thread.current[@key_count] == 1
|
85
|
+
@available.push(::Thread.current[@key])
|
86
|
+
::Thread.current[@key] = nil
|
87
|
+
::Thread.current[@key_count] = nil
|
88
|
+
else
|
89
|
+
::Thread.current[@key_count] -= 1
|
90
|
+
end
|
91
|
+
else
|
92
|
+
raise ConnectionPool::Error, "no connections are checked out"
|
93
|
+
end
|
90
94
|
|
91
95
|
nil
|
92
96
|
end
|
93
97
|
|
98
|
+
##
|
99
|
+
# Shuts down the ConnectionPool by passing each connection to +block+ and
|
100
|
+
# then removing it from the pool. Attempting to checkout a connection after
|
101
|
+
# shutdown will raise +ConnectionPool::PoolShuttingDownError+.
|
102
|
+
|
94
103
|
def shutdown(&block)
|
95
104
|
@available.shutdown(&block)
|
96
105
|
end
|
97
106
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
raise ConnectionPool::Error, 'no connections are checked out'
|
103
|
-
else
|
104
|
-
stack.pop
|
105
|
-
end
|
106
|
-
end
|
107
|
+
##
|
108
|
+
# Reloads the ConnectionPool by passing each connection to +block+ and then
|
109
|
+
# removing it the pool. Subsequent checkouts will create new connections as
|
110
|
+
# needed.
|
107
111
|
|
108
|
-
def
|
109
|
-
|
112
|
+
def reload(&block)
|
113
|
+
@available.shutdown(reload: true, &block)
|
110
114
|
end
|
111
115
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
def initialize(options = {}, &block)
|
116
|
-
@pool = ::ConnectionPool.new(options, &block)
|
117
|
-
end
|
118
|
-
|
119
|
-
def with(&block)
|
120
|
-
@pool.with(&block)
|
121
|
-
end
|
122
|
-
|
123
|
-
def pool_shutdown(&block)
|
124
|
-
@pool.shutdown(&block)
|
125
|
-
end
|
126
|
-
|
127
|
-
def respond_to?(id, *args)
|
128
|
-
METHODS.include?(id) || with { |c| c.respond_to?(id, *args) }
|
129
|
-
end
|
116
|
+
# Size of this connection pool
|
117
|
+
attr_reader :size
|
130
118
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
end
|
135
|
-
end
|
119
|
+
# Number of pool entries available for checkout at this instant.
|
120
|
+
def available
|
121
|
+
@available.length
|
136
122
|
end
|
137
123
|
end
|
124
|
+
|
125
|
+
require "connection_pool/timed_stack"
|
126
|
+
require "connection_pool/wrapper"
|
@@ -1,12 +1,3 @@
|
|
1
|
-
require 'thread'
|
2
|
-
require 'timeout'
|
3
|
-
|
4
|
-
##
|
5
|
-
# Raised when you attempt to retrieve a connection from a pool that has been
|
6
|
-
# shut down.
|
7
|
-
|
8
|
-
class ConnectionPool::PoolShuttingDownError < RuntimeError; end
|
9
|
-
|
10
1
|
##
|
11
2
|
# The TimedStack manages a pool of homogeneous connections (or any resource
|
12
3
|
# you wish to manage). Connections are created lazily up to a given maximum
|
@@ -24,9 +15,10 @@ class ConnectionPool::PoolShuttingDownError < RuntimeError; end
|
|
24
15
|
#
|
25
16
|
# conn = ts.pop
|
26
17
|
# ts.pop timeout: 5
|
27
|
-
# #=> raises
|
18
|
+
# #=> raises ConnectionPool::TimeoutError after 5 seconds
|
28
19
|
|
29
20
|
class ConnectionPool::TimedStack
|
21
|
+
attr_reader :max
|
30
22
|
|
31
23
|
##
|
32
24
|
# Creates a new pool with +size+ connections that are created from the given
|
@@ -57,12 +49,12 @@ class ConnectionPool::TimedStack
|
|
57
49
|
@resource.broadcast
|
58
50
|
end
|
59
51
|
end
|
60
|
-
|
52
|
+
alias << push
|
61
53
|
|
62
54
|
##
|
63
55
|
# Retrieves a connection from the stack. If a connection is available it is
|
64
56
|
# immediately returned. If no connection is available within the given
|
65
|
-
# timeout a
|
57
|
+
# timeout a ConnectionPool::TimeoutError is raised.
|
66
58
|
#
|
67
59
|
# +:timeout+ is the only checked entry in +options+ and is preferred over
|
68
60
|
# the +timeout+ argument (which will be removed in a future release). Other
|
@@ -72,7 +64,7 @@ class ConnectionPool::TimedStack
|
|
72
64
|
options, timeout = timeout, 0.5 if Hash === timeout
|
73
65
|
timeout = options.fetch :timeout, timeout
|
74
66
|
|
75
|
-
deadline =
|
67
|
+
deadline = current_time + timeout
|
76
68
|
@mutex.synchronize do
|
77
69
|
loop do
|
78
70
|
raise ConnectionPool::PoolShuttingDownError if @shutdown_block
|
@@ -81,18 +73,20 @@ class ConnectionPool::TimedStack
|
|
81
73
|
connection = try_create(options)
|
82
74
|
return connection if connection
|
83
75
|
|
84
|
-
to_wait = deadline -
|
85
|
-
raise
|
76
|
+
to_wait = deadline - current_time
|
77
|
+
raise ConnectionPool::TimeoutError, "Waited #{timeout} sec" if to_wait <= 0
|
86
78
|
@resource.wait(@mutex, to_wait)
|
87
79
|
end
|
88
80
|
end
|
89
81
|
end
|
90
82
|
|
91
83
|
##
|
92
|
-
# Shuts down the TimedStack
|
93
|
-
#
|
84
|
+
# Shuts down the TimedStack by passing each connection to +block+ and then
|
85
|
+
# removing it from the pool. Attempting to checkout a connection after
|
86
|
+
# shutdown will raise +ConnectionPool::PoolShuttingDownError+ unless
|
87
|
+
# +:reload+ is +true+.
|
94
88
|
|
95
|
-
def shutdown(&block)
|
89
|
+
def shutdown(reload: false, &block)
|
96
90
|
raise ArgumentError, "shutdown must receive a block" unless block_given?
|
97
91
|
|
98
92
|
@mutex.synchronize do
|
@@ -100,6 +94,7 @@ class ConnectionPool::TimedStack
|
|
100
94
|
@resource.broadcast
|
101
95
|
|
102
96
|
shutdown_connections
|
97
|
+
@shutdown_block = nil if reload
|
103
98
|
end
|
104
99
|
end
|
105
100
|
|
@@ -117,30 +112,12 @@ class ConnectionPool::TimedStack
|
|
117
112
|
@max - @created + @que.length
|
118
113
|
end
|
119
114
|
|
120
|
-
|
121
|
-
# Indicates that a connection isn't coming back, allowing a new one to be
|
122
|
-
# created to replace it.
|
123
|
-
|
124
|
-
def discard!(obj)
|
125
|
-
@mutex.synchronize do
|
126
|
-
if @shutdown_block
|
127
|
-
@shutdown_block.call(obj)
|
128
|
-
else
|
129
|
-
# try to shut down the connection before throwing it away
|
130
|
-
if obj.respond_to?(:close) # Dalli::Client
|
131
|
-
obj.close rescue nil
|
132
|
-
elsif obj.respond_to?(:disconnect!) # Redis
|
133
|
-
obj.disconnect! rescue nil
|
134
|
-
end
|
135
|
-
@created -= 1
|
136
|
-
end
|
115
|
+
private
|
137
116
|
|
138
|
-
|
139
|
-
|
117
|
+
def current_time
|
118
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
140
119
|
end
|
141
120
|
|
142
|
-
private
|
143
|
-
|
144
121
|
##
|
145
122
|
# This is an extension point for TimedStack and is called with a mutex.
|
146
123
|
#
|
@@ -169,6 +146,7 @@ class ConnectionPool::TimedStack
|
|
169
146
|
conn = fetch_connection(options)
|
170
147
|
@shutdown_block.call(conn)
|
171
148
|
end
|
149
|
+
@created = 0
|
172
150
|
end
|
173
151
|
|
174
152
|
##
|