Permalink
Browse files

Deadlock retry allows the database adapter (currently only tested wit…

…h the MySQLAdapter) to retry transactions that fall into deadlock. It will retry such transactions three times before finally failing.
  • Loading branch information...
0 parents commit 61712b79756cfeb305f45367057c9e3f1a894cb2 @dhh dhh committed Oct 29, 2005
Showing with 145 additions and 0 deletions.
  1. +10 −0 README
  2. +10 −0 Rakefile
  3. +2 −0 init.rb
  4. +58 −0 lib/deadlock_retry.rb
  5. +65 −0 test/deadlock_retry_test.rb
@@ -0,0 +1,10 @@
+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 is required.
+
+Copyright (c) 2005 Jamis Buck, released under the MIT license
@@ -0,0 +1,10 @@
+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
@@ -0,0 +1,2 @@
+require 'deadlock_retry'
+ActiveRecord::Base.send :include, DeadlockRetry
@@ -0,0 +1,58 @@
+# 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"
+ ]
+
+ 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
+ if DEADLOCK_ERROR_MESSAGES.any? { |msg| error.message =~ /^#{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
@@ -0,0 +1,65 @@
+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
+ def self.transaction(*objects, &block)
+ block.call
+ end
+
+ def self.logger
+ @logger ||= Logger.new(nil)
+ end
+
+ include DeadlockRetry
+end
+
+class DeadlockRetryTest < Test::Unit::TestCase
+ DEADLOCK_ERROR = "Deadlock found when trying to get lock"
+ TIMEOUT_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
+end

0 comments on commit 61712b7

Please sign in to comment.