connection_pool 2.1.0 → 2.1.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
  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