activerecord-bogacs 0.6.0 → 0.7.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.
@@ -1,7 +1,8 @@
1
+ require 'active_record/version'
2
+
1
3
  require 'thread'
2
4
  require 'monitor'
3
-
4
- require 'active_record/version'
5
+ require 'concurrent/atomic/atomic_boolean'
5
6
 
6
7
  require 'active_record/connection_adapters/adapter_compat'
7
8
  require 'active_record/bogacs/pool_support'
@@ -18,13 +19,9 @@ module ActiveRecord
18
19
  # http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/ConnectionPool.html
19
20
  #
20
21
  class DefaultPool
21
- # Threadsafe, fair, FIFO queue. Meant to be used by ConnectionPool
22
- # with which it shares a Monitor. But could be a generic Queue.
23
- #
24
- # The Queue in stdlib's 'thread' could replace this class except
25
- # stdlib's doesn't support waiting with a timeout.
22
+
26
23
  # @private
27
- class Queue
24
+ class Queue # ConnectionLeasingQueue
28
25
  def initialize(lock)
29
26
  @lock = lock
30
27
  @cond = @lock.new_cond
@@ -82,18 +79,25 @@ module ActiveRecord
82
79
  #
83
80
  # @raise [ActiveRecord::ConnectionTimeoutError] if +timeout+ given and no element
84
81
  # becomes available after +timeout+ seconds
85
- def poll(timeout = nil, &block)
86
- synchronize do
87
- if timeout
88
- no_wait_poll || wait_poll(timeout, &block)
89
- else
90
- no_wait_poll
91
- end
92
- end
82
+ def poll(timeout = nil)
83
+ synchronize { internal_poll(timeout) }
93
84
  end
94
85
 
95
86
  private
96
87
 
88
+ def internal_poll(timeout)
89
+ conn = no_wait_poll || (timeout && wait_poll(timeout))
90
+ # Connections must be leased while holding the main pool mutex. This is
91
+ # an internal subclass that also +.leases+ returned connections while
92
+ # still in queue's critical section (queue synchronizes with the same
93
+ # <tt>@lock</tt> as the main pool) so that a returned connection is already
94
+ # leased and there is no need to re-enter synchronized block.
95
+ #
96
+ # NOTE: avoid the need for ConnectionLeasingQueue, since BiasableQueue is not implemented
97
+ conn.lease if conn
98
+ conn
99
+ end
100
+
97
101
  def synchronize(&block)
98
102
  @lock.synchronize(&block)
99
103
  end
@@ -104,21 +108,21 @@ module ActiveRecord
104
108
  end
105
109
 
106
110
  # A thread can remove an element from the queue without
107
- # waiting if an only if the number of currently available
111
+ # waiting if and only if the number of currently available
108
112
  # connections is strictly greater than the number of waiting
109
113
  # threads.
110
114
  def can_remove_no_wait?
111
115
  @queue.size > @num_waiting
112
116
  end
113
117
 
114
- # Removes and returns the head of the queue if possible, or nil.
118
+ # Removes and returns the head of the queue if possible, or +nil+.
115
119
  def remove
116
- @queue.shift
120
+ @queue.pop
117
121
  end
118
122
 
119
123
  # Remove and return the head the queue if the number of
120
124
  # available elements is strictly greater than the number of
121
- # threads currently waiting. Otherwise, return nil.
125
+ # threads currently waiting. Otherwise, return +nil+.
122
126
  def no_wait_poll
123
127
  remove if can_remove_no_wait?
124
128
  end
@@ -126,13 +130,10 @@ module ActiveRecord
126
130
  # Waits on the queue up to +timeout+ seconds, then removes and
127
131
  # returns the head of the queue.
128
132
  def wait_poll(timeout)
129
- t0 = Time.now
130
- elapsed = 0
131
-
132
133
  @num_waiting += 1
133
134
 
134
- yield if block_given?
135
-
135
+ t0 = Time.now
136
+ elapsed = 0
136
137
  while true
137
138
  @cond.wait(timeout - elapsed)
138
139
 
@@ -156,8 +157,8 @@ module ActiveRecord
156
157
  require 'active_record/bogacs/reaper.rb'
157
158
 
158
159
  attr_accessor :automatic_reconnect, :checkout_timeout
159
- attr_reader :spec, :connections, :size, :reaper, :validator
160
- attr_reader :initial_size
160
+ attr_reader :spec, :size, :reaper
161
+ attr_reader :validator, :initial_size
161
162
 
