activerecord-bogacs 0.5.1 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,75 +2,11 @@ module ActiveRecord
2
2
  module Bogacs
3
3
  module ThreadSafe
4
4
 
5
- #def self.load_map
6
- begin
7
- require 'concurrent/map.rb'
8
- rescue LoadError => e
9
- begin
10
- require 'thread_safe'
11
- rescue
12
- warn "activerecord-bogacs needs gem 'concurrent-ruby', '~> 1.0' (or the old 'thread_safe' gem) " <<
13
- "please install or add it to your Gemfile"
14
- raise e
15
- end
16
- end
17
- #end
18
-
19
- #load_map # always pre-load thread_safe
20
-
21
- if defined? ::Concurrent::Map
22
- Map = ::Concurrent::Map
23
- else
24
- Map = ::ThreadSafe::Cache
25
- end
5
+ require 'concurrent/map.rb'
6
+ Map = ::Concurrent::Map
26
7
 
27
8
  autoload :Synchronized, 'active_record/bogacs/thread_safe/synchronized'
28
9
 
29
- def self.load_atomic_reference
30
- return const_get :AtomicReference if const_defined? :AtomicReference
31
-
32
- begin
33
- require 'concurrent/atomic/atomic_reference.rb'
34
- rescue LoadError => e
35
- begin
36
- require 'atomic'
37
- rescue LoadError
38
- warn "shareable pool needs gem 'concurrent-ruby', '>= 0.9.1' (or the old 'atomic' gem) " <<
39
- "please install or add it to your Gemfile"
40
- raise e
41
- end
42
- end
43
-
44
- if defined? ::Concurrent::AtomicReference
45
- const_set :AtomicReference, ::Concurrent::AtomicReference
46
- else
47
- const_set :AtomicReference, ::Atomic
48
- end
49
- end
50
-
51
- def self.load_cheap_lockable(required = true)
52
- return const_get :CheapLockable if const_defined? :CheapLockable
53
-
54
- begin
55
- require 'concurrent/thread_safe/util/cheap_lockable.rb'
56
- rescue LoadError => e
57
- begin
58
- require 'thread_safe'
59
- rescue
60
- return nil unless required
61
- warn "activerecord-bogacs needs gem 'concurrent-ruby', '~> 1.0' (or the old 'thread_safe' gem) " <<
62
- "please install or add it to your Gemfile"
63
- raise e
64
- end
65
- end
66
-
67
- if defined? ::Concurrent::ThreadSafe::Util::CheapLockable
68
- const_set :CheapLockable, ::Concurrent::ThreadSafe::Util::CheapLockable
69
- else
70
- const_set :CheapLockable, ::ThreadSafe::Util::CheapLockable
71
- end
72
- end
73
-
74
10
  end
75
11
  end
76
- end
12
+ end
@@ -1,10 +1,5 @@
1
- begin
2
- require 'concurrent/executors'
3
- require 'concurrent/timer_task'
4
- rescue LoadError => e
5
- warn "activerecord-bogacs' validator feature needs gem 'concurrent-ruby', please install or add it to your Gemfile"
6
- raise e
7
- end
1
+ require 'concurrent/executors'
2
+ require 'concurrent/timer_task'
8
3
 
9
4
  require 'active_record/connection_adapters/adapter_compat'
10
5
  require 'active_record/bogacs/thread_safe'
@@ -81,20 +76,19 @@ module ActiveRecord
81
76
  connections = pool.connections.dup
82
77
  connections.map! do |conn|
83
78
  if conn
84
- owner = conn.owner
85
- if conn.in_use? # we'll do a pool.sync-ed check ...
86
- if owner && ! owner.alive? # stale-conn (reaping)
79
+ if owner = conn.owner
80
+ if owner.alive?
81
+ nil # owner.alive? ... do not touch
82
+ else # stale-conn (reaping)
87
83
  pool.remove conn # remove is synchronized
88
84
  conn.disconnect! rescue nil
89
85
  nil
