umlaut 3.0.0beta9 → 3.0.0beta10

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.
@@ -2,12 +2,14 @@
2
2
  #
3
3
  # ActiveRecord's ConnectionPool in Rails 3.2.3 allows threads to 'steal'
4
4
  # connections from each other, so some threads get starved out.
5
+ #
5
6
  # This monkey patch uses an implementation from https://github.com/rails/rails/pull/6492
6
- # that ensures 'fair' queue in ConnectionPool.
7
+ # that ensures 'fair' queue in ConnectionPool.
8
+ #
9
+ # It's actually a weird hybrid which ALSO maintains the clear_stale_cached_connections!
10
+ # behavior to reclaim leaked orphaned connections, and calls that method
11
+ # in checkout when pool has no avail connections.
7
12
  #
8
- # Can be removed if/when we are on an AR that incorporates above patch
9
- # or equivalent.
10
- #
11
13
  # This file referenced from an initializer in our main engine
12
14
  # class, that loads it to monkey patch AR.
13
15
  #
@@ -20,6 +22,8 @@ unless ENV["NO_AR_PATCH"]
20
22
  require 'active_record'
21
23
  ActiveRecord::ConnectionAdapters::ConnectionPool
22
24
 
25
+ # Unload it so we can redefine it completely
26
+ ActiveRecord::ConnectionAdapters.send(:remove_const, :ConnectionPool)
23
27
 
24
28
  # Some require's our new definition will need
25
29
  require 'thread'
@@ -27,427 +31,436 @@ unless ENV["NO_AR_PATCH"]
27
31
  require 'set'
28
32
  require 'active_support/core_ext/module/deprecation'
29
33
 
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:
34
+ # And completely redefine ConnectionPool
35
+
36
+ class ActiveRecord::ConnectionAdapters::ConnectionPool
37
+ # Threadsafe, fair, FIFO queue. Meant to be used by ConnectionPool
38
+ # with which it shares a Monitor. But could be a generic Queue.
79
39
  #
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
40
+ # The Queue in stdlib's 'thread' could replace this class except
41
+ # stdlib's doesn't support waiting with a timeout.
42
+ class Queue
43
+ def initialize(lock = Monitor.new)
44
+ @lock = lock
45
+ @cond = @lock.new_cond
46
+ @num_waiting = 0
47
+ @queue = []
247
48
  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
49
+
50
+ # Test if any threads are currently waiting on the queue.
51
+ def any_waiting?
256
52
  synchronize do
257
- @reserved_connections[current_connection_id] ||= checkout
53
+ @num_waiting > 0
258
54
  end
259
55
  end
260
-
261
- # Is there an open connection that is being used for the current thread?
262
- def active_connection?
56
+
57
+ # Return the number of threads currently waiting on this
58
+ # queue.
59
+ def num_waiting
263
60
  synchronize do
264
- @reserved_connections.fetch(current_connection_id) {
265
- return false
266
- }.in_use?
61
+ @num_waiting
267
62
  end
268
63
  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)
64
+
65
+ # Add +element+ to the queue. Never blocks.
66
+ def add(element)
274
67
  synchronize do
275
- conn = @reserved_connections.delete(with_id)
276
- checkin conn if conn
68
+ @queue.push element
69
+ @cond.signal
277
70
  end
278
71
  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!
72
+
73
+ # If +element+ is in the queue, remove and return it, or nil.
74
+ def delete(element)
298
75
  synchronize do
299
- @reserved_connections = {}
300
- @connections.each do |conn|
301
- checkin conn
302
- conn.disconnect!
303
- end
304
- @connections = []
305
- @available.clear
76
+ @queue.delete(element)
306
77
  end
307
78
  end
308
-
309
- # Clears the cache which maps classes.
310
- def clear_reloadable_connections!
79
+
80
+ # Remove all elements from the queue.
81
+ def clear
311
82
  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
83
+ @queue.clear
324
84
  end
325
85
  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.
86
+
87
+ # Remove the head of the queue.
333
88
  #
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.
89
+ # If +timeout+ is not given, remove and return the head the
90
+ # queue if the number of available elements is strictly
91
+ # greater than the number of threads currently waiting (that
92
+ # is, don't jump ahead in line). Otherwise, return nil.
337
93
  #
338
- # Returns: an AbstractAdapter object.
94
+ # If +timeout+ is given, block if it there is no element
95
+ # available, waiting up to +timeout+ seconds for an element to
96
+ # become available.
339
97
  #
340
98
  # 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)
99
+ # - ConnectionTimeoutError if +timeout+ is given and no element
100
+ # becomes available after +timeout+ seconds,
101
+ def poll(timeout = nil)
356
102
  synchronize do
357
- conn.run_callbacks :checkin do
358
- conn.expire
103
+ if timeout
104
+ no_wait_poll || wait_poll(timeout)
105
+ else
106
+ no_wait_poll
359
107
  end
360
-
361
- release conn
362
-
363
- @available.add conn
364
108
  end
365
109
  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)
110
+
111
+ def num_available
370
112
  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?
113
+ @queue.size
379
114
  end
380
115
  end
381
-
382
-
383
-
116
+
384
117
  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
118
+
119
+ def synchronize(&block)
120
+ @lock.synchronize(&block)
121
+ end
122
+
123
+ # Test if the queue currently contains any elements.
124
+ def any?
125
+ !@queue.empty?
126
+ end
127
+
128
+ # A thread can remove an element from the queue without
129
+ # waiting if an only if the number of currently available
130
+ # connections is strictly greater than the number of waiting
131
+ # threads.
132
+ def can_remove_no_wait?
133
+ @queue.size > @num_waiting
134
+ end
135
+
136
+ # Removes and returns the head of the queue if possible, or nil.
137
+ def remove
138
+ @queue.shift
139
+ end
140
+
141
+ # Remove and return the head the queue if the number of
142
+ # available elements is strictly greater than the number of
143
+ # threads currently waiting. Otherwise, return nil.
144
+ def no_wait_poll
145
+ remove if can_remove_no_wait?
146
+ end
147
+
148
+ # Waits on the queue up to +timeout+ seconds, then removes and
149
+ # returns the head of the queue.
150
+ def wait_poll(timeout)
151
+ @num_waiting += 1
152
+
153
+ t0 = Time.now
154
+ elapsed = 0
155
+ loop do
156
+ @cond.wait(timeout - elapsed)
157
+
158
+ return remove if any?
159
+
160
+ elapsed = Time.now - t0
161
+ raise ActiveRecord::ConnectionTimeoutError if elapsed >= timeout
162
+ end
163
+ ensure
164
+ @num_waiting -= 1
165
+ end
166
+ end
167
+
168
+ # Every +frequency+ seconds, the reaper will call +reap+ on +pool+.
169
+ # A reaper instantiated with a nil frequency will never reap the
170
+ # connection pool.
171
+ #
172
+ # Configure the frequency by setting "reaping_frequency" in your
173
+ # database yaml file.
174
+ class Reaper
175
+ attr_reader :pool, :frequency
176
+
177
+ def initialize(pool, frequency)
178
+ @pool = pool
179
+ @frequency = frequency
180
+ end
181
+
182
+ def run
183
+ return unless frequency
184
+ Thread.new(frequency, pool) { |t, p|
185
+ while true
186
+ sleep t
187
+ p.reap
407
188
  end
189
+ }
190
+ end
191
+ end
192
+
193
+ include MonitorMixin
194
+
195
+ attr_accessor :automatic_reconnect, :checkout_timeout, :dead_connection_timeout
196
+ attr_reader :spec, :connections, :size, :reaper
197
+
198
+ # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
199
+ # object which describes database connection information (e.g. adapter,
200
+ # host name, username, password, etc), as well as the maximum size for
201
+ # this ConnectionPool.
202
+ #
203
+ # The default ConnectionPool maximum size is 5.
204
+ def initialize(spec)
205
+ super()
206
+
207
+ @spec = spec
208
+
209
+ # The cache of reserved connections mapped to threads
210
+ @reserved_connections = {}
211
+
212
+ @checkout_timeout = spec.config[:checkout_timeout] || 5
213
+ @dead_connection_timeout = spec.config[:dead_connection_timeout]
214
+ @reaper = Reaper.new self, spec.config[:reaping_frequency]
215
+ @reaper.run
216
+
217
+ # default max pool size to 5
218
+ @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
219
+
220
+ @connections = []
221
+ @automatic_reconnect = true
222
+
223
+ @available = Queue.new self
224
+ end
225
+
226
+ # Hack for tests to be able to add connections. Do not call outside of tests
227
+ def insert_connection_for_test!(c) #:nodoc:
228
+ synchronize do
229
+ @connections << c
230
+ @available.add c
231
+ end
232
+ end
233
+
234
+ # Retrieve the connection associated with the current thread, or call
235
+ # #checkout to obtain one if necessary.
236
+ #
237
+ # #connection can be called any number of times; the connection is
238
+ # held in a hash keyed by the thread id.
239
+ def connection
240
+ synchronize do
241
+ @reserved_connections[current_connection_id] ||= checkout
242
+ end
243
+ end
244
+
245
+ # Is there an open connection that is being used for the current thread?
246
+ def active_connection?
247
+ synchronize do
248
+ @reserved_connections.fetch(current_connection_id) {
249
+ return false
250
+ }.in_use?
251
+ end
252
+ end
253
+
254
+ # Signal that the thread is finished with the current connection.
255
+ # #release_connection releases the connection-thread association
256
+ # and returns the connection to the pool.
257
+ def release_connection(with_id = current_connection_id)
258
+ synchronize do
259
+ conn = @reserved_connections.delete(with_id)
260
+ checkin conn if conn
261
+ end
262
+ end
263
+
264
+ # If a connection already exists yield it to the block. If no connection
265
+ # exists checkout a connection, yield it to the block, and checkin the
266
+ # connection when finished.
267
+ def with_connection
268
+ connection_id = current_connection_id
269
+ fresh_connection = true unless active_connection?
270
+ yield connection
271
+ ensure
272
+ release_connection(connection_id) if fresh_connection
273
+ end
274
+
275
+ # Returns true if a connection has already been opened.
276
+ def connected?
277
+ synchronize { @connections.any? }
278
+ end
279
+
280
+ # Disconnects all connections in the pool, and clears the pool.
281
+ def disconnect!
282
+ synchronize do
283
+ @reserved_connections = {}
284
+ @connections.each do |conn|
285
+ checkin conn
286
+ conn.disconnect!
408
287
  end
288
+ @connections = []
289
+ @available.clear
409
290
  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
- }
291
+ end
292
+
293
+ # Clears the cache which maps classes.
294
+ def clear_reloadable_connections!
295
+ synchronize do
296
+ @reserved_connections = {}
297
+ @connections.each do |conn|
298
+ checkin conn
299
+ conn.disconnect! if conn.requires_reloading?
300
+ end
301
+ @connections.delete_if do |conn|
302
+ conn.requires_reloading?
303
+ end
304
+ @available.clear
305
+ @connections.each do |conn|
306
+ @available.add conn
418
307
  end
419
-
420
- @reserved_connections.delete thread_id if thread_id
421
308
  end
422
-
423
- def new_connection
424
- ActiveRecord::Base.send(spec.adapter_method, spec.config)
309
+ end
310
+
311
+ # clear_stale_cached imp from Rails 3.2, still using Threads.
312
+ # Yes, we've created a monster.
313
+ # Return any checked-out connections back to the pool by threads that
314
+ # are no longer alive.
315
+ def clear_stale_cached_connections!
316
+ keys = @reserved_connections.keys - Thread.list.find_all { |t|
317
+ t.alive?
318
+ }.map { |thread| thread.object_id }
319
+ keys.each do |key|
320
+ conn = @reserved_connections[key]
321
+ ActiveSupport::Deprecation.warn(<<-eowarn) if conn.in_use?
322
+ Database connections will not be closed automatically, please close your
323
+ database connection at the end of the thread by calling `close` on your
324
+ connection. For example: ActiveRecord::Base.connection.close
325
+ eowarn
326
+ checkin conn
327
+ @reserved_connections.delete(key)
425
328
  end
426
-
427
- def current_connection_id #:nodoc:
428
- ActiveRecord::Base.connection_id ||= Thread.current.object_id
329
+ end
330
+
331
+ # Check-out a database connection from the pool, indicating that you want
332
+ # to use it. You should call #checkin when you no longer need this.
333
+ #
334
+ # This is done by either returning and leasing existing connection, or by
335
+ # creating a new connection and leasing it.
336
+ #
337
+ # If all connections are leased and the pool is at capacity (meaning the
338
+ # number of currently leased connections is greater than or equal to the
339
+ # size limit set), an ActiveRecord::PoolFullError exception will be raised.
340
+ #
341
+ # Returns: an AbstractAdapter object.
342
+ #
343
+ # Raises:
344
+ # - PoolFullError: no connection can be obtained from the pool.
345
+ def checkout
346
+ synchronize do
347
+ conn = acquire_connection
348
+ conn.lease
349
+ checkout_and_verify(conn)
429
350
  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
351
+ end
352
+
353
+ # Check-in a database connection back into the pool, indicating that you
354
+ # no longer need this connection.
355
+ #
356
+ # +conn+: an AbstractAdapter object, which was obtained by earlier by
357
+ # calling +checkout+ on this pool.
358
+ def checkin(conn)
359
+ synchronize do
360
+ conn.run_callbacks :checkin do
361
+ conn.expire
362
+ end
363
+
364
+ release conn
365
+
366
+ @available.add conn
438
367
  end
439
-
440
- def checkout_and_verify(c)
441
- c.run_callbacks :checkout do
442
- c.verify!
368
+ end
369
+
370
+ # Remove a connection from the connection pool. The connection will
371
+ # remain open and active but will no longer be managed by this pool.
372
+ def remove(conn)
373
+ synchronize do
374
+ @connections.delete conn
375
+ @available.delete conn
376
+
377
+ # FIXME: we might want to store the key on the connection so that removing
378
+ # from the reserved hash will be a little easier.
379
+ release conn
380
+
381
+ @available.add checkout_new_connection if @available.any_waiting?
382
+ end
383
+ end
384
+
385
+ # Removes dead connections from the pool. A dead connection can occur
386
+ # if a programmer forgets to close a connection at the end of a thread
387
+ # or a thread dies unexpectedly.
388
+ def reap
389
+ synchronize do
390
+ stale = Time.now - @dead_connection_timeout
391
+ connections.dup.each do |conn|
392
+ remove conn if conn.in_use? && stale > conn.last_use && !conn.active?
443
393
  end
444
- c
445
394
  end
446
395
  end
447
-
448
-
396
+
397
+ private
398
+
399
+ # Acquire a connection by one of 1) immediately removing one
400
+ # from the queue of available connections, 2) creating a new
401
+ # connection if the pool is not at capacity, 3) waiting on the
402
+ # queue for a connection to become available.
403
+ #
404
+ # Raises:
405
+ # - PoolFullError if a connection could not be acquired (FIXME:
406
+ # why not ConnectionTimeoutError?
407
+ def acquire_connection
408
+ if conn = @available.poll
409
+ conn
410
+ elsif @connections.size < @size
411
+ checkout_new_connection
412
+ else
413
+ clear_stale_cached_connections!
414
+
415
+ t0 = Time.now
416
+
417
+ Rails.logger.info("POLLED_CHECKOUT: num avail connections: #{@available.num_available}; num waiting: #{@available.num_waiting}; total connections: #{@connections.size}")
418
+ begin
419
+ @available.poll(@checkout_timeout)
420
+ rescue ActiveRecord::ConnectionTimeoutError
421
+ msg = 'could not obtain a database connection within %0.3f seconds (waited %0.3f seconds)' %
422
+ [@checkout_timeout, Time.now - t0]
423
+ raise ActiveRecord::ConnectionTimeoutError, msg
424
+ end
425
+ end
426
+ end
427
+
428
+ def release(conn)
429
+ thread_id = if @reserved_connections[current_connection_id] == conn
430
+ current_connection_id
431
+ else
432
+ @reserved_connections.keys.find { |k|
433
+ @reserved_connections[k] == conn
434
+ }
435
+ end
436
+
437
+ @reserved_connections.delete thread_id if thread_id
438
+ end
439
+
440
+ def new_connection
441
+ ActiveRecord::Base.send(spec.adapter_method, spec.config)
442
+ end
443
+
444
+ def current_connection_id #:nodoc:
445
+ ActiveRecord::Base.connection_id ||= Thread.current.object_id
446
+ end
447
+
448
+ def checkout_new_connection
449
+ raise ConnectionNotEstablished unless @automatic_reconnect
450
+
451
+ c = new_connection
452
+ c.pool = self
453
+ @connections << c
454
+ c
455
+ end
456
+
457
+ def checkout_and_verify(c)
458
+ c.run_callbacks :checkout do
459
+ c.verify!
460
+ end
461
+ c
462
+ end
449
463
  end
450
- end
451
464
 
452
465
  end
453
466
 
@@ -156,8 +156,8 @@ module SearchMethods
156
156
 
157
157
  extra_info_xml = Nokogiri::XML( sfx_obj["EXTRA_INFO_XML"] )
158
158
 
159
- # Put SFX object id in rft.object_id, that's what SFX does.
160
- ctx.referent.set_metadata('object_id', sfx_obj["OBJECT_ID"])
159
+ # Put SFX object id in rft.object_id, that's what SFX does.
160
+ ctx.referent.set_metadata('object_id', sfx_obj["OBJECT_ID"].to_s )
161
161
  ctx.referent.set_metadata("jtitle", sfx_obj["TITLE_DISPLAY"] || "Unknown Title")
162
162
 
163
163
  issn = extra_info_xml.search("item[key=issn]").text
@@ -60,7 +60,7 @@ class Collection
60
60
  # Then actually executes the services that are dispatchable (queued).
61
61
  def dispatch_services!
62
62
  queued_service_ids = prepare_for_dispatch!
63
-
63
+
64
64
  dispatch_foreground!(queued_service_ids)
65
65
 
66
66
  dispatch_background!(queued_service_ids)
@@ -152,68 +152,70 @@ class Collection
152
152
  def prepare_for_dispatch!
153
153
  # Go through currently dispatched services, looking for timed out
154
154
  # services -- services still in progress that have taken too long,
155
- # as well as service responses that are too old to be used.
156
- umlaut_request.dispatched_services.each do | ds |
157
- # go through dispatched_services and set stil in progress but too long to failed temporary
158
- if ( (ds.status == DispatchedService::InProgress ||
159
- ds.status == DispatchedService::Queued ) &&
160
- (Time.now - ds.updated_at) > self.background_service_timeout)
161
-
162
- ds.store_exception( Exception.new("background service timed out (took longer than #{self.background_service_timeout} to run); thread assumed dead.")) unless ds.exception_info
163
- # Fail it temporary, it'll be run again.
164
- ds.status = DispatchedService::FailedTemporary
165
- ds.save!
166
- logger.warn("Background service timed out, thread assumed dead. #{umlaut_request.id} / #{ds.service_id}")
167
- end
168
-
169
- # go through dispatched_services and delete:
170
- # 1) old completed dispatches, too old to use.
171
- # 2) failedtemporary dispatches that are older than our resurrection time
172
- # -> And all responses associated with those dispatches.
173
- # After being deleted, they'll end up re-queued.
174
- if ( (ds.completed? && completed_dispatch_expired?(ds) ) ||
175
- ( ds.status == DispatchedService::FailedTemporary &&
176
- (Time.now - ds.updated_at) > self.requeue_failedtemporary_services
155
+ # as well as service responses that are too old to be used.
156
+ queued_service_ids = []
157
+ DispatchedService.transaction do
158
+ umlaut_request.dispatched_services.each do | ds |
159
+ # go through dispatched_services and set stil in progress but too long to failed temporary
160
+ if ( (ds.status == DispatchedService::InProgress ||
161
+ ds.status == DispatchedService::Queued ) &&
162
+ (Time.now - ds.updated_at) > self.background_service_timeout)
163
+
164
+ ds.store_exception( Exception.new("background service timed out (took longer than #{self.background_service_timeout} to run); thread assumed dead.")) unless ds.exception_info
165
+ # Fail it temporary, it'll be run again.
166
+ ds.status = DispatchedService::FailedTemporary
167
+ ds.save!
168
+ logger.warn("Background service timed out, thread assumed dead. #{umlaut_request.id} / #{ds.service_id}")
169
+ end
170
+
171
+ # go through dispatched_services and delete:
172
+ # 1) old completed dispatches, too old to use.
173
+ # 2) failedtemporary dispatches that are older than our resurrection time
174
+ # -> And all responses associated with those dispatches.
175
+ # After being deleted, they'll end up re-queued.
176
+ if ( (ds.completed? && completed_dispatch_expired?(ds) ) ||
177
+ ( ds.status == DispatchedService::FailedTemporary &&
178
+ (Time.now - ds.updated_at) > self.requeue_failedtemporary_services
179
+ )
177
180
  )
178
- )
181
+
182
+ # Need to expire. Delete all the service responses, and
183
+ # the DispatchedService record, and service will be automatically
184
+ # run again.
185
+ serv_id = ds.service_id
179
186
 
180
- # Need to expire. Delete all the service responses, and
181
- # the DispatchedService record, and service will be automatically
182
- # run again.
183
- serv_id = ds.service_id
184
-
185
- umlaut_request.service_responses.each do |response|
186
- if response.service_id == serv_id
187
- umlaut_request.service_responses.delete(response)
188
- response.destroy
187
+ umlaut_request.service_responses.each do |response|
188
+ if response.service_id == serv_id
189
+ umlaut_request.service_responses.delete(response)
190
+ response.destroy
191
+ end
189
192
  end
193
+
194
+ umlaut_request.dispatched_services.delete(ds)
195
+ ds.destroy
190
196
  end
197
+ end
191
198
 
192
- umlaut_request.dispatched_services.delete(ds)
193
- ds.destroy
194
- end
195
- end
196
-
197
- # Queue any services without a dispatch marker at all, keeping
198
- # track of queued services, already existing or newly created.
199
-
200
- # Just in case, we're going to refetch dispatched_services from the db,
201
- # in case some other http request or background service updated things
202
- # recently.
203
- umlaut_request.dispatched_services.reset
204
-
205
- queued_service_ids = []
206
- self.get_service_definitions.each do |service|
207
- service_id = service['service_id']
208
- # use in-memory #to_a search, don't go to db each time!
209
- if found = umlaut_request.dispatched_services.to_a.find {|s| s.service_id == service_id}
210
- queued_service_ids.push(service_id) if found.status == DispatchedService::Queued
211
- else
212
- umlaut_request.new_dispatch_object!(service_id, DispatchedService::Queued).save!
213
- queued_service_ids.push(service_id)
214
- end
199
+ # Queue any services without a dispatch marker at all, keeping
200
+ # track of queued services, already existing or newly created.
201
+
202
+ # Just in case, we're going to refetch dispatched_services from the db,
203
+ # in case some other http request or background service updated things
204
+ # recently.
205
+ umlaut_request.dispatched_services.reset
206
+
207
+ self.get_service_definitions.each do |service|
208
+ service_id = service['service_id']
209
+ # use in-memory #to_a search, don't go to db each time!
210
+ if found = umlaut_request.dispatched_services.to_a.find {|s| s.service_id == service_id}
211
+ queued_service_ids.push(service_id) if found.status == DispatchedService::Queued
212
+ else
213
+ umlaut_request.new_dispatch_object!(service_id, DispatchedService::Queued).save!
214
+ queued_service_ids.push(service_id)
215
+ end
216
+ end
215
217
  end
216
-
218
+
217
219
  return queued_service_ids
218
220
  end
219
221
 
@@ -109,6 +109,17 @@ class ServiceResponse < ActiveRecord::Base
109
109
  self.service.view_data_from_service_type(self)
110
110
  end
111
111
 
112
+
113
+ def service_data
114
+ # Fix weird-ass char encoding bug with AR serialize and hashes.
115
+ # https://github.com/rails/rails/issues/6538
116
+ data = super
117
+ if data.kind_of? Hash
118
+ data.values.each {|v| v.force_encoding "UTF-8" if v.respond_to? :force_encoding }
119
+ end
120
+ return data
121
+ end
122
+
112
123
  # Should take a ServiceTypeValue object, or symbol name of
113
124
  # ServiceTypeValue object.
114
125
  def service_type_value=(value)
@@ -95,7 +95,7 @@ class ServiceWave
95
95
 
96
96
  # Log it too, although experience shows it may never make it to the
97
97
  # log for mysterious reasons.
98
- Rails.logger.error(TermColor.color("Umlaut: Threaded service raised exception.", :red, true) + "Service: #{service.service_id}, #{e}\n #{clean_backtrace(e).join("\n ")}")
98
+ Rails.logger.error(TermColor.color("Umlaut: Threaded service raised exception.", :red, true) + " Service: #{service.service_id}, #{e.class} #{e.message}\n #{clean_backtrace(e).join("\n ")}")
99
99
 
100
100
  # And stick it in a thread variable too
101
101
  Thread.current[:exception] = e
@@ -0,0 +1,9 @@
1
+ class UmlautAddServiceResponseIndex < ActiveRecord::Migration
2
+ def up
3
+ add_index "service_responses", ["request_id"]
4
+ end
5
+
6
+ def down
7
+ remove_index "service_responses", ["request_id"]
8
+ end
9
+ end
@@ -131,7 +131,7 @@ class HathiTrust < Service
131
131
  isbn = isbn.gsub(/[\-\[\]]/, '') unless isbn.blank?
132
132
 
133
133
  oclcnum = get_identifier(:info, "oclcnum", rft)
134
- oclcnum = oclnum.gsub(/[\-\[\]]/, '') unless oclcnum.blank?
134
+ oclcnum = oclcnum.gsub(/[\-\[\]]/, '') unless oclcnum.blank?
135
135
 
136
136
  lccn = get_lccn(rft)
137
137
  lccn = lccn.gsub(/[\-\[\]]/, '') unless lccn.blank?
@@ -39,7 +39,7 @@ namespace :umlaut do
39
39
  # dynamically create a model for Permalinks in old
40
40
  # db, set up with connection info to old db.
41
41
  OldPermalink = Class.new(ActiveRecord::Base) do
42
- self.set_table_name("permalinks")
42
+ self.table_name = "permalinks"
43
43
  # just to be safe
44
44
  def read_only?
45
45
  true
data/lib/umlaut.rb CHANGED
@@ -38,9 +38,9 @@ module Umlaut
38
38
 
39
39
  # Patch with fixed 'fair' version of ConnectionPool, see
40
40
  # active_record_patch/connection_pool.rb
41
- initializer("#{engine_name}.patch_connection_pool", :before => "active_record.initialize_database") do |app|
41
+ #initializer("#{engine_name}.patch_connection_pool", :before => "active_record.initialize_database") do |app|
42
42
  load File.join(self.root, "active_record_patch", "connection_pool.rb")
43
- end
43
+ #end
44
44
 
45
45
 
46
46
 
@@ -1,3 +1,3 @@
1
1
  module Umlaut
2
- VERSION = "3.0.0beta9"
2
+ VERSION = "3.0.0beta10"
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.0beta9
4
+ version: 3.0.0beta10
5
5
  prerelease: 5
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-05-29 00:00:00.000000000 Z
12
+ date: 2012-05-30 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -318,7 +318,6 @@ files:
318
318
  - app/models/referent_value.rb
319
319
  - app/models/service_type_value.rb
320
320
  - app/models/service_store.rb
321
- - app/models/#Untitled-1#
322
321
  - app/models/sfx_db.rb
323
322
  - app/models/collection.rb
324
323
  - app/models/dispatched_service.rb
@@ -377,6 +376,7 @@ files:
377
376
  - app/helpers/resolve_helper.rb
378
377
  - app/helpers/search_helper.rb
379
378
  - db/seeds.rb
379
+ - db/migrate/02_umlaut_add_service_response_index.rb
380
380
  - db/migrate/01_umlaut_init.rb
381
381
  - db/orig_fixed_data/service_type_values.yml
382
382
  - lib/opensearch_query.rb
@@ -558,7 +558,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
558
558
  version: '0'
559
559
  segments:
560
560
  - 0
561
- hash: 1502362427621060492
561
+ hash: 2903597674342542131
562
562
  required_rubygems_version: !ruby/object:Gem::Requirement
563
563
  none: false
564
564
  requirements:
Binary file