activerecord-bogacs 0.1.0

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