connection_pool 2.2.5 → 2.4.1

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
2
  SHA256:
3
- metadata.gz: '0993b72c233b027a1229d532a126b4a1623ef3f146a7b56502c539084f9a228d'
4
- data.tar.gz: 274efa04fc445ca0044f62da29484a4ee9fb5e02b5f213fb873fedfd63cadff0
3
+ metadata.gz: ea0776fcb09a3cc48ef4ca03774399e20b09e51039d0c47c1e4cb3bac621c52b
4
+ data.tar.gz: b955d6b4e984259f20ae8cf6414f59692f9a51848424231363643e0c16dd2a3f
5
5
  SHA512:
6
- metadata.gz: 17c5bea167386115e8672b394138e0f66b55e6371b803dcfee7be08e51c84353aa9cd0257f169ae04053bc95be6b0c9b06576579f4915e68271146b1c9d602a5
7
- data.tar.gz: 4eeccb9eaf397e8e386a41f81005b10d43d7441297661ab0d7656b6be30ac7ff1e751a95ed5c5b5a412124a29a0af3f5c90ae2882d70d3fa29d9dc8b2df412e3
6
+ metadata.gz: bf57d8b5547502d91f5550ca6ea0be16905604c90e61efb6741e5ec3ce607c7a65f0b31e1673c96c60a06a2f64f5239cab6e94d3a50095fb822ea9b1c1bb2f0a
7
+ data.tar.gz: 4b42aa5aa67b0e45bbbc8a9f29ca3a969efd8ade3b6dfca6cff082f526ec65a2a2e5c8fa17f512d33470b82535af8b675fc80903ccd75db26748e9845dd9a612
data/Changes.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # connection_pool Changelog
2
2
 