90
- elsif ! owner # NOTE: this is likely a nasty bug
91
- logger && logger.warn("[validator] found in-use connection ##{conn.object_id} without owner - removing from pool")
92
- pool.remove_without_owner conn # synchronized
93
- conn.disconnect! rescue nil
94
- nil
95
- else
96
- nil # owner.alive? ... do not touch
97
86
  end
87
+ elsif conn.in_use? # no owner? (likely a nasty bug)
88
+ logger && logger.warn("[validator] found in-use connection ##{conn.object_id} without owner - removing from pool")
89
+ pool.remove_without_owner conn # synchronized
90
+ conn.disconnect! rescue nil
91
+ nil
98
92
  else
99
93
  conn # conn not in-use - candidate for validation
100
94
  end
@@ -116,7 +110,7 @@ module ActiveRecord
116
110
  logger && logger.info("[validator] connection ##{conn.object_id} failed to validate: #{e.inspect}")
117
111
  end
118
112
 
119
- # TODO support last_use - only validate if certain amount since use passed
113
+ # TODO support seconds_idle - only validate if certain amount since use passed
120
114
 
121
115
  logger && logger.debug("[validator] found non-active connection ##{conn.object_id} - removing from pool")
122
116
  pool.remove_without_owner conn # not active - remove
@@ -170,7 +164,8 @@ module ActiveRecord
170
164
 
171
165
  def release_without_owner(conn)
172
166
  if owner_id = cached_conn_owner_id(conn)
173
- thread_cached_conns.delete owner_id; return true
167
+ thread_cached_conns.delete owner_id
168
+ return true
174
169
  end
175
170
  end
176
171
 
@@ -179,4 +174,4 @@ module ActiveRecord
179
174
  end
180
175
 
181
176
  end
182
- end
177
+ end
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module Bogacs
3
- VERSION = '0.5.1'
3
+ VERSION = '0.7.1'
4
4
  end
5
5
  end
@@ -1,6 +1,10 @@
1
- require 'active_record'
2
1
 
3
2
  require 'active_record/bogacs/version'
4
3
  require 'active_record/bogacs/autoload'
5
4
 
6
- require 'active_record/connection_adapters/pool_class.rb'
5
+ if defined?(Rails::Railtie)
6
+ require 'active_record/bogacs/railtie'
7
+ else
8
+ require 'active_record'
9
+ require 'active_record/connection_adapters/pool_class'
10
+ end
@@ -1,4 +1,5 @@
1
1
  require 'active_record/connection_adapters/abstract_adapter'
2
+ require 'concurrent/utility/monotonic_time.rb'
2
3
 
3
4
  module ActiveRecord
4
5
  module ConnectionAdapters
@@ -8,38 +9,59 @@ module ActiveRecord
8
9
 
9
10
  if method_defined? :owner # >= 4.2
10
11
 
11
- attr_reader :last_use
12
+ if ActiveRecord::VERSION::STRING > '5.2'
12
13
 
13
- if ActiveRecord::VERSION::MAJOR > 4
14
+ # THIS IS OUR COMPATIBILITY BASE-LINE
14
15
 
15
- # @private added @last_use
16
+ elsif ActiveRecord::VERSION::MAJOR > 4
17
+
18
+ # this method must only be called while holding connection pool's mutex
16
19
  def lease
17
20
  if in_use?
18
- msg = 'Cannot lease connection, '
21
+ msg = "Cannot lease connection, ".dup
19
22
  if @owner == Thread.current
20
- msg += 'it is already leased by the current thread.'
23
+ msg << "it is already leased by the current thread."
21
24
  else
22
- msg += "it is already in use by a different thread: #{@owner}. Current thread: #{Thread.current}."
25
+ msg << "it is already in use by a different thread: #{@owner}. " \
26
+ "Current thread: #{Thread.current}."
23
27
  end
24
28
  raise ActiveRecordError, msg
25
29
  end
26
30
 
27
- @owner = Thread.current; @last_use = Time.now
31
+ @owner = Thread.current
28
32
  end
