umlaut 3.0.0beta7 → 3.0.0beta8

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