connection_pool 2.1.0 → 2.1.1

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
2
  SHA1:
3
- metadata.gz: d46787d5f1cdf8901aff57ded2eb518418fea245
4
- data.tar.gz: 0ffd3058f4269e9155070e901dd4eb29db2e81d7
3
+ metadata.gz: c0e26a3abd66776ac9ebe51e38226c1efdbee324
4
+ data.tar.gz: dfba06ec37dc8fc8a9a5b11e79ca09627e6d439c
5
5
  SHA512:
6
- metadata.gz: 9adff3406500853bcf8fcd480cce510cab1cf03b3d31cf2429648e92973b012000ba9d4a8e252efbce486f38f00b7df86527073d9d3f0212e967d8624675fe03
7
- data.tar.gz: 609a9f24d0472bf9df5989e6830118873a9ae779738945e80c7b5e7010aa205b545af949fc4fa2233ccb5bb4379e9b54d2118f42c4900ac81bb262e790b53a53
6
+ metadata.gz: 462848c7cb5bcd7be90e1b1b7ea8af8db2b785b45918e73be80e9a901fbe4a3de366ca6bd0c57ef609ef1cfaaa4a2eae6e924b5990b1a716b67db4d2451d3ef7
7
+ data.tar.gz: 177e4e03153eecb3b04b8bf0d4e98d5f5e9f3be2bb2a74a3464a0901cfc7dfe7679c257d4d180a061debc6cf71dd33d32ba4ad85f21cbf989189ef6bd24e4c02
data/.travis.yml CHANGED
@@ -1,10 +1,14 @@
1
1
  ---
2
+ sudo: false
3
+ cache: bundler
2
4
  language: ruby
5
+ rvm:
6
+ - 1.9.3
7
+ - 2.0.0
8
+ - 2.1.0
9
+ - 2.2.0
10
+ - jruby
11
+ - rbx-2
3
12
  notifications:
4
13
  email:
