activerecord-bogacs 0.1.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.
@@ -0,0 +1,18 @@
1
+ require File.expand_path('../test_helper', File.dirname(__FILE__))
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ class BuiltinPoolTest < Test::Unit::TestCase
6
+
7
+ include ConnectionAdapters::ConnectionPoolTestMethods
8
+
9
+ def config; AR_CONFIG end
10
+
11
+ def setup
12
+ super
13
+ @pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,336 @@
1
+ # NOTE: based on connection pool test (from AR's test suite)
2
+ module ActiveRecord
3
+ module ConnectionAdapters
4
+ module ConnectionPoolTestMethods
5
+
6
+ attr_reader :pool
7
+
8
+ def setup
9
+ ActiveRecord::Base.establish_connection(config)
10
+ end
11
+
12
+ def teardown
13
+ pool.disconnect! if pool
14
+ end
15
+
16
+ def test_checkout_after_close
17
+ connection = pool.connection
18
+ assert connection.in_use?
19
+
20
+ connection.close
21
+ assert ! connection.in_use?
22
+
23
+ assert pool.connection.in_use?
24
+ end
25
+
26
+ def test_released_connection_moves_between_threads
27
+ thread_conn = nil
28
+
29
+ Thread.new {
30
+ pool.with_connection do |conn|
31
+ thread_conn = conn
32
+ end
33
+ }.join
34
+
35
+ assert thread_conn
36
+
37
+ Thread.new {
38
+ pool.with_connection do |conn|
39
+ assert_equal thread_conn, conn
40
+ end
41
+ }.join
42
+ end
43
+
44
+ def test_with_connection
45
+ assert_equal 0, active_connections(pool).size
46
+
47
+ main_thread = pool.connection
48
+ assert_equal 1, active_connections(pool).size
49
+
50
+ Thread.new {
51
+ pool.with_connection do |conn|
52
+ assert conn
53
+ assert_equal 2, active_connections(pool).size
54
+ end
55
+ assert_equal 1, active_connections(pool).size
56
+ }.join
57
+
58
+ main_thread.close
59
+ assert_equal 0, active_connections(pool).size
60
+ end
61
+
62
+ def test_active_connection_in_use
63
+ assert !pool.active_connection?
64
+ main_thread = pool.connection
65
+
66
+ assert pool.active_connection?
67
+
68
+ main_thread.close
69
+
70
+ assert !pool.active_connection?
71
+ end
72
+
73
+ def test_full_pool_exception
74
+ pool.size.times { pool.checkout }
75
+ assert_raise(ConnectionTimeoutError) do
76
+ pool.checkout
77
+ end
78
+ end
79
+
80
+ def test_full_pool_blocks
81
+ cs = pool.size.times.map { pool.checkout }
82
+ t = Thread.new { pool.checkout }
83
+
84
+ # make sure our thread is in the timeout section
85
+ Thread.pass until t.status == "sleep"
86
+
87
+ connection = cs.first
88
+ connection.close
89
+ assert_equal connection, t.join.value
90
+ end
91
+
92
+ def test_removing_releases_latch
93
+ cs = pool.size.times.map { pool.checkout }
94
+ t = Thread.new { pool.checkout }
95
+
96
+ # make sure our thread is in the timeout section
97
+ Thread.pass until t.status == "sleep"
98
+
99
+ connection = cs.first
100
+ pool.remove connection
101
+ assert_respond_to t.join.value, :execute
102
+ connection.close
103
+ end
104
+
105
+ def test_reap_and_active
106
+ pool.checkout
107
+ pool.checkout
108
+ pool.checkout
109
+
110
+ connections = pool.connections.dup
111
+
112
+ pool.reap
113
+
114
+ assert_equal connections.length, pool.connections.length
115
+ end
116
+
117
+ def test_reap_inactive
118
+ ready = Queue.new
119
+ pool.checkout
120
+ child = Thread.new do
121
+ pool.checkout
122
+ pool.checkout
123
+ ready.push 42
124
+ # Thread.stop
125
+ end
126
+ ready.pop # awaits
127
+
128
+ assert_equal 3, active_connections.size
129
+
130
+ child.terminate
131
+ child.join
132
+ pool.reap
133
+
134
+ # TODO this does not pass on built-in pool (MRI assumption) :
135
+ #assert_equal 1, active_connections.size
136
+ ensure
137
+ pool.connections.each(&:close)
138
+ end
139
+
140
+ def test_remove_connection
141
+ conn = pool.checkout
142
+ assert conn.in_use?
143
+
144
+ length = pool.connections.length
145
+ pool.remove conn
146
+ assert conn.in_use?
147
+ assert_equal(length - 1, pool.connections.length)
148
+ ensure
149
+ conn.close if conn
150
+ end
151
+
152
+ def test_remove_connection_for_thread
153
+ conn = pool.connection
154
+ pool.remove conn
155
+ assert_not_equal(conn, pool.connection)
156
+ ensure
157
+ conn.close if conn
158
+ end
159
+
160
+ def test_active_connection?
161
+ assert_false pool.active_connection?
162
+ assert pool.connection
163
+ assert_true pool.active_connection?
164
+ pool.release_connection
165
+ assert_false pool.active_connection?
166
+ end
167
+
168
+ def test_checkout_behaviour
169
+ assert connection = pool.connection
170
+ threads = []
171
+ 4.times do |i|
172
+ threads << Thread.new(i) do
173
+ connection = pool.connection
174
+ assert_not_nil connection
175
+ connection.close
176
+ end
177
+ end
178
+
179
+ threads.each(&:join)
180
+
181
+ Thread.new do
182
+ assert pool.connection
183
+ pool.connection.close
184
+ end.join
185
+ end
186
+
187
+ # The connection pool is "fair" if threads waiting for
188
+ # connections receive them the order in which they began
189
+ # waiting. This ensures that we don't timeout one HTTP request
190
+ # even while well under capacity in a multi-threaded environment
191
+ # such as a Java servlet container.
192
+ #
193
+ # We don't need strict fairness: if two connections become
194
+ # available at the same time, it's fine of two threads that were
195
+ # waiting acquire the connections out of order.
196
+ #
197
+ # Thus this test prepares waiting threads and then trickles in
198
+ # available connections slowly, ensuring the wakeup order is
199
+ # correct in this case.
200
+ def test_checkout_fairness
201
+ @pool.instance_variable_set(:@size, 10)
202
+ expected = (1..@pool.size).to_a.freeze
203
+ # check out all connections so our threads start out waiting
204
+ conns = expected.map { @pool.checkout }
205
+ mutex = Mutex.new
206
+ order = []
207
+ errors = []
208
+
209
+ threads = expected.map do |i|
210
+ t = Thread.new {
211
+ begin
212
+ @pool.checkout # never checked back in
213
+ mutex.synchronize { order << i }
214
+ rescue => e
215
+ mutex.synchronize { errors << e }
216
+ end
217
+ }
218
+ sleep(0.01) # "tuning" for JRuby
219
+ Thread.pass until t.status == 'sleep'
220
+ t
221
+ end
222
+
223
+ # this should wake up the waiting threads one by one in order
224
+ conns.each { |conn| @pool.checkin(conn); sleep 0.1 }
225
+
226
+ threads.each(&:join)
227
+
228
+ raise errors.first if errors.any?
229
+
230
+ assert_equal(expected, order)
231
+ end
232
+
233
+ # As mentioned in #test_checkout_fairness, we don't care about
234
+ # strict fairness. This test creates two groups of threads:
235
+ # group1 whose members all start waiting before any thread in
236
+ # group2. Enough connections are checked in to wakeup all
237
+ # group1 threads, and the fact that only group1 and no group2
238
+ # threads acquired a connection is enforced.
239
+ def test_checkout_fairness_by_group
240
+ @pool.instance_variable_set(:@size, 10)
241
+ # take all the connections
242
+ conns = (1..10).map { @pool.checkout }
243
+ mutex = Mutex.new
244
+ successes = [] # threads that successfully got a connection
245
+ errors = []
246
+
247
+ make_thread = proc do |i|
248
+ t = Thread.new {
249
+ begin
250
+ @pool.checkout # never checked back in
251
+ mutex.synchronize { successes << i }
252
+ rescue => e
253
+ mutex.synchronize { errors << e }
254
+ end
255
+ }
256
+ sleep(0.01) # "tuning" for JRuby
257
+ Thread.pass until t.status == 'sleep'
258
+ t
259
+ end
260
+
261
+ # all group1 threads start waiting before any in group2
262
+ group1 = (1..5).map(&make_thread)
263
+ sleep(0.05) # "tuning" for JRuby
264
+ group2 = (6..10).map(&make_thread)
265
+
266
+ # checkin n connections back to the pool
267
+ checkin = proc do |n|
268
+ n.times do
269
+ c = conns.pop
270
+ @pool.checkin(c)
271
+ end
272
+ end
273
+
274
+ checkin.call(group1.size) # should wake up all group1
275
+
276
+ loop do
277
+ sleep 0.1
278
+ break if mutex.synchronize { (successes.size + errors.size) == group1.size }
279
+ end
280
+
281
+ winners = mutex.synchronize { successes.dup }
282
+ checkin.call(group2.size) # should wake up everyone remaining
283
+
284
+ group1.each(&:join); group2.each(&:join)
285
+
286
+ assert_equal (1..group1.size).to_a, winners.sort
287
+
288
+ if errors.any?
289
+ raise errors.first
290
+ end
291
+ end
292
+
293
+ def test_automatic_reconnect=
294
+ assert pool.automatic_reconnect
295
+ assert pool.connection
296
+
297
+ pool.disconnect!
298
+ assert pool.connection
299
+
300
+ pool.disconnect!
301
+ pool.automatic_reconnect = false
302
+
303
+ assert_raise(ConnectionNotEstablished) do
304
+ pool.connection
305
+ end
306
+
307
+ assert_raise(ConnectionNotEstablished) do
308
+ pool.with_connection
309
+ end
310
+ end
311
+
312
+ def test_pool_sets_connection_visitor
313
+ assert pool.connection.visitor.is_a?(Arel::Visitors::ToSql)
314
+ end
315
+
316
+ # make sure exceptions are thrown when establish_connection
317
+ # is called with an anonymous class
318
+ #def test_anonymous_class_exception
319
+ #anonymous = Class.new(ActiveRecord::Base)
320
+ #handler = ActiveRecord::Base.connection_handler
321
+
322
+ #assert_raises(RuntimeError) {
323
+ # handler.establish_connection anonymous, nil
324
+ #}
325
+ # assert_raises { handler.establish_connection anonymous, nil }
326
+ #end
327
+
328
+ private
329
+
330
+ def active_connections(pool = self.pool)
331
+ pool.connections.find_all(&:in_use?)
332
+ end
333
+
334
+ end
335
+ end
336
+ end
@@ -0,0 +1,304 @@
1
+ require 'bundler/setup'
2
+
3
+ # ENV variables supported :
4
+ # - AR_POOL_SIZE=42 for testing out with higher pools
5
+ # - AR_POOL_CHECKOUT_TIMEOUT=2 for changing the pool connection acquire timeout
6
+ # - AR_POOL_PREFILL=10 how many connections to "prefill" the pool with on start
7
+ # - AR_POOL_SHARED=true/false or size/percentage (0.0-1.0) connection sharing
8
+ # - AR_PREPARED_STATEMENTS=true/false
9
+
10
+ connect_timeout = 5
11
+ checkout_timeout = 2.5 # default is to wait 5 seconds when all connections used
12
+ pool_size = 10
13
+ #pool_prefill = 10 # (or simply true/false) how many connections to initialize
14
+ shared_pool = 0.75 # shareable pool true/false or size (integer or percentage)
15
+ ENV['DB_POOL_SHARED'] ||= 0.5.to_s
16
+
17
+ # NOTE: max concurrent threads handled before explicit locking with shared :
18
+ # pool_size - ( pool_size * shared_pool ) * MAX_THREAD_SHARING (5)
19
+ # e.g. 40 - ( 40 * 0.75 ) * 5 = 160
20
+
21
+ require 'active_record'
22
+
23
+ require 'logger'
24
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
25
+
26
+ #shared_pool = ENV['AR_POOL_SHARED'] ? # with AR_POOL_SHARED=true use default
27
+ # ( ENV['AR_POOL_SHARED'] == 'true' ? shared_pool : ENV['AR_POOL_SHARED'] ) :
28
+ # shared_pool
29
+ #if shared_pool && shared_pool.to_s != 'false'
30
+ # shared_pool = Float(shared_pool) rescue nil # to number if number
31
+ # require 'active_record/connection_adapters/shareable_connection_pool'
32
+ # # ActiveRecord::Base.default_connection_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
33
+ # pool_class = ActiveRecord::ConnectionAdapters::ShareableConnectionPool
34
+ # ActiveRecord::ConnectionAdapters::ConnectionHandler.connection_pool_class = pool_class
35
+ #end
36
+
37
+ config = { :'adapter' => ENV['AR_ADAPTER'] || 'sqlite3' }
38
+ config[:'username'] = ENV['AR_USERNAME'] if ENV['AR_USERNAME']
39
+ config[:'password'] = ENV['AR_PASSWORD'] if ENV['AR_PASSWORD']
40
+ if url = ENV['AR_URL'] || ENV['JDBC_URL']
41
+ config[:'url'] = url
42
+ else
43
+ config[:'database'] = ENV['AR_DATABASE'] || 'ar_basin'
44
+ end
45
+
46
+ config[:'pool'] = ENV['AR_POOL_SIZE'] ? ENV['AR_POOL_SIZE'].to_i : pool_size
47
+ config[:'shared_pool'] = ENV['AR_POOL_SHARED'] || shared_pool
48
+ config[:'connect_timeout'] = connect_timeout
49
+ prepared_statements = ENV['AR_PREPARED_STATEMENTS'] # || true
50
+ config[:'prepared_statements'] = prepared_statements if prepared_statements
51
+ #jdbc_properties = { 'logUnclosedConnections' => true, 'loginTimeout' => 5 }
52
+ #config['properties'] = jdbc_properties
53
+
54
+ checkout_timeout = ENV['AR_POOL_CHECKOUT_TIMEOUT'] || checkout_timeout
55
+ config[:'checkout_timeout'] = checkout_timeout.to_f if checkout_timeout
56
+
57
+ AR_CONFIG = config
58
+
59
+ pool_prefill = ENV['AR_POOL_PREFILL']
60
+ pool_prefill = config[:'pool'] if pool_prefill.to_s == 'true'
61
+ pool_prefill = 0 if pool_prefill.to_s == 'false'
62
+ config[:'pool_prefill'] = pool_prefill.to_i if pool_prefill # NOTE: not yet used
63
+
64
+ unless ENV['Rake'] == 'true'
65
+ gem 'test-unit'
66
+ require 'test/unit'
67
+
68
+ ActiveRecord::Base.logger.debug "database configuration: #{config.inspect}"
69
+ end
70
+
71
+ #if ( pool_prefill = ( pool_prefill || 10 ).to_i ) > 0
72
+ # pool_prefill = pool.size if pool_prefill > pool.size
73
+ #
74
+ # conns = []; start = Time.now
75
+ # ActiveRecord::Base.logger.info "pre-filling pool with #{pool_prefill}/#{pool.size} connections"
76
+ # begin
77
+ # pool_prefill.times { conns << pool.checkout }
78
+ # ensure
79
+ # conns.each { |conn| pool.checkin(conn) }
80
+ # end
81
+ #
82
+ # # NOTE: for 50 connections ~ 2 seconds but in real time this might get
83
+ # # slower due synchronization - more threads using the pool = more time
84
+ # ActiveRecord::Base.logger.debug "pre-filling connection pool took #{Time.now - start}"
85
+ #end
86
+ #
87
+ #if ENV['AR_POOL_BENCHMARK'] && ENV['AR_POOL_BENCHMARK'].to_s != 'false'
88
+ # require 'active_record/connection_adapters/pool_benchmark'
89
+ # pool.extend ActiveRecord::ConnectionAdapters::PoolBenchmark
90
+ #end
91
+
92
+ require 'active_record/bogacs'
93
+
94
+ $LOAD_PATH << File.expand_path(File.dirname(__FILE__))
95
+
96
+ module ActiveRecord
97
+ module ConnectionAdapters
98
+ ConnectionPool.class_eval do
99
+ attr_reader :size # only since 4.x
100
+ attr_reader :available # the custom Queue
101
+ attr_reader :reserved_connections # Thread-Cache
102
+ # attr_reader :connections # created connections
103
+ end
104
+ autoload :ConnectionPoolTestMethods, 'active_record/connection_pool_test_methods'
105
+ end
106
+ end
107
+
108
+ module ActiveRecord
109
+ module Bogacs
110
+
111
+ module TestHelper
112
+
113
+ def _test_name
114
+ @method_name # @__name__ on mini-test
115
+ end
116
+ private :_test_name
117
+
118
+ def with_connection(config)
119
+ ActiveRecord::Base.establish_connection config
120
+ yield ActiveRecord::Base.connection
121
+ ensure
122
+ ActiveRecord::Base.connection.disconnect!
123
+ end
124
+
125
+ def with_connection_removed
126
+ connection = ActiveRecord::Base.remove_connection
127
+ begin
128
+ yield
129
+ ensure
130
+ ActiveRecord::Base.establish_connection connection
131
+ end
132
+ end
133
+
134
+ # def with_connection_removed
135
+ # configurations = ActiveRecord::Base.configurations
136
+ # connection_config = current_connection_config
137
+ # # ActiveRecord::Base.connection.disconnect!
138
+ # ActiveRecord::Base.remove_connection
139
+ # begin
140
+ # yield connection_config.dup
141
+ # ensure
142
+ # # ActiveRecord::Base.connection.disconnect!
143
+ # ActiveRecord::Base.remove_connection
144
+ # ActiveRecord::Base.configurations = configurations
145
+ # ActiveRecord::Base.establish_connection connection_config
146
+ # end
147
+ # end
148
+
149
+ module_function
150
+
151
+ def current_connection_config
152
+ if ActiveRecord::Base.respond_to?(:connection_config)
153
+ ActiveRecord::Base.connection_config
154
+ else
155
+ ActiveRecord::Base.connection_pool.spec.config
156
+ end
157
+ end
158
+
159
+ def silence_deprecations(&block)
160
+ ActiveSupport::Deprecation.silence(&block)
161
+ end
162
+
163
+ # def disable_logger(connection, &block)
164
+ # raise "need a block" unless block_given?
165
+ # return disable_connection_logger(connection, &block) if connection
166
+ # logger = ActiveRecord::Base.logger
167
+ # begin
168
+ # ActiveRecord::Base.logger = nil
169
+ # yield
170
+ # ensure
171
+ # ActiveRecord::Base.logger = logger
172
+ # end
173
+ # end
174
+ #
175
+ # def disable_connection_logger(connection)
176
+ # logger = connection.send(:instance_variable_get, :@logger)
177
+ # begin
178
+ # connection.send(:instance_variable_set, :@logger, nil)
179
+ # yield
180
+ # ensure
181
+ # connection.send(:instance_variable_set, :@logger, logger)
182
+ # end
183
+ # end
184
+
185
+ protected
186
+
187
+ @@sample_query = ENV['SAMPLE_QUERY']
188
+ @@test_query = ENV['TEST_QUERY'] || @@sample_query
189
+
190
+ def sample_query
191
+ @@sample_query ||= begin
192
+ case current_connection_config[:adapter]
193
+ when /mysql/ then 'SHOW VARIABLES LIKE "%version%"'
194
+ when /postgresql/ then 'SELECT version()'
195
+ else 'SELECT 42'
196
+ end
197
+ end
198
+ end
199
+
200
+ def test_query
201
+ @@test_query ||= begin
202
+ case current_connection_config[:adapter]
203
+ when /mysql/ then 'SELECT DATABASE() FROM DUAL'
204
+ when /postgresql/ then 'SELECT current_database()'
205
+ else sample_query
206
+ end
207
+ end
208
+ end
209
+
210
+ end
211
+
212
+ module JndiTestHelper
213
+
214
+ def setup_jdbc_context
215
+ load 'test/jars/tomcat-juli.jar'
216
+ load 'test/jars/tomcat-catalina.jar'
217
+
218
+ java.lang.System.set_property(
219
+ javax.naming.Context::INITIAL_CONTEXT_FACTORY,
220
+ 'org.apache.naming.java.javaURLContextFactory'
221
+ )
222
+ java.lang.System.set_property(
223
+ javax.naming.Context::URL_PKG_PREFIXES,
224
+ 'org.apache.naming'
225
+ )
226
+
227
+ init_context = javax.naming.InitialContext.new
228
+ begin
229
+ init_context.create_subcontext 'jdbc'
230
+ rescue javax.naming.NameAlreadyBoundException
231
+ end
232
+ end
233
+
234
+ def init_tomcat_jdbc_data_source(ar_jdbc_config = AR_CONFIG)
235
+ load 'test/jars/tomcat-jdbc.jar'
236
+
237
+ data_source = org.apache.tomcat.jdbc.pool.DataSource.new
238
+ configure_dbcp_data_source_attributes(data_source, ar_jdbc_config)
239
+
240
+ data_source.setJmxEnabled false
241
+
242
+ data_source
243
+ end
244
+
245
+ def configure_dbcp_data_source_attributes(data_source, ar_jdbc_config)
246
+ unless driver = ar_jdbc_config[:driver]
247
+ jdbc_driver_module.load_driver
248
+ driver = jdbc_driver_module.driver_name
249
+ end
250
+
251
+ data_source.setDriverClassName driver
252
+ data_source.setUrl ar_jdbc_config[:url]
253
+ data_source.setUsername ar_jdbc_config[:username] if ar_jdbc_config[:username]
254
+ data_source.setPassword ar_jdbc_config[:password] if ar_jdbc_config[:password]
255
+ if ar_jdbc_config[:properties]
256
+ properties = java.util.Properties.new
257
+ properties.putAll ar_jdbc_config[:properties]
258
+ data_source.setDbProperties properties
259
+ end
260
+ # JDBC pool tunings (some mapped from AR configuration) :
261
+ if ar_jdbc_config[:pool] # default is 100
262
+ data_source.setMaxActive ar_jdbc_config[:pool]
263
+ if prefill = ar_jdbc_config[:pool_prefill]
264
+ data_source.setInitialSize prefill
265
+ end
266
+ if data_source.max_active < data_source.max_idle
267
+ data_source.setMaxIdle data_source.max_active
268
+ end
269
+ end
270
+ max_wait = ar_jdbc_config[:checkout_timeout] || 5
271
+ data_source.setMaxWait max_wait * 1000 # default is 30s
272
+
273
+ data_source.setTestWhileIdle false # default
274
+
275
+ #data_source.setRemoveAbandoned false
276
+ #data_source.setLogAbandoned true
277
+ end
278
+ private :configure_dbcp_data_source_attributes
279
+
280
+ def bind_data_source(data_source, jndi_name = jndi_config[:jndi])
281
+ load_driver
282
+ javax.naming.InitialContext.new.bind jndi_name, data_source
283
+ end
284
+
285
+ def load_driver
286
+ jdbc_driver_module.load_driver
287
+ end
288
+
289
+ def jdbc_driver_module
290
+ driver = jndi_config[:adapter]
291
+ driver = 'postgres' if driver == 'postgresql'
292
+ require "jdbc/#{driver}"
293
+ ::Jdbc.const_get ::Jdbc.constants.first
294
+ end
295
+
296
+ def jndi_config
297
+ @jndi_config ||= { :adapter => AR_CONFIG[:adapter], :jndi => jndi_name }
298
+ end
299
+
300
+ def jndi_name; 'jdbc/TestDB' end
301
+
302
+ end
303
+ end
304
+ end