activerecord-bogacs 0.5.1 → 0.7.1

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.
@@ -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