pg_helper 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,179 +1,168 @@
1
- #Main module of PgHelper gem/plugin
1
+ # Main module of PgHelper gem/plugin
2
2
  module PgHelper
3
-
4
- #Indicates that query returned unexpected columnt count
5
- class PgHelperErrorInvalidColumnCount < PGError; end
6
-
7
- #Indicates that query returned too much rows
8
- class PgHelperErrorInvalidRowCount < PGError; end
9
-
10
- # Indicates that transaction was called while inside transaction
11
- class PgHelperErrorNestedTransactionNotAllowed < PGError; end
12
-
13
- #For use inside transaction to cause rollback.
14
- class PgHelperErrorRollback < PGError; end
15
-
16
- #Indicates that call is invalid outside transaction
17
- class PgHelperErrorInvalidOutsideTransaction < PGError; end
18
-
19
- #Invalid argument
20
- class PgHelperErrorParamsMustBeArrayOfStrings < PGError; end
21
-
22
- #Main api class
23
- class QueryHelper
24
-
25
- # @return [Hash] connection params
26
- attr_accessor :connection_params
27
-
28
- # Active database connection
29
- # @return [PGconn] connection see {http://rubygems.org/gems/pg Pg gem on rubygems} for details
30
- attr_accessor :pg_connection
31
-
32
-
33
- # Creates a new instance of the QueryHelper
34
- def initialize(params)
35
- if params.kind_of? PGconn
36
- @pg_connection = params
37
- @connection_params = nil
38
- else
39
- @connection_params = params
40
- reconnect
3
+ # Main api class
4
+ class QueryHelper
5
+ # @return [Hash] connection params
6
+ attr_accessor :connection_params
7
+
8
+ # Active database connection
9
+ # @return [PGconn] connection see {http://rubygems.org/gems/pg gem}
10
+ attr_accessor :pg_connection
11
+
12
+ # Creates a new instance of the QueryHelper
13
+ def self.using_pool(pool, &_block)
14
+ helper = nil
15
+ pool.with_connection do |conn|
16
+ helper = new(conn)
17
+ yield helper
18
+ end
19
+ ensure
20
+ helper = nil
41
21
  end
42
- end
43
22
 
44
- # @param [String] query SQL select that should return one cell, may include $1, $2 etc to be replaced by arguments
45
- # @param [Array<String>] params query arguments to be passed on to PostgreSql
46
- # @return [String]
47
- def value(query, params = [])
48
- exec(query, params) do |pg_result|
49
- verify_single_cell!(pg_result)
50
- pg_result.getvalue(0,0)
23
+ def initialize(params)
24
+ if params.is_a? PGconn
25
+ @pg_connection = params
26
+ @connection_params = nil
27
+ else
28
+ @connection_params = params
29
+ reconnect
30
+ end
51
31
  end
52
- end
53
32
 
54
- # @param [String] query SQL select that should return one column, may include $1, $2 etc to be replaced by arguments
55
- # @param [Array<String>] params query arguments to be passed on to PostgreSql
56
- # @return [Array<String>] Values of selected column
57
- def get_column(query, params = [])
58
- exec(query, params) do |pg_result|
59
- require_single_column!(pg_result)
60
- pg_result.column_values(0)
33
+ # @param [String] query SQL select that should return one cell,
34
+ # may include $1, $2 etc to be replaced by query arguments
35
+ # @param [Array<String>] params query arguments
36
+ # @return [String]
37
+ def value(query, params = [])
38
+ exec(query, params) do |pg_result|
39
+ ValidationHelper.verify_single_cell!(pg_result)
40
+ pg_result.getvalue(0, 0)
41
+ end
61
42
  end
62
- end
63
43
 
64
- # @param [String] query SQL select that should return one row, may include $1, $2 etc to be replaced by arguments
65
- # @param [Array<String>] params query arguments to be passed on to PostgreSql
66
- # @return [Hash] Hash of column_name => row_value for resulting row
67
- def get_hash(query, params = [])
68
- exec(query, params) do |pg_result|
69
- require_single_row!(pg_result)
70
- pg_result.res[0]
44
+ # @param [String] query SQL select that should return one column,
45
+ # may include $1, $2 etc to be replaced by query arguments
46
+ # @param [Array<String>] params query arguments
47
+ # @return [Array<String>] Values of selected column
48
+ def get_column(query, params = [])
49
+ exec(query, params) do |pg_result|
50
+ ValidationHelper.require_single_column!(pg_result)
51
+ pg_result.column_values(0)
52
+ end
71
53
  end
