connection_pool 2.1.2 → 2.2.3

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: cba01c34d4f2ac4cd9beb4d71ade9808e66095a0
4
- data.tar.gz: 541e73dc97f93b16a7a2725be6157395a76b2295
2
+ SHA256:
3
+ metadata.gz: e9fefa40cd9db0f5add54482a9f980d808b5bb578e83a5a8bf287c636c41704d
4
+ data.tar.gz: ec25d36c42cfb863e768bf49b18316d955397edaa8862f92d27687ab2a089bf0
5
5
  SHA512:
6
- metadata.gz: d2d9e0deee042e07bd04a4715c97d67bc0c6d59fc6670a307a740024d7ff38ad6fe46dbe9edc482bdef7d6ed51abaeb5297df1a3664c6ec26ba0f2d0e51512c7
7
- data.tar.gz: 2dbf1090f068f706141e92aae4dfefb1f596e96be621e0ad1d4e4824c9abae734f337d7ea2e4c5d7943f612835f4ca429469347f1270359c4e604033329dabc4
6
+ metadata.gz: d8653437078b6334be998d16f4851e12a21e8caca8338a07559c9bfbe646dc6023a7370aad6e33be905b05d6eaab8f249387eaa72b16a478f57c9c497adbbf30
7
+ data.tar.gz: 2a437d085a3f11376338ee32debe5e7ff38fc28fdc8d0d318287c2890bc90ce2d282468981dde43c8391798d113e1391d7998a5f5e571f1021ae99dfff6893e2
@@ -1,14 +1,12 @@
1
1
  ---
2
- sudo: false
3
2
  cache: bundler
4
3
  language: ruby
5
4
  rvm:
6
- - 1.9.3
7
- - 2.0.0
8
- - 2.1.0
9
- - 2.2.0
5
+ - 2.3
6
+ - 2.4
7
+ - 2.5
8
+ - 2.6
9
+ - 2.7
10
10
  - jruby