162
163
  # Creates a new `ConnectionPool` object. +spec+ is a ConnectionSpecification
163
164
  # object which describes database connection information (e.g. adapter,
@@ -186,8 +187,12 @@ module ActiveRecord
186
187
 
187
188
  @spec = spec
188
189
 
189
- @checkout_timeout = ( spec.config[:checkout_timeout] ||
190
- spec.config[:wait_timeout] || 5.0 ).to_f # <= 3.2 supports wait_timeout
190
+ @checkout_timeout = ( spec.config[:checkout_timeout] || 5 ).to_f
191
+ if @idle_timeout = spec.config.fetch(:idle_timeout, 300)
192
+ @idle_timeout = @idle_timeout.to_f
193
+ @idle_timeout = nil if @idle_timeout <= 0
194
+ end
195
+
191
196
  @reaper = Reaper.new self, spec.config[:reaping_frequency]
192
197
  @reaping = !! @reaper.run
193
198
 
@@ -202,13 +207,24 @@ module ActiveRecord
202
207
  end
203
208
 
204
209
  # The cache of reserved connections mapped to threads
205
- @reserved_connections = ThreadSafe::Map.new(:initial_capacity => @size)
210
+ @thread_cached_conns = ThreadSafe::Map.new(initial_capacity: @size)
206
211
 
207
212
  @connections = []
208
213
  @automatic_reconnect = true
209
214
 
215
+ # Connection pool allows for concurrent (outside the main +synchronize+ section)
216
+ # establishment of new connections. This variable tracks the number of threads
217
+ # currently in the process of independently establishing connections to the DB.
218
+ @now_connecting = 0
219
+
220
+ @threads_blocking_new_connections = 0 # TODO: dummy for now
221
+
210
222
  @available = Queue.new self
211
223
 
224
+ @lock_thread = false
225
+
226
+ @connected = ::Concurrent::AtomicBoolean.new
227
+
212
228
  initial_size = spec.config[:pool_initial] || 0
213
229
  initial_size = @size if initial_size == true
214
230
  initial_size = (@size * initial_size).to_i if initial_size <= 1.0
@@ -232,12 +248,9 @@ module ActiveRecord
232
248
  #
233
249
  # @return [ActiveRecord::ConnectionAdapters::AbstractAdapter]
234
250
  def connection
235
- connection_id = current_connection_id
236
- unless conn = @reserved_connections.fetch(connection_id, nil)
237
- synchronize do
238
- conn = ( @reserved_connections[connection_id] ||= checkout )
239
- end
240
- end
251
+ connection_id = current_connection_id(current_thread)
252
+ conn = @thread_cached_conns.fetch(connection_id, nil)
253
+ conn = ( @thread_cached_conns[connection_id] ||= checkout ) unless conn
241
254
  conn
242
255
  end
243
256
 
@@ -245,22 +258,16 @@ module ActiveRecord
245
258
  #
246
259
  # @return [true, false]
247
260
  def active_connection?
248
- connection_id = current_connection_id
249
- if conn = @reserved_connections.fetch(connection_id, nil)
250
- !! conn.in_use? # synchronize { conn.in_use? }
251
- else
252
- false
253
- end
261
+ connection_id = current_connection_id(current_thread)
262
+ @thread_cached_conns.fetch(connection_id, nil)
254
263
  end
255
264
 
256
265
  # Signal that the thread is finished with the current connection.
257
266
  # #release_connection releases the connection-thread association
258
267
  # and returns the connection to the pool.
259
- def release_connection(with_id = current_connection_id)
260
- #synchronize do
261
- conn = @reserved_connections.delete(with_id)
262
- checkin conn, true if conn
263
- #end
268
+ def release_connection(owner_thread = Thread.current)
269
+ conn = @thread_cached_conns.delete(current_connection_id(owner_thread))
270
+ checkin conn if conn
264
271
  end
265
272
 
266
273
  # If a connection already exists yield it to the block. If no connection
@@ -270,25 +277,48 @@ module ActiveRecord
270
277
  # @yield [ActiveRecord::ConnectionAdapters::AbstractAdapter]
271
278
  def with_connection
272
279
  connection_id = current_connection_id