29
33
 
30
- else
31
-
32
- # @private removed synchronization + added @last_use
33
- def lease
34
+ # this method must only be called while holding connection pool's mutex
35
+ # @private AR 5.2
36
+ def expire
34
37
  if in_use?
35
- if @owner == Thread.current
36
- # NOTE: could do a warning if 4.2.x cases do not end up here ...
38
+ if @owner != Thread.current
39
+ raise ActiveRecordError, "Cannot expire connection, " \
40
+ "it is owned by a different thread: #{@owner}. " \
41
+ "Current thread: #{Thread.current}."
37
42
  end
43
+
44
+ @idle_since = ::Concurrent.monotonic_time
45
+ @owner = nil
38
46
  else
39
- @owner = Thread.current; @last_use = Time.now
47
+ raise ActiveRecordError, "Cannot expire connection, it is not currently leased."
48
+ end
49
+ end
50
+
51
+ else
52
+
53
+ # @private removed synchronization
54
+ def lease
55
+ unless in_use?
56
+ @owner = Thread.current
40
57
  end
41
58
  end
42
59
 
60
+ # @private added @idle_since
61
+ def expire
62
+ @owner = nil; @idle_since = ::Concurrent.monotonic_time
63
+ end
64
+
43
65
  end
44
66
 
45
67
  else
@@ -69,7 +91,7 @@ module ActiveRecord
69
91
  end
70
92
 
71
93
  def expire
72
- @in_use = false; @owner = nil
94
+ @in_use = false; @owner = nil; @idle_since = ::Concurrent.monotonic_time
73
95
  end
74
96
 
75
97
  else
@@ -83,13 +105,37 @@ module ActiveRecord
83
105
  end
84
106
 
85
107
  def expire
86
- @owner = nil
108
+ @owner = nil; @idle_since = ::Concurrent.monotonic_time
87
109
  end
88
110
 
89
111
  end
90
112
 
91
113
  end
92
114
 
115
+ # this method must only be called while holding connection pool's mutex (and a desire for segfaults)
116
+ def steal! # :nodoc:
117
+ if in_use?
118
+ if @owner != Thread.current
119
+ pool.send :release, self, @owner # release exists in both default/false pool
120
+
121
+ @owner = Thread.current
122
+ end
123
+ else
124
+ raise ActiveRecordError, "Cannot steal connection, it is not currently leased."
125
+ end
126
+ end
127
+
128
+ def discard!
129
+ # no-op
130
+ end unless method_defined? :discard! # >= 5.2
131
+
132
+ # Seconds since this connection was returned to the pool
133
+ def seconds_idle # :nodoc:
134
+ return 0 if in_use?
135
+ time = ::Concurrent.monotonic_time
136
+ time - ( @idle_since || time )
137
+ end unless method_defined? :seconds_idle # >= 5.2
138
+
93
139
  end
94
140
  end
95
- end
141
+ end
@@ -10,14 +10,45 @@ require 'active_record/connection_adapters/abstract/connection_pool'
10
10
  module ActiveRecord
11
11
  module ConnectionAdapters
12
12
  # @private there's no other way to change the pool class to use but to patch :(
13
- ConnectionHandler.class_eval do
13
+ class ConnectionHandler
14
14
 
15
15
  @@connection_pool_class = ConnectionAdapters::ConnectionPool
16
16
 
17
17
  def connection_pool_class; @@connection_pool_class end
18
18
  def self.connection_pool_class=(klass); @@connection_pool_class = klass end
19
19
 
20
- if ActiveRecord::VERSION::MAJOR > 3 # 4.x
20
+ if ActiveRecord::VERSION::MAJOR > 4 && # 5.1 - 5.2
21
+ !(ActiveRecord::VERSION::MAJOR == 5 && ActiveRecord::VERSION::MINOR == 0)
22
+
23
+ def establish_connection(config)
24
+ resolver = ConnectionSpecification::Resolver.new(Base.configurations)
25
+ spec = resolver.spec(config)
26
+
27
+ remove_connection(spec.name)
28
+
29
+ message_bus = ActiveSupport::Notifications.instrumenter
30
+ payload = {
31
+ connection_id: object_id
32
+ }
33
+ if spec
34
+ payload[:spec_name] = spec.name
35
+ payload[:config] = spec.config
36
+ end
37
+
38
+ message_bus.instrument("!connection.active_record", payload) do
39
+ owner_to_pool[spec.name] = connection_pool_class.new(spec) # changed
40
+ end
41
+
42
+ owner_to_pool[spec.name]
43
+ end
44
+
45
+ elsif ActiveRecord::VERSION::MAJOR == 5 && ActiveRecord::VERSION::MINOR == 0
46
+
47
+ def establish_connection(spec)
48
+ owner_to_pool[spec.name] = connection_pool_class.new(spec)
49
+ end
50
+
51
+ elsif ActiveRecord::VERSION::MAJOR > 3 # 4.x
21
52
 
22
53
  def establish_connection(owner, spec)
23
54
  @class_to_pool.clear
@@ -0,0 +1 @@
1
+ require 'active_record/bogacs' # auto-loading with gem '...' declarations
@@ -100,6 +100,20 @@ module ActiveRecord
100
100
  assert ActiveRecord::Base.connection.exec_query('SELECT 42')
101
101
  end
102
102
 
103
+ # @override
104
+ def test_checkout_after_close
105
+ connection = pool.connection
106
+ assert connection.in_use?
107
+ assert_equal connection.object_id, pool.connection.object_id
108
+
109
+ connection.close # pool.checkin conn
110
+ assert ! connection.in_use?
111
+
112
+ # NOTE: we do not care for connection re-use - it's okay to instantiate a new one
113
+ #assert_equal connection.object_id, pool.connection.object_id
114
+ assert pool.connection.in_use?
115
+ end
116
+
103
117
  # @override
104
118
  def test_remove_connection
105
119
  conn = pool.checkout
@@ -115,12 +129,13 @@ module ActiveRecord
115
129
 
116
130
  # @override
117
131
  def test_full_pool_exception
132
+ ActiveRecord::Base.connection_pool.disconnect! # start clean - with no connections
118
133
  # ~ pool_size.times { pool.checkout }
119
134
  threads_ready = Queue.new; threads_block = Atomic.new(0); threads = []
120
135
  max_pool_size.times do |i|
121
136
  threads << Thread.new do
122
137
  begin
123
- conn = ActiveRecord::Base.connection
138
+ conn = ActiveRecord::Base.connection.tap { |conn| conn.tables }
124
139
  threads_block.update { |v| v + 1 }
125
140
  threads_ready << i
126
141
  while threads_block.value != -1 # await
@@ -135,8 +150,26 @@ module ActiveRecord
135
150
  end
136
151
  max_pool_size.times { threads_ready.pop } # awaits
137
152
 
138
- assert_raise(ConnectionTimeoutError) do
139
- ActiveRecord::Base.connection # ~ pool.checkout
153
+ assert_equal max_pool_size, ActiveRecord::Base.connection_pool.connections.size
154
+
155
+ threads.each { |thread| assert thread.alive? }
156
+
157
+ #puts "data_source.active: #{data_source.getActive} - #{data_source.getNumActive}"
158
+
159
+ begin
160
+ # NOTE: in AR 4.x and before AR::Base.connection was enough
161
+ # but due the lazy nature of connection init in AR-JDBC 5X we need to force a connection
162
+ ActiveRecord::Base.connection.tap { |conn| conn.tables } # ~ pool.checkout
163
+ rescue ActiveRecordError, java.util.NoSuchElementException => e
164
+ # DBCP: Java::JavaUtil::NoSuchElementException: Timeout waiting for idle object
165
+ if ActiveRecord::VERSION::STRING < '5.2'
166
+ assert_instance_of ConnectionTimeoutError, e
167
+ else
168
+ # TODO unfortunately we can not map a TimeoutError and will end up with a StatementInvalid
169
+ # from the tables call :
170
+ # assert_instance_of ConnectionTimeoutError, e
171
+ assert ActiveRecord::Base.connection_pool.send(:timeout_error?, e)
172
+ end
140
173
  end
