activerecord-bogacs 0.1.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.
@@ -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