72
- end
73
54
 
74
- # @param [String] query SQL select, may include $1, $2 etc to be replaced by arguments
75
- # @param [Array<String>] params query arguments to be passed on to PostgreSql
76
- # @return [Array<Array>] Array containing Array of values for each row
77
- def get_all(query, params = [])
78
- exec(query, params) do |pg_result|
79
- pg_result.values
55
+ # @param [String] query SQL select that should return one row,
56
+ # may include $1, $2 etc to be replaced by query arguments
57
+ # @param [Array<String>] params query arguments
58
+ # @return [Hash] Hash of column_name => row_value for resulting row
59
+ def get_hash(query, params = [])
60
+ exec(query, params) do |pg_result|
61
+ ValidationHelper.require_single_row!(pg_result)
62
+ pg_result[0]
63
+ end
80
64
  end
81
- end
82
65
 
83
- # @param [String] query SQL select, may include $1, $2 etc to be replaced by arguments
84
- # @param [Array<String>] params query arguments to be passed on to PostgreSql
85
- # @return [Array<Hash>] Array containing hash of column_name => row_value for each row
86
- def get_all_hashes(query, params = [])
87
- exec(query, params) do |pg_result|
88
- pg_result.to_a
66
+ # @param [String] query SQL select,
67
+ # may include $1, $2 etc to be replaced by query arguments
68
+ # @param [Array<String>] params query arguments
69
+ # @return [Array<Array>] Array containing Array of values for each row
70
+ def get_all(query, params = [])
71
+ exec(query, params) do |pg_result|
72
+ pg_result.values
73
+ end
89
74
  end
90
- end
91
75
 
92
- # @param [String] query SQL select, may include $1, $2 etc to be replaced by arguments
93
- # @param [Array<String>] params query arguments to be passed on to PostgreSql
94
- # @return String csv representation of query result with csv header
95
- def csv(query)
96
- csv_query = "COPY (#{query}) TO STDOUT with CSV HEADER"
97
- exec(csv_query, params = []) do
98
- csv_data = ""
99
- csv_data += buf while buf = @pg_connection.get_copy_data(true)
100
- csv_data
76
+ # @param [String] query SQL select,
77
+ # may include $1, $2 etc to be replaced by query arguments
78
+ # @param [Array<String>] params query arguments
79
+ # @return [Array<Hash>] Array of row hashes column_name => row_value
80
+ def get_all_hashes(query, params = [])
81
+ exec(query, params) do |pg_result|
82
+ pg_result.to_a
83
+ end
101
84
  end
102
- end
103
85
 
104
- # @param [String] query SQL update, may include $1, $2 etc to be replaced by arguments
105
- # @param [Array<String>] params query arguments to be passed on to PostgreSql
106
- # @return [Integer] Number of rows changed
107
- def modify(query, params = [])
108
- exec(query, params) do |pg_result|
109
- pg_result.cmd_tuples
86
+ # @param [String] query SQL select
87
+ # may include $1, $2 etc to be replaced by query arguments
88
+ # @param [Array<String>] params query arguments
89
+ # @return String csv representation of query result with csv header
90
+ def csv(query, params = [])
91
+ csv_query = "COPY (#{query}) TO STDOUT with CSV HEADER"
92
+ exec(csv_query, params) do
93
+ csv_data = ''
94
+ buf = @pg_connection.get_copy_data(true)
95
+ while buf
96
+ csv_data += buf
97
+ buf = @pg_connection.get_copy_data(true)
98
+ end
99
+ csv_data
100
+ end
110
101
  end
111
- end
112
102
 
113
- # Executes content of given block inside database transaction
114
- #@yield [QueryHelper]
115
- def transaction(&block)
116
- verify_transaction_possible!(&block)
117
- perform_transaction(&block)
118
- end
103
+ # @param [String] query SQL update,
104
+ # may include $1, $2 etc to be replaced by query arguments
105
+ # @param [Array<String>] params query arguments
106
+ # @return [Integer] Number of rows changed
107
+ def modify(query, params = [])
108
+ exec(query, params) do |pg_result|
109
+ pg_result.cmd_tuples
110
+ end
111
+ end
119
112
 
120
- # Aborts current transaction, or raises exception if invoked outside transaction.
121
- #@return [void]
122
- def rollback!
123
- raise PgHelperErrorInvalidOutsideTransaction if connection_idle?
124
- raise PgHelperErrorRollback.new
125
- end
113
+ # Executes content of given block inside database transaction
114
+ # @yield [QueryHelper]
115
+ def transaction(&block)
116
+ verify_transaction_possible!(&block)
117
+ perform_transaction(&block)
118
+ end
126
119
 