273
- fresh_connection = true unless active_connection?
274
- yield connection
280
+ unless conn = @thread_cached_conns[connection_id]
281
+ conn = connection
282
+ fresh_connection = true
283
+ end
284
+ yield conn
275
285
  ensure
276
- release_connection(connection_id) if fresh_connection
286
+ release_connection if fresh_connection
277
287
  end
278
288
 
279
289
  # Returns true if a connection has already been opened.
280
290
  #
281
291
  # @return [true, false]
282
292
  def connected?
283
- @connections.size > 0 # synchronize { @connections.any? }
293
+ @connected.true? # synchronize { @connections.any? }
294
+ end
295
+
296
+ # Returns an array containing the connections currently in the pool.
297
+ # Access to the array does not require synchronization on the pool because
298
+ # the array is newly created and not retained by the pool.
299
+ #
300
+ # However; this method bypasses the ConnectionPool's thread-safe connection
301
+ # access pattern. A returned connection may be owned by another thread,
302
+ # unowned, or by happen-stance owned by the calling thread.
303
+ #
304
+ # Calling methods on a connection without ownership is subject to the
305
+ # thread-safety guarantees of the underlying method. Many of the methods
306
+ # on connection adapter classes are inherently multi-thread unsafe.
307
+ def connections
308
+ synchronize { @connections.dup }
284
309
  end
285
310
 
286
311
  # Disconnects all connections in the pool, and clears the pool.
287
312
  def disconnect!
288
313
  synchronize do
289
- @reserved_connections.clear
314
+ @connected.make_false
315
+
316
+ @thread_cached_conns.clear
290
317
  @connections.each do |conn|
291
- checkin conn
318
+ if conn.in_use?
319
+ conn.steal!
320
+ checkin conn
321
+ end
292
322
  conn.disconnect!
293
323
  end
294
324
  @connections.clear
@@ -296,21 +326,39 @@ module ActiveRecord
296
326
  end
297
327
  end
298
328
 
299
- # Clears the cache which maps classes.
329
+ # Discards all connections in the pool (even if they're currently
330
+ # leased!), along with the pool itself. Any further interaction with the
331
+ # pool (except #spec and #schema_cache) is undefined.
332
+ #
333
+ # See AbstractAdapter#discard!
334
+ def discard! # :nodoc:
335
+ synchronize do
336
+ return if @connections.nil? # already discarded
337
+ @connected.make_false
338
+
339
+ @connections.each do |conn|
340
+ conn.discard!
341
+ end
342
+ @connections = @available = @thread_cached_conns = nil
343
+ end
344
+ end
300
345
  def clear_reloadable_connections!
301
346
  synchronize do
302
- @reserved_connections.clear
347
+ @thread_cached_conns.clear
303
348
  @connections.each do |conn|
304
- checkin conn
349
+ if conn.in_use?
350
+ conn.steal!
351
+ checkin conn
352
+ end
305
353
  conn.disconnect! if conn.requires_reloading?
306
354
  end
307
- @connections.delete_if do |conn|
308
- conn.requires_reloading?
309
- end
355
+ @connections.delete_if(&:requires_reloading?)
310
356
  @available.clear
311
357
  @connections.each do |conn|
312
358
  @available.add conn
313
359
  end
360
+
361
+ @connected.value = @connections.any?
314
362
  end
315
363
  end
316
364
 
@@ -331,11 +379,11 @@ module ActiveRecord
331
379
  # @private AR 3.2 compatibility
332
380
  def clear_stale_cached_connections!
333
381
  keys = Thread.list.find_all { |t| t.alive? }.map(&:object_id)
334
- keys = @reserved_connections.keys - keys
382
+ keys = @thread_cached_conns.keys - keys
335
383
  keys.each do |key|
336
- conn = @reserved_connections[key]
384
+ conn = @thread_cached_conns[key]
337
385
  checkin conn
338
- @reserved_connections.delete(key)
386
+ @thread_cached_conns.delete(key)
339
387
  end
340
388
  end if ActiveRecord::VERSION::MAJOR < 4
341
389
 
@@ -350,28 +398,25 @@ module ActiveRecord
350
398
  # @raise [ActiveRecord::ConnectionTimeoutError] if all connections are leased
351
399
  # and the pool is at capacity (meaning the number of currently leased
352
400
  # connections is greater than or equal to the size limit set)