141
174
 
142
175
  ensure
@@ -151,6 +184,7 @@ module ActiveRecord
151
184
  t1 = Thread.new do
152
185
  begin
153
186
  conn = ActiveRecord::Base.connection
187
+ conn.tables # force connection
154
188
  t1_ready.push(conn)
155
189
  t1_block.pop # await
156
190
  rescue => e
@@ -165,6 +199,7 @@ module ActiveRecord
165
199
  threads << Thread.new do
166
200
  begin
167
201
  conn = ActiveRecord::Base.connection
202
+ conn.tables # force connection
168
203
  threads_block.update { |v| v + 1 }
169
204
  threads_ready << i
170
205
  while threads_block.value != -1 # await
@@ -186,7 +221,7 @@ module ActiveRecord
186
221
 
187
222
  t2 = Thread.new do
188
223
  begin
189
- ActiveRecord::Base.connection
224
+ ActiveRecord::Base.connection.tap { |conn| conn.tables }
190
225
  rescue => e
191
226
  puts "t2 thread failed: #{e.inspect}"
192
227
  end
@@ -214,64 +249,61 @@ module ActiveRecord
214
249
  threads && threads.each(&:join)
215
250
  end
216
251
 
217
- protected
252
+ # @override
253
+ def test_pooled_connection_checkin_two
254
+ checkout_checkin_connections_loop 2, 3
218
255
 
219
- def unwrap_connection(connection)
220
- connection.jdbc_connection(true)
256
+ assert_equal 3, @connection_count
257
+ assert_equal 0, @timed_out
258
+ assert_equal 1, @pool.connections.size
221
259
  end
222
260
 
223
- end
261
+ protected
224
262
 
225
- class ConnectionPoolWrappingTomcatJdbcDataSourceTest < TestBase
226
- include ConnectionPoolWrappingDataSourceTestMethods
263
+ def unwrap_connection(connection)
264
+ # NOTE: AR-JDBC 5X messed up jdbc_connection(true) - throws a NPE, work-around:
265
+ connection.tables # force underlying connection into an initialized state ^^^
227
266
 
228
- def self.build_data_source(config)
229
- build_tomcat_jdbc_data_source(config)
267
+ jdbc_connection = connection.jdbc_connection(true)
268
+ begin
269
+ jdbc_connection.delegate
270
+ rescue NoMethodError
271
+ jdbc_connection
272
+ end
230
273
  end
231
274
 
232
- def self.jndi_name; 'jdbc/TestTomcatJdbcDB' end
233
-
234
- def self.close_data_source
235
- @@data_source.send(:close, true) if @@data_source
275
+ def change_pool_size(size)
276
+ # noop - @pool.instance_variable_set(:@size, size)
236
277
  end
237
278
 
238
- def max_pool_size; @@data_source.max_active end
239
-
240
- def teardown
241
- self.class.close_data_source
279
+ def change_pool_checkout_timeout(timeout)
280
+ # noop - @pool.instance_variable_set(:@checkout_timeout, timeout)
242
281
  end
243
282
 
244
283
  end
245
284
 
246
- class ConnectionPoolWrappingTomcatDbcpDataSourceTest < TestBase
285
+ class ConnectionPoolWrappingTomcatJdbcDataSourceTest < TestBase
247
286
  include ConnectionPoolWrappingDataSourceTestMethods
248
287
 
249
288
  def self.build_data_source(config)
250
- build_tomcat_dbcp_data_source(config)
289
+ build_tomcat_jdbc_data_source(config)
251
290
  end
252
291
 
253
- def self.jndi_name; 'jdbc/TestTomcatDbcpDB' end
292
+ def self.jndi_name; 'jdbc/TestTomcatJdbcDB' end
254
293
 
