umlaut 3.0.0beta9 → 3.0.0beta10

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