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.
@@ -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 = connection_cache_key(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 = connection_cache_key(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(connection_cache_key(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
@@ -269,26 +276,49 @@ module ActiveRecord
269
276
  #
270
277
  # @yield [ActiveRecord::ConnectionAdapters::AbstractAdapter]
271
278
  def with_connection
272
- connection_id = current_connection_id
273
- fresh_connection = true unless active_connection?
274
- yield connection
279
+ connection_id = connection_cache_key
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,44 @@ 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 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
345
+
346
+ def discarded? # :nodoc:
347
+ @connections.nil?
348
+ end
349
+
300
350
  def clear_reloadable_connections!
301
351
  synchronize do
302
- @reserved_connections.clear
352
+ @thread_cached_conns.clear
303
353
  @connections.each do |conn|
304
- checkin conn
354
+ if conn.in_use?
355
+ conn.steal!
356
+ checkin conn
357
+ end
305
358
  conn.disconnect! if conn.requires_reloading?
306
359
  end
307
- @connections.delete_if do |conn|
308
- conn.requires_reloading?
309
- end
360
+ @connections.delete_if(&:requires_reloading?)
310
361
  @available.clear
311
362
  @connections.each do |conn|
312
363
  @available.add conn
313
364
  end
365
+
366
+ @connected.value = @connections.any?
314
367
  end
315
368
  end
316
369
 
@@ -331,11 +384,11 @@ module ActiveRecord
331
384
  # @private AR 3.2 compatibility
332
385
  def clear_stale_cached_connections!
333
386
  keys = Thread.list.find_all { |t| t.alive? }.map(&:object_id)
334
- keys = @reserved_connections.keys - keys
387
+ keys = @thread_cached_conns.keys - keys
335
388
  keys.each do |key|
336
- conn = @reserved_connections[key]
389
+ conn = @thread_cached_conns[key]
337
390
  checkin conn
338
- @reserved_connections.delete(key)
391
+ @thread_cached_conns.delete(key)
339
392
  end
340
393
  end if ActiveRecord::VERSION::MAJOR < 4
341
394
 
@@ -350,28 +403,25 @@ module ActiveRecord
350
403
  # @raise [ActiveRecord::ConnectionTimeoutError] if all connections are leased
351
404
  # and the pool is at capacity (meaning the number of currently leased
352
405
  # 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)
406
+ def checkout(checkout_timeout = @checkout_timeout)
407
+ checkout_and_verify(acquire_connection(checkout_timeout))
360
408
  end
361
409
 
362
- # Check-in a database connection back into the pool.
410
+ # Check-in a database connection back into the pool, indicating that you
411
+ # no longer need this connection.
363
412
  #
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)
413
+ # +conn+: an AbstractAdapter object, which was obtained by earlier by
414
+ # calling #checkout on this pool.
415
+ def checkin(conn)
416
+ #conn.lock.synchronize do
368
417
  synchronize do
369
- _run_checkin_callbacks(conn)
418
+ remove_connection_from_thread_cache conn
370
419
 
371
- release conn, conn.owner unless released
420
+ _run_checkin_callbacks(conn)
372
421
 
373
422
  @available.add conn
374
423
  end
424
+ #end
375
425
  end
376
426
 
377
427
  # Remove a connection from the connection pool. The returned connection
@@ -379,14 +429,25 @@ module ActiveRecord
379
429
  #
380
430
  # @return [ActiveRecord::ConnectionAdapters::AbstractAdapter]
381
431
  def remove(conn)
432
+ needs_new_connection = false
433
+
382
434
  synchronize do
435
+ remove_connection_from_thread_cache conn
436
+
383
437
  @connections.delete conn
384
438
  @available.delete conn
385
439
 
386
- release conn, conn.owner
440
+ @connected.value = @connections.any?
387
441
 
388
- @available.add checkout_new_connection if @available.any_waiting?
442
+ needs_new_connection = @available.any_waiting?
389
443
  end
444
+
445
+ # This is intentionally done outside of the synchronized section as we
446
+ # would like not to hold the main mutex while checking out new connections.
447
+ # Thus there is some chance that needs_new_connection information is now
448
+ # stale, we can live with that (bulk_make_new_connections will make
449
+ # sure not to exceed the pool's @size limit).
450
+ bulk_make_new_connections(1) if needs_new_connection
390
451
  end
391
452
 
392
453
  # Recover lost connections for the pool. A lost connection can occur if
@@ -396,6 +457,8 @@ module ActiveRecord
396
457
  stale_connections = synchronize do
397
458
  @connections.select do |conn|
398
459
  conn.in_use? && !conn.owner.alive?
460
+ end.each do |conn|
461
+ conn.steal!
399
462
  end
400
463
  end
401
464
 
@@ -410,6 +473,61 @@ module ActiveRecord
410
473
  end
411
474
  end
412
475
  end
476
+
477
+ # Disconnect all connections that have been idle for at least
478
+ # +minimum_idle+ seconds. Connections currently checked out, or that were
479
+ # checked in less than +minimum_idle+ seconds ago, are unaffected.
480
+ def flush(minimum_idle = @idle_timeout)
481
+ return if minimum_idle.nil?
482
+
483
+ idle_connections = synchronize do
484
+ @connections.select do |conn|
485
+ !conn.in_use? && conn.seconds_idle >= minimum_idle
486
+ end.each do |conn|
487
+ conn.lease
488
+
489
+ @available.delete conn
490
+ @connections.delete conn
491
+
492
+ @connected.value = @connections.any?
493
+ end
494
+ end
495
+
496
+ idle_connections.each do |conn|
497
+ conn.disconnect!
498
+ end
499
+ end
500
+
501
+ # Disconnect all currently idle connections. Connections currently checked
502
+ # out are unaffected.
503
+ def flush!
504
+ reap
505
+ flush(-1)
506
+ end
507
+
508
+ def num_waiting_in_queue # :nodoc:
509
+ @available.num_waiting
510
+ end
511
+ private :num_waiting_in_queue
512
+
513
+ # Return connection pool's usage statistic
514
+ # Example:
515
+ #
516
+ # ActiveRecord::Base.connection_pool.stat # => { size: 15, connections: 1, busy: 1, dead: 0, idle: 0, waiting: 0, checkout_timeout: 5 }
517
+ def stat
518
+ synchronize do
519
+ {
520
+ size: size,
521
+ connections: @connections.size,
522
+ busy: @connections.count { |c| c.in_use? && c.owner.alive? },
523
+ dead: @connections.count { |c| c.in_use? && !c.owner.alive? },
524
+ idle: @connections.count { |c| !c.in_use? },
525
+ waiting: num_waiting_in_queue,
526
+ checkout_timeout: checkout_timeout
527
+ }
528
+ end
529
+ end
530
+
413
531
  # NOTE: active? and reset! are >= AR 2.3
414
532
 
415
533
  def reaper?; (@reaper ||= nil) && @reaper.frequency end
@@ -424,6 +542,16 @@ module ActiveRecord
424
542
 
425
543
  private
426
544
 
545
+ def bulk_make_new_connections(num_new_conns_needed)
546
+ num_new_conns_needed.times do
547
+ # try_to_checkout_new_connection will not exceed pool's @size limit
548
+ if new_conn = try_to_checkout_new_connection
549
+ # make the new_conn available to the starving threads stuck @available Queue
550
+ checkin(new_conn)
551
+ end
552
+ end
553
+ end
554
+
427
555
  # Acquire a connection by one of 1) immediately removing one
428
556
  # from the queue of available connections, 2) creating a new
429
557
  # connection if the pool is not at capacity, 3) waiting on the
@@ -431,36 +559,73 @@ module ActiveRecord
431
559
  #
432
560
  # @raise [ActiveRecord::ConnectionTimeoutError]
433
561
  # @raise [ActiveRecord::ConnectionNotEstablished]
434
- def acquire_connection
435
- if conn = @available.poll
562
+ def acquire_connection(checkout_timeout)
563
+ if conn = @available.poll || try_to_checkout_new_connection
436
564
  conn
437
- elsif @connections.size < @size
438
- checkout_new_connection
439
565
  else
440
566
  reap unless @reaping
441
- @available.poll(@checkout_timeout)
567
+ @available.poll(checkout_timeout)
442
568
  end
443
569
  end
444
570
 
445
- def release(conn, owner)
446
- thread_id = owner.object_id
447
- if @reserved_connections[thread_id] == conn
448
- @reserved_connections.delete thread_id
571
+ #--
572
+ # if owner_thread param is omitted, this must be called in synchronize block
573
+ def remove_connection_from_thread_cache(conn, owner_thread = conn.owner)
574
+ @thread_cached_conns.delete_pair(connection_cache_key(owner_thread), conn)
575
+ end
576
+ alias_method :release, :remove_connection_from_thread_cache
577
+
578
+ # If the pool is not at a <tt>@size</tt> limit, establish new connection. Connecting
579
+ # to the DB is done outside main synchronized section.
580
+ #--
581
+ # Implementation constraint: a newly established connection returned by this
582
+ # method must be in the +.leased+ state.
583
+ def try_to_checkout_new_connection
584
+ # first in synchronized section check if establishing new conns is allowed
585
+ # and increment @now_connecting, to prevent overstepping this pool's @size
586
+ # constraint
587
+ do_checkout = synchronize do
588
+ if @threads_blocking_new_connections.zero? && (@connections.size + @now_connecting) < @size
589
+ @now_connecting += 1
590
+ end
591
+ end
592
+ if do_checkout
593
+ begin
594
+ # if successfully incremented @now_connecting establish new connection
595
+ # outside of synchronized section
596
+ conn = checkout_new_connection
597
+ ensure
598
+ synchronize do
599
+ if conn
600
+ adopt_connection(conn)
601
+ # returned conn needs to be already leased
602
+ conn.lease
603
+ end
604
+ @now_connecting -= 1
605
+ end
606
+ end
449
607
  end
450
608
  end
451
609
 
610
+ def adopt_connection(conn)
611
+ conn.pool = self
612
+ @connections << conn
613
+ end
614
+
452
615
  def checkout_new_connection
453
616
  raise ConnectionNotEstablished unless @automatic_reconnect
454
-
455
617
  conn = new_connection
456
- conn.pool = self
457
- @connections << conn
618
+ @connected.make_true
458
619
  conn
459
620
  end
460
621
 
461
622
  def checkout_and_verify(conn)
462
623
  _run_checkout_callbacks(conn)
463
624
  conn
625
+ rescue => e
626
+ remove conn
627
+ conn.disconnect!
628
+ raise e
464
629
  end
465
630
 
466
631
  def prefill_initial_connections