3
+ 2.4.1
4
+ ------
5
+
6
+ - New `auto_reload_after_fork` config option to disable auto-drop [#177, shayonj]
7
+
8
+ 2.4.0
9
+ ------
10
+
11
+ - Automatically drop all connections after fork [#166]
12
+
13
+ 2.3.0
14
+ ------
15
+
16
+ - Minimum Ruby version is now 2.5.0
17
+ - Add pool size to TimeoutError message
18
+
3
19
  2.2.5
4
20
  ------
5
21
 
data/README.md CHANGED
@@ -4,16 +4,14 @@ connection\_pool
4
4
 
5
5
  Generic connection pooling for Ruby.
6
6
 
7
- MongoDB has its own connection pool. ActiveRecord has its own connection pool.
8
- This is a generic connection pool that can be used with anything, e.g. Redis,
9
- Dalli and other Ruby network clients.
10
-
7
+ MongoDB has its own connection pool.
8
+ ActiveRecord has its own connection pool.
9
+ This is a generic connection pool that can be used with anything, e.g. Redis, Dalli and other Ruby network clients.
11
10
 
12
11
  Usage
13
12
  -----
14
13
 
15
- Create a pool of objects to share amongst the fibers or threads in your Ruby
16
- application:
14
+ Create a pool of objects to share amongst the fibers or threads in your Ruby application:
17
15
 
18
16
  ``` ruby
19
17
  $memcached = ConnectionPool.new(size: 5, timeout: 5) { Dalli::Client.new }
@@ -28,11 +26,12 @@ end
28
26
  ```
29
27
 
30
28
  If all the objects in the connection pool are in use, `with` will block
31
- until one becomes available. If no object is available within `:timeout` seconds,
32
- `with` will raise a `Timeout::Error`.
29
+ until one becomes available.
30
+ If no object is available within `:timeout` seconds,
31
+ `with` will raise a `ConnectionPool::TimeoutError` (a subclass of `Timeout::Error`).
33
32
 
34
33
  You can also use `ConnectionPool#then` to support _both_ a
35
- connection pool and a raw client (requires Ruby 2.5+).
34
+ connection pool and a raw client.
36
35
 
37
36
  ```ruby
38
37
  # Compatible with a raw Redis::Client, and ConnectionPool Redis
@@ -48,15 +47,14 @@ end
48
47
  ```
49
48
 
50
49
  This will only modify the resource-get timeout for this particular
51
- invocation. This is useful if you want to fail-fast on certain non critical
52
- sections when a resource is not available, or conversely if you are comfortable
53
- blocking longer on a particular resource. This is not implemented in the below
54
- `ConnectionPool::Wrapper` class.
50
+ invocation.
51
+ This is useful if you want to fail-fast on certain non-critical
52
+ sections when a resource is not available, or conversely if you are comfortable blocking longer on a particular resource.
53
+ This is not implemented in the `ConnectionPool::Wrapper` class.
55
54
 
56
55
  ## Migrating to a Connection Pool
57
56
 
58
- You can use `ConnectionPool::Wrapper` to wrap a single global connection,
59
- making it easier to migrate existing connection code over time:
57
+ You can use `ConnectionPool::Wrapper` to wrap a single global connection, making it easier to migrate existing connection code over time:
60
58
 
61
59
  ``` ruby
62
60
  $redis = ConnectionPool::Wrapper.new(size: 5, timeout: 3) { Redis.new }
@@ -64,10 +62,8 @@ $redis.sadd('foo', 1)
64
62
  $redis.smembers('foo')
65
63
  ```
66
64
 
67
- The wrapper uses `method_missing` to checkout a connection, run the requested
68
- method and then immediately check the connection back into the pool. It's
69
- **not** high-performance so you'll want to port your performance sensitive code
70
- to use `with` as soon as possible.
65
+ The wrapper uses `method_missing` to checkout a connection, run the requested method and then immediately check the connection back into the pool.
66
+ It's **not** high-performance so you'll want to port your performance sensitive code to use `with` as soon as possible.
71
67
 
72
68
  ``` ruby
73
69
  $redis.with do |conn|
@@ -76,31 +72,26 @@ $redis.with do |conn|
76
72
  end
77
73
  ```
78
74
 
79
- Once you've ported your entire system to use `with`, you can simply remove
80
- `Wrapper` and use the simpler and faster `ConnectionPool`.
75
+ Once you've ported your entire system to use `with`, you can simply remove `Wrapper` and use the simpler and faster `ConnectionPool`.
81
76
 
82
77
 
83
78
  ## Shutdown
84
79
 
85
80
  You can shut down a ConnectionPool instance once it should no longer be used.
86
- Further checkout attempts will immediately raise an error but existing checkouts
87
- will work.
81
+ Further checkout attempts will immediately raise an error but existing checkouts will work.
88
82
 
89
83
  ```ruby
90
84
  cp = ConnectionPool.new { Redis.new }
91
- cp.shutdown { |conn| conn.quit }
85
+ cp.shutdown { |c| c.close }
92
86
  ```
93
87
 
94
88
  Shutting down a connection pool will block until all connections are checked in and closed.
95
- **Note that shutting down is completely optional**; Ruby's garbage collector will reclaim
96
- unreferenced pools under normal circumstances.
89
+ **Note that shutting down is completely optional**; Ruby's garbage collector will reclaim unreferenced pools under normal circumstances.
97
90
 
98
91
  ## Reload
99
92
 
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.
93
+ You can reload a ConnectionPool instance in the case it is desired to close all connections to the pool and, unlike `shutdown`, afterwards recreate connections so the pool may continue to be used.
94
+ Reloading may be useful after forking the process.
104
95
 
105
96
  ```ruby
106
97
  cp = ConnectionPool.new { Redis.new }
@@ -108,8 +99,7 @@ cp.reload { |conn| conn.quit }
108
99
  cp.with { |conn| conn.get('some-count') }
109
100
  ```
110
101
 
111
- Like `shutdown`, this will block until all connections are checked in and
112
- closed.
102
+ Like `shutdown`, this will block until all connections are checked in and closed.
113
103
 
114
104
  ## Current State
115
105
 
@@ -9,13 +9,16 @@ Gem::Specification.new do |s|
9
9
  s.homepage = "https://github.com/mperham/connection_pool"
10
10
  s.description = s.summary = "Generic connection pool for Ruby"
11
11
 
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) }
12
+ s.files = ["Changes.md", "LICENSE", "README.md", "connection_pool.gemspec",
13
+ "lib/connection_pool.rb", "lib/connection_pool/timed_stack.rb",
14
+ "lib/connection_pool/version.rb", "lib/connection_pool/wrapper.rb"]
15
+ s.executables = []
15
16
  s.require_paths = ["lib"]
16
17
  s.license = "MIT"
17
18
  s.add_development_dependency "bundler"
18
19
  s.add_development_dependency "minitest", ">= 5.0.0"
19
20
  s.add_development_dependency "rake"
20
- s.required_ruby_version = ">= 2.2.0"
21
+ s.required_ruby_version = ">= 2.5.0"
22
+
23
+ s.metadata = {"changelog_uri" => "https://github.com/mperham/connection_pool/blob/main/Changes.md", "rubygems_mfa_required" => "true"}
21
24
  end
@@ -29,8 +29,8 @@ class ConnectionPool::TimedStack
29
29
  @created = 0
30
30
  @que = []
31
31
  @max = size
32
- @mutex = Mutex.new
33
- @resource = ConditionVariable.new
32
+ @mutex = Thread::Mutex.new
33
+ @resource = Thread::ConditionVariable.new
34
34
  @shutdown_block = nil
35
35
  end
36
36
 
@@ -49,7 +49,7 @@ class ConnectionPool::TimedStack
49
49
  @resource.broadcast
50
50
  end
51
51
  end
52
- alias << push
52
+ alias_method :<<, :push
53
53
 
54
54
  ##
55
55
  # Retrieves a connection from the stack. If a connection is available it is
@@ -74,7 +74,7 @@ class ConnectionPool::TimedStack
74
74
  return connection if connection
75
75
 
76
76
  to_wait = deadline - current_time
77
- raise ConnectionPool::TimeoutError, "Waited #{timeout} sec" if to_wait <= 0
77
+ raise ConnectionPool::TimeoutError, "Waited #{timeout} sec, #{length}/#{@max} available" if to_wait <= 0
78
78
  @resource.wait(@mutex, to_wait)