127
- protected
120
+ # Aborts current transaction, or raises exception if invoked
121
+ # outside transaction.
122
+ # @return [void]
123
+ def rollback!
124
+ fail PgHelperErrorInvalidOutsideTransaction if connection_idle?
125
+ fail PgHelperErrorRollback
126
+ end
128
127
 
129
- def connection_idle?
130
- PGconn::PQTRANS_IDLE == @pg_connection.transaction_status
131
- end
128
+ protected
132
129
 
133
- def require_single_row!(pg_result)
134
- raise PgHelperErrorInvalidRowCount.new if pg_result.ntuples != 1
135
- end
130
+ def connection_idle?
131
+ PGconn::PQTRANS_IDLE == @pg_connection.transaction_status
132
+ end
136
133
 
137
- def require_single_column!(pg_result)
138
- raise PgHelperErrorInvalidColumnCount.new if pg_result.nfields != 1
139
- end
134
+ def exec(query, params = [], &block)
135
+ check_query_params(params)
136
+ pg_result = nil
137
+ begin
138
+ pg_result = @pg_connection.exec(query, params)
139
+ block.call(pg_result)
140
+ ensure
141
+ pg_result && pg_result.clear
142
+ end
143
+ end
140
144
 
141
- def exec(query, params=[], &block)
142
- check_query_params(params)
143
- pg_result = nil
144
- begin
145
- pg_result = @pg_connection.exec(query, params)
146
- block.call(pg_result)
147
- ensure
148
- pg_result && pg_result.clear
145
+ def check_query_params(params)
146
+ params.is_a?(Array) || fail(PgHelperErrorParamsMustBeArrayOfStrings)
149
147
  end
150
- end
151
148
 
152
- def check_query_params(params)
153
- raise PgHelperErrorParamsMustBeArrayOfStrings.new unless params.is_a?(Array)
154
- end
155
- def reconnect
156
- @pg_connection = PGconn.open(@connection_params)
157
- end
149
+ def reconnect
150
+ @pg_connection = PGconn.open(@connection_params) if @connection_params
151
+ end
158
152
 
159
- def perform_transaction(&block)
160
- @pg_connection.transaction do
161
- begin
162
- block.call(self)
163
- rescue PgHelperErrorRollback
164
- true
153
+ def perform_transaction(&block)
154
+ @pg_connection.transaction do
155
+ begin
156
+ block.call(self)
157
+ rescue PgHelperErrorRollback
158
+ true
159
+ end
165
160
  end
166
161
  end
167
- end
168
162
 
169
- def verify_transaction_possible!(&block)
170
- raise PgHelperErrorNestedTransactionNotAllowed.new unless connection_idle?
171
- raise ArgumentError.new('missing block') unless block_given?
172
- end
173
-
174
- def verify_single_cell!(pg_result)
175
- require_single_row!(pg_result)
176
- require_single_column!(pg_result)
163
+ def verify_transaction_possible!(&_block)
164
+ fail PgHelperErrorNestedTransactionNotAllowed unless connection_idle?
165
+ fail ArgumentError, 'missing block' unless block_given?
166
+ end
177
167
  end
178
168
  end
179
- end
@@ -0,0 +1,44 @@
1
+ # Main module of PgHelper gem/plugin
2
+ module PgHelper
3
+ # Indicates that query returned unexpected columnt count
4
+ class PgHelperErrorInvalidColumnCount < PGError; end
5
+
6
+ # Indicates that query returned too much rows
7
+ class PgHelperErrorInvalidRowCount < PGError; end
8
+
9
+ # Indicates that transaction was called while inside transaction
10
+ class PgHelperErrorNestedTransactionNotAllowed < PGError; end
11
+
12
+ # For use inside transaction to cause rollback.
13
+ class PgHelperErrorRollback < PGError; end
14
+
15
+ # Indicates that call is invalid outside transaction
16
+ class PgHelperErrorInvalidOutsideTransaction < PGError; end
17
+
18
+ # Invalid argument
19
+ class PgHelperErrorParamsMustBeArrayOfStrings < PGError; end
20
+
21
+ # data validation
22
+ class ValidationHelper
23
+ class << self
24
+ def require_single_row!(pg_result)
25
+ fail(
26
+ PgHelperErrorInvalidRowCount,
27
+ "expected 1 row, got #{pg_result.ntuples}"
28
+ ) if pg_result.ntuples != 1
29
+ end
30
+
31
+ def require_single_column!(pg_result)
32
+ fail(
33
+ PgHelperErrorInvalidColumnCount,
34
+ "expected 1 column, got #{pg_result.nfields}"
35
+ ) if pg_result.nfields != 1
36
+ end
37
+
38
+ def verify_single_cell!(pg_result)
39
+ require_single_row!(pg_result)
40
+ require_single_column!(pg_result)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,5 +1,5 @@
1
- #Main module of PgHelper gem/plugin
1
+ # Main module of PgHelper gem/plugin
2
2
  module PgHelper
