connection_pool 2.1.3 → 2.2.4

Sign up to get free protection for your applications and to get access to all the features.
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
  ##