79
79
  end
80
80
  end
@@ -87,7 +87,7 @@ class ConnectionPool::TimedStack
87
87
  # +:reload+ is +true+.
88
88
 
89
89
  def shutdown(reload: false, &block)
90
- raise ArgumentError, "shutdown must receive a block" unless block_given?
90
+ raise ArgumentError, "shutdown must receive a block" unless block
91
91
 
92
92
  @mutex.synchronize do
93
93
  @shutdown_block = block
@@ -1,3 +1,3 @@
1
1
  class ConnectionPool
2
- VERSION = "2.2.5"
2
+ VERSION = "2.4.1"
3
3
  end
@@ -30,7 +30,6 @@ class ConnectionPool
30
30
  METHODS.include?(id) || with { |c| c.respond_to?(id, *args) }
31
31
  end
32
32
 
33
- # rubocop:disable Style/MethodMissingSuper
34
33
  # rubocop:disable Style/MissingRespondToMissing
35
34
  if ::RUBY_VERSION >= "3.0.0"
36
35
  def method_missing(name, *args, **kwargs, &block)
@@ -1,9 +1,11 @@
1
1
  require "timeout"
2
- require "connection_pool/version"
2
+ require_relative "connection_pool/version"
3
3
 
4
4
  class ConnectionPool
5
5
  class Error < ::RuntimeError; end
6
+
6
7
  class PoolShuttingDownError < ::ConnectionPool::Error; end
8
+
7
9
  class TimeoutError < ::Timeout::Error; end
8
10
  end
9
11
 
@@ -34,14 +36,57 @@ end
34
36
  # Accepts the following options:
35
37
  # - :size - number of connections to pool, defaults to 5
36
38
  # - :timeout - amount of time to wait for a connection if none currently available, defaults to 5 seconds
39
+ # - :auto_reload_after_fork - automatically drop all connections after fork, defaults to true
37
40
  #
38
41
  class ConnectionPool
39
- DEFAULTS = {size: 5, timeout: 5}
42
+ DEFAULTS = {size: 5, timeout: 5, auto_reload_after_fork: true}
40
43
 
41
44
  def self.wrap(options, &block)
42
45
  Wrapper.new(options, &block)
43
46
  end
44
47
 
48
+ if Process.respond_to?(:fork)
49
+ INSTANCES = ObjectSpace::WeakMap.new
50
+ private_constant :INSTANCES
51
+
52
+ def self.after_fork
53
+ INSTANCES.values.each do |pool|
54
+ next unless pool.auto_reload_after_fork
55
+
56
+ # We're on after fork, so we know all other threads are dead.
57
+ # All we need to do is to ensure the main thread doesn't have a
58
+ # checked out connection
59
+ pool.checkin(force: true)
60
+ pool.reload do |connection|
61
+ # Unfortunately we don't know what method to call to close the connection,
62
+ # so we try the most common one.
63
+ connection.close if connection.respond_to?(:close)
64
+ end
65
+ end
66
+ nil
67
+ end
68
+
69
+ if ::Process.respond_to?(:_fork) # MRI 3.1+
70
+ module ForkTracker
71
+ def _fork
72
+ pid = super
73
+ if pid == 0
74
+ ConnectionPool.after_fork
75
+ end
76
+ pid
77
+ end
78
+ end
79
+ Process.singleton_class.prepend(ForkTracker)
80
+ end
81
+ else
82
+ INSTANCES = nil
83
+ private_constant :INSTANCES
84
+
85
+ def self.after_fork
86
+ # noop
87
+ end
88
+ end
89
+
45
90
  def initialize(options = {}, &block)
46
91
  raise ArgumentError, "Connection pool requires a block" unless block
47
92
 
@@ -49,10 +94,12 @@ class ConnectionPool
49
94
 
50
95
  @size = Integer(options.fetch(:size))
51
96
  @timeout = options.fetch(:timeout)
97
+ @auto_reload_after_fork = options.fetch(:auto_reload_after_fork)
52
98
 
53
99
  @available = TimedStack.new(@size, &block)
54
100
  @key = :"pool-#{@available.object_id}"
55
101
  @key_count = :"pool-#{@available.object_id}-count"
102
+ INSTANCES[self] = self if INSTANCES
56
103
  end
57
104
 
58
105
  def with(options = {})
@@ -67,7 +114,7 @@ class ConnectionPool
67
114
  end
68
115
  end
69
116
  end
70
- alias then with
117
+ alias_method :then, :with
71
118
 
72
119
  def checkout(options = {})
73
120
  if ::Thread.current[@key]
@@ -79,16 +126,16 @@ class ConnectionPool
79
126
  end
80
127
  end
81
128
 
82
- def checkin
129
+ def checkin(force: false)
83
130
  if ::Thread.current[@key]
84
- if ::Thread.current[@key_count] == 1
131
+ if ::Thread.current[@key_count] == 1 || force
85
132
  @available.push(::Thread.current[@key])
