Skip to content

Commit 71edd53

Browse files
author
ballot
committed
adding deadlock retry plugin
1 parent af19897 commit 71edd53

File tree

6 files changed

+177
-0
lines changed

6 files changed

+177
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.swp

vendor/plugins/deadlock_retry/README

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Deadlock Retry
2+
==============
3+
4+
Deadlock retry allows the database adapter (currently only tested with the
5+
MySQLAdapter) to retry transactions that fall into deadlock. It will retry
6+
such transactions three times before finally failing.
7+
8+
This capability is automatically added to ActiveRecord. No code changes or otherwise are required.
9+
10+
Copyright (c) 2005 Jamis Buck, released under the MIT license
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
require 'rake'
2+
require 'rake/testtask'
3+
4+
desc "Default task"
5+
task :default => [ :test ]
6+
7+
Rake::TestTask.new do |t|
8+
t.test_files = Dir["test/**/*_test.rb"]
9+
t.verbose = true
10+
end

vendor/plugins/deadlock_retry/init.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
require 'deadlock_retry'
2+
ActiveRecord::Base.send :include, DeadlockRetry
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Copyright (c) 2005 Jamis Buck
2+
#
3+
# Permission is hereby granted, free of charge, to any person obtaining
4+
# a copy of this software and associated documentation files (the
5+
# "Software"), to deal in the Software without restriction, including
6+
# without limitation the rights to use, copy, modify, merge, publish,
7+
# distribute, sublicense, and/or sell copies of the Software, and to
8+
# permit persons to whom the Software is furnished to do so, subject to
9+
# the following conditions:
10+
#
11+
# The above copyright notice and this permission notice shall be
12+
# included in all copies or substantial portions of the Software.
13+
#
14+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21+
module DeadlockRetry
22+
def self.append_features(base)
23+
super
24+
base.extend(ClassMethods)
25+
base.class_eval do
26+
class <<self
27+
alias_method :transaction_without_deadlock_handling, :transaction
28+
alias_method :transaction, :transaction_with_deadlock_handling
29+
end
30+
end
31+
end
32+
33+
module ClassMethods
34+
DEADLOCK_ERROR_MESSAGES = [
35+
"Deadlock found when trying to get lock",
36+
"Lock wait timeout exceeded"
37+
]
38+
39+
MAXIMUM_RETRIES_ON_DEADLOCK = 3
40+
41+
def transaction_with_deadlock_handling(*objects, &block)
42+
retry_count = 0
43+
44+
begin
45+
transaction_without_deadlock_handling(*objects, &block)
46+
rescue ActiveRecord::StatementInvalid => error
47+
raise unless connection.open_transactions.zero?
48+
if DEADLOCK_ERROR_MESSAGES.any? { |msg| error.message =~ /#{Regexp.escape(msg)}/ }
49+
raise if retry_count >= MAXIMUM_RETRIES_ON_DEADLOCK
50+
retry_count += 1
51+
logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
52+
retry
53+
else
54+
raise
55+
end
56+
end
57+
end
58+
end
59+
end
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
begin
2+
require 'active_record'
3+
rescue LoadError
4+
if ENV['ACTIVERECORD_PATH'].nil?
5+
abort <<MSG
6+
Please set the ACTIVERECORD_PATH environment variable to the directory
7+
containing the active_record.rb file.
8+
MSG
9+
else
10+
$LOAD_PATH.unshift << ENV['ACTIVERECORD_PATH']
11+
begin
12+
require 'active_record'
13+
rescue LoadError
14+
abort "ActiveRecord could not be found."
15+
end
16+
end
17+
end
18+
19+
require 'test/unit'
20+
require "#{File.dirname(__FILE__)}/../lib/deadlock_retry"
21+
22+
class MockModel
23+
@@open_transactions = 0
24+
25+
def self.transaction(*objects)
26+
@@open_transactions += 1
27+
yield
28+
ensure
29+
@@open_transactions -= 1
30+
end
31+
32+
def self.open_transactions
33+
@@open_transactions
34+
end
35+
36+
def self.connection
37+
self
38+
end
39+
40+
def self.logger
41+
@logger ||= Logger.new(nil)
42+
end
43+
44+
include DeadlockRetry
45+
end
46+
47+
class DeadlockRetryTest < Test::Unit::TestCase
48+
DEADLOCK_ERROR = "MySQL::Error: Deadlock found when trying to get lock"
49+
TIMEOUT_ERROR = "MySQL::Error: Lock wait timeout exceeded"
50+
51+
def test_no_errors
52+
assert_equal :success, MockModel.transaction { :success }
53+
end
54+
55+
def test_no_errors_with_deadlock
56+
errors = [ DEADLOCK_ERROR ] * 3
57+
assert_equal :success, MockModel.transaction { raise ActiveRecord::StatementInvalid, errors.shift unless errors.empty?; :success }
58+
assert errors.empty?
59+
end
60+
61+
def test_no_errors_with_lock_timeout
62+
errors = [ TIMEOUT_ERROR ] * 3
63+
assert_equal :success, MockModel.transaction { raise ActiveRecord::StatementInvalid, errors.shift unless errors.empty?; :success }
64+
assert errors.empty?
65+
end
66+
67+
def test_error_if_limit_exceeded
68+
assert_raise(ActiveRecord::StatementInvalid) do
69+
MockModel.transaction { raise ActiveRecord::StatementInvalid, DEADLOCK_ERROR }
70+
end
71+
end
72+
73+
def test_error_if_unrecognized_error
74+
assert_raise(ActiveRecord::StatementInvalid) do
75+
MockModel.transaction { raise ActiveRecord::StatementInvalid, "Something else" }
76+
end
77+
end
78+
79+
def test_error_in_nested_transaction_should_retry_outermost_transaction
80+
tries = 0
81+
errors = 0
82+
83+
MockModel.transaction do
84+
tries += 1
85+
MockModel.transaction do
86+
MockModel.transaction do
87+
errors += 1
88+
raise ActiveRecord::StatementInvalid, "MySQL::Error: Lock wait timeout exceeded" unless errors > 3
89+
end
90+
end
91+
end
92+
93+
assert_equal 4, tries
94+
end
95+
end

0 commit comments

Comments
 (0)