255
294
  def self.close_data_source
256
- @@data_source.close if @@data_source
295
+ @@data_source.send(:close, true) if @@data_source
257
296
  end
258
297
 
259
298
  def max_pool_size; @@data_source.max_active end
260
299
 
261
300
  def teardown
262
- self.class.establish_jndi_connection # for next test
263
- end
264
-
265
- protected
266
-
267
- def unwrap_connection(connection)
268
- connection = connection.jdbc_connection(true)
269
- connection.delegate
301
+ self.class.close_data_source
270
302
  end
271
303
 
272
304
  end
273
305
 
274
- class ConnectionPoolWrappingDbcpDataSourceTest < TestBase
306
+ class ConnectionPoolWrappingTomcatDbcpDataSourceTest < TestBase
275
307
  include ConnectionPoolWrappingDataSourceTestMethods
276
308
 
277
309
  def self.build_data_source(config)
@@ -292,50 +324,6 @@ module ActiveRecord
292
324
 
293
325
  protected
294
326
 
295
- def unwrap_connection(connection)
296
- connection = connection.jdbc_connection(true)
297
- connection.delegate
298
- end
299
-
300
- end
301
-
302
- class ConnectionPoolWrappingC3P0DataSourceTest < TestBase
303
- include ConnectionPoolWrappingDataSourceTestMethods
304
-
305
- def self.build_data_source(config)
306
- build_c3p0_data_source(config)
307
- end
308
-
309
- def self.jndi_config
310
- config = super
311
- config[:connection_alive_sql] = 'SELECT 1' if old_c3p0?
312
- config
313
- end
314
-
315
- def self.old_c3p0?
316
- if c3p0_jar = $CLASSPATH.find { |jar| jar =~ /c3p0/ }
317
- if match = File.basename(c3p0_jar).match(/c3p0\-(.*).jar/)
318
- return true if match[1] <= '0.9.2.1'
319
- end
320
- return false
321
- end
322
- nil
323
- end
324
-
325
- def test_full_pool_blocks
326
- return if self.class.old_c3p0?
327
- super
328
- end
329
-
330
- def self.jndi_name; 'jdbc/TestC3P0DB' end
331
-
332
- def max_pool_size; @@data_source.max_pool_size end
333
-
334
- def teardown
335
- # self.class.close_data_source # @@data_source = nil
336
- self.class.establish_jndi_connection # for next test
337
- end
338
-
339
327
  end
340
328
 
341
329
  class ConnectionPoolWrappingHikariDataSourceTest < TestBase
@@ -368,4 +356,4 @@ module ActiveRecord
368
356
 
369
357
  end
370
358
  end
371
- end
359
+ end
@@ -123,7 +123,8 @@ module ActiveRecord
123
123
 
124
124
  end
125
125
 
126
- class CustomAPITest < TestBase
126
+ # TODO: ShareablePool is pretty much broken since 0.7 :
127
+ class CustomAPITest #< TestBase
127
128
 
128
129
  def setup
129
130
  connection_pool.disconnect!
@@ -170,7 +170,7 @@ module ActiveRecord
170
170
  assert shared_connection?(conn)
171
171
  conn
172
172
  end
173
-
173
+
174
174
  assert_equal 5, shared_conns.uniq.size
175
175
 
176
176
  # still one left for normal connections :
@@ -243,7 +243,7 @@ module ActiveRecord
243
243
  sleep(0.005)
244
244
 
245
245
  with_shared_connection do |conn2|
246
- assert conn1 == conn2
246
+ assert conn1.equal?(conn2)
247
247
 
248
248
  # we can not do any-more here without a timeout :
249
249
  failed = nil
@@ -24,7 +24,7 @@ module ActiveRecord
24
24
  alias_method :connections, :initialized_connections
25
25
 
26
26
  def reserved_connections
27
- ActiveRecord::Base.connection_pool.reserved_connections
27
+ connection_pool.instance_variable_get :@thread_cached_conns
28
28
  end
29
29
 
30
30
  def available_connections