86
133
  ::Thread.current[@key] = nil
87
134
  ::Thread.current[@key_count] = nil
88
135
  else
89
136
  ::Thread.current[@key_count] -= 1
90
137
  end
91
- else
138
+ elsif !force
92
139
  raise ConnectionPool::Error, "no connections are checked out"
93
140
  end
94
141
 
@@ -115,6 +162,8 @@ class ConnectionPool
115
162
 
116
163
  # Size of this connection pool
117
164
  attr_reader :size
165
+ # Automatically drop all connections after fork
166
+ attr_reader :auto_reload_after_fork
118
167
 
119
168
  # Number of pool entries available for checkout at this instant.
120
169
  def available
@@ -122,5 +171,5 @@ class ConnectionPool
122
171
  end
123
172
  end
124
173
 
125
- require "connection_pool/timed_stack"
126
- require "connection_pool/wrapper"
174
+ require_relative "connection_pool/timed_stack"
175
+ require_relative "connection_pool/wrapper"
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.2.5
4
+ version: 2.4.1
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: 2021-04-14 00:00:00.000000000 Z
12
+ date: 2023-05-19 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -61,25 +61,20 @@ executables: []
61
61
  extensions: []
62
62
  extra_rdoc_files: []
63
63
  files:
64
- - ".github/workflows/ci.yml"
65
- - ".gitignore"
66
64
  - Changes.md
67
- - Gemfile
68
65
  - LICENSE
69
66
  - README.md
70
- - Rakefile
71
67
  - connection_pool.gemspec
72
68
  - lib/connection_pool.rb
73
69
  - lib/connection_pool/timed_stack.rb
74
70
  - lib/connection_pool/version.rb
75
71
  - lib/connection_pool/wrapper.rb
76
- - test/helper.rb
77
- - test/test_connection_pool.rb
78
- - test/test_connection_pool_timed_stack.rb
79
72
  homepage: https://github.com/mperham/connection_pool
80
73
  licenses:
81
74
  - MIT
82
- metadata: {}
75
+ metadata:
76
+ changelog_uri: https://github.com/mperham/connection_pool/blob/main/Changes.md
77
+ rubygems_mfa_required: 'true'
83
78
  post_install_message:
84
79
  rdoc_options: []
85
80
  require_paths:
@@ -88,18 +83,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
88
83
  requirements:
89
84
  - - ">="
90
85
  - !ruby/object:Gem::Version
91
- version: 2.2.0
86
+ version: 2.5.0
92
87
  required_rubygems_version: !ruby/object:Gem::Requirement
93
88
  requirements:
94
89
  - - ">="
95
90
  - !ruby/object:Gem::Version
96
91
  version: '0'
97
92
  requirements: []
98
- rubygems_version: 3.1.4
93
+ rubygems_version: 3.4.7
99
94
  signing_key:
100
95
  specification_version: 4
101
96
  summary: Generic connection pool for Ruby
102
- test_files:
103
- - test/helper.rb
104
- - test/test_connection_pool.rb
105
- - test/test_connection_pool_timed_stack.rb
97
+ test_files: []
@@ -1,26 +0,0 @@
1
- name: CI
2
-
3
- on: [push, pull_request]
4
-
5
- jobs:
6
- test:
7
- runs-on: ubuntu-latest
8
- continue-on-error: ${{ matrix.experimental }}
9
- strategy:
10
- fail-fast: false
11
- matrix:
12
- ruby: ["2.4", "2.5", "2.6", "2.7", "3.0", "jruby"]
13
- experimental: [false]
14
- include:
15
- - ruby: "truffleruby"
16
- experimental: true
17
- steps:
18
- - uses: actions/checkout@v2
19
- - uses: ruby/setup-ruby@v1
20
- with:
21
- ruby-version: ${{matrix.ruby}}
22
- bundler-cache: true
23
-
24
- - name: Run tests
25
- timeout-minutes: 5
26
- run: ${{matrix.env}} bundle exec rake
data/.gitignore DELETED
@@ -1,4 +0,0 @@
1
- *.gem
2
- .bundle
3
- Gemfile.lock
4
- pkg/*
data/Gemfile DELETED
@@ -1,3 +0,0 @@
1
- source "https://rubygems.org"
2
-
3
- gemspec(development_group: :runtime)
data/Rakefile DELETED
@@ -1,6 +0,0 @@
1
- require "bundler/gem_tasks"
2
-
3
- require "rake/testtask"
4
- Rake::TestTask.new
5
-
6
- task default: :test
data/test/helper.rb DELETED
@@ -1,8 +0,0 @@
1
- gem "minitest"
2
-
3
- require "minitest/pride"
4
- require "minitest/autorun"
5
-
6
- $VERBOSE = 1
7
-
8
- require_relative "../lib/connection_pool"
@@ -1,567 +0,0 @@
1
- require_relative "helper"
2
-
3
- class TestConnectionPool < Minitest::Test
4
- class NetworkConnection
5
- SLEEP_TIME = 0.1
6
-
7
- def initialize
8
- @x = 0
9
- end
10
-
11
- def do_something(*_args, increment: 1)
12
- @x += increment
13
- sleep SLEEP_TIME
14
- @x
15
- end
16
-
17
- def do_something_with_positional_hash(options)
18
- @x += options[:increment] || 1
19
- sleep SLEEP_TIME
20
- @x
21
- end
22
-
23
- def fast
24
- @x += 1
25
- end
26
-
27
- def do_something_with_block
28
- @x += yield
29
- sleep SLEEP_TIME
30
- @x
31
- end
32
-
33
- def respond_to?(method_id, *args)
34
- method_id == :do_magic || super(method_id, *args)
35
- end
36
- end
37
-
38
- class Recorder
39
- def initialize
40
- @calls = []
41
- end
42
-
43
- attr_reader :calls
44
-
45
- def do_work(label)
46
- @calls << label
47
- end
48
- end
49
-
50
- def use_pool(pool, size)
51
- Array.new(size) {
52
- Thread.new do
53
- pool.with { sleep }
54
- end
55
- }.each do |thread|
56
- Thread.pass until thread.status == "sleep"
57
- end
58
- end
59
-
60
- def kill_threads(threads)
61
- threads.each do |thread|
62
- thread.kill
63
- thread.join
64
- end
65
- end
66
-
67
- def test_basic_multithreaded_usage
68
- pool_size = 5
69
- pool = ConnectionPool.new(size: pool_size) { NetworkConnection.new }
70
-
71
- start = Time.new
72
-
73
- generations = 3
74
-
75
- result = Array.new(pool_size * generations) {
76
- Thread.new do
77
- pool.with do |net|
78
- net.do_something
79
- end
80
- end
81
- }.map(&:value)
82
-
83
- finish = Time.new
84
-
85
- assert_equal((1..generations).cycle(pool_size).sort, result.sort)
86
-
87
- assert_operator(finish - start, :>, generations * NetworkConnection::SLEEP_TIME)
88
- end
89
-
90
- def test_timeout
91
- pool = ConnectionPool.new(timeout: 0, size: 1) { NetworkConnection.new }
92
- thread = Thread.new {
93
- pool.with do |net|
94
- net.do_something
95
- sleep 0.01
96
- end
97
- }
98
-
99
- Thread.pass while thread.status == "run"
100
-
101
- assert_raises Timeout::Error do
102
- pool.with { |net| net.do_something }
103
- end
104
-
105
- thread.join
106
-
107
- pool.with do |conn|
108
- refute_nil conn
109
- end
110
- end
111
-
112
- def test_with
113
- pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new }
114
-
115
- pool.with do
116
- Thread.new {
117
- assert_raises Timeout::Error do
118
- pool.checkout
119
- end
120
- }.join
121
- end
122
-
123
- assert Thread.new { pool.checkout }.join
124
- end
125
-
126
- def test_then
127
- pool = ConnectionPool.new { Object.new }
128
-
129
- assert_equal pool.method(:then), pool.method(:with)
130
- end
131
-
132
- def test_with_timeout
133
- pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new }
134
-
135
- assert_raises Timeout::Error do
136
- Timeout.timeout(0.01) do
137
- pool.with do |obj|
138
- assert_equal 0, pool.available
139
- sleep 0.015
140
- end
141
- end
142
- end
143
- assert_equal 1, pool.available
144
- end
145
-
146
- def test_invalid_size
147
- assert_raises ArgumentError, TypeError do
148
- ConnectionPool.new(timeout: 0, size: nil) { Object.new }
149
- end
150
- assert_raises ArgumentError, TypeError do
151
- ConnectionPool.new(timeout: 0, size: "") { Object.new }
152
- end
153
- end
154
-
155
- def test_handle_interrupt_ensures_checkin
156
- pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new }
157
- def pool.checkout(options)
158
- sleep 0.015
159
- super
160
- end
161
-
162
- did_something = false
163
-
164
- action = lambda do
165
- Timeout.timeout(0.01) do
166
- pool.with do |obj|
167
- did_something = true
168
- # Timeout::Error will be triggered by any non-trivial Ruby code
169
- # executed here since it couldn't be raised during checkout.
170
- # It looks like setting the local variable above does not trigger
171
- # the Timeout check in MRI 2.2.1.
172
- obj.tap { obj.hash }
173
- end
174
- end
175
- end
176
-
177
- if RUBY_ENGINE == "ruby"
178
- # These asserts rely on the Ruby implementation reaching `did_something =
179
- # true` before the interrupt is detected by the thread. Interrupt
180
- # detection timing is implementation-specific in practice, with JRuby,
181
- # Rubinius, and TruffleRuby all having different interrupt timings to MRI.
182
- # In fact they generally detect interrupts more quickly than MRI, so they
183
- # may not reach `did_something = true` before detecting the interrupt.
184
-
185
- assert_raises Timeout::Error, &action
186
-
187
- assert did_something
188
- else
189
- action.call
190
- end
191
-
192
- assert_equal 1, pool.available
193
- end
194
-
195
- def test_explicit_return
196
- pool = ConnectionPool.new(timeout: 0, size: 1) {
197
- mock = Minitest::Mock.new
198
- def mock.disconnect!
199
- raise "should not disconnect upon explicit return"
200
- end
201
- mock
202
- }
203
-
204
- pool.with do |conn|
205
- return true
206
- end
207
- end
208
-
209
- def test_with_timeout_override
210
- pool = ConnectionPool.new(timeout: 0, size: 1) { NetworkConnection.new }
211
-
212
- t = Thread.new {
213
- pool.with do |net|
214
- net.do_something
215
- sleep 0.01
216
- end
217
- }
218
-
219
- Thread.pass while t.status == "run"
220
-
221
- assert_raises Timeout::Error do
222
- pool.with { |net| net.do_something }
223
- end
224
-
225
- pool.with(timeout: 2 * NetworkConnection::SLEEP_TIME) do |conn|
226
- refute_nil conn
227
- end
228
- end
229
-
230
- def test_checkin
231
- pool = ConnectionPool.new(timeout: 0, size: 1) { NetworkConnection.new }
232
- conn = pool.checkout
233
-
234
- Thread.new {
235
- assert_raises Timeout::Error do
236
- pool.checkout
237
- end
238
- }.join
239
-
240
- pool.checkin
241
-
242
- assert_same conn, Thread.new { pool.checkout }.value
243
- end
244
-
245
- def test_returns_value
246
- pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new }
247
- assert_equal 1, pool.with { |o| 1 }
248
- end
249
-
250
- def test_checkin_never_checkout
251
- pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new }
252
-
253
- e = assert_raises(ConnectionPool::Error) { pool.checkin }
254
- assert_equal "no connections are checked out", e.message
255
- end
256
-
257
- def test_checkin_no_current_checkout
258
- pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new }
259
-
260
- pool.checkout
261
- pool.checkin
262
-
263
- assert_raises ConnectionPool::Error do
264
- pool.checkin
265
- end
266
- end
267
-
268
- def test_checkin_twice
269
- pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new }
270
-
271
- pool.checkout
272
- pool.checkout
273
-
274
- pool.checkin
275
-
276
- Thread.new {
277
- assert_raises Timeout::Error do
278
- pool.checkout
279
- end
280
- }.join
281
-
282
- pool.checkin
283
-
284
- assert Thread.new { pool.checkout }.join
285
- end
286
-
287
- def test_checkout
288
- pool = ConnectionPool.new(size: 1) { NetworkConnection.new }
289
-
290
- conn = pool.checkout
291
-
292
- assert_kind_of NetworkConnection, conn
293
-
294
- assert_same conn, pool.checkout
295
- end
296
-
297
- def test_checkout_multithread
298
- pool = ConnectionPool.new(size: 2) { NetworkConnection.new }
299
- conn = pool.checkout
300
-
301
- t = Thread.new {
302
- pool.checkout
303
- }
304
-
305
- refute_same conn, t.value
306
- end
307
-
308
- def test_checkout_timeout
309
- pool = ConnectionPool.new(timeout: 0, size: 0) { Object.new }
310
-
311
- assert_raises Timeout::Error do
312
- pool.checkout
313
- end
314
- end
315
-
316
- def test_checkout_timeout_override
317
- pool = ConnectionPool.new(timeout: 0, size: 1) { NetworkConnection.new }
318
-
319
- thread = Thread.new {
320
- pool.with do |net|
321
- net.do_something
322
- sleep 0.01
323
- end
324
- }
325
-
326
- Thread.pass while thread.status == "run"
327
-
328
- assert_raises Timeout::Error do
329
- pool.checkout
330
- end
331
-
332
- assert pool.checkout timeout: 2 * NetworkConnection::SLEEP_TIME
333
- end
334
-
335
- def test_passthru
336
- pool = ConnectionPool.wrap(timeout: 2 * NetworkConnection::SLEEP_TIME, size: 1) { NetworkConnection.new }
337
- assert_equal 1, pool.do_something
338
- assert_equal 2, pool.do_something
339
- assert_equal 5, pool.do_something_with_block { 3 }
340
- assert_equal 6, pool.with { |net| net.fast }
341
- assert_equal 8, pool.do_something(increment: 2)
342
- assert_equal 10, pool.do_something_with_positional_hash({ increment: 2, symbol_key: 3, "string_key" => 4 })
343
- end
344
-
345
- def test_passthru_respond_to
346
- pool = ConnectionPool.wrap(timeout: 2 * NetworkConnection::SLEEP_TIME, size: 1) { NetworkConnection.new }
347
- assert pool.respond_to?(:with)
348
- assert pool.respond_to?(:do_something)
349
- assert pool.respond_to?(:do_magic)
350
- refute pool.respond_to?(:do_lots_of_magic)
351
- end
352
-
353
- def test_return_value
354
- pool = ConnectionPool.new(timeout: 2 * NetworkConnection::SLEEP_TIME, size: 1) { NetworkConnection.new }
355
- result = pool.with { |net|
356
- net.fast
357
- }
358
- assert_equal 1, result
359
- end
360
-
361
- def test_heavy_threading
362
- pool = ConnectionPool.new(timeout: 0.5, size: 3) { NetworkConnection.new }
363
-
364
- threads = Array.new(20) {
365
- Thread.new do
366
- pool.with do |net|
367
- sleep 0.01
368
- end
369
- end
370
- }
371
-
372
- threads.map { |thread| thread.join }
373
- end
374
-
375
- def test_reuses_objects_when_pool_not_saturated
376
- pool = ConnectionPool.new(size: 5) { NetworkConnection.new }
377
-
378
- ids = 10.times.map {
379
- pool.with { |c| c.object_id }
380
- }
381
-
382
- assert_equal 1, ids.uniq.size
383
- end
384
-
385
- def test_nested_checkout
386
- recorder = Recorder.new
387
- pool = ConnectionPool.new(size: 1) { recorder }
388
- pool.with do |r_outer|
389
- @other = Thread.new { |t|
390
- pool.with do |r_other|
391
- r_other.do_work("other")
392
- end
393
- }
394
-
395
- pool.with do |r_inner|
396
- r_inner.do_work("inner")
397
- end
398
-
399
- Thread.pass
400
-
401
- r_outer.do_work("outer")
402
- end
403
-
404
- @other.join
405
-
406
- assert_equal ["inner", "outer", "other"], recorder.calls
407
- end
408
-
409
- def test_shutdown_is_executed_for_all_connections
410
- recorders = []
411
-
412
- pool = ConnectionPool.new(size: 3) {
413
- Recorder.new.tap { |r| recorders << r }
414
- }
415
-
416
- threads = use_pool pool, 3
417
-
418
- pool.shutdown do |recorder|
419
- recorder.do_work("shutdown")
420
- end
421
-
422
- kill_threads(threads)
423
-
424
- assert_equal [["shutdown"]] * 3, recorders.map { |r| r.calls }
425
- end
426
-
427
- def test_raises_error_after_shutting_down
428
- pool = ConnectionPool.new(size: 1) { true }
429
-
430
- pool.shutdown {}
431
-
432
- assert_raises ConnectionPool::PoolShuttingDownError do
433
- pool.checkout
434
- end
435
- end
436
-
437
- def test_runs_shutdown_block_asynchronously_if_connection_was_in_use
438
- recorders = []
439
-
440
- pool = ConnectionPool.new(size: 3) {
441
- Recorder.new.tap { |r| recorders << r }
442
- }
443
-
444
- threads = use_pool pool, 2
445
-
446
- pool.checkout
447
-
448
- pool.shutdown do |recorder|
449
- recorder.do_work("shutdown")
450
- end
451
-
452
- kill_threads(threads)
453
-
454
- assert_equal [["shutdown"], ["shutdown"], []], recorders.map { |r| r.calls }
455
-
456
- pool.checkin
457
-
458
- assert_equal [["shutdown"], ["shutdown"], ["shutdown"]], recorders.map { |r| r.calls }
459
- end
460
-
461
- def test_raises_an_error_if_shutdown_is_called_without_a_block
462
- pool = ConnectionPool.new(size: 1) {}
463
-
464
- assert_raises ArgumentError do
465
- pool.shutdown
466
- end
467
- end
468
-
469
- def test_shutdown_is_executed_for_all_connections_in_wrapped_pool
470
- recorders = []
471
-
472
- wrapper = ConnectionPool::Wrapper.new(size: 3) {
473
- Recorder.new.tap { |r| recorders << r }
474
- }
475
-
476
- threads = use_pool wrapper, 3
477
-
478
- wrapper.pool_shutdown do |recorder|
479
- recorder.do_work("shutdown")
480
- end
481
-
482
- kill_threads(threads)
483
-
484
- assert_equal [["shutdown"]] * 3, recorders.map { |r| r.calls }
485
- end
486
-
487
- def test_wrapper_wrapped_pool
488
- wrapper = ConnectionPool::Wrapper.new { NetworkConnection.new }
489
- assert_equal ConnectionPool, wrapper.wrapped_pool.class
490
- end
491
-
492
- def test_wrapper_method_missing
493
- wrapper = ConnectionPool::Wrapper.new { NetworkConnection.new }
494
-
495
- assert_equal 1, wrapper.fast
496
- end
497
-
498
- def test_wrapper_respond_to_eh
499
- wrapper = ConnectionPool::Wrapper.new { NetworkConnection.new }
500
-
501
- assert_respond_to wrapper, :with
502
-
503
- assert_respond_to wrapper, :fast
504
- refute_respond_to wrapper, :"nonexistent method"
505
- end
506
-
507
- def test_wrapper_with
508
- wrapper = ConnectionPool::Wrapper.new(timeout: 0, size: 1) { Object.new }
509
-
510
- wrapper.with do
511
- Thread.new {
512
- assert_raises Timeout::Error do
513
- wrapper.with { flunk "connection checked out :(" }
514
- end
515
- }.join
516
- end
517
-
518
- assert Thread.new { wrapper.with {} }.join
519
- end
520
-
521
- class ConnWithEval
522
- def eval(arg)
523
- "eval'ed #{arg}"
524
- end
525
- end
526
-
527
- def test_wrapper_kernel_methods
528
- wrapper = ConnectionPool::Wrapper.new(timeout: 0, size: 1) { ConnWithEval.new }
529
-
530
- assert_equal "eval'ed 1", wrapper.eval(1)
531
- end
532
-
533
- def test_wrapper_with_connection_pool
534
- recorder = Recorder.new
535
- pool = ConnectionPool.new(size: 1) { recorder }
536
- wrapper = ConnectionPool::Wrapper.new(pool: pool)
537
-
538
- pool.with { |r| r.do_work("with") }
539
- wrapper.do_work("wrapped")
540
-
541
- assert_equal ["with", "wrapped"], recorder.calls
542
- end
543
-
544
- def test_stats_without_active_connection
545
- pool = ConnectionPool.new(size: 2) { NetworkConnection.new }
546
-
547
- assert_equal(2, pool.size)
548
- assert_equal(2, pool.available)
549
- end
550
-
551
- def test_stats_with_active_connection
552
- pool = ConnectionPool.new(size: 2) { NetworkConnection.new }
553
-
554
- pool.with do
555
- assert_equal(1, pool.available)
556
- end
557
- end
558
-
559
- def test_stats_with_string_size
560
- pool = ConnectionPool.new(size: "2") { NetworkConnection.new }
561
-
562
- pool.with do
563
- assert_equal(2, pool.size)
564
- assert_equal(1, pool.available)
565
- end
566
- end
567
- end
@@ -1,150 +0,0 @@
1
- require_relative "helper"
2
-
3
- class TestConnectionPoolTimedStack < Minitest::Test
4
- def setup
5
- @stack = ConnectionPool::TimedStack.new { Object.new }
6
- end
7
-
8
- def test_empty_eh
9
- stack = ConnectionPool::TimedStack.new(1) { Object.new }
10
-
11
- refute_empty stack
12
-
13
- popped = stack.pop
14
-
15
- assert_empty stack
16
-
17
- stack.push popped
18
-
19
- refute_empty stack
20
- end
21
-
22
- def test_length
23
- stack = ConnectionPool::TimedStack.new(1) { Object.new }
24
-
25
- assert_equal 1, stack.length
26
-
27
- popped = stack.pop
28
-
29
- assert_equal 0, stack.length
30
-
31
- stack.push popped
32
-
33
- assert_equal 1, stack.length
34
- end
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
-
55
- def test_pop
56
- object = Object.new
57
- @stack.push object
58
-
59
- popped = @stack.pop
60
-
61
- assert_same object, popped
62
- end
63
-
64
- def test_pop_empty
65
- e = assert_raises(ConnectionPool::TimeoutError) { @stack.pop timeout: 0 }
66
- assert_equal "Waited 0 sec", e.message
67
- end
68
-
69
- def test_pop_empty_2_0_compatibility
70
- e = assert_raises(Timeout::Error) { @stack.pop 0 }
71
- assert_equal "Waited 0 sec", e.message
72
- end
73
-
74
- def test_pop_full
75
- stack = ConnectionPool::TimedStack.new(1) { Object.new }
76
-
77
- popped = stack.pop
78
-
79
- refute_nil popped
80
- assert_empty stack
81
- end
82
-
83
- def test_pop_wait
84
- thread = Thread.start {
85
- @stack.pop
86
- }
87
-
88
- Thread.pass while thread.status == "run"
89
-
90
- object = Object.new
91
-
92
- @stack.push object
93
-
94
- assert_same object, thread.value
95
- end
96
-
97
- def test_pop_shutdown
98
- @stack.shutdown {}
99
-
100
- assert_raises ConnectionPool::PoolShuttingDownError do
101
- @stack.pop
102
- end
103
- end
104
-
105
- def test_pop_shutdown_reload
106
- stack = ConnectionPool::TimedStack.new(1) { Object.new }
107
- object = stack.pop
108
- stack.push(object)
109
-
110
- stack.shutdown(reload: true) {}
111
-
112
- refute_equal object, stack.pop
113
- end
114
-
115
- def test_push
116
- stack = ConnectionPool::TimedStack.new(1) { Object.new }
117
-
118
- conn = stack.pop
119
-
120
- stack.push conn
121
-
122
- refute_empty stack
123
- end
124
-
125
- def test_push_shutdown
126
- called = []
127
-
128
- @stack.shutdown do |object|
129
- called << object
130
- end
131
-
132
- @stack.push Object.new
133
-
134
- refute_empty called
135
- assert_empty @stack
136
- end
137
-
138
- def test_shutdown
139
- @stack.push Object.new
140
-
141
- called = []
142
-
143
- @stack.shutdown do |object|
144
- called << object
145
- end
146
-
147
- refute_empty called
148
- assert_empty @stack
149
- end
150
- end