5
- - drbrain@segment7.net
6
- rvm:
7
- - 1.9.3
8
- - 2.0.0
9
- - 2.1.0
10
- script: rake test
14
+ - drbrain@segment7.net
data/Changes.md CHANGED
@@ -1,3 +1,11 @@
1
+ 2.1.1
2
+ ------
3
+
4
+ - Work around a subtle race condition with code which uses `Timeout.timeout` and
5
+ checks out a connection within the timeout block. This might cause
6
+ connections to get into a bad state and raise very odd errors. [tamird, #67]
7
+
8
+
1
9
  2.1.0
2
10
  ------
3
11
 
data/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  connection\_pool
2
2
  =================
3
+ [![Build Status](https://travis-ci.org/mperham/connection_pool.svg)](https://travis-ci.org/mperham/connection_pool)
3
4
 
4
5
  Generic connection pooling for Ruby.
5
6
 
data/Rakefile CHANGED
@@ -1,14 +1,6 @@
1
- begin
2
- require 'bundler'
3
- Bundler::GemHelper.install_tasks
4
- rescue LoadError
5
- end
1
+ require 'bundler/gem_tasks'
6
2
 
7
3
  require 'rake/testtask'
8
- Rake::TestTask.new(:test) do |test|
9
- test.libs << 'test'
10
- test.warning = true
11
- test.pattern = 'test/**/test_*.rb'
12
- end
4
+ Rake::TestTask.new
13
5
 
14
6
  task :default => :test
@@ -15,6 +15,7 @@ Gem::Specification.new do |s|
15
15
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16
16
  s.require_paths = ["lib"]
17
17
  s.license = "MIT"
18
+ s.add_development_dependency 'bundler'
18
19
  s.add_development_dependency 'minitest', '>= 5.0.0'
19
20
  s.add_development_dependency 'rake'
20
21
  end
@@ -2,7 +2,7 @@ require_relative 'connection_pool/version'
2
2
  require_relative 'connection_pool/timed_stack'
3
3
 
4
4
  # Generic connection pool class for e.g. sharing a limited number of network connections
5
- # among many threads. Note: Connections are eager created.
5
+ # among many threads. Note: Connections are lazily created.
6
6
  #
7
7
  # Example usage with block (faster):
8
8
  #
@@ -53,22 +53,31 @@ class ConnectionPool
53
53
  end
54
54
 
55
55
  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
56
60
  conn = checkout(options)
57
61
  begin
58
- yield conn
62
+ result = yield conn
63
+ success = true # means the connection wasn't interrupted
64
+ result
59
65
  ensure
60
- checkin
66
+ if success
67
+ # everything is roses, we can safely check the connection back in
68
+ checkin
69
+ else
70
+ @available.discard!(pop_connection)
71
+ end
61
72
  end
62
73
  end
63
74
 
64
75
  def checkout(options = {})
65
- stack = ::Thread.current[@key] ||= []
66
-
67
- if stack.empty?
76
+ conn = if stack.empty?
68
77
  timeout = options[:timeout] || @timeout
69
- conn = @available.pop(timeout: timeout)
78
+ @available.pop(timeout: timeout)
70
79
  else
71
- conn = stack.last
80
+ stack.last
72
81
  end
73
82
 
74
83
  stack.push conn
@@ -76,14 +85,9 @@ class ConnectionPool
76
85
  end
77
86
 
78
87
  def checkin
79
- stack = ::Thread.current[@key]
80
- raise ConnectionPool::Error, 'no connections are checked out' if
81
- !stack || stack.empty?
88
+ conn = pop_connection # mutates stack, must be on its own line
89
+ @available.push(conn) if stack.empty?
82
90
 
83
- conn = stack.pop
84
- if stack.empty?
85
- @available << conn
86
- end
87
91
  nil
88
92
  end
89
93
 
@@ -91,6 +95,20 @@ class ConnectionPool
91
95
  @available.shutdown(&block)
92
96
  end
93
97
 
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
+
94
112
  class Wrapper < ::BasicObject
95
113
  METHODS = [:with, :pool_shutdown]
96
114
 
@@ -98,13 +116,8 @@ class ConnectionPool
98
116
  @pool = ::ConnectionPool.new(options, &block)
99
117
  end
100
118
 
101
- def with
102
- conn = @pool.checkout
103
- begin
104
- yield conn
105
- ensure
106
- @pool.checkin
107
- end
119
+ def with(&block)
120
+ @pool.with(&block)
108
121
  end
109
122
 
110
123
  def pool_shutdown(&block)
@@ -112,11 +125,11 @@ class ConnectionPool
112
125
  end
113
126
 
114
127
  def respond_to?(id, *args)
115
- METHODS.include?(id) || @pool.with { |c| c.respond_to?(id, *args) }
128
+ METHODS.include?(id) || with { |c| c.respond_to?(id, *args) }
116
129
  end
117
130
 
118
131
  def method_missing(name, *args, &block)
119
- @pool.with do |connection|
132
+ with do |connection|
120
133
  connection.send(name, *args, &block)
121
134
  end
122
135
  end
@@ -117,6 +117,22 @@ class ConnectionPool::TimedStack
117
117
  @max - @created + @que.length
118
118
  end
119
119
 
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
+ @created -= 1
130
+ end
131
+
132
+ @resource.broadcast
133
+ end
134
+ end
135
+
120
136
  private
121
137
 
122
138
  ##
@@ -1,3 +1,3 @@
1
1
  class ConnectionPool
2
- VERSION = "2.1.0"
2
+ VERSION = "2.1.1"
3
3
  end
data/test/helper.rb CHANGED
@@ -3,8 +3,6 @@ gem 'minitest'
3
3
  require 'minitest/pride'
4
4
  require 'minitest/autorun'
5
5
 
6
- Thread.abort_on_exception = true
7
-
8
6
  $VERBOSE = 1
9
7
 
10
8
  require_relative '../lib/connection_pool'
@@ -47,9 +47,13 @@ class TestConnectionPool < Minitest::Test
47
47
  end
48
48
  end.each do |thread|
49
49
  Thread.pass until thread.status == 'sleep'
50
- end.map do |thread|
50
+ end
51
+ end
52
+
53
+ def kill_threads(threads)
54
+ threads.each do |thread|
51
55
  thread.kill
52
- Thread.pass while thread.alive?
56
+ thread.join
53
57
  end
54
58
  end
55
59
 
@@ -105,6 +109,40 @@ class TestConnectionPool < Minitest::Test
105
109
  assert Thread.new { pool.checkout }.join
106
110
  end
107
111
 
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")
119
+ end
120
+ end
121
+
122
+ marker_class = Class.new
123
+ pool = ConnectionPool.new(:timeout => 0, :size => 1) { marker_class.new }
124
+
125
+ # no "connections" allocated yet
126
+ assert_equal [], ObjectSpace.each_object(marker_class).to_a
127
+
128
+ checkin_time = 0.05
129
+
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
136
+ end
137
+ end
138
+ end
139
+
140
+ GC.start
141
+
142
+ # no dangling references to this "connection" remain
143
+ assert_equal [], ObjectSpace.each_object(marker_class).to_a
144
+ end
145
+
108
146
  def test_with_timeout_override
109
147
  pool = ConnectionPool.new(:timeout => 0, :size => 1) { NetworkConnection.new }
110
148
 
@@ -130,21 +168,13 @@ class TestConnectionPool < Minitest::Test
130
168
  pool = ConnectionPool.new(:timeout => 0, :size => 1) { NetworkConnection.new }
131
169
  conn = pool.checkout
132
170
 
133
- t1 = Thread.new do
134
- pool.checkout
135
- end
136
-
137
171
  assert_raises Timeout::Error do
138
- t1.join
172
+ Thread.new { pool.checkout }.join
139
173
  end
140
174
 
141
175
  pool.checkin
142
176
 
143
- t2 = Thread.new do
144
- pool.checkout
145
- end
146
-
147
- assert_same conn, t2.value
177
+ assert_same conn, Thread.new { pool.checkout }.value
148
178
  end
149
179
 
150
180
  def test_returns_value
@@ -319,12 +349,14 @@ class TestConnectionPool < Minitest::Test
319
349
  Recorder.new.tap { |r| recorders << r }
320
350
  end
321
351
 
322
- use_pool pool, 3
352
+ threads = use_pool pool, 3
323
353
 
324
354
  pool.shutdown do |recorder|
325
355
  recorder.do_work("shutdown")
326
356
  end
327
357
 
358
+ kill_threads(threads)
359
+
328
360
  assert_equal [["shutdown"]] * 3, recorders.map { |r| r.calls }
329
361
  end
330
362
 
@@ -345,7 +377,7 @@ class TestConnectionPool < Minitest::Test
345
377
  Recorder.new.tap { |r| recorders << r }
346
378
  end
347
379
 
348
- use_pool pool, 3
380
+ threads = use_pool pool, 2
349
381
 
350
382
  pool.checkout
351
383
 
@@ -353,7 +385,9 @@ class TestConnectionPool < Minitest::Test
353
385
  recorder.do_work("shutdown")
354
386
  end
355
387
 
356
- assert_equal [[], ["shutdown"], ["shutdown"]], recorders.map { |r| r.calls }.sort
388
+ kill_threads(threads)
389
+
390
+ assert_equal [["shutdown"], ["shutdown"], []], recorders.map { |r| r.calls }
357
391
 
358
392
  pool.checkin
359
393
 
@@ -375,12 +409,14 @@ class TestConnectionPool < Minitest::Test
375
409
  Recorder.new.tap { |r| recorders << r }
376
410
  end
377
411
 
378
- use_pool wrapper, 3
412
+ threads = use_pool wrapper, 3
379
413
 
380
414
  wrapper.pool_shutdown do |recorder|
381
415
  recorder.do_work("shutdown")
382
416
  end
383
417
 
418
+ kill_threads(threads)
419
+
384
420
  assert_equal [["shutdown"]] * 3, recorders.map { |r| r.calls }
385
421
  end
386
422
 
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.0
4
+ version: 2.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Perham
@@ -9,8 +9,22 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-11-18 00:00:00.000000000 Z
12
+ date: 2015-01-20 00:00:00.000000000 Z
13
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
14
28
  - !ruby/object:Gem::Dependency
15
29
  name: minitest
16
30
  requirement: !ruby/object:Gem::Requirement
@@ -81,7 +95,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
81
95
  version: '0'
82
96
  requirements: []
83
97
  rubyforge_project:
84
- rubygems_version: 2.4.1
98
+ rubygems_version: 2.4.5
85
99
  signing_key:
86
100
  specification_version: 4
87
101
  summary: Generic connection pool for Ruby