353
- def checkout
354
- conn = nil
355
- synchronize do
356
- conn = acquire_connection
357
- conn.lease
358
- end
359
- checkout_and_verify(conn)
401
+ def checkout(checkout_timeout = @checkout_timeout)
402
+ checkout_and_verify(acquire_connection(checkout_timeout))
360
403
  end
361
404
 
362
- # Check-in a database connection back into the pool.
405
+ # Check-in a database connection back into the pool, indicating that you
406
+ # no longer need this connection.
363
407
  #
364
- # @param conn [ActiveRecord::ConnectionAdapters::AbstractAdapter] connection
365
- # object, which was obtained earlier by calling #checkout on this pool
366
- # @see #checkout
367
- def checkin(conn, released = nil)
408
+ # +conn+: an AbstractAdapter object, which was obtained by earlier by
409
+ # calling #checkout on this pool.
410
+ def checkin(conn)
411
+ #conn.lock.synchronize do
368
412
  synchronize do
369
- _run_checkin_callbacks(conn)
413
+ remove_connection_from_thread_cache conn
370
414
 
371
- release conn, conn.owner unless released
415
+ _run_checkin_callbacks(conn)
372
416
 
373
417
  @available.add conn
374
418
  end
419
+ #end
375
420
  end
376
421
 
377
422
  # Remove a connection from the connection pool. The returned connection
@@ -379,14 +424,25 @@ module ActiveRecord
379
424
  #
380
425
  # @return [ActiveRecord::ConnectionAdapters::AbstractAdapter]
381
426
  def remove(conn)
427
+ needs_new_connection = false
428
+
382
429
  synchronize do
430
+ remove_connection_from_thread_cache conn
431
+
383
432
  @connections.delete conn
384
433
  @available.delete conn
385
434
 
386
- release conn, conn.owner
435
+ @connected.value = @connections.any?
387
436
 
388
- @available.add checkout_new_connection if @available.any_waiting?
437
+ needs_new_connection = @available.any_waiting?
389
438
  end
439
+
440
+ # This is intentionally done outside of the synchronized section as we
441
+ # would like not to hold the main mutex while checking out new connections.
442
+ # Thus there is some chance that needs_new_connection information is now
443
+ # stale, we can live with that (bulk_make_new_connections will make
444
+ # sure not to exceed the pool's @size limit).
445
+ bulk_make_new_connections(1) if needs_new_connection
390
446
  end
391
447
 
392
448
  # Recover lost connections for the pool. A lost connection can occur if
@@ -396,6 +452,8 @@ module ActiveRecord
396
452
  stale_connections = synchronize do
397
453
  @connections.select do |conn|
398
454
  conn.in_use? && !conn.owner.alive?
455
+ end.each do |conn|
456
+ conn.steal!
399
457
  end
400
458
  end
401
459
 
@@ -410,6 +468,61 @@ module ActiveRecord
410
468
  end
411
469
  end
412
470
  end
471
+
472
+ # Disconnect all connections that have been idle for at least
473
+ # +minimum_idle+ seconds. Connections currently checked out, or that were
474
+ # checked in less than +minimum_idle+ seconds ago, are unaffected.
475
+ def flush(minimum_idle = @idle_timeout)
476
+ return if minimum_idle.nil?
477
+
478
+ idle_connections = synchronize do
479
+ @connections.select do |conn|
480
+ !conn.in_use? && conn.seconds_idle >= minimum_idle
481
+ end.each do |conn|
482
+ conn.lease
483
+
484
+ @available.delete conn
485
+ @connections.delete conn
486
+
487
+ @connected.value = @connections.any?
488
+ end
489
+ end
490
+
491
+ idle_connections.each do |conn|
492
+ conn.disconnect!
493
+ end
494
+ end
495
+
496
+ # Disconnect all currently idle connections. Connections currently checked
497
+ # out are unaffected.
498
+ def flush!
499
+ reap
500
+ flush(-1)
501
+ end
502
+
503
+ def num_waiting_in_queue # :nodoc:
504
+ @available.num_waiting
505
+ end
506
+ private :num_waiting_in_queue
507
+
508
+ # Return connection pool's usage statistic
509
+ # Example:
510
+ #
511
+ # ActiveRecord::Base.connection_pool.stat # => { size: 15, connections: 1, busy: 1, dead: 0, idle: 0, waiting: 0, checkout_timeout: 5 }
512
+ def stat
513
+ synchronize do
514
+ {
515
+ size: size,
516
+ connections: @connections.size,
517
+ busy: @connections.count { |c| c.in_use? && c.owner.alive? },
518
+ dead: @connections.count { |c| c.in_use? && !c.owner.alive? },
519
+ idle: @connections.count { |c| !c.in_use? },
520
+ waiting: num_waiting_in_queue,
521
+ checkout_timeout: checkout_timeout
522
+ }
523
+ end
524
+ end
525
+
413
526
  # NOTE: active? and reset! are >= AR 2.3
