mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Merge pull request #25093 from Erol/activerecord-transaction-serialization-error
Introduce AR::TransactionSerializationError for transaction serialization failures or deadlocks
This commit is contained in:
commit
c0d4aa2293
6 changed files with 163 additions and 3 deletions
|
@ -8,4 +8,9 @@
|
||||||
|
|
||||||
*Johannes Opper*
|
*Johannes Opper*
|
||||||
|
|
||||||
|
* Introduce ActiveRecord::TransactionSerializationError for catching
|
||||||
|
transaction serialization failures or deadlocks.
|
||||||
|
|
||||||
|
*Erol Fornoles*
|
||||||
|
|
||||||
Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/activerecord/CHANGELOG.md) for previous changes.
|
Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/activerecord/CHANGELOG.md) for previous changes.
|
||||||
|
|
|
@ -727,14 +727,22 @@ module ActiveRecord
|
||||||
column_names.map {|name| quote_column_name(name) + option_strings[name]}
|
column_names.map {|name| quote_column_name(name) + option_strings[name]}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html
|
||||||
|
ER_DUP_ENTRY = 1062
|
||||||
|
ER_NO_REFERENCED_ROW_2 = 1452
|
||||||
|
ER_DATA_TOO_LONG = 1406
|
||||||
|
ER_LOCK_DEADLOCK = 1213
|
||||||
|
|
||||||
def translate_exception(exception, message)
|
def translate_exception(exception, message)
|
||||||
case error_number(exception)
|
case error_number(exception)
|
||||||
when 1062
|
when ER_DUP_ENTRY
|
||||||
RecordNotUnique.new(message)
|
RecordNotUnique.new(message)
|
||||||
when 1452
|
when ER_NO_REFERENCED_ROW_2
|
||||||
InvalidForeignKey.new(message)
|
InvalidForeignKey.new(message)
|
||||||
when 1406
|
when ER_DATA_TOO_LONG
|
||||||
ValueTooLong.new(message)
|
ValueTooLong.new(message)
|
||||||
|
when ER_LOCK_DEADLOCK
|
||||||
|
TransactionSerializationError.new(message)
|
||||||
else
|
else
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
|
@ -406,6 +406,7 @@ module ActiveRecord
|
||||||
VALUE_LIMIT_VIOLATION = "22001"
|
VALUE_LIMIT_VIOLATION = "22001"
|
||||||
FOREIGN_KEY_VIOLATION = "23503"
|
FOREIGN_KEY_VIOLATION = "23503"
|
||||||
UNIQUE_VIOLATION = "23505"
|
UNIQUE_VIOLATION = "23505"
|
||||||
|
SERIALIZATION_FAILURE = "40001"
|
||||||
|
|
||||||
def translate_exception(exception, message)
|
def translate_exception(exception, message)
|
||||||
return exception unless exception.respond_to?(:result)
|
return exception unless exception.respond_to?(:result)
|
||||||
|
@ -417,6 +418,8 @@ module ActiveRecord
|
||||||
InvalidForeignKey.new(message)
|
InvalidForeignKey.new(message)
|
||||||
when VALUE_LIMIT_VIOLATION
|
when VALUE_LIMIT_VIOLATION
|
||||||
ValueTooLong.new(message)
|
ValueTooLong.new(message)
|
||||||
|
when SERIALIZATION_FAILURE
|
||||||
|
TransactionSerializationError.new(message)
|
||||||
else
|
else
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
|
@ -285,6 +285,16 @@ module ActiveRecord
|
||||||
class TransactionIsolationError < ActiveRecordError
|
class TransactionIsolationError < ActiveRecordError
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TransactionSerializationError will be raised when a transaction is rolled
|
||||||
|
# back by the database due to a serialization failure or a deadlock.
|
||||||
|
#
|
||||||
|
# See the following:
|
||||||
|
#
|
||||||
|
# * http://www.postgresql.org/docs/current/static/transaction-iso.html
|
||||||
|
# * https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html#error_er_lock_deadlock
|
||||||
|
class TransactionSerializationError < ActiveRecordError
|
||||||
|
end
|
||||||
|
|
||||||
# IrreversibleOrderError is raised when a relation's order is too complex for
|
# IrreversibleOrderError is raised when a relation's order is too complex for
|
||||||
# +reverse_order+ to automatically reverse.
|
# +reverse_order+ to automatically reverse.
|
||||||
class IrreversibleOrderError < ActiveRecordError
|
class IrreversibleOrderError < ActiveRecordError
|
||||||
|
|
62
activerecord/test/cases/adapters/mysql2/transaction_test.rb
Normal file
62
activerecord/test/cases/adapters/mysql2/transaction_test.rb
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
require "cases/helper"
|
||||||
|
require 'support/connection_helper'
|
||||||
|
|
||||||
|
module ActiveRecord
|
||||||
|
class Mysql2TransactionTest < ActiveRecord::Mysql2TestCase
|
||||||
|
self.use_transactional_tests = false
|
||||||
|
|
||||||
|
class Sample < ActiveRecord::Base
|
||||||
|
self.table_name = 'samples'
|
||||||
|
end
|
||||||
|
|
||||||
|
setup do
|
||||||
|
@connection = ActiveRecord::Base.connection
|
||||||
|
@connection.clear_cache!
|
||||||
|
|
||||||
|
@connection.transaction do
|
||||||
|
@connection.drop_table 'samples', if_exists: true
|
||||||
|
@connection.create_table('samples') do |t|
|
||||||
|
t.integer 'value'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Sample.reset_column_information
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
@connection.drop_table 'samples', if_exists: true
|
||||||
|
end
|
||||||
|
|
||||||
|
test "raises error when a serialization failure occurs" do
|
||||||
|
assert_raises(ActiveRecord::TransactionSerializationError) do
|
||||||
|
thread = Thread.new do
|
||||||
|
Sample.transaction isolation: :serializable do
|
||||||
|
Sample.delete_all
|
||||||
|
|
||||||
|
10.times do |i|
|
||||||
|
sleep 0.1
|
||||||
|
|
||||||
|
Sample.create value: i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
sleep 0.1
|
||||||
|
|
||||||
|
Sample.transaction isolation: :serializable do
|
||||||
|
Sample.delete_all
|
||||||
|
|
||||||
|
10.times do |i|
|
||||||
|
sleep 0.1
|
||||||
|
|
||||||
|
Sample.create value: i
|
||||||
|
end
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
end
|
||||||
|
|
||||||
|
thread.join
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,72 @@
|
||||||
|
require "cases/helper"
|
||||||
|
require 'support/connection_helper'
|
||||||
|
|
||||||
|
module ActiveRecord
|
||||||
|
class PostgresqlTransactionTest < ActiveRecord::PostgreSQLTestCase
|
||||||
|
self.use_transactional_tests = false
|
||||||
|
|
||||||
|
class Sample < ActiveRecord::Base
|
||||||
|
self.table_name = 'samples'
|
||||||
|
end
|
||||||
|
|
||||||
|
setup do
|
||||||
|
@connection = ActiveRecord::Base.connection
|
||||||
|
|
||||||
|
@connection.transaction do
|
||||||
|
@connection.drop_table 'samples', if_exists: true
|
||||||
|
@connection.create_table('samples') do |t|
|
||||||
|
t.integer 'value'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Sample.reset_column_information
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
@connection.drop_table 'samples', if_exists: true
|
||||||
|
end
|
||||||
|
|
||||||
|
test "raises error when a serialization failure occurs" do
|
||||||
|
with_warning_suppression do
|
||||||
|
assert_raises(ActiveRecord::TransactionSerializationError) do
|
||||||
|
thread = Thread.new do
|
||||||
|
Sample.transaction isolation: :serializable do
|
||||||
|
Sample.delete_all
|
||||||
|
|
||||||
|
10.times do |i|
|
||||||
|
sleep 0.1
|
||||||
|
|
||||||
|
Sample.create value: i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
sleep 0.1
|
||||||
|
|
||||||
|
Sample.transaction isolation: :serializable do
|
||||||
|
Sample.delete_all
|
||||||
|
|
||||||
|
10.times do |i|
|
||||||
|
sleep 0.1
|
||||||
|
|
||||||
|
Sample.create value: i
|
||||||
|
end
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
end
|
||||||
|
|
||||||
|
thread.join
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def with_warning_suppression
|
||||||
|
log_level = @connection.client_min_messages
|
||||||
|
@connection.client_min_messages = 'error'
|
||||||
|
yield
|
||||||
|
@connection.client_min_messages = log_level
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue