activerecord-bogacs 0.6.0 → 0.7.0

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