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.
- checksums.yaml +7 -0
- data/.ruby-version +1 -0
- data/Gemfile +1 -1
- data/HISTORY.md +3 -0
- data/README.md +4 -0
- data/lib/pg_helper.rb +5 -4
- data/lib/pg_helper/connection_pool.rb +361 -0
- data/lib/pg_helper/query_builder.rb +80 -0
- data/lib/pg_helper/query_helper.rb +137 -148
- data/lib/pg_helper/support_classes.rb +44 -0
- data/lib/pg_helper/version.rb +2 -2
- data/pg_helper.gemspec +4 -2
- data/spec/lib/connection_pool_spec.rb +50 -0
- data/spec/lib/connection_pool_test.rb +310 -0
- data/spec/lib/pg_helper_spec.rb +147 -89
- data/spec/lib/query_builder_spec.rb +91 -0
- data/spec/spec_helper.rb +6 -0
- metadata +58 -40
@@ -1,179 +1,168 @@
|
|
1
|
-
#Main module of PgHelper gem/plugin
|
1
|
+
# Main module of PgHelper gem/plugin
|
2
2
|
module PgHelper
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
pg_result
|
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
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
pg_result
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
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
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
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
|
-
|
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
|
-
|
130
|
-
PGconn::PQTRANS_IDLE == @pg_connection.transaction_status
|
131
|
-
end
|
128
|
+
protected
|
132
129
|
|
133
|
-
|
134
|
-
|
135
|
-
|
130
|
+
def connection_idle?
|
131
|
+
PGconn::PQTRANS_IDLE == @pg_connection.transaction_status
|
132
|
+
end
|
136
133
|
|
137
|
-
|
138
|
-
|
139
|
-
|
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
|
-
|
142
|
-
|
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
|
-
|
153
|
-
|
154
|
-
|
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
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
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
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
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
|
data/lib/pg_helper/version.rb
CHANGED
data/pg_helper.gemspec
CHANGED
@@ -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
|
-
|
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
|
+
# =
|