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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: ec91685df50fe18b79ef2cd2ed2fb038e322ff81
4
- data.tar.gz: 99b88e1aa01811e18c2737e8e062c4726ce54666
2
+ SHA256:
3
+ metadata.gz: 0af12e8d2f551b932ee863a1385ddc9a3f3140fff886cacaca25298a4c21d218
4
+ data.tar.gz: 16bd7b78d7b35337d35befe063ceeb2cb749dfc9e68cd373e7f76bd856f6cba3
5
5
  SHA512:
6
- metadata.gz: 53d34f8ca6a7d51ce535a83e2821dd33eb15a623df3022b89775c232876c8fec28a2b33adda24248136014d99f0ebe9f09c1f49e1f0ced76435db26538e907a7
7
- data.tar.gz: 660f22d0f828c789942da1da3d7c4131e008dd383a83c74e59ea03f306a59b966e879d42f9704ffa3b7de6accb6c114cfff982d3343bbe34c5e60ecc8f705ac5
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 port your connection code over time:
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.connect }
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.close }
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; Ruby's garbage collector will reclaim
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. This is true of the Dalli and Redis
134
+ connections should be self-repairing. This is true of the Dalli and Redis
92
135
  clients.
93
-
94
-
95
- Install
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, [@mperham](https://twitter.com/mperham), <http://mikeperham.com>
145
+ Mike Perham, [@getajobmike](https://twitter.com/getajobmike), <https://www.mikeperham.com>
data/Rakefile CHANGED
@@ -1,6 +1,6 @@
1
- require 'bundler/gem_tasks'
1
+ require "bundler/gem_tasks"
2
2
 
3
- require 'rake/testtask'
3
+ require "rake/testtask"
4
4
  Rake::TestTask.new
5
5
 
6
- task :default => :test
6
+ task default: :test
@@ -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 = "connection_pool"
6
- s.version = ConnectionPool::VERSION
7
- s.platform = Gem::Platform::RUBY
8
- s.authors = ["Mike Perham", "Damian Janowski"]
9
- s.email = ["mperham@gmail.com", "damian@educabilia.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 = `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 'bundler'
19
- s.add_development_dependency 'minitest', '>= 5.0.0'
20
- s.add_development_dependency 'rake'
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
@@ -1,20 +1,25 @@
1
- require_relative 'connection_pool/version'
2
- require_relative 'connection_pool/timed_stack'
1
+ require "timeout"
2
+ require "connection_pool/version"
3
3
 
4
- # Generic connection pool class for e.g. sharing a limited number of network connections
5
- # among many threads. Note: Connections are lazily created.
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(:timeout => 2.0) do |redis|
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, 'Connection pool requires a block' unless block
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 = :"current-#{@available.object_id}"
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
- # Connections can become corrupted via Timeout::Error. Discard
57
- # any connection whose usage after checkout does not finish as expected.
58
- # See #67
59
- success = false
60
- conn = checkout(options)
61
- begin
62
- result = yield conn
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
- conn = if stack.empty?
77
- timeout = options[:timeout] || @timeout
78
- @available.pop(timeout: timeout)
73
+ if ::Thread.current[@key]
74
+ ::Thread.current[@key_count] += 1
75
+ ::Thread.current[@key]
79
76
  else
80
- stack.last
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
- conn = pop_connection # mutates stack, must be on its own line
89
- @available.push(conn) if stack.empty?
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
- private
99
-
100
- def pop_connection
101
- if stack.empty?
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 stack
109
- ::Thread.current[@key] ||= []
112
+ def reload(&block)
113
+ @available.shutdown(reload: true, &block)
110
114
  end
111
115
 
112
- class Wrapper < ::BasicObject
113
- METHODS = [:with, :pool_shutdown]
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
- def method_missing(name, *args, &block)
132
- with do |connection|
133
- connection.send(name, *args, &block)
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 Timeout::Error after 5 seconds
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
- alias_method :<<, :push
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 Timeout::Error is raised.
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 = Time.now + timeout
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 - Time.now
85
- raise Timeout::Error, "Waited #{timeout} sec" if to_wait <= 0
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 which prevents connections from being checked
93
- # out. The +block+ is called once for each connection on the stack.
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
- @resource.broadcast
139
- end
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
  ##