3
3
  # @return [String] gem version
4
- VERSION = "0.3.1"
4
+ VERSION = '0.4.0'
5
5
  end
@@ -19,11 +19,13 @@ Gem::Specification.new do |s|
19
19
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
20
  s.require_paths = ["lib"]
21
21
 
22
- s.add_runtime_dependency 'pg'
22
+ s.add_runtime_dependency 'pg', '~> 0.18.0'
23
+ s.add_runtime_dependency 'thread_safe'
23
24
 
24
25
  s.add_development_dependency 'rake'
25
26
  s.add_development_dependency 'rspec'
26
27
  s.add_development_dependency 'wirble'
27
28
  s.add_development_dependency 'yard'
28
29
  s.add_development_dependency 'bluecloth' #yard hidden dependency
29
- end
30
+ s.add_development_dependency 'rubocop' #yard hidden dependency
31
+ end
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+ require 'pg_helper/connection_pool'
3
+
4
+ RSpec.describe PgHelper::ConnectionPool do
5
+ describe '.new'
6
+ # OPTIONS: :pool_size, :checkout_timeout,
7
+ # # REST is passed directly to pg_connection.new
8
+ describe '#checkout'
9
+ describe '#checkin(connection)'
10
+ describe '#'
11
+
12
+ def connection_options
13
+ { dbname: 'postgres', user: 'postgres', host: 'localhost' }
14
+ end
15
+
16
+ subject { described_class.new(connection_options) }
17
+
18
+ describe '#auto_connect' do
19
+ it 'is true by default' do
20
+ expect(subject.auto_connect).to be true
21
+ end
22
+
23
+ context 'when is true' do
24
+ before { subject.auto_connect = true }
25
+
26
+ it 'provides active connection' do
27
+ expect(subject.connection).not_to be_finished
28
+ end
29
+
30
+ it 'yields active connection' do
31
+ subject.with_connection { |c| expect(c).not_to be_finished }
32
+ end
33
+ end
34
+
35
+ context 'when is false' do
36
+ before { subject.auto_connect = false }
37
+ it 'raises error instead of active connection' do
38
+ expect do
39
+ subject.connection
40
+ end.to raise_exception
41
+ end
42
+
43
+ it 'raises error instead of yielding active connection' do
44
+ expect do
45
+ subject.connection
46
+ end.to raise_exception
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,310 @@
1
+ # module ActiveRecord
2
+ # module ConnectionAdapters
3
+ # class ConnectionPoolTest < ActiveRecord::TestCase
4
+ # attr_reader :pool
5
+ #
6
+ # def setup
7
+ # super
8
+ #
9
+ # # Keep a duplicate pool so we do not bother others
10
+ # @pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec
11
+ #
12
+ # if in_memory_db?
13
+ # # Separate connections to an in-memory database
14
+ # # create an entirely new database,
15
+ # # with an empty schema etc, so we just stub out this
16
+ # # schema on the fly.
17
+ # @pool.with_connection do |connection|
18
+ # connection.create_table :posts do |t|
19
+ # t.integer :cololumn
20
+ # end
21
+ # end
22
+ # end
23
+ # end
24
+ #
25
+ # def teardown
26
+ # super
27
+ # @pool.disconnect!
28
+ # end
29
+ #
30
+ # def active_connections(pool)
31
+ # pool.connections.find_all(&:in_use?)
32
+ # end
33
+ #
34
+ # def test_checkout_after_close
35
+ # connection = pool.connection
36
+ # assert connection.in_use?
37
+ #
38
+ # connection.close
39
+ # assert !connection.in_use?
40
+ #
41
+ # assert pool.connection.in_use?
42
+ # end
43
+ #
44
+ # def test_released_connection_moves_between_threads
45
+ # thread_conn = nil
46
+ #
47
+ # Thread.new {
48
+ # pool.with_connection do |conn|
49
+ # thread_conn = conn
50
+ # end
51
+ # }.join
52
+ #
53
+ # assert thread_conn
54
+ #
55
+ # Thread.new {
56
+ # pool.with_connection do |conn|
57
+ # assert_equal thread_conn, conn
58
+ # end
59
+ # }.join
60
+ # end
61
+ #
62
+ # def test_with_connection
63
+ # assert_equal 0, active_connections(pool).size
64
+ #
65
+ # main_thread = pool.connection
66
+ # assert_equal 1, active_connections(pool).size
67
+ #
68
+ # Thread.new {
69
+ # pool.with_connection do |conn|
70
+ # assert conn
71
+ # assert_equal 2, active_connections(pool).size
72
+ # end
73
+ # assert_equal 1, active_connections(pool).size
74
+ # }.join
75
+ #
76
+ # main_thread.close
77
+ # assert_equal 0, active_connections(pool).size
78
+ # end
79
+ #
80
+ # def test_active_connection_in_use
81
+ # assert !pool.active_connection?
82
+ # main_thread = pool.connection
83
+ #
84
+ # assert pool.active_connection?
85
+ #
86
+ # main_thread.close
87
+ #
88
+ # assert !pool.active_connection?
89
+ # end
90
+ #
91
+ # def test_full_pool_exception
92
+ # assert_raises(CouldNotObtainConnection) do
93
+ # (@pool.size + 1).times do
94
+ # @pool.checkout
95
+ # end
96
+ # end
97
+ # end
98
+ #
99
+ # def test_full_pool_blocks
100
+ # cs = @pool.size.times.map { @pool.checkout }
101
+ # t = Thread.new { @pool.checkout }
102
+ #
103
+ # # make sure our thread is in the timeout section
104
+ # Thread.pass until t.status == "sleep"
105
+ #
106
+ # connection = cs.first
107
+ # connection.close
108
+ # assert_equal connection, t.join.value
109
+ # end
110
+ #
111
+ # def test_removing_releases_latch
112
+ # cs = @pool.size.times.map { @pool.checkout }
113
+ # t = Thread.new { @pool.checkout }
114
+ #
115
+ # # make sure our thread is in the timeout section
116
+ # Thread.pass until t.status == "sleep"
117
+ #
118
+ # connection = cs.first
119
+ # @pool.remove connection
120
+ # assert_respond_to t.join.value, :execute
121
+ # end
122
+ #
123
+ # def test_reap_and_active
124
+ # @pool.checkout
125
+ # @pool.checkout
126
+ # @pool.checkout
127
+ # @pool.dead_connection_timeout = 0
128
+ #
129
+ # connections = @pool.connections.dup
130
+ #
131
+ # @pool.reap
132
+ #
133
+ # assert_equal connections.length, @pool.connections.length
134
+ # end
135
+ #
136
+ # def test_reap_inactive
137
+ # @pool.checkout
138
+ # @pool.checkout
139
+ # @pool.checkout
140
+ # @pool.dead_connection_timeout = 0
141
+ #
142
+ # connections = @pool.connections.dup
143
+ # connections.each do |conn|
144
+ # conn.extend(Module.new { def active?; false; end; })
145
+ # end
146
+ #
147
+ # @pool.reap
148
+ #
149
+ # assert_equal 0, @pool.connections.length
150
+ # ensure
151
+ # connections.each(&:close)
152
+ # end
153
+ #
154
+ # def test_remove_connection
155
+ # conn = @pool.checkout
156
+ # assert conn.in_use?
157
+ #
158
+ # length = @pool.connections.length
159
+ # @pool.remove conn
160
+ # assert conn.in_use?
161
+ # assert_equal(length - 1, @pool.connections.length)
162
+ # ensure
163
+ # conn.close
164
+ # end
165
+ #
166
+ # def test_remove_connection_for_thread
167
+ # conn = @pool.connection
168
+ # @pool.remove conn
169
+ # assert_not_equal(conn, @pool.connection)
170
+ # ensure
171
+ # conn.close if conn
172
+ # end
173
+ #
174
+ # def test_active_connection?
175
+ # assert !@pool.active_connection?
176
+ # assert @pool.connection
177
+ # assert @pool.active_connection?
178
+ # @pool.release_connection
179
+ # assert !@pool.active_connection?
180
+ # end
181
+ #
182
+ # def test_checkout_behaviour
183
+ # pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec
184
+ # connection = pool.connection
185
+ # assert_not_nil connection
186
+ # threads = []
187
+ # 4.times do |i|
188
+ # threads << Thread.new(i) do
189
+ # connection = pool.connection
190
+ # assert_not_nil connection
191
+ # connection.close
192
+ # end
193
+ # end
194
+ #
195
+ # threads.each(&:join)
196
+ #
197
+ # Thread.new do
198
+ # assert pool.connection
199
+ # pool.connection.close
200
+ # end.join
201
+ # end
202
+ #
203
+ # # The connection pool is "fair" if threads waiting for
204
+ # # connections receive them the order in which they began
205
+ # # waiting. This ensures that we don't timeout one HTTP request
206
+ # # even while well under capacity in a multi-threaded environment
207
+ # # such as a Java servlet container.
208
+ # #
209
+ # # We don't need strict fairness: if two connections become
210
+ # # available at the same time, it's fine of two threads that were
211
+ # # waiting acquire the connections out of order.
212
+ # #
213
+ # # Thus this test prepares waiting threads and then trickles in
214
+ # # available connections slowly, ensuring the wakeup order is
215
+ # # correct in this case.
216
+ # def test_checkout_fairness
217
+ # @pool.instance_variable_set(:@size, 10)
218
+ # expected = (1..@pool.size).to_a.freeze
219
+ # # check out all connections so our threads start out waiting
220
+ # conns = expected.map { @pool.checkout }
221
+ # mutex = Mutex.new
222
+ # order = []
223
+ # errors = []
224
+ #
225
+ # threads = expected.map do |i|
226
+ # t = Thread.new {
227
+ # begin
228
+ # @pool.checkout # never checked back in
229
+ # mutex.synchronize { order << i }
230
+ # rescue => e
231
+ # mutex.synchronize { errors << e }
232
+ # end
233
+ # }
234
+ # Thread.pass until t.status == "sleep"
235
+ # t
236
+ # end
237
+ #
238
+ # # this should wake up the waiting threads one by one in order
239
+ # conns.each { |conn| @pool.checkin(conn); sleep 0.1 }
240
+ #
241
+ # threads.each(&:join)
242
+ #
243
+ # raise errors.first if errors.any?
244
+ #
245
+ # assert_equal(expected, order)
246
+ # end
247
+ #
248
+ # # As mentioned in #test_checkout_fairness, we don't care about
249
+ # # strict fairness. This test creates two groups of threads:
250
+ # # group1 whose members all start waiting before any thread in
251
+ # # group2. Enough connections are checked in to wakeup all
252
+ # # group1 threads, and the fact that only group1 and no group2
253
+ # # threads acquired a connection is enforced.
254
+ # def test_checkout_fairness_by_group
255
+ # @pool.instance_variable_set(:@size, 10)
256
+ # # take all the connections
257
+ # conns = (1..10).map { @pool.checkout }
258
+ # mutex = Mutex.new
259
+ # successes = [] # threads that successfully got a connection
260
+ # errors = []
261
+ #
262
+ # make_thread = proc do |i|
263
+ # t = Thread.new {
264
+ # begin
265
+ # @pool.checkout # never checked back in
266
+ # mutex.synchronize { successes << i }
267
+ # rescue => e
268
+ # mutex.synchronize { errors << e }
269
+ # end
270
+ # }
271
+ # Thread.pass until t.status == "sleep"
272
+ # t
273
+ # end
274
+ #
275
+ # # all group1 threads start waiting before any in group2
276
+ # group1 = (1..5).map(&make_thread)
277
+ # group2 = (6..10).map(&make_thread)
278
+ #
279
+ # # checkin n connections back to the pool
280
+ # checkin = proc do |n|
281
+ # n.times do
282
+ # c = conns.pop
283
+ # @pool.checkin(c)
284
+ # end
285
+ # end
286
+ #
287
+ # checkin.call(group1.size) # should wake up all group1
288
+ #
289
+ # loop do
290
+ # sleep 0.1
291
+ # break if mutex.synchronize {
292
+ # (successes.size + errors.size) == group1.size }
293
+ # end
294
+ #
295
+ # winners = mutex.synchronize { successes.dup }
296
+ # checkin.call(group2.size) # should wake up everyone remaining
297
+ #
298
+ # group1.each(&:join)
299
+ # group2.each(&:join)
300
+ #
301
+ # assert_equal((1..group1.size).to_a, winners.sort)
302
+ #
303
+ # if errors.any?
304
+ # raise errors.first
305
+ # end
306
+ # end
307
+ # end
308
+ # end
309
+ # end
310
+ # =