414
527
 
415
528
  def reaper?; (@reaper ||= nil) && @reaper.frequency end
@@ -424,6 +537,16 @@ module ActiveRecord
424
537
 
425
538
  private
426
539
 
540
+ def bulk_make_new_connections(num_new_conns_needed)
541
+ num_new_conns_needed.times do
542
+ # try_to_checkout_new_connection will not exceed pool's @size limit
543
+ if new_conn = try_to_checkout_new_connection
544
+ # make the new_conn available to the starving threads stuck @available Queue
545
+ checkin(new_conn)
546
+ end
547
+ end
548
+ end
549
+
427
550
  # Acquire a connection by one of 1) immediately removing one
428
551
  # from the queue of available connections, 2) creating a new
429
552
  # connection if the pool is not at capacity, 3) waiting on the
@@ -431,36 +554,73 @@ module ActiveRecord
431
554
  #
432
555
  # @raise [ActiveRecord::ConnectionTimeoutError]
433
556
  # @raise [ActiveRecord::ConnectionNotEstablished]
434
- def acquire_connection
435
- if conn = @available.poll
557
+ def acquire_connection(checkout_timeout)
558
+ if conn = @available.poll || try_to_checkout_new_connection
436
559
  conn
437
- elsif @connections.size < @size
438
- checkout_new_connection
439
560
  else
440
561
  reap unless @reaping
441
- @available.poll(@checkout_timeout)
562
+ @available.poll(checkout_timeout)
442
563
  end
443
564
  end
444
565
 
445
- def release(conn, owner)
446
- thread_id = owner.object_id
447
- if @reserved_connections[thread_id] == conn
448
- @reserved_connections.delete thread_id
566
+ #--
567
+ # if owner_thread param is omitted, this must be called in synchronize block
568
+ def remove_connection_from_thread_cache(conn, owner_thread = conn.owner)
569
+ @thread_cached_conns.delete_pair(current_connection_id(owner_thread), conn)
570
+ end
571
+ alias_method :release, :remove_connection_from_thread_cache
572
+
573
+ # If the pool is not at a <tt>@size</tt> limit, establish new connection. Connecting
574
+ # to the DB is done outside main synchronized section.
575
+ #--
576
+ # Implementation constraint: a newly established connection returned by this
577
+ # method must be in the +.leased+ state.
578
+ def try_to_checkout_new_connection
579
+ # first in synchronized section check if establishing new conns is allowed
580
+ # and increment @now_connecting, to prevent overstepping this pool's @size
581
+ # constraint
582
+ do_checkout = synchronize do
583
+ if @threads_blocking_new_connections.zero? && (@connections.size + @now_connecting) < @size
584
+ @now_connecting += 1
585
+ end
586
+ end
587
+ if do_checkout
588
+ begin
589
+ # if successfully incremented @now_connecting establish new connection
590
+ # outside of synchronized section
591
+ conn = checkout_new_connection
592
+ ensure
593
+ synchronize do
594
+ if conn
595
+ adopt_connection(conn)
596
+ # returned conn needs to be already leased
597
+ conn.lease
598
+ end
599
+ @now_connecting -= 1
600
+ end
601
+ end
449
602
  end
450
603
  end
451
604
 
605
+ def adopt_connection(conn)
606
+ conn.pool = self
607
+ @connections << conn
608
+ end
609
+
452
610
  def checkout_new_connection
453
611
  raise ConnectionNotEstablished unless @automatic_reconnect
454
-
455
612
  conn = new_connection
456
- conn.pool = self
457
- @connections << conn
613
+ @connected.make_true
458
614
  conn
459
615
  end
460
616
 
461
617
  def checkout_and_verify(conn)
462
618
  _run_checkout_callbacks(conn)
463
619
  conn
620
+ rescue => e
621
+ remove conn
622
+ conn.disconnect!
623
+ raise e
464
624
  end
465
625
 
466
626
  def prefill_initial_connections