umlaut 3.0.0beta7 → 3.0.0beta8

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,454 @@
1
+ ######################
2
+ #
3
+ # ActiveRecord's ConnectionPool in Rails 3.2.3 allows threads to 'steal'
4
+ # connections from each other, so some threads get starved out.
5
+ # This monkey patch uses an implementation from https://github.com/rails/rails/pull/6492
6
+ # that ensures 'fair' queue in ConnectionPool.
7
+ #
8
+ # Can be removed if/when we are on an AR that incorporates above patch
9
+ # or equivalent.
10
+ #
11
+ # This file referenced from an initializer in our main engine
12
+ # class, that loads it to monkey patch AR.
13
+ #
14
+ ##########################
15
+
16
+ # give a backdoor to disable this patch
17
+ unless ENV["NO_AR_PATCH"]
18
+
19
+ # make sure it's there so we can monkey patch
20
+ require 'active_record'
21
+ ActiveRecord::ConnectionAdapters::ConnectionPool
22
+
23
+
24
+ # Some require's our new definition will need
25
+ require 'thread'
26
+ require 'monitor'
27
+ require 'set'
28
+ require 'active_support/core_ext/module/deprecation'
29
+
30
+
31
+
32
+ # And monkey patch
33
+ module ActiveRecord
34
+
35
+
36
+ module ConnectionAdapters
37
+ # Connection pool base class for managing Active Record database
38
+ # connections.
39
+ #
40
+ # == Introduction
41
+ #
42
+ # A connection pool synchronizes thread access to a limited number of
43
+ # database connections. The basic idea is that each thread checks out a
44
+ # database connection from the pool, uses that connection, and checks the
45
+ # connection back in. ConnectionPool is completely thread-safe, and will
46
+ # ensure that a connection cannot be used by two threads at the same time,
47
+ # as long as ConnectionPool's contract is correctly followed. It will also
48
+ # handle cases in which there are more threads than connections: if all
49
+ # connections have been checked out, and a thread tries to checkout a
50
+ # connection anyway, then ConnectionPool will wait until some other thread
51
+ # has checked in a connection.
52
+ #
53
+ # == Obtaining (checking out) a connection
54
+ #
55
+ # Connections can be obtained and used from a connection pool in several
56
+ # ways:
57
+ #
58
+ # 1. Simply use ActiveRecord::Base.connection as with Active Record 2.1 and
59
+ # earlier (pre-connection-pooling). Eventually, when you're done with
60
+ # the connection(s) and wish it to be returned to the pool, you call
61
+ # ActiveRecord::Base.clear_active_connections!. This will be the
62
+ # default behavior for Active Record when used in conjunction with
63
+ # Action Pack's request handling cycle.
64
+ # 2. Manually check out a connection from the pool with
65
+ # ActiveRecord::Base.connection_pool.checkout. You are responsible for
66
+ # returning this connection to the pool when finished by calling
67
+ # ActiveRecord::Base.connection_pool.checkin(connection).
68
+ # 3. Use ActiveRecord::Base.connection_pool.with_connection(&block), which
69
+ # obtains a connection, yields it as the sole argument to the block,
70
+ # and returns it to the pool after the block completes.
71
+ #
72
+ # Connections in the pool are actually AbstractAdapter objects (or objects
73
+ # compatible with AbstractAdapter's interface).
74
+ #
75
+ # == Options
76
+ #
77
+ # There are several connection-pooling-related options that you can add to
78
+ # your database connection configuration:
79
+ #
80
+ # * +pool+: number indicating size of connection pool (default 5)
81
+ # * +checkout_timeout+: number of seconds to block and wait for a connection
82
+ # before giving up and raising a timeout error (default 5 seconds).
83
+ # * +reaping_frequency+: frequency in seconds to periodically run the
84
+ # Reaper, which attempts to find and close dead connections, which can
85
+ # occur if a programmer forgets to close a connection at the end of a
86
+ # thread or a thread dies unexpectedly. (Default nil, which means don't
87
+ # run the Reaper).
88
+ # * +dead_connection_timeout+: number of seconds from last checkout
89
+ # after which the Reaper will consider a connection reapable. (default
90
+ # 5 seconds).
91
+ class ConnectionPool
92
+ # Threadsafe, fair, FIFO queue. Meant to be used by ConnectionPool
93
+ # with which it shares a Monitor. But could be a generic Queue.
94
+ #
95
+ # The Queue in stdlib's 'thread' could replace this class except
96
+ # stdlib's doesn't support waiting with a timeout.
97
+ class Queue
98
+ def initialize(lock = Monitor.new)
99
+ @lock = lock
100
+ @cond = @lock.new_cond
101
+ @num_waiting = 0
102
+ @queue = []
103
+ end
104
+
105
+ # Test if any threads are currently waiting on the queue.
106
+ def any_waiting?
107
+ synchronize do
108
+ @num_waiting > 0
109
+ end
110
+ end
111
+
112
+ # Return the number of threads currently waiting on this
113
+ # queue.
114
+ def num_waiting
115
+ synchronize do
116
+ @num_waiting
117
+ end
118
+ end
119
+
120
+ # Add +element+ to the queue. Never blocks.
121
+ def add(element)
122
+ synchronize do
123
+ @queue.push element
124
+ @cond.signal
125
+ end
126
+ end
127
+
128
+ # If +element+ is in the queue, remove and return it, or nil.
129
+ def delete(element)
130
+ synchronize do
131
+ @queue.delete(element)
132
+ end
133
+ end
134
+
135
+ # Remove all elements from the queue.
136
+ def clear
137
+ synchronize do
138
+ @queue.clear
139
+ end
140
+ end
141
+
142
+ # Remove the head of the queue.
143
+ #
144
+ # If +timeout+ is not given, remove and return the head the
145
+ # queue if the number of available elements is strictly
146
+ # greater than the number of threads currently waiting (that
147
+ # is, don't jump ahead in line). Otherwise, return nil.
148
+ #
149
+ # If +timeout+ is given, block if it there is no element
150
+ # available, waiting up to +timeout+ seconds for an element to
151
+ # become available.
152
+ #
153
+ # Raises:
154
+ # - ConnectionTimeoutError if +timeout+ is given and no element
155
+ # becomes available after +timeout+ seconds,
156
+ def poll(timeout = nil)
157
+ synchronize do
158
+ if timeout
159
+ no_wait_poll || wait_poll(timeout)
160
+ else
161
+ no_wait_poll
162
+ end
163
+ end
164
+ end
165
+
166
+ private
167
+
168
+ def synchronize(&block)
169
+ @lock.synchronize(&block)
170
+ end
171
+
172
+ # Test if the queue currently contains any elements.
173
+ def any?
174
+ !@queue.empty?
175
+ end
176
+
177
+ # A thread can remove an element from the queue without
178
+ # waiting if an only if the number of currently available
179
+ # connections is strictly greater than the number of waiting
180
+ # threads.
181
+ def can_remove_no_wait?
182
+ @queue.size > @num_waiting
183
+ end
184
+
185
+ # Removes and returns the head of the queue if possible, or nil.
186
+ def remove
187
+ @queue.shift
188
+ end
189
+
190
+ # Remove and return the head the queue if the number of
191
+ # available elements is strictly greater than the number of
192
+ # threads currently waiting. Otherwise, return nil.
193
+ def no_wait_poll
194
+ remove if can_remove_no_wait?
195
+ end
196
+
197
+ # Waits on the queue up to +timeout+ seconds, then removes and
198
+ # returns the head of the queue.
199
+ def wait_poll(timeout)
200
+ @num_waiting += 1
201
+
202
+ t0 = Time.now
203
+ elapsed = 0
204
+ loop do
205
+ @cond.wait(timeout - elapsed)
206
+
207
+ return remove if any?
208
+
209
+ elapsed = Time.now - t0
210
+ raise ConnectionTimeoutError if elapsed >= timeout
211
+ end
212
+ ensure
213
+ @num_waiting -= 1
214
+ end
215
+ end
216
+
217
+
218
+ include MonitorMixin
219
+
220
+ attr_accessor :automatic_reconnect, :checkout_timeout, :dead_connection_timeout
221
+ attr_reader :spec, :connections, :size, :reaper
222
+
223
+ # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
224
+ # object which describes database connection information (e.g. adapter,
225
+ # host name, username, password, etc), as well as the maximum size for
226
+ # this ConnectionPool.
227
+ #
228
+ # The default ConnectionPool maximum size is 5.
229
+ def initialize(spec)
230
+ super()
231
+
232
+ @spec = spec
233
+
234
+ # The cache of reserved connections mapped to threads
235
+ @reserved_connections = {}
236
+
237
+ @checkout_timeout = spec.config[:checkout_timeout] || 5
238
+
239
+
240
+ # default max pool size to 5
241
+ @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
242
+
243
+ @connections = []
244
+ @automatic_reconnect = true
245
+
246
+ @available = Queue.new self
247
+ end
248
+
249
+
250
+ # Retrieve the connection associated with the current thread, or call
251
+ # #checkout to obtain one if necessary.
252
+ #
253
+ # #connection can be called any number of times; the connection is
254
+ # held in a hash keyed by the thread id.
255
+ def connection
256
+ synchronize do
257
+ @reserved_connections[current_connection_id] ||= checkout
258
+ end
259
+ end
260
+
261
+ # Is there an open connection that is being used for the current thread?
262
+ def active_connection?
263
+ synchronize do
264
+ @reserved_connections.fetch(current_connection_id) {
265
+ return false
266
+ }.in_use?
267
+ end
268
+ end
269
+
270
+ # Signal that the thread is finished with the current connection.
271
+ # #release_connection releases the connection-thread association
272
+ # and returns the connection to the pool.
273
+ def release_connection(with_id = current_connection_id)
274
+ synchronize do
275
+ conn = @reserved_connections.delete(with_id)
276
+ checkin conn if conn
277
+ end
278
+ end
279
+
280
+ # If a connection already exists yield it to the block. If no connection
281
+ # exists checkout a connection, yield it to the block, and checkin the
282
+ # connection when finished.
283
+ def with_connection
284
+ connection_id = current_connection_id
285
+ fresh_connection = true unless active_connection?
286
+ yield connection
287
+ ensure
288
+ release_connection(connection_id) if fresh_connection
289
+ end
290
+
291
+ # Returns true if a connection has already been opened.
292
+ def connected?
293
+ synchronize { @connections.any? }
294
+ end
295
+
296
+ # Disconnects all connections in the pool, and clears the pool.
297
+ def disconnect!
298
+ synchronize do
299
+ @reserved_connections = {}
300
+ @connections.each do |conn|
301
+ checkin conn
302
+ conn.disconnect!
303
+ end
304
+ @connections = []
305
+ @available.clear
306
+ end
307
+ end
308
+
309
+ # Clears the cache which maps classes.
310
+ def clear_reloadable_connections!
311
+ synchronize do
312
+ @reserved_connections = {}
313
+ @connections.each do |conn|
314
+ checkin conn
315
+ conn.disconnect! if conn.requires_reloading?
316
+ end
317
+ @connections.delete_if do |conn|
318
+ conn.requires_reloading?
319
+ end
320
+ @available.clear
321
+ @connections.each do |conn|
322
+ @available.add conn
323
+ end
324
+ end
325
+ end
326
+
327
+
328
+ # Check-out a database connection from the pool, indicating that you want
329
+ # to use it. You should call #checkin when you no longer need this.
330
+ #
331
+ # This is done by either returning and leasing existing connection, or by
332
+ # creating a new connection and leasing it.
333
+ #
334
+ # If all connections are leased and the pool is at capacity (meaning the
335
+ # number of currently leased connections is greater than or equal to the
336
+ # size limit set), an ActiveRecord::PoolFullError exception will be raised.
337
+ #
338
+ # Returns: an AbstractAdapter object.
339
+ #
340
+ # Raises:
341
+ # - ConnectionTimeoutError: no connection can be obtained from the pool.
342
+ def checkout
343
+ synchronize do
344
+ conn = acquire_connection
345
+ conn.lease
346
+ checkout_and_verify(conn)
347
+ end
348
+ end
349
+
350
+ # Check-in a database connection back into the pool, indicating that you
351
+ # no longer need this connection.
352
+ #
353
+ # +conn+: an AbstractAdapter object, which was obtained by earlier by
354
+ # calling +checkout+ on this pool.
355
+ def checkin(conn)
356
+ synchronize do
357
+ conn.run_callbacks :checkin do
358
+ conn.expire
359
+ end
360
+
361
+ release conn
362
+
363
+ @available.add conn
364
+ end
365
+ end
366
+
367
+ # Remove a connection from the connection pool. The connection will
368
+ # remain open and active but will no longer be managed by this pool.
369
+ def remove(conn)
370
+ synchronize do
371
+ @connections.delete conn
372
+ @available.delete conn
373
+
374
+ # FIXME: we might want to store the key on the connection so that removing
375
+ # from the reserved hash will be a little easier.
376
+ release conn
377
+
378
+ @available.add checkout_new_connection if @available.any_waiting?
379
+ end
380
+ end
381
+
382
+
383
+
384
+ private
385
+
386
+ # Acquire a connection by one of 1) immediately removing one
387
+ # from the queue of available connections, 2) creating a new
388
+ # connection if the pool is not at capacity, 3) waiting on the
389
+ # queue for a connection to become available.
390
+ #
391
+ # Raises:
392
+ # - ConnectionTimeoutError if a connection could not be acquired (FIXME:
393
+ # why not ConnectionTimeoutError?
394
+ def acquire_connection
395
+ if conn = @available.poll
396
+ conn
397
+ elsif @connections.size < @size
398
+ checkout_new_connection
399
+ else
400
+ t0 = Time.now
401
+ begin
402
+ @available.poll(@checkout_timeout)
403
+ rescue ConnectionTimeoutError
404
+ msg = 'could not obtain a database connection within %0.3f seconds (waited %0.3f seconds)' %
405
+ [@checkout_timeout, Time.now - t0]
406
+ raise ConnectionTimeoutError, msg
407
+ end
408
+ end
409
+ end
410
+
411
+ def release(conn)
412
+ thread_id = if @reserved_connections[current_connection_id] == conn
413
+ current_connection_id
414
+ else
415
+ @reserved_connections.keys.find { |k|
416
+ @reserved_connections[k] == conn
417
+ }
418
+ end
419
+
420
+ @reserved_connections.delete thread_id if thread_id
421
+ end
422
+
423
+ def new_connection
424
+ ActiveRecord::Base.send(spec.adapter_method, spec.config)
425
+ end
426
+
427
+ def current_connection_id #:nodoc:
428
+ ActiveRecord::Base.connection_id ||= Thread.current.object_id
429
+ end
430
+
431
+ def checkout_new_connection
432
+ raise ConnectionNotEstablished unless @automatic_reconnect
433
+
434
+ c = new_connection
435
+ c.pool = self
436
+ @connections << c
437
+ c
438
+ end
439
+
440
+ def checkout_and_verify(c)
441
+ c.run_callbacks :checkout do
442
+ c.verify!
443
+ end
444
+ c
445
+ end
446
+ end
447
+
448
+
449
+ end
450
+ end
451
+
452
+ end
453
+
454
+
@@ -1,3 +1,3 @@
1
1
  module Umlaut
2
- VERSION = "3.0.0beta7"
2
+ VERSION = "3.0.0beta8"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: umlaut
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0beta7
4
+ version: 3.0.0beta8
5
5
  prerelease: 5
6
6
  platform: ruby
7
7
  authors:
@@ -445,6 +445,7 @@ files:
445
445
  - lib/tasks/umlaut_migrate_permalinks.rake
446
446
  - lib/tasks/umlaut.rake
447
447
  - lib/tasks/Untitled-1
448
+ - active_record_patch/connection_pool.rb
448
449
  - LICENSE
449
450
  - Rakefile
450
451
  - README.md
@@ -557,7 +558,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
557
558
  version: '0'
558
559
  segments:
559
560
  - 0
560
- hash: -1531856936954228616
561
+ hash: 1259454745651416624
561
562
  required_rubygems_version: !ruby/object:Gem::Requirement
562
563
  none: false
563
564
  requirements: