Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

log_innodb_status is not safe #5

Merged
merged 4 commits into from
May 13, 2011
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion deadlock_retry.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Gem::Specification.new do |s|
s.name = %q{deadlock_retry}
s.version = "1.1.0"
s.version = "1.1.1"
s.authors = ["Jamis Buck", "Mike Perham"]
s.description = s.summary = %q{Provides automatic deadlock retry and logging functionality for ActiveRecord and MySQL}
s.email = %q{mperham@gmail.com}
Expand Down
37 changes: 33 additions & 4 deletions lib/deadlock_retry.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
module DeadlockRetry

def self.included(base)
base.extend(ClassMethods)
base.class_eval do
Expand All @@ -9,6 +8,16 @@ class << self
end
end

@@innodb_status_available = nil

def self.innodb_status_available?
@@innodb_status_available
end

def self.innodb_status_available=(bool)
@@innodb_status_available = bool
end

module ClassMethods
DEADLOCK_ERROR_MESSAGES = [
"Deadlock found when trying to get lock",
Expand All @@ -17,9 +26,12 @@ module ClassMethods

MAXIMUM_RETRIES_ON_DEADLOCK = 3


def transaction_with_deadlock_handling(*objects, &block)
retry_count = 0

check_innodb_status_available

begin
transaction_without_deadlock_handling(*objects, &block)
rescue ActiveRecord::StatementInvalid => error
Expand All @@ -28,7 +40,7 @@ def transaction_with_deadlock_handling(*objects, &block)
raise if retry_count >= MAXIMUM_RETRIES_ON_DEADLOCK
retry_count += 1
logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
log_innodb_status
log_innodb_status if DeadlockRetry.innodb_status_available?
retry
else
raise
Expand All @@ -43,10 +55,27 @@ def in_nested_transaction?
connection.open_transactions != 0
end

def show_innodb_status
self.connection.select_value("show innodb status")
end

# Should we try to log innodb status -- if we don't have permission to,
# we actually break in-flight transactions, silently (!)
def check_innodb_status_available
return unless DeadlockRetry.innodb_status_available?.nil?

begin
show_innodb_status
DeadlockRetry.innodb_status_available = true
rescue
DeadlockRetry.innodb_status_available = false
end
end

def log_innodb_status
# show innodb status is the only way to get visiblity into why
# the transaction deadlocked. log it.
lines = connection.select_value("show innodb status")
lines = show_innodb_status
logger.warn "INNODB Status follows:"
lines.each_line do |line|
logger.warn line
Expand All @@ -59,4 +88,4 @@ def log_innodb_status
end
end

ActiveRecord::Base.send(:include, DeadlockRetry)
ActiveRecord::Base.send(:include, DeadlockRetry)
20 changes: 19 additions & 1 deletion test/deadlock_retry_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,24 @@ def self.logger
@logger ||= Logger.new(nil)
end

def self.show_innodb_status
[]
end

def self.reset_innodb_status_availability
DeadlockRetry.innodb_status_availability = 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 setup
end

def test_no_errors
assert_equal :success, MockModel.transaction { :success }
end
Expand Down Expand Up @@ -71,6 +82,13 @@ def test_included_by_default
assert ActiveRecord::Base.respond_to?(:transaction_with_deadlock_handling)
end

def test_innodb_status_availability
DeadlockRetry.innodb_status_available = nil
MockModel.transaction {}
assert_equal true, DeadlockRetry.innodb_status_available?
end


def test_error_in_nested_transaction_should_retry_outermost_transaction
tries = 0
errors = 0
Expand All @@ -84,7 +102,7 @@ def test_error_in_nested_transaction_should_retry_outermost_transaction
end
end
end

assert_equal 4, tries
end
end