11
- - rbx-2
12
- notifications:
13
- email:
14
- - drbrain@segment7.net
11
+ jdk:
12
+ - openjdk11
data/Changes.md CHANGED
@@ -1,3 +1,38 @@
1
+ # connection_pool Changelog
2
+
3
+ 2.2.3
4
+ ------
5
+
6
+ - Pool now throws `ConnectionPool::TimeoutError` on timeout. [#130]
7
+ - Use monotonic clock present in all modern Rubies [Tero Tasanen, #109]
8
+ - Remove code hacks necessary for JRuby 1.7
9
+ - Expose wrapped pool from ConnectionPool::Wrapper [Thomas Lecavelier, #113]
10
+
11
+ 2.2.2
12
+ ------
13
+
14
+ - Add pool `size` and `available` accessors for metrics and monitoring
15
+ purposes [#97, robholland]
16
+
17
+ 2.2.1
18
+ ------
19
+
20
+ - Allow CP::Wrapper to use an existing pool [#87, etiennebarrie]
21
+ - Use monotonic time for more accurate timeouts [#84, jdantonio]
22
+
23
+ 2.2.0
24
+ ------
25
+
26
+ - Rollback `Timeout` handling introduced in 2.1.1 and 2.1.2. It seems
27
+ impossible to safely work around the issue. Please never, ever use
28
+ `Timeout.timeout` in your code or you will see rare but mysterious bugs. [#75]
29
+
30
+ 2.1.3
31
+ ------
32
+
33
+ - Don't increment created count until connection is successfully
34
+ created. [mylesmegyesi, #73]
35
+
1
36
  2.1.2
2
37
  ------
3
38
 
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
1
  source "https://rubygems.org"
2
2
 
3
3
  gemspec(development_group: :runtime)
4
+
5
+ gem "standard"
data/README.md CHANGED
@@ -45,11 +45,13 @@ sections when a resource is not available, or conversely if you are comfortable
45
45
  blocking longer on a particular resource. This is not implemented in the below
46
46
  `ConnectionPool::Wrapper` class.
47
47
 
48
+ ## Migrating to a Connection Pool
49
+
48
50
  You can use `ConnectionPool::Wrapper` to wrap a single global connection,
49
- making it easier to port your connection code over time:
51
+ making it easier to migrate existing connection code over time:
50
52
 
51
53
  ``` ruby
52
- $redis = ConnectionPool::Wrapper.new(size: 5, timeout: 3) { Redis.connect }
54
+ $redis = ConnectionPool::Wrapper.new(size: 5, timeout: 3) { Redis.new }
53
55
  $redis.sadd('foo', 1)
54
56
  $redis.smembers('foo')
55
57
  ```
@@ -69,19 +71,36 @@ end
69
71
  Once you've ported your entire system to use `with`, you can simply remove
70
72
  `Wrapper` and use the simpler and faster `ConnectionPool`.
71
73
 
74
+
75
+ ## Shutdown
76
+
72
77
  You can shut down a ConnectionPool instance once it should no longer be used.
73
78
  Further checkout attempts will immediately raise an error but existing checkouts
74
79
  will work.
75
80
 
76
81
  ```ruby
77
82
  cp = ConnectionPool.new { Redis.new }
78
- cp.shutdown { |conn| conn.close }
83
+ cp.shutdown { |conn| conn.quit }
79
84
  ```
80
85
 
81
86
  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
87
+ **Note that shutting down is completely optional**; Ruby's garbage collector will reclaim
83
88
  unreferenced pools under normal circumstances.
84
89
 
90
+ ## Current State
91
+
92
+ There are several methods that return information about a pool.
93
+
94
+ ```ruby
95
+ cp = ConnectionPool.new(size: 10) { Redis.new }
96
+ cp.size # => 10
97
+ cp.available # => 10
98
+
99
+ cp.with do |conn|
100
+ cp.size # => 10
101
+ cp.available # => 9
102
+ end
103
+ ```
85
104
 
86
105
  Notes
87
106
  -----
@@ -90,14 +109,10 @@ Notes
90
109
  - There is no provision for repairing or checking the health of a connection;
91
110
  connections should be self-repairing. This is true of the Dalli and Redis
92
111
  clients.
93
-
94
-
95
- Install
96
- -------
97
-
98
- ```
99
- $ gem install connection_pool
100
- ```
112
+ - **WARNING**: Don't ever use `Timeout.timeout` in your Ruby code or you will see
113
+ occasional silent corruption and mysterious errors. The Timeout API is unsafe
114
+ and cannot be used correctly, ever. Use proper socket timeout options as
115
+ exposed by Net::HTTP, Redis, Dalli, etc.
101
116
 
102
117
 
103
118
  Author
data/Rakefile CHANGED
@@ -1,6 +1,7 @@
1
- require 'bundler/gem_tasks'
1
+ require "bundler/gem_tasks"
2
2
 
3
- require 'rake/testtask'
3
+ require "rake/testtask"
4
+ require "standard/rake"
4
5
  Rake::TestTask.new
5
6
 
6
- task :default => :test
7
+ task default: :test
@@ -1,21 +1,20 @@
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"
21
20
  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,60 +38,57 @@ 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
74
70
 
75
71
  def checkout(options = {})
76
- conn = if stack.empty?
77
- timeout = options[:timeout] || @timeout
78
- @available.pop(timeout: timeout)
72
+ if ::Thread.current[@key]
73
+ ::Thread.current[@key_count] += 1
74
+ ::Thread.current[@key]
79
75
  else
80
- stack.last
76
+ ::Thread.current[@key_count] = 1
77
+ ::Thread.current[@key] = @available.pop(options[:timeout] || @timeout)
81
78
  end
82
-
83
- stack.push conn
84
- conn
85
79
  end
86
80
 
87
81
  def checkin
88
- conn = pop_connection # mutates stack, must be on its own line
89
- @available.push(conn) if stack.empty?
82
+ if ::Thread.current[@key]
83
+ if ::Thread.current[@key_count] == 1
84
+ @available.push(::Thread.current[@key])
85
+ ::Thread.current[@key] = nil
86
+ else
87
+ ::Thread.current[@key_count] -= 1
88
+ end
89
+ else
90
+ raise ConnectionPool::Error, "no connections are checked out"
91
+ end
90
92
 
91
93
  nil
92
94
  end
@@ -95,43 +97,14 @@ class ConnectionPool
95
97
  @available.shutdown(&block)
96
98
  end
97
99
 
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
- def stack
109
- ::Thread.current[@key] ||= []
110
- end
111
-
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
100
+ # Size of this connection pool
101
+ attr_reader :size
122
102
 
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
130
-
131
- def method_missing(name, *args, &block)
132
- with do |connection|
133
- connection.send(name, *args, &block)
134
- end
135
- end
103
+ # Number of pool entries available for checkout at this instant.
104
+ def available
105
+ @available.length
136
106
  end
137
107
  end
108
+
109
+ require "connection_pool/timed_stack"
110
+ 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
@@ -27,6 +18,7 @@ class ConnectionPool::PoolShuttingDownError < RuntimeError; end
27
18
  # #=> raises Timeout::Error 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,7 +49,7 @@ 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
@@ -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,8 +73,8 @@ 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
@@ -117,30 +109,12 @@ class ConnectionPool::TimedStack
117
109
  @max - @created + @que.length
118
110
  end
119
111
 
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
112
+ private
137
113
 
138
- @resource.broadcast
139
- end
114
+ def current_time
115
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
140
116
  end
141
117
 
142
- private
143
-
144
118
  ##
145
119
  # This is an extension point for TimedStack and is called with a mutex.
146
120
  #
@@ -188,8 +162,9 @@ class ConnectionPool::TimedStack
188
162
 
189
163
  def try_create(options = nil)
190
164
  unless @created == @max
165
+ object = @create_block.call
191
166
  @created += 1
192
- @create_block.call
167
+ object
193
168
  end
194
169
  end
195
170
  end
@@ -1,3 +1,3 @@
1
1
  class ConnectionPool
2
- VERSION = "2.1.2"
2
+ VERSION = "2.2.3"
3
3
  end
@@ -0,0 +1,43 @@
1
+ class ConnectionPool
2
+ class Wrapper < ::BasicObject
3
+ METHODS = [:with, :pool_shutdown, :wrapped_pool]
4
+
5
+ def initialize(options = {}, &block)
6
+ @pool = options.fetch(:pool) { ::ConnectionPool.new(options, &block) }
7
+ end
8
+
9
+ def wrapped_pool
10
+ @pool
11
+ end
12
+
13
+ def with(&block)
14
+ @pool.with(&block)
15
+ end
16
+
17
+ def pool_shutdown(&block)
18
+ @pool.shutdown(&block)
19
+ end
20
+
21
+ def pool_size
22
+ @pool.size
23
+ end
24
+
25
+ def pool_available
26
+ @pool.available
27
+ end
28
+
29
+ def respond_to?(id, *args)
30
+ METHODS.include?(id) || with { |c| c.respond_to?(id, *args) }
31
+ end
32
+
33
+ # rubocop:disable Style/MethodMissingSuper
34
+ # rubocop:disable Style/MissingRespondToMissing
35
+ def method_missing(name, *args, &block)
36
+ with do |connection|
37
+ connection.send(name, *args, &block)
38
+ end
39
+ end
40
+ # rubocop:enable Style/MethodMissingSuper
41
+ # rubocop:enable Style/MissingRespondToMissing
42
+ end
43
+ end
@@ -1,8 +1,8 @@
1
- gem 'minitest'
1
+ gem "minitest"
2
2
 
3
- require 'minitest/pride'
4
- require 'minitest/autorun'
3
+ require "minitest/pride"
4
+ require "minitest/autorun"
5
5
 
6
6
  $VERBOSE = 1
7
7
 
8
- require_relative '../lib/connection_pool'
8
+ require_relative "../lib/connection_pool"
@@ -1,15 +1,16 @@
1
- require_relative 'helper'
1
+ require_relative "helper"
2
2
 
3
3
  class TestConnectionPool < Minitest::Test
4
-
5
4
  class NetworkConnection
5
+ SLEEP_TIME = 0.1
6
+
6
7
  def initialize
7
8
  @x = 0
8
9
  end
9
10
 
10
11
  def do_something
11
12
  @x += 1
12
- sleep 0.05
13
+ sleep SLEEP_TIME
13
14
  @x
14
15
  end
15
16
 
@@ -19,7 +20,7 @@ class TestConnectionPool < Minitest::Test
19
20
 
20
21
  def do_something_with_block
21
22
  @x += yield
22
- sleep 0.05
23
+ sleep SLEEP_TIME
23
24
  @x
24
25
  end
25
26
 
@@ -41,12 +42,12 @@ class TestConnectionPool < Minitest::Test
41
42
  end
42
43
 
43
44
  def use_pool(pool, size)
44
- Array.new(size) do
45
+ Array.new(size) {
45
46
  Thread.new do
46
- pool.with do sleep end
47
+ pool.with { sleep }
47
48
  end
48
- end.each do |thread|
49
- Thread.pass until thread.status == 'sleep'
49
+ }.each do |thread|
50
+ Thread.pass until thread.status == "sleep"
50
51
  end
51
52
  end
52
53
 
@@ -58,33 +59,38 @@ class TestConnectionPool < Minitest::Test
58
59
  end
59
60
 
60
61
  def test_basic_multithreaded_usage
61
- pool = ConnectionPool.new(:size => 5) { NetworkConnection.new }
62
+ pool_size = 5
63
+ pool = ConnectionPool.new(size: pool_size) { NetworkConnection.new }
64
+
65
+ start = Time.new
62
66
 
63
- threads = Array.new(15) do
67
+ generations = 3
68
+
69
+ result = Array.new(pool_size * generations) {
64
70
  Thread.new do
65
71
  pool.with do |net|
66
72
  net.do_something
67
73
  end
68
74
  end
69
- end
75
+ }.map(&:value)
70
76
 
71
- a = Time.now
72
- result = threads.map(&:value)
73
- b = Time.now
74
- assert_operator((b - a), :>, 0.125)
75
- assert_equal([1,2,3].cycle(5).sort, result.sort)
77
+ finish = Time.new
78
+
79
+ assert_equal((1..generations).cycle(pool_size).sort, result.sort)
80
+
81
+ assert_operator(finish - start, :>, generations * NetworkConnection::SLEEP_TIME)
76
82
  end
77
83
 
78
84
  def test_timeout
79
- pool = ConnectionPool.new(:timeout => 0, :size => 1) { NetworkConnection.new }
80
- thread = Thread.new do
85
+ pool = ConnectionPool.new(timeout: 0, size: 1) { NetworkConnection.new }
86
+ thread = Thread.new {
81
87
  pool.with do |net|
82
88
  net.do_something
83
89
  sleep 0.01
84
90
  end
85
- end
91
+ }
86
92
 
87
- Thread.pass while thread.status == 'run'
93
+ Thread.pass while thread.status == "run"
88
94
 
89
95
  assert_raises Timeout::Error do
90
96
  pool.with { |net| net.do_something }
@@ -98,79 +104,126 @@ class TestConnectionPool < Minitest::Test
98
104
  end
99
105
 
100
106
  def test_with
101
- pool = ConnectionPool.new(:timeout => 0, :size => 1) { Object.new }
107
+ pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new }
102
108
 
103
109
  pool.with do
104
- assert_raises Timeout::Error do
105
- Thread.new { pool.checkout }.join
106
- end
110
+ Thread.new {
111
+ assert_raises Timeout::Error do
112
+ pool.checkout
113
+ end
114
+ }.join
107
115
  end
108
116
 
109
117
  assert Thread.new { pool.checkout }.join
110
118
  end
111
119
 
112
- def test_with_with_dangerous_timeouts
113
- case RUBY_ENGINE.to_sym
114
- when :jruby
115
- skip('JRuby GC dislikes this test')
116
- when :ruby
117
- if RUBY_VERSION == '2.0.0' && RUBY_PATCHLEVEL == 598
118
- skip("#{RUBY_VERSION}p#{RUBY_PATCHLEVEL} GC dislikes this test")
120
+ def test_with_timeout
121
+ pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new }
122
+
123
+ assert_raises Timeout::Error do
124
+ Timeout.timeout(0.01) do
125
+ pool.with do |obj|
126
+ assert_equal 0, pool.available
127
+ sleep 0.015
128
+ end
119
129
  end
120
130
  end
131
+ assert_equal 1, pool.available
132
+ end
121
133
 
122
- marker_class = Class.new
123
- pool = ConnectionPool.new(:timeout => 0, :size => 1) { marker_class.new }
134
+ def test_invalid_size
135
+ assert_raises ArgumentError, TypeError do
136
+ ConnectionPool.new(timeout: 0, size: nil) { Object.new }
137
+ end
138
+ assert_raises ArgumentError, TypeError do
139
+ ConnectionPool.new(timeout: 0, size: "") { Object.new }
140
+ end
141
+ end
124
142
 
125
- # no "connections" allocated yet
126
- assert_equal [], ObjectSpace.each_object(marker_class).to_a
143
+ def test_handle_interrupt_ensures_checkin
144
+ pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new }
145
+ def pool.checkout(options)
146
+ sleep 0.015
147
+ super
148
+ end
127
149
 
128
- checkin_time = 0.05
150
+ did_something = false
129
151
 
130
- assert_raises Timeout::Error do
131
- Timeout.timeout(checkin_time) do
132
- pool.with do
133
- # a "connection" has been allocated
134
- refute_equal [], ObjectSpace.each_object(marker_class).to_a
135
- sleep 2 * checkin_time
152
+ action = lambda do
153
+ Timeout.timeout(0.01) do
154
+ pool.with do |obj|
155
+ did_something = true
156
+ # Timeout::Error will be triggered by any non-trivial Ruby code
157
+ # executed here since it couldn't be raised during checkout.
158
+ # It looks like setting the local variable above does not trigger
159
+ # the Timeout check in MRI 2.2.1.
160
+ obj.tap { obj.hash }
136
161
  end
137
162
  end
138
163
  end
139
164
 
140
- GC.start
165
+ if RUBY_ENGINE == "ruby"
166
+ # These asserts rely on the Ruby implementation reaching `did_something =
167
+ # true` before the interrupt is detected by the thread. Interrupt
168
+ # detection timing is implementation-specific in practice, with JRuby,
169
+ # Rubinius, and TruffleRuby all having different interrupt timings to MRI.
170
+ # In fact they generally detect interrupts more quickly than MRI, so they
171
+ # may not reach `did_something = true` before detecting the interrupt.
172
+
173
+ assert_raises Timeout::Error, &action
174
+
175
+ assert did_something
176
+ else
177
+ action.call
178
+ end
179
+
180
+ assert_equal 1, pool.available
181
+ end
182
+
183
+ def test_explicit_return
184
+ pool = ConnectionPool.new(timeout: 0, size: 1) {
185
+ mock = Minitest::Mock.new
186
+ def mock.disconnect!
187
+ raise "should not disconnect upon explicit return"
188
+ end
189
+ mock
190
+ }
141
191
 
142
- # no dangling references to this "connection" remain
143
- assert_equal [], ObjectSpace.each_object(marker_class).to_a
192
+ pool.with do |conn|
193
+ return true
194
+ end
144
195
  end
145
196
 
146
197
  def test_with_timeout_override
147
- pool = ConnectionPool.new(:timeout => 0, :size => 1) { NetworkConnection.new }
198
+ pool = ConnectionPool.new(timeout: 0, size: 1) { NetworkConnection.new }
148
199
 
149
- t = Thread.new do
200
+ t = Thread.new {
150
201
  pool.with do |net|
151
202
  net.do_something
152
203
  sleep 0.01
153
204
  end
154
- end
205
+ }
155
206
 
156
- Thread.pass while t.status == 'run'
207
+ Thread.pass while t.status == "run"
157
208
 
158
209
  assert_raises Timeout::Error do
159
210
  pool.with { |net| net.do_something }
160
211
  end
161
212
 
162
- pool.with(:timeout => 0.1) do |conn|
213
+ pool.with(timeout: 2 * NetworkConnection::SLEEP_TIME) do |conn|
163
214
  refute_nil conn
164
215
  end
165
216
  end
166
217
 
167
218
  def test_checkin
168
- pool = ConnectionPool.new(:timeout => 0, :size => 1) { NetworkConnection.new }
219
+ pool = ConnectionPool.new(timeout: 0, size: 1) { NetworkConnection.new }
169
220
  conn = pool.checkout
170
221
 
171
- assert_raises Timeout::Error do
172
- Thread.new { pool.checkout }.join
173
- end
222
+ Thread.new {
223
+ assert_raises Timeout::Error do
224
+ pool.checkout
225
+ end
226
+ }.join
174
227
 
175
228
  pool.checkin
176
229
 
@@ -178,22 +231,19 @@ class TestConnectionPool < Minitest::Test
178
231
  end
179
232
 
180
233
  def test_returns_value
181
- pool = ConnectionPool.new(:timeout => 0, :size => 1) { Object.new }
182
- assert_equal 1, pool.with {|o| 1 }
234
+ pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new }
235
+ assert_equal 1, pool.with { |o| 1 }
183
236
  end
184
237
 
185
238
  def test_checkin_never_checkout
186
- pool = ConnectionPool.new(:timeout => 0, :size => 1) { Object.new }
187
-
188
- e = assert_raises ConnectionPool::Error do
189
- pool.checkin
190
- end
239
+ pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new }
191
240
 
192
- assert_equal 'no connections are checked out', e.message
241
+ e = assert_raises(ConnectionPool::Error) { pool.checkin }
242
+ assert_equal "no connections are checked out", e.message
193
243
  end
194
244
 
195
245
  def test_checkin_no_current_checkout
196
- pool = ConnectionPool.new(:timeout => 0, :size => 1) { Object.new }
246
+ pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new }
197
247
 
198
248
  pool.checkout
199
249
  pool.checkin
@@ -204,18 +254,18 @@ class TestConnectionPool < Minitest::Test
204
254
  end
205
255
 
206
256
  def test_checkin_twice
207
- pool = ConnectionPool.new(:timeout => 0, :size => 1) { Object.new }
257
+ pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new }
208
258
 
209
259
  pool.checkout
210
260
  pool.checkout
211
261
 
212
262
  pool.checkin
213
263
 
214
- assert_raises Timeout::Error do
215
- Thread.new do
264
+ Thread.new {
265
+ assert_raises Timeout::Error do
216
266
  pool.checkout
217
- end.join
218
- end
267
+ end
268
+ }.join
219
269
 
220
270
  pool.checkin
221
271
 
@@ -223,7 +273,7 @@ class TestConnectionPool < Minitest::Test
223
273
  end
224
274
 
225
275
  def test_checkout
226
- pool = ConnectionPool.new(:size => 1) { NetworkConnection.new }
276
+ pool = ConnectionPool.new(size: 1) { NetworkConnection.new }
227
277
 
228
278
  conn = pool.checkout
229
279
 
@@ -233,18 +283,18 @@ class TestConnectionPool < Minitest::Test
233
283
  end
234
284
 
235
285
  def test_checkout_multithread
236
- pool = ConnectionPool.new(:size => 2) { NetworkConnection.new }
286
+ pool = ConnectionPool.new(size: 2) { NetworkConnection.new }
237
287
  conn = pool.checkout
238
288
 
239
- t = Thread.new do
289
+ t = Thread.new {
240
290
  pool.checkout
241
- end
291
+ }
242
292
 
243
293
  refute_same conn, t.value
244
294
  end
245
295
 
246
296
  def test_checkout_timeout
247
- pool = ConnectionPool.new(:timeout => 0, :size => 0) { Object.new }
297
+ pool = ConnectionPool.new(timeout: 0, size: 0) { Object.new }
248
298
 
249
299
  assert_raises Timeout::Error do
250
300
  pool.checkout
@@ -252,26 +302,26 @@ class TestConnectionPool < Minitest::Test
252
302
  end
253
303
 
254
304
  def test_checkout_timeout_override
255
- pool = ConnectionPool.new(:timeout => 0, :size => 1) { NetworkConnection.new }
305
+ pool = ConnectionPool.new(timeout: 0, size: 1) { NetworkConnection.new }
256
306
 
257
- thread = Thread.new do
307
+ thread = Thread.new {
258
308
  pool.with do |net|
259
309
  net.do_something
260
310
  sleep 0.01
261
311
  end
262
- end
312
+ }
263
313
 
264
- Thread.pass while thread.status == 'run'
314
+ Thread.pass while thread.status == "run"
265
315
 
266
316
  assert_raises Timeout::Error do
267
317
  pool.checkout
268
318
  end
269
319
 
270
- assert pool.checkout :timeout => 0.1
320
+ assert pool.checkout timeout: 2 * NetworkConnection::SLEEP_TIME
271
321
  end
272
322
 
273
323
  def test_passthru
274
- pool = ConnectionPool.wrap(:timeout => 0.1, :size => 1) { NetworkConnection.new }
324
+ pool = ConnectionPool.wrap(timeout: 2 * NetworkConnection::SLEEP_TIME, size: 1) { NetworkConnection.new }
275
325
  assert_equal 1, pool.do_something
276
326
  assert_equal 2, pool.do_something
277
327
  assert_equal 5, pool.do_something_with_block { 3 }
@@ -279,7 +329,7 @@ class TestConnectionPool < Minitest::Test
279
329
  end
280
330
 
281
331
  def test_passthru_respond_to
282
- pool = ConnectionPool.wrap(:timeout => 0.1, :size => 1) { NetworkConnection.new }
332
+ pool = ConnectionPool.wrap(timeout: 2 * NetworkConnection::SLEEP_TIME, size: 1) { NetworkConnection.new }
283
333
  assert pool.respond_to?(:with)
284
334
  assert pool.respond_to?(:do_something)
285
335
  assert pool.respond_to?(:do_magic)
@@ -287,67 +337,67 @@ class TestConnectionPool < Minitest::Test
287
337
  end
288
338
 
289
339
  def test_return_value
290
- pool = ConnectionPool.new(:timeout => 0.1, :size => 1) { NetworkConnection.new }
291
- result = pool.with do |net|
340
+ pool = ConnectionPool.new(timeout: 2 * NetworkConnection::SLEEP_TIME, size: 1) { NetworkConnection.new }
341
+ result = pool.with { |net|
292
342
  net.fast
293
- end
343
+ }
294
344
  assert_equal 1, result
295
345
  end
296
346
 
297
347
  def test_heavy_threading
298
- pool = ConnectionPool.new(:timeout => 0.5, :size => 3) { NetworkConnection.new }
348
+ pool = ConnectionPool.new(timeout: 0.5, size: 3) { NetworkConnection.new }
299
349
 
300
- threads = Array.new(20) do
350
+ threads = Array.new(20) {
301
351
  Thread.new do
302
352
  pool.with do |net|
303
353
  sleep 0.01
304
354
  end
305
355
  end
306
- end
356
+ }
307
357
 
308
358
  threads.map { |thread| thread.join }
309
359
  end
310
360
 
311
361
  def test_reuses_objects_when_pool_not_saturated
312
- pool = ConnectionPool.new(:size => 5) { NetworkConnection.new }
362
+ pool = ConnectionPool.new(size: 5) { NetworkConnection.new }
313
363
 
314
- ids = 10.times.map do
364
+ ids = 10.times.map {
315
365
  pool.with { |c| c.object_id }
316
- end
366
+ }
317
367
 
318
368
  assert_equal 1, ids.uniq.size
319
369
  end
320
370
 
321
371
  def test_nested_checkout
322
372
  recorder = Recorder.new
323
- pool = ConnectionPool.new(:size => 1) { recorder }
373
+ pool = ConnectionPool.new(size: 1) { recorder }
324
374
  pool.with do |r_outer|
325
- @other = Thread.new do |t|
375
+ @other = Thread.new { |t|
326
376
  pool.with do |r_other|
327
- r_other.do_work('other')
377
+ r_other.do_work("other")
328
378
  end
329
- end
379
+ }
330
380
 
331
381
  pool.with do |r_inner|
332
- r_inner.do_work('inner')
382
+ r_inner.do_work("inner")
333
383
  end
334
384
 
335
385
  Thread.pass
336
386
 
337
- r_outer.do_work('outer')
387
+ r_outer.do_work("outer")
338
388
  end
339
389
 
340
390
  @other.join
341
391
 
342
- assert_equal ['inner', 'outer', 'other'], recorder.calls
392
+ assert_equal ["inner", "outer", "other"], recorder.calls
343
393
  end
344
394
 
345
395
  def test_shutdown_is_executed_for_all_connections
346
396
  recorders = []
347
397
 
348
- pool = ConnectionPool.new(:size => 3) do
398
+ pool = ConnectionPool.new(size: 3) {
349
399
  Recorder.new.tap { |r| recorders << r }
350
- end
400
+ }
351
401
 
352
402
  threads = use_pool pool, 3
353
403
 
@@ -361,9 +411,9 @@ class TestConnectionPool < Minitest::Test
361
411
  end
362
412
 
363
413
  def test_raises_error_after_shutting_down
364
- pool = ConnectionPool.new(:size => 1) { true }
414
+ pool = ConnectionPool.new(size: 1) { true }
365
415
 
366
- pool.shutdown { }
416
+ pool.shutdown {}
367
417
 
368
418
  assert_raises ConnectionPool::PoolShuttingDownError do
369
419
  pool.checkout
@@ -373,9 +423,9 @@ class TestConnectionPool < Minitest::Test
373
423
  def test_runs_shutdown_block_asynchronously_if_connection_was_in_use
374
424
  recorders = []
375
425
 
376
- pool = ConnectionPool.new(:size => 3) do
426
+ pool = ConnectionPool.new(size: 3) {
377
427
  Recorder.new.tap { |r| recorders << r }
378
- end
428
+ }
379
429
 
380
430
  threads = use_pool pool, 2
381
431
 
@@ -395,7 +445,7 @@ class TestConnectionPool < Minitest::Test
395
445
  end
396
446
 
397
447
  def test_raises_an_error_if_shutdown_is_called_without_a_block
398
- pool = ConnectionPool.new(:size => 1) { }
448
+ pool = ConnectionPool.new(size: 1) {}
399
449
 
400
450
  assert_raises ArgumentError do
401
451
  pool.shutdown
@@ -405,9 +455,9 @@ class TestConnectionPool < Minitest::Test
405
455
  def test_shutdown_is_executed_for_all_connections_in_wrapped_pool
406
456
  recorders = []
407
457
 
408
- wrapper = ConnectionPool::Wrapper.new(:size => 3) do
458
+ wrapper = ConnectionPool::Wrapper.new(size: 3) {
409
459
  Recorder.new.tap { |r| recorders << r }
410
- end
460
+ }
411
461
 
412
462
  threads = use_pool wrapper, 3
413
463
 
@@ -420,6 +470,11 @@ class TestConnectionPool < Minitest::Test
420
470
  assert_equal [["shutdown"]] * 3, recorders.map { |r| r.calls }
421
471
  end
422
472
 
473
+ def test_wrapper_wrapped_pool
474
+ wrapper = ConnectionPool::Wrapper.new { NetworkConnection.new }
475
+ assert_equal ConnectionPool, wrapper.wrapped_pool.class
476
+ end
477
+
423
478
  def test_wrapper_method_missing
424
479
  wrapper = ConnectionPool::Wrapper.new { NetworkConnection.new }
425
480
 
@@ -436,17 +491,17 @@ class TestConnectionPool < Minitest::Test
436
491
  end
437
492
 
438
493
  def test_wrapper_with
439
- wrapper = ConnectionPool::Wrapper.new(:timeout => 0, :size => 1) { Object.new }
494
+ wrapper = ConnectionPool::Wrapper.new(timeout: 0, size: 1) { Object.new }
440
495
 
441
496
  wrapper.with do
442
- assert_raises Timeout::Error do
443
- Thread.new do
444
- wrapper.with { flunk 'connection checked out :(' }
445
- end.join
446
- end
497
+ Thread.new {
498
+ assert_raises Timeout::Error do
499
+ wrapper.with { flunk "connection checked out :(" }
500
+ end
501
+ }.join
447
502
  end
448
503
 
449
- assert Thread.new { wrapper.with { } }.join
504
+ assert Thread.new { wrapper.with {} }.join
450
505
  end
451
506
 
452
507
  class ConnWithEval
@@ -461,4 +516,38 @@ class TestConnectionPool < Minitest::Test
461
516
  assert_equal "eval'ed 1", wrapper.eval(1)
462
517
  end
463
518
 
519
+ def test_wrapper_with_connection_pool
520
+ recorder = Recorder.new
521
+ pool = ConnectionPool.new(size: 1) { recorder }
522
+ wrapper = ConnectionPool::Wrapper.new(pool: pool)
523
+
524
+ pool.with { |r| r.do_work("with") }
525
+ wrapper.do_work("wrapped")
526
+
527
+ assert_equal ["with", "wrapped"], recorder.calls
528
+ end
529
+
530
+ def test_stats_without_active_connection
531
+ pool = ConnectionPool.new(size: 2) { NetworkConnection.new }
532
+
533
+ assert_equal(2, pool.size)
534
+ assert_equal(2, pool.available)
535
+ end
536
+
537
+ def test_stats_with_active_connection
538
+ pool = ConnectionPool.new(size: 2) { NetworkConnection.new }
539
+
540
+ pool.with do
541
+ assert_equal(1, pool.available)
542
+ end
543
+ end
544
+
545
+ def test_stats_with_string_size
546
+ pool = ConnectionPool.new(size: "2") { NetworkConnection.new }
547
+
548
+ pool.with do
549
+ assert_equal(2, pool.size)
550
+ assert_equal(1, pool.available)
551
+ end
552
+ end
464
553
  end
@@ -1,7 +1,6 @@
1
- require_relative 'helper'
1
+ require_relative "helper"
2
2
 
3
3
  class TestConnectionPoolTimedStack < Minitest::Test
4
-
5
4
  def setup
6
5
  @stack = ConnectionPool::TimedStack.new { Object.new }
7
6
  end
@@ -34,6 +33,25 @@ class TestConnectionPoolTimedStack < Minitest::Test
34
33
  assert_equal 1, stack.length
35
34
  end
36
35
 
36
+ def test_object_creation_fails
37
+ stack = ConnectionPool::TimedStack.new(2) { raise "failure" }
38
+
39
+ begin
40
+ stack.pop
41
+ rescue => error
42
+ assert_equal "failure", error.message
43
+ end
44
+
45
+ begin
46
+ stack.pop
47
+ rescue => error
48
+ assert_equal "failure", error.message
49
+ end
50
+
51
+ refute_empty stack
52
+ assert_equal 2, stack.length
53
+ end
54
+
37
55
  def test_pop
38
56
  object = Object.new
39
57
  @stack.push object
@@ -44,19 +62,13 @@ class TestConnectionPoolTimedStack < Minitest::Test
44
62
  end
45
63
 
46
64
  def test_pop_empty
47
- e = assert_raises Timeout::Error do
48
- @stack.pop timeout: 0
49
- end
50
-
51
- assert_equal 'Waited 0 sec', e.message
65
+ e = assert_raises(ConnectionPool::TimeoutError) { @stack.pop timeout: 0 }
66
+ assert_equal "Waited 0 sec", e.message
52
67
  end
53
68
 
54
69
  def test_pop_empty_2_0_compatibility
55
- e = assert_raises Timeout::Error do
56
- @stack.pop 0
57
- end
58
-
59
- assert_equal 'Waited 0 sec', e.message
70
+ e = assert_raises(Timeout::Error) { @stack.pop 0 }
71
+ assert_equal "Waited 0 sec", e.message
60
72
  end
61
73
 
62
74
  def test_pop_full
@@ -69,11 +81,11 @@ class TestConnectionPoolTimedStack < Minitest::Test
69
81
  end
70
82
 
71
83
  def test_pop_wait
72
- thread = Thread.start do
84
+ thread = Thread.start {
73
85
  @stack.pop
74
- end
86
+ }
75
87
 
76
- Thread.pass while thread.status == 'run'
88
+ Thread.pass while thread.status == "run"
77
89
 
78
90
  object = Object.new
79
91
 
@@ -83,7 +95,7 @@ class TestConnectionPoolTimedStack < Minitest::Test
83
95
  end
84
96
 
85
97
  def test_pop_shutdown
86
- @stack.shutdown { }
98
+ @stack.shutdown {}
87
99
 
88
100
  assert_raises ConnectionPool::PoolShuttingDownError do
89
101
  @stack.pop
@@ -125,6 +137,4 @@ class TestConnectionPoolTimedStack < Minitest::Test
125
137
  refute_empty called
126
138
  assert_empty @stack
127
139
  end
128
-
129
140
  end
130
-
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: connection_pool
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.2
4
+ version: 2.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Perham
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-03-02 00:00:00.000000000 Z
12
+ date: 2020-06-02 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -72,6 +72,7 @@ files:
72
72
  - lib/connection_pool.rb
73
73
  - lib/connection_pool/timed_stack.rb
74
74
  - lib/connection_pool/version.rb
75
+ - lib/connection_pool/wrapper.rb
75
76
  - test/helper.rb
76
77
  - test/test_connection_pool.rb
77
78
  - test/test_connection_pool_timed_stack.rb
@@ -94,8 +95,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
95
  - !ruby/object:Gem::Version
95
96
  version: '0'
96
97
  requirements: []
97
- rubyforge_project:
98
- rubygems_version: 2.4.5
98
+ rubygems_version: 3.1.2
99
99
  signing_key:
100
100
  specification_version: 4
101
101
  summary: Generic connection pool for Ruby