pg_helper 0.3.1 → 0.4.0
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 +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
|
+
# =
|