activerecord-bogacs 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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