Switch to using deadlock_retry as a gem

This commit is contained in:
Tom Hughes 2012-01-21 15:00:56 +00:00
parent e70396d70b
commit 682ccc18df
7 changed files with 3 additions and 180 deletions

View file

@ -20,6 +20,7 @@ gem 'validates_email_format_of', '>= 1.5.1'
gem 'composite_primary_keys', '>= 4.1.2'
gem 'http_accept_language', '>= 1.0.2'
gem 'paperclip', '~> 2.0'
gem 'deadlock_retry', '>= 1.1.2'
# Character conversion support for ruby 1.8
gem 'iconv', :platforms => :ruby_18

View file

@ -42,6 +42,7 @@ GEM
coffee-script-source (1.2.0)
composite_primary_keys (4.1.2)
activerecord (~> 3.1)
deadlock_retry (1.1.2)
dynamic_form (1.1.4)
erubis (2.7.0)
execjs (1.3.0)
@ -157,6 +158,7 @@ DEPENDENCIES
SystemTimer (>= 1.1.3)
coffee-rails (~> 3.2.1)
composite_primary_keys (>= 4.1.2)
deadlock_retry (>= 1.1.2)
dynamic_form
http_accept_language (>= 1.0.2)
httpclient

View file

@ -1,10 +0,0 @@
Deadlock Retry
==============
Deadlock retry allows the database adapter (currently only tested with the
MySQLAdapter) to retry transactions that fall into deadlock. It will retry
such transactions three times before finally failing.
This capability is automatically added to ActiveRecord. No code changes or otherwise are required.
Copyright (c) 2005 Jamis Buck, released under the MIT license

View file

@ -1,10 +0,0 @@
require 'rake'
require 'rake/testtask'
desc "Default task"
task :default => [ :test ]
Rake::TestTask.new do |t|
t.test_files = Dir["test/**/*_test.rb"]
t.verbose = true
end

View file

@ -1,5 +0,0 @@
require 'deadlock_retry'
if defined?(ActiveRecord::Base)
ActiveRecord::Base.send :include, DeadlockRetry
end

View file

@ -1,60 +0,0 @@
# Copyright (c) 2005 Jamis Buck
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module DeadlockRetry
def self.append_features(base)
super
base.extend(ClassMethods)
base.class_eval do
class <<self
alias_method :transaction_without_deadlock_handling, :transaction
alias_method :transaction, :transaction_with_deadlock_handling
end
end
end
module ClassMethods
DEADLOCK_ERROR_MESSAGES = [
"Deadlock found when trying to get lock",
"Lock wait timeout exceeded",
"deadlock detected"
]
MAXIMUM_RETRIES_ON_DEADLOCK = 3
def transaction_with_deadlock_handling(*objects, &block)
retry_count = 0
begin
transaction_without_deadlock_handling(*objects, &block)
rescue ActiveRecord::StatementInvalid => error
raise unless connection.open_transactions.zero?
if DEADLOCK_ERROR_MESSAGES.any? { |msg| error.message =~ /#{Regexp.escape(msg)}/ }
raise if retry_count >= MAXIMUM_RETRIES_ON_DEADLOCK
retry_count += 1
logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
retry
else
raise
end
end
end
end
end

View file

@ -1,95 +0,0 @@
begin
require 'active_record'
rescue LoadError
if ENV['ACTIVERECORD_PATH'].nil?
abort <<MSG
Please set the ACTIVERECORD_PATH environment variable to the directory
containing the active_record.rb file.
MSG
else
$LOAD_PATH.unshift << ENV['ACTIVERECORD_PATH']
begin
require 'active_record'
rescue LoadError
abort "ActiveRecord could not be found."
end
end
end
require 'test/unit'
require "#{File.dirname(__FILE__)}/../lib/deadlock_retry"
class MockModel
@@open_transactions = 0
def self.transaction(*objects)
@@open_transactions += 1
yield
ensure
@@open_transactions -= 1
end
def self.open_transactions
@@open_transactions
end
def self.connection
self
end
def self.logger
@logger ||= Logger.new(nil)
end
include DeadlockRetry
end
class DeadlockRetryTest < Test::Unit::TestCase
DEADLOCK_ERROR = "MySQL::Error: Deadlock found when trying to get lock"
TIMEOUT_ERROR = "MySQL::Error: Lock wait timeout exceeded"
def test_no_errors
assert_equal :success, MockModel.transaction { :success }
end
def test_no_errors_with_deadlock
errors = [ DEADLOCK_ERROR ] * 3
assert_equal :success, MockModel.transaction { raise ActiveRecord::StatementInvalid, errors.shift unless errors.empty?; :success }
assert errors.empty?
end
def test_no_errors_with_lock_timeout
errors = [ TIMEOUT_ERROR ] * 3
assert_equal :success, MockModel.transaction { raise ActiveRecord::StatementInvalid, errors.shift unless errors.empty?; :success }
assert errors.empty?
end
def test_error_if_limit_exceeded
assert_raise(ActiveRecord::StatementInvalid) do
MockModel.transaction { raise ActiveRecord::StatementInvalid, DEADLOCK_ERROR }
end
end
def test_error_if_unrecognized_error
assert_raise(ActiveRecord::StatementInvalid) do
MockModel.transaction { raise ActiveRecord::StatementInvalid, "Something else" }
end
end
def test_error_in_nested_transaction_should_retry_outermost_transaction
tries = 0
errors = 0
MockModel.transaction do
tries += 1
MockModel.transaction do
MockModel.transaction do
errors += 1
raise ActiveRecord::StatementInvalid, "MySQL::Error: Lock wait timeout exceeded" unless errors > 3
end
end
end
assert_equal 4, tries
end
end