activerecord-bogacs 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +26 -0
- data/Gemfile +33 -0
- data/LICENSE.txt +22 -0
- data/README.md +124 -0
- data/Rakefile +167 -0
- data/activerecord-bogacs.gemspec +26 -0
- data/lib/active_record/bogacs.rb +55 -0
- data/lib/active_record/bogacs/default_pool.rb +672 -0
- data/lib/active_record/bogacs/false_pool.rb +259 -0
- data/lib/active_record/bogacs/pool_support.rb +21 -0
- data/lib/active_record/bogacs/shareable_pool.rb +255 -0
- data/lib/active_record/bogacs/version.rb +5 -0
- data/lib/active_record/connection_adapters/adapter_compat.rb +57 -0
- data/lib/active_record/shared_connection.rb +24 -0
- data/test/active_record/bogacs/default_pool_test.rb +34 -0
- data/test/active_record/bogacs/false_pool_test.rb +200 -0
- data/test/active_record/bogacs/shareable_pool/connection_pool_test.rb +186 -0
- data/test/active_record/bogacs/shareable_pool/connection_sharing_test.rb +429 -0
- data/test/active_record/bogacs/shareable_pool_helper.rb +81 -0
- data/test/active_record/builtin_pool_test.rb +18 -0
- data/test/active_record/connection_pool_test_methods.rb +336 -0
- data/test/test_helper.rb +304 -0
- metadata +130 -0
@@ -0,0 +1,672 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'thread_safe'
|
3
|
+
require 'monitor'
|
4
|
+
|
5
|
+
require 'active_record/connection_adapters/adapter_compat'
|
6
|
+
require 'active_record/bogacs/pool_support'
|
7
|
+
|
8
|
+
module ActiveRecord
|
9
|
+
module Bogacs
|
10
|
+
|
11
|
+
# == Obtaining (checking out) a connection
|
12
|
+
#
|
13
|
+
# Connections can be obtained and used from a connection pool in several
|
14
|
+
# ways:
|
15
|
+
#
|
16
|
+
# 1. Simply use ActiveRecord::Base.connection as with Active Record 2.1 and
|
17
|
+
# earlier (pre-connection-pooling). Eventually, when you're done with
|
18
|
+
# the connection(s) and wish it to be returned to the pool, you call
|
19
|
+
# ActiveRecord::Base.clear_active_connections!.
|
20
|
+
# 2. Manually check out a connection from the pool with
|
21
|
+
# ActiveRecord::Base.connection_pool.checkout. You are responsible for
|
22
|
+
# returning this connection to the pool when finished by calling
|
23
|
+
# ActiveRecord::Base.connection_pool.checkin(connection).
|
24
|
+
# 3. Use ActiveRecord::Base.connection_pool.with_connection(&block), which
|
25
|
+
# obtains a connection, yields it as the sole argument to the block,
|
26
|
+
# and returns it to the pool after the block completes.
|
27
|
+
#
|
28
|
+
# Connections in the pool are actually AbstractAdapter objects (or objects
|
29
|
+
# compatible with AbstractAdapter's interface).
|
30
|
+
#
|
31
|
+
# == Options
|
32
|
+
#
|
33
|
+
# There are several connection-pooling-related options that you can add to
|
34
|
+
# your database connection configuration:
|
35
|
+
#
|
36
|
+
# * +pool+: number indicating size of connection pool (default 5)
|
37
|
+
# * +checkout_timeout+: number of seconds to block and wait for a connection
|
38
|
+
# before giving up and raising a timeout error (default 5 seconds).
|
39
|
+
# * +reaping_frequency+: frequency in seconds to periodically run the
|
40
|
+
# Reaper, which attempts to find and close dead connections, which can
|
41
|
+
# occur if a programmer forgets to close a connection at the end of a
|
42
|
+
# thread or a thread dies unexpectedly. (Default nil, which means don't
|
43
|
+
# run the Reaper - reaping will still happen occasionally).
|
44
|
+
class DefaultPool
|
45
|
+
# Threadsafe, fair, FIFO queue. Meant to be used by ConnectionPool
|
46
|
+
# with which it shares a Monitor. But could be a generic Queue.
|
47
|
+
#
|
48
|
+
# The Queue in stdlib's 'thread' could replace this class except
|
49
|
+
# stdlib's doesn't support waiting with a timeout.
|
50
|
+
# @private
|
51
|
+
class Queue
|
52
|
+
def initialize(lock = Monitor.new)
|
53
|
+
@lock = lock
|
54
|
+
@cond = @lock.new_cond
|
55
|
+
@num_waiting = 0
|
56
|
+
@queue = []
|
57
|
+
end
|
58
|
+
|
59
|
+
# Test if any threads are currently waiting on the queue.
|
60
|
+
def any_waiting?
|
61
|
+
synchronize do
|
62
|
+
@num_waiting > 0
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns the number of threads currently waiting on this
|
67
|
+
# queue.
|
68
|
+
def num_waiting
|
69
|
+
synchronize do
|
70
|
+
@num_waiting
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Add +element+ to the queue. Never blocks.
|
75
|
+
def add(element)
|
76
|
+
synchronize do
|
77
|
+
@queue.push element
|
78
|
+
@cond.signal
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# If +element+ is in the queue, remove and return it, or nil.
|
83
|
+
def delete(element)
|
84
|
+
synchronize do
|
85
|
+
@queue.delete(element)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Remove all elements from the queue.
|
90
|
+
def clear
|
91
|
+
synchronize do
|
92
|
+
@queue.clear
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Remove the head of the queue.
|
97
|
+
#
|
98
|
+
# If +timeout+ is not given, remove and return the head the
|
99
|
+
# queue if the number of available elements is strictly
|
100
|
+
# greater than the number of threads currently waiting (that
|
101
|
+
# is, don't jump ahead in line). Otherwise, return nil.
|
102
|
+
#
|
103
|
+
# If +timeout+ is given, block if it there is no element
|
104
|
+
# available, waiting up to +timeout+ seconds for an element to
|
105
|
+
# become available.
|
106
|
+
#
|
107
|
+
# Raises:
|
108
|
+
# - ConnectionTimeoutError if +timeout+ is given and no element
|
109
|
+
# becomes available after +timeout+ seconds,
|
110
|
+
def poll(timeout = nil, &block)
|
111
|
+
synchronize do
|
112
|
+
if timeout
|
113
|
+
no_wait_poll || wait_poll(timeout, &block)
|
114
|
+
else
|
115
|
+
no_wait_poll
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def synchronize(&block)
|
123
|
+
@lock.synchronize(&block)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Test if the queue currently contains any elements.
|
127
|
+
def any?
|
128
|
+
!@queue.empty?
|
129
|
+
end
|
130
|
+
|
131
|
+
# A thread can remove an element from the queue without
|
132
|
+
# waiting if an only if the number of currently available
|
133
|
+
# connections is strictly greater than the number of waiting
|
134
|
+
# threads.
|
135
|
+
def can_remove_no_wait?
|
136
|
+
@queue.size > @num_waiting
|
137
|
+
end
|
138
|
+
|
139
|
+
# Removes and returns the head of the queue if possible, or nil.
|
140
|
+
def remove
|
141
|
+
@queue.shift
|
142
|
+
end
|
143
|
+
|
144
|
+
# Remove and return the head the queue if the number of
|
145
|
+
# available elements is strictly greater than the number of
|
146
|
+
# threads currently waiting. Otherwise, return nil.
|
147
|
+
def no_wait_poll
|
148
|
+
remove if can_remove_no_wait?
|
149
|
+
end
|
150
|
+
|
151
|
+
# Waits on the queue up to +timeout+ seconds, then removes and
|
152
|
+
# returns the head of the queue.
|
153
|
+
def wait_poll(timeout)
|
154
|
+
t0 = Time.now
|
155
|
+
elapsed = 0
|
156
|
+
|
157
|
+
@num_waiting += 1
|
158
|
+
|
159
|
+
yield if block_given?
|
160
|
+
|
161
|
+
loop do
|
162
|
+
@cond.wait(timeout - elapsed)
|
163
|
+
|
164
|
+
return remove if any?
|
165
|
+
|
166
|
+
elapsed = Time.now - t0
|
167
|
+
if elapsed >= timeout
|
168
|
+
msg = 'could not obtain a database connection within %0.3f seconds (waited %0.3f seconds)' %
|
169
|
+
[timeout, elapsed]
|
170
|
+
raise ConnectionTimeoutError, msg
|
171
|
+
end
|
172
|
+
end
|
173
|
+
ensure
|
174
|
+
@num_waiting -= 1
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Every +frequency+ seconds, the reaper will call +reap+ on +pool+.
|
179
|
+
# A reaper instantiated with a nil frequency will never reap the
|
180
|
+
# connection pool.
|
181
|
+
#
|
182
|
+
# Configure the frequency by setting "reaping_frequency" in your
|
183
|
+
# database yaml file.
|
184
|
+
class Reaper
|
185
|
+
attr_reader :pool, :frequency
|
186
|
+
|
187
|
+
def initialize(pool, frequency)
|
188
|
+
@pool = pool
|
189
|
+
@frequency = frequency
|
190
|
+
end
|
191
|
+
|
192
|
+
def run
|
193
|
+
return unless frequency
|
194
|
+
Thread.new(frequency, pool) { |t, p|
|
195
|
+
while true
|
196
|
+
sleep t
|
197
|
+
p.reap
|
198
|
+
end
|
199
|
+
}
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
include PoolSupport
|
204
|
+
include MonitorMixin # TODO consider avoiding ?!
|
205
|
+
|
206
|
+
attr_accessor :automatic_reconnect, :checkout_timeout
|
207
|
+
attr_reader :spec, :connections, :size, :reaper
|
208
|
+
attr_reader :initial_size
|
209
|
+
|
210
|
+
# Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
|
211
|
+
# object which describes database connection information (e.g. adapter,
|
212
|
+
# host name, username, password, etc), as well as the maximum size for
|
213
|
+
# this ConnectionPool.
|
214
|
+
#
|
215
|
+
# The default ConnectionPool maximum size is 5.
|
216
|
+
def initialize(spec)
|
217
|
+
super()
|
218
|
+
|
219
|
+
@spec = spec
|
220
|
+
|
221
|
+
@checkout_timeout = ( spec.config[:checkout_timeout] ||
|
222
|
+
spec.config[:wait_timeout] || 5.0 ).to_f # <= 3.2 supports wait_timeout
|
223
|
+
@reaper = Reaper.new self, spec.config[:reaping_frequency]
|
224
|
+
@reaping = !! @reaper.run
|
225
|
+
|
226
|
+
# default max pool size to 5
|
227
|
+
if spec.config[:pool]
|
228
|
+
@size = spec.config[:pool].to_i
|
229
|
+
else
|
230
|
+
if defined? Rails.env && Rails.env.production?
|
231
|
+
logger && logger.debug("pool: option not set, using a default = 5")
|
232
|
+
end
|
233
|
+
@size = 5
|
234
|
+
end
|
235
|
+
|
236
|
+
# The cache of reserved connections mapped to threads
|
237
|
+
@reserved_connections = ThreadSafe::Cache.new(:initial_capacity => @size)
|
238
|
+
|
239
|
+
@connections = []
|
240
|
+
@automatic_reconnect = true
|
241
|
+
|
242
|
+
@available = Queue.new self
|
243
|
+
|
244
|
+
initial_size = spec.config[:pool_initial] || 0
|
245
|
+
initial_size = @size if initial_size == true
|
246
|
+
initial_size = (@size * initial_size).to_i if initial_size <= 1.0
|
247
|
+
# NOTE: warn on onitial_size > size !
|
248
|
+
prefill_initial_connections if ( @initial_size = initial_size.to_i ) > 0
|
249
|
+
end
|
250
|
+
|
251
|
+
# Retrieve the connection associated with the current thread, or call
|
252
|
+
# #checkout to obtain one if necessary.
|
253
|
+
#
|
254
|
+
# #connection can be called any number of times; the connection is
|
255
|
+
# held in a hash keyed by the thread id.
|
256
|
+
def connection
|
257
|
+
connection_id = current_connection_id
|
258
|
+
unless conn = @reserved_connections.fetch(connection_id, nil)
|
259
|
+
synchronize do
|
260
|
+
conn = ( @reserved_connections[connection_id] ||= checkout )
|
261
|
+
end
|
262
|
+
end
|
263
|
+
conn
|
264
|
+
end
|
265
|
+
|
266
|
+
# Is there an open connection that is being used for the current thread?
|
267
|
+
def active_connection?
|
268
|
+
connection_id = current_connection_id
|
269
|
+
if conn = @reserved_connections.fetch(connection_id, nil)
|
270
|
+
!! conn.in_use? # synchronize { conn.in_use? }
|
271
|
+
else
|
272
|
+
false
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
# Signal that the thread is finished with the current connection.
|
277
|
+
# #release_connection releases the connection-thread association
|
278
|
+
# and returns the connection to the pool.
|
279
|
+
def release_connection(with_id = current_connection_id)
|
280
|
+
#synchronize do
|
281
|
+
conn = @reserved_connections.delete(with_id)
|
282
|
+
checkin conn if conn
|
283
|
+
#end
|
284
|
+
end
|
285
|
+
|
286
|
+
# If a connection already exists yield it to the block. If no connection
|
287
|
+
# exists checkout a connection, yield it to the block, and checkin the
|
288
|
+
# connection when finished.
|
289
|
+
def with_connection
|
290
|
+
connection_id = current_connection_id
|
291
|
+
fresh_connection = true unless active_connection?
|
292
|
+
yield connection
|
293
|
+
ensure
|
294
|
+
release_connection(connection_id) if fresh_connection
|
295
|
+
end
|
296
|
+
|
297
|
+
# Returns true if a connection has already been opened.
|
298
|
+
def connected?
|
299
|
+
@connections.size > 0 # synchronize { @connections.any? }
|
300
|
+
end
|
301
|
+
|
302
|
+
# Disconnects all connections in the pool, and clears the pool.
|
303
|
+
def disconnect!
|
304
|
+
synchronize do
|
305
|
+
@reserved_connections.clear
|
306
|
+
@connections.each do |conn|
|
307
|
+
checkin conn
|
308
|
+
conn.disconnect!
|
309
|
+
end
|
310
|
+
@connections.clear
|
311
|
+
@available.clear
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
# Clears the cache which maps classes.
|
316
|
+
def clear_reloadable_connections!
|
317
|
+
synchronize do
|
318
|
+
@reserved_connections.clear
|
319
|
+
@connections.each do |conn|
|
320
|
+
checkin conn
|
321
|
+
conn.disconnect! if conn.requires_reloading?
|
322
|
+
end
|
323
|
+
@connections.delete_if do |conn|
|
324
|
+
conn.requires_reloading?
|
325
|
+
end
|
326
|
+
@available.clear
|
327
|
+
@connections.each do |conn|
|
328
|
+
@available.add conn
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
# Verify active connections and remove and disconnect connections
|
334
|
+
# associated with stale threads.
|
335
|
+
# @private AR 3.2 compatibility
|
336
|
+
def verify_active_connections!
|
337
|
+
synchronize do
|
338
|
+
clear_stale_cached_connections!
|
339
|
+
@connections.each do |connection|
|
340
|
+
connection.verify!
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end if ActiveRecord::VERSION::MAJOR < 4
|
344
|
+
|
345
|
+
# Return any checked-out connections back to the pool by threads that
|
346
|
+
# are no longer alive.
|
347
|
+
def clear_stale_cached_connections!
|
348
|
+
keys = Thread.list.find_all { |t| t.alive? }.map(&:object_id)
|
349
|
+
keys = @reserved_connections.keys - keys
|
350
|
+
keys.each do |key|
|
351
|
+
conn = @reserved_connections[key]
|
352
|
+
checkin conn
|
353
|
+
@reserved_connections.delete(key)
|
354
|
+
end
|
355
|
+
end if ActiveRecord::VERSION::MAJOR < 4
|
356
|
+
|
357
|
+
# Check-out a database connection from the pool, indicating that you want
|
358
|
+
# to use it. You should call #checkin when you no longer need this.
|
359
|
+
#
|
360
|
+
# This is done by either returning and leasing existing connection, or by
|
361
|
+
# creating a new connection and leasing it.
|
362
|
+
#
|
363
|
+
# If all connections are leased and the pool is at capacity (meaning the
|
364
|
+
# number of currently leased connections is greater than or equal to the
|
365
|
+
# size limit set), an ActiveRecord::ConnectionTimeoutError exception will be raised.
|
366
|
+
#
|
367
|
+
# Returns: an AbstractAdapter object.
|
368
|
+
#
|
369
|
+
# Raises:
|
370
|
+
# - ConnectionTimeoutError: no connection can be obtained from the pool.
|
371
|
+
def checkout
|
372
|
+
synchronize do
|
373
|
+
conn = acquire_connection
|
374
|
+
conn.lease
|
375
|
+
checkout_and_verify(conn)
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
# Check-in a database connection back into the pool, indicating that you
|
380
|
+
# no longer need this connection.
|
381
|
+
#
|
382
|
+
# +conn+: an AbstractAdapter object, which was obtained by earlier by
|
383
|
+
# calling +checkout+ on this pool.
|
384
|
+
def checkin(conn)
|
385
|
+
synchronize do
|
386
|
+
owner = conn.owner
|
387
|
+
|
388
|
+
conn.run_callbacks :checkin do
|
389
|
+
conn.expire
|
390
|
+
end
|
391
|
+
|
392
|
+
release owner
|
393
|
+
|
394
|
+
@available.add conn
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
# Remove a connection from the connection pool. The connection will
|
399
|
+
# remain open and active but will no longer be managed by this pool.
|
400
|
+
def remove(conn)
|
401
|
+
synchronize do
|
402
|
+
@connections.delete conn
|
403
|
+
@available.delete conn
|
404
|
+
|
405
|
+
release conn.owner
|
406
|
+
|
407
|
+
@available.add checkout_new_connection if @available.any_waiting?
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
# Recover lost connections for the pool. A lost connection can occur if
|
412
|
+
# a programmer forgets to checkin a connection at the end of a thread
|
413
|
+
# or a thread dies unexpectedly.
|
414
|
+
def reap
|
415
|
+
stale_connections = synchronize do
|
416
|
+
@connections.select do |conn|
|
417
|
+
conn.in_use? && !conn.owner.alive?
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
stale_connections.each do |conn|
|
422
|
+
synchronize do
|
423
|
+
if conn.active?
|
424
|
+
conn.reset!
|
425
|
+
checkin conn
|
426
|
+
else
|
427
|
+
remove conn
|
428
|
+
end
|
429
|
+
end
|
430
|
+
end
|
431
|
+
end
|
432
|
+
# NOTE: active? and reset! are >= AR 2.3
|
433
|
+
|
434
|
+
private
|
435
|
+
|
436
|
+
# Acquire a connection by one of 1) immediately removing one
|
437
|
+
# from the queue of available connections, 2) creating a new
|
438
|
+
# connection if the pool is not at capacity, 3) waiting on the
|
439
|
+
# queue for a connection to become available.
|
440
|
+
#
|
441
|
+
# Raises:
|
442
|
+
# - ConnectionTimeoutError if a connection could not be acquired
|
443
|
+
def acquire_connection
|
444
|
+
if conn = @available.poll
|
445
|
+
conn
|
446
|
+
elsif @connections.size < @size
|
447
|
+
checkout_new_connection
|
448
|
+
else
|
449
|
+
reap unless @reaping
|
450
|
+
@available.poll(@checkout_timeout)
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
def release(owner)
|
455
|
+
thread_id = owner.object_id
|
456
|
+
|
457
|
+
@reserved_connections.delete thread_id
|
458
|
+
end
|
459
|
+
|
460
|
+
def checkout_new_connection
|
461
|
+
raise ConnectionNotEstablished unless @automatic_reconnect
|
462
|
+
|
463
|
+
c = new_connection
|
464
|
+
c.pool = self
|
465
|
+
@connections << c
|
466
|
+
c
|
467
|
+
end
|
468
|
+
|
469
|
+
def checkout_and_verify(c)
|
470
|
+
c.run_callbacks :checkout do
|
471
|
+
c.verify!
|
472
|
+
end
|
473
|
+
c
|
474
|
+
end
|
475
|
+
|
476
|
+
def prefill_initial_connections
|
477
|
+
conns = []; start = Time.now
|
478
|
+
begin
|
479
|
+
@initial_size.times { conns << checkout }
|
480
|
+
ensure
|
481
|
+
conns.each { |conn| checkin(conn) }
|
482
|
+
end
|
483
|
+
logger && logger.debug("pre-filled pool with #{@initial_size}/#{@size} connections in #{Time.now - start}")
|
484
|
+
conns
|
485
|
+
end
|
486
|
+
|
487
|
+
def logger
|
488
|
+
ActiveRecord::Base.logger
|
489
|
+
end
|
490
|
+
|
491
|
+
end
|
492
|
+
|
493
|
+
=begin
|
494
|
+
|
495
|
+
# ConnectionHandler is a collection of ConnectionPool objects. It is used
|
496
|
+
# for keeping separate connection pools for Active Record models that connect
|
497
|
+
# to different databases.
|
498
|
+
#
|
499
|
+
# For example, suppose that you have 5 models, with the following hierarchy:
|
500
|
+
#
|
501
|
+
# |
|
502
|
+
# +-- Book
|
503
|
+
# | |
|
504
|
+
# | +-- ScaryBook
|
505
|
+
# | +-- GoodBook
|
506
|
+
# +-- Author
|
507
|
+
# +-- BankAccount
|
508
|
+
#
|
509
|
+
# Suppose that Book is to connect to a separate database (i.e. one other
|
510
|
+
# than the default database). Then Book, ScaryBook and GoodBook will all use
|
511
|
+
# the same connection pool. Likewise, Author and BankAccount will use the
|
512
|
+
# same connection pool. However, the connection pool used by Author/BankAccount
|
513
|
+
# is not the same as the one used by Book/ScaryBook/GoodBook.
|
514
|
+
#
|
515
|
+
# Normally there is only a single ConnectionHandler instance, accessible via
|
516
|
+
# ActiveRecord::Base.connection_handler. Active Record models use this to
|
517
|
+
# determine the connection pool that they should use.
|
518
|
+
class ConnectionHandler
|
519
|
+
def initialize
|
520
|
+
# These caches are keyed by klass.name, NOT klass. Keying them by klass
|
521
|
+
# alone would lead to memory leaks in development mode as all previous
|
522
|
+
# instances of the class would stay in memory.
|
523
|
+
@owner_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k|
|
524
|
+
h[k] = ThreadSafe::Cache.new(:initial_capacity => 2)
|
525
|
+
end
|
526
|
+
@class_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k|
|
527
|
+
h[k] = ThreadSafe::Cache.new
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
def connection_pool_list
|
532
|
+
owner_to_pool.values.compact
|
533
|
+
end
|
534
|
+
|
535
|
+
def connection_pools
|
536
|
+
ActiveSupport::Deprecation.warn(
|
537
|
+
"In the next release, this will return the same as #connection_pool_list. " \
|
538
|
+
"(An array of pools, rather than a hash mapping specs to pools.)"
|
539
|
+
)
|
540
|
+
Hash[connection_pool_list.map { |pool| [pool.spec, pool] }]
|
541
|
+
end
|
542
|
+
|
543
|
+
def establish_connection(owner, spec)
|
544
|
+
@class_to_pool.clear
|
545
|
+
raise RuntimeError, "Anonymous class is not allowed." unless owner.name
|
546
|
+
owner_to_pool[owner.name] = ConnectionAdapters::ConnectionPool.new(spec)
|
547
|
+
end
|
548
|
+
|
549
|
+
# Returns true if there are any active connections among the connection
|
550
|
+
# pools that the ConnectionHandler is managing.
|
551
|
+
def active_connections?
|
552
|
+
connection_pool_list.any?(&:active_connection?)
|
553
|
+
end
|
554
|
+
|
555
|
+
# Returns any connections in use by the current thread back to the pool,
|
556
|
+
# and also returns connections to the pool cached by threads that are no
|
557
|
+
# longer alive.
|
558
|
+
def clear_active_connections!
|
559
|
+
connection_pool_list.each(&:release_connection)
|
560
|
+
end
|
561
|
+
|
562
|
+
# Clears the cache which maps classes.
|
563
|
+
def clear_reloadable_connections!
|
564
|
+
connection_pool_list.each(&:clear_reloadable_connections!)
|
565
|
+
end
|
566
|
+
|
567
|
+
def clear_all_connections!
|
568
|
+
connection_pool_list.each(&:disconnect!)
|
569
|
+
end
|
570
|
+
|
571
|
+
# Locate the connection of the nearest super class. This can be an
|
572
|
+
# active or defined connection: if it is the latter, it will be
|
573
|
+
# opened and set as the active connection for the class it was defined
|
574
|
+
# for (not necessarily the current class).
|
575
|
+
def retrieve_connection(klass) #:nodoc:
|
576
|
+
pool = retrieve_connection_pool(klass)
|
577
|
+
(pool && pool.connection) or raise ConnectionNotEstablished
|
578
|
+
end
|
579
|
+
|
580
|
+
# Returns true if a connection that's accessible to this class has
|
581
|
+
# already been opened.
|
582
|
+
def connected?(klass)
|
583
|
+
conn = retrieve_connection_pool(klass)
|
584
|
+
conn && conn.connected?
|
585
|
+
end
|
586
|
+
|
587
|
+
# Remove the connection for this class. This will close the active
|
588
|
+
# connection and the defined connection (if they exist). The result
|
589
|
+
# can be used as an argument for establish_connection, for easily
|
590
|
+
# re-establishing the connection.
|
591
|
+
def remove_connection(owner)
|
592
|
+
if pool = owner_to_pool.delete(owner.name)
|
593
|
+
@class_to_pool.clear
|
594
|
+
pool.automatic_reconnect = false
|
595
|
+
pool.disconnect!
|
596
|
+
pool.spec.config
|
597
|
+
end
|
598
|
+
end
|
599
|
+
|
600
|
+
# Retrieving the connection pool happens a lot so we cache it in @class_to_pool.
|
601
|
+
# This makes retrieving the connection pool O(1) once the process is warm.
|
602
|
+
# When a connection is established or removed, we invalidate the cache.
|
603
|
+
#
|
604
|
+
# Ideally we would use #fetch here, as class_to_pool[klass] may sometimes be nil.
|
605
|
+
# However, benchmarking (https://gist.github.com/jonleighton/3552829) showed that
|
606
|
+
# #fetch is significantly slower than #[]. So in the nil case, no caching will
|
607
|
+
# take place, but that's ok since the nil case is not the common one that we wish
|
608
|
+
# to optimise for.
|
609
|
+
def retrieve_connection_pool(klass)
|
610
|
+
class_to_pool[klass.name] ||= begin
|
611
|
+
until pool = pool_for(klass)
|
612
|
+
klass = klass.superclass
|
613
|
+
break unless klass <= Base
|
614
|
+
end
|
615
|
+
|
616
|
+
class_to_pool[klass.name] = pool
|
617
|
+
end
|
618
|
+
end
|
619
|
+
|
620
|
+
private
|
621
|
+
|
622
|
+
def owner_to_pool
|
623
|
+
@owner_to_pool[Process.pid]
|
624
|
+
end
|
625
|
+
|
626
|
+
def class_to_pool
|
627
|
+
@class_to_pool[Process.pid]
|
628
|
+
end
|
629
|
+
|
630
|
+
def pool_for(owner)
|
631
|
+
owner_to_pool.fetch(owner.name) {
|
632
|
+
if ancestor_pool = pool_from_any_process_for(owner)
|
633
|
+
# A connection was established in an ancestor process that must have
|
634
|
+
# subsequently forked. We can't reuse the connection, but we can copy
|
635
|
+
# the specification and establish a new connection with it.
|
636
|
+
establish_connection owner, ancestor_pool.spec
|
637
|
+
else
|
638
|
+
owner_to_pool[owner.name] = nil
|
639
|
+
end
|
640
|
+
}
|
641
|
+
end
|
642
|
+
|
643
|
+
def pool_from_any_process_for(owner)
|
644
|
+
owner_to_pool = @owner_to_pool.values.find { |v| v[owner.name] }
|
645
|
+
owner_to_pool && owner_to_pool[owner.name]
|
646
|
+
end
|
647
|
+
end
|
648
|
+
|
649
|
+
class ConnectionManagement
|
650
|
+
def initialize(app)
|
651
|
+
@app = app
|
652
|
+
end
|
653
|
+
|
654
|
+
def call(env)
|
655
|
+
testing = env.key?('rack.test')
|
656
|
+
|
657
|
+
response = @app.call(env)
|
658
|
+
response[2] = ::Rack::BodyProxy.new(response[2]) do
|
659
|
+
ActiveRecord::Base.clear_active_connections! unless testing
|
660
|
+
end
|
661
|
+
|
662
|
+
response
|
663
|
+
rescue Exception
|
664
|
+
ActiveRecord::Base.clear_active_connections! unless testing
|
665
|
+
raise
|
666
|
+
end
|
667
|
+
end
|
668
|
+
|
669
|
+
=end
|
670
|
+
|
671
|
+
end
|
672
|
+
end
|