mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Support for specifying transaction isolation level
If your database supports setting the isolation level for a transaction, you can set it like so: Post.transaction(isolation: :serializable) do # ... end Valid isolation levels are: * `:read_uncommitted` * `:read_committed` * `:repeatable_read` * `:serializable` You should consult the documentation for your database to understand the semantics of these different levels: * http://www.postgresql.org/docs/9.1/static/transaction-iso.html * https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html An `ActiveRecord::TransactionIsolationError` will be raised if: * The adapter does not support setting the isolation level * You are joining an existing open transaction * You are creating a nested (savepoint) transaction The mysql, mysql2 and postgresql adapters support setting the transaction isolation level. However, support is disabled for mysql versions below 5, because they are affected by a bug (http://bugs.mysql.com/bug.php?id=39170) which means the isolation level gets persisted outside the transaction.
This commit is contained in:
parent
834d6da54e
commit
392eeecc11
9 changed files with 263 additions and 15 deletions
|
@ -1,5 +1,40 @@
|
|||
## Rails 4.0.0 (unreleased) ##
|
||||
|
||||
* Support for specifying transaction isolation level
|
||||
|
||||
If your database supports setting the isolation level for a transaction, you can set
|
||||
it like so:
|
||||
|
||||
Post.transaction(isolation: :serializable) do
|
||||
# ...
|
||||
end
|
||||
|
||||
Valid isolation levels are:
|
||||
|
||||
* `:read_uncommitted`
|
||||
* `:read_committed`
|
||||
* `:repeatable_read`
|
||||
* `:serializable`
|
||||
|
||||
You should consult the documentation for your database to understand the
|
||||
semantics of these different levels:
|
||||
|
||||
* http://www.postgresql.org/docs/9.1/static/transaction-iso.html
|
||||
* https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html
|
||||
|
||||
An `ActiveRecord::TransactionIsolationError` will be raised if:
|
||||
|
||||
* The adapter does not support setting the isolation level
|
||||
* You are joining an existing open transaction
|
||||
* You are creating a nested (savepoint) transaction
|
||||
|
||||
The mysql, mysql2 and postgresql adapters support setting the transaction
|
||||
isolation level. However, support is disabled for mysql versions below 5,
|
||||
because they are affected by a bug (http://bugs.mysql.com/bug.php?id=39170)
|
||||
which means the isolation level gets persisted outside the transaction.
|
||||
|
||||
*Jon Leighton*
|
||||
|
||||
* `ActiveModel::ForbiddenAttributesProtection` is included by default
|
||||
in Active Record models. Check the docs of `ActiveModel::ForbiddenAttributesProtection`
|
||||
for more details.
|
||||
|
|
|
@ -155,10 +155,47 @@ module ActiveRecord
|
|||
# # active_record_1 now automatically released
|
||||
# end # RELEASE SAVEPOINT active_record_1 <--- BOOM! database error!
|
||||
# end
|
||||
#
|
||||
# == Transaction isolation
|
||||
#
|
||||
# If your database supports setting the isolation level for a transaction, you can set
|
||||
# it like so:
|
||||
#
|
||||
# Post.transaction(isolation: :serializable) do
|
||||
# # ...
|
||||
# end
|
||||
#
|
||||
# Valid isolation levels are:
|
||||
#
|
||||
# * <tt>:read_uncommitted</tt>
|
||||
# * <tt>:read_committed</tt>
|
||||
# * <tt>:repeatable_read</tt>
|
||||
# * <tt>:serializable</tt>
|
||||
#
|
||||
# You should consult the documentation for your database to understand the
|
||||
# semantics of these different levels:
|
||||
#
|
||||
# * http://www.postgresql.org/docs/9.1/static/transaction-iso.html
|
||||
# * https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html
|
||||
#
|
||||
# An <tt>ActiveRecord::TransactionIsolationError</tt> will be raised if:
|
||||
#
|
||||
# * The adapter does not support setting the isolation level
|
||||
# * You are joining an existing open transaction
|
||||
# * You are creating a nested (savepoint) transaction
|
||||
#
|
||||
# The mysql, mysql2 and postgresql adapters support setting the transaction
|
||||
# isolation level. However, support is disabled for mysql versions below 5,
|
||||
# because they are affected by a bug[http://bugs.mysql.com/bug.php?id=39170]
|
||||
# which means the isolation level gets persisted outside the transaction.
|
||||
def transaction(options = {})
|
||||
options.assert_valid_keys :requires_new, :joinable
|
||||
options.assert_valid_keys :requires_new, :joinable, :isolation
|
||||
|
||||
if !options[:requires_new] && current_transaction.joinable?
|
||||
if options[:isolation]
|
||||
raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction"
|
||||
end
|
||||
|
||||
yield
|
||||
else
|
||||
within_new_transaction(options) { yield }
|
||||
|
@ -168,10 +205,10 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def within_new_transaction(options = {}) #:nodoc:
|
||||
begin_transaction(options)
|
||||
transaction = begin_transaction(options)
|
||||
yield
|
||||
rescue Exception => error
|
||||
rollback_transaction
|
||||
rollback_transaction if transaction
|
||||
raise
|
||||
ensure
|
||||
begin
|
||||
|
@ -191,9 +228,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def begin_transaction(options = {}) #:nodoc:
|
||||
@transaction = @transaction.begin
|
||||
@transaction.joinable = options.fetch(:joinable, true)
|
||||
@transaction
|
||||
@transaction = @transaction.begin(options)
|
||||
end
|
||||
|
||||
def commit_transaction #:nodoc:
|
||||
|
@ -217,6 +252,22 @@ module ActiveRecord
|
|||
# Begins the transaction (and turns off auto-committing).
|
||||
def begin_db_transaction() end
|
||||
|
||||
def transaction_isolation_levels
|
||||
{
|
||||
read_uncommitted: "READ UNCOMMITTED",
|
||||
read_committed: "READ COMMITTED",
|
||||
repeatable_read: "REPEATABLE READ",
|
||||
serializable: "SERIALIZABLE"
|
||||
}
|
||||
end
|
||||
|
||||
# Begins the transaction with the isolation level set. Raises an error by
|
||||
# default; adapters that support setting the isolation level should implement
|
||||
# this method.
|
||||
def begin_isolated_db_transaction(isolation)
|
||||
raise ActiveRecord::TransactionIsolationError, "adapter does not support setting transaction isolation"
|
||||
end
|
||||
|
||||
# Commits the transaction (and turns on auto-committing).
|
||||
def commit_db_transaction() end
|
||||
|
||||
|
|
|
@ -13,8 +13,8 @@ module ActiveRecord
|
|||
0
|
||||
end
|
||||
|
||||
def begin
|
||||
RealTransaction.new(connection, self)
|
||||
def begin(options = {})
|
||||
RealTransaction.new(connection, self, options)
|
||||
end
|
||||
|
||||
def closed?
|
||||
|
@ -38,13 +38,13 @@ module ActiveRecord
|
|||
attr_reader :parent, :records
|
||||
attr_writer :joinable
|
||||
|
||||
def initialize(connection, parent)
|
||||
def initialize(connection, parent, options = {})
|
||||
super connection
|
||||
|
||||
@parent = parent
|
||||
@records = []
|
||||
@finishing = false
|
||||
@joinable = true
|
||||
@joinable = options.fetch(:joinable, true)
|
||||
end
|
||||
|
||||
# This state is necesarry so that we correctly handle stuff that might
|
||||
|
@ -66,11 +66,11 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def begin
|
||||
def begin(options = {})
|
||||
if finishing?
|
||||
parent.begin
|
||||
else
|
||||
SavepointTransaction.new(connection, self)
|
||||
SavepointTransaction.new(connection, self, options)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -120,9 +120,14 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
class RealTransaction < OpenTransaction #:nodoc:
|
||||
def initialize(connection, parent)
|
||||
def initialize(connection, parent, options = {})
|
||||
super
|
||||
connection.begin_db_transaction
|
||||
|
||||
if options[:isolation]
|
||||
connection.begin_isolated_db_transaction(options[:isolation])
|
||||
else
|
||||
connection.begin_db_transaction
|
||||
end
|
||||
end
|
||||
|
||||
def perform_rollback
|
||||
|
@ -137,7 +142,11 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
class SavepointTransaction < OpenTransaction #:nodoc:
|
||||
def initialize(connection, parent)
|
||||
def initialize(connection, parent, options = {})
|
||||
if options[:isolation]
|
||||
raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction"
|
||||
end
|
||||
|
||||
super
|
||||
connection.create_savepoint
|
||||
end
|
||||
|
|
|
@ -167,6 +167,11 @@ module ActiveRecord
|
|||
false
|
||||
end
|
||||
|
||||
# Does this adapter support setting the isolation level for a transaction?
|
||||
def supports_transaction_isolation?
|
||||
false
|
||||
end
|
||||
|
||||
# QUOTING ==================================================
|
||||
|
||||
# Returns a bind substitution value given a +column+ and list of current
|
||||
|
|
|
@ -169,6 +169,14 @@ module ActiveRecord
|
|||
true
|
||||
end
|
||||
|
||||
# MySQL 4 technically support transaction isolation, but it is affected by a bug
|
||||
# where the transaction level gets persisted for the whole session:
|
||||
#
|
||||
# http://bugs.mysql.com/bug.php?id=39170
|
||||
def supports_transaction_isolation?
|
||||
version[0] >= 5
|
||||
end
|
||||
|
||||
def native_database_types
|
||||
NATIVE_DATABASE_TYPES
|
||||
end
|
||||
|
@ -269,6 +277,13 @@ module ActiveRecord
|
|||
# Transactions aren't supported
|
||||
end
|
||||
|
||||
def begin_isolated_db_transaction(isolation)
|
||||
execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}"
|
||||
begin_db_transaction
|
||||
rescue
|
||||
# Transactions aren't supported
|
||||
end
|
||||
|
||||
def commit_db_transaction #:nodoc:
|
||||
execute "COMMIT"
|
||||
rescue
|
||||
|
|
|
@ -205,6 +205,11 @@ module ActiveRecord
|
|||
execute "BEGIN"
|
||||
end
|
||||
|
||||
def begin_isolated_db_transaction(isolation)
|
||||
begin_db_transaction
|
||||
execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}"
|
||||
end
|
||||
|
||||
# Commits a transaction.
|
||||
def commit_db_transaction
|
||||
execute "COMMIT"
|
||||
|
|
|
@ -370,6 +370,10 @@ module ActiveRecord
|
|||
true
|
||||
end
|
||||
|
||||
def supports_transaction_isolation?
|
||||
true
|
||||
end
|
||||
|
||||
class StatementPool < ConnectionAdapters::StatementPool
|
||||
def initialize(connection, max)
|
||||
super
|
||||
|
|
|
@ -195,4 +195,7 @@ module ActiveRecord
|
|||
|
||||
class ImmutableRelation < ActiveRecordError
|
||||
end
|
||||
|
||||
class TransactionIsolationError < ActiveRecordError
|
||||
end
|
||||
end
|
||||
|
|
121
activerecord/test/cases/transaction_isolation_test.rb
Normal file
121
activerecord/test/cases/transaction_isolation_test.rb
Normal file
|
@ -0,0 +1,121 @@
|
|||
require 'cases/helper'
|
||||
|
||||
class TransactionIsolationUnsupportedTest < ActiveRecord::TestCase
|
||||
self.use_transactional_fixtures = false
|
||||
|
||||
class Tag < ActiveRecord::Base
|
||||
end
|
||||
|
||||
setup do
|
||||
if ActiveRecord::Base.connection.supports_transaction_isolation?
|
||||
skip "database supports transaction isolation; test is irrelevant"
|
||||
end
|
||||
end
|
||||
|
||||
test "setting the isolation level raises an error" do
|
||||
assert_raises(ActiveRecord::TransactionIsolationError) do
|
||||
Tag.transaction(isolation: :serializable) { }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class TransactionIsolationTest < ActiveRecord::TestCase
|
||||
self.use_transactional_fixtures = false
|
||||
|
||||
class Tag < ActiveRecord::Base
|
||||
self.table_name = 'tags'
|
||||
end
|
||||
|
||||
class Tag2 < ActiveRecord::Base
|
||||
self.table_name = 'tags'
|
||||
end
|
||||
|
||||
setup do
|
||||
unless ActiveRecord::Base.connection.supports_transaction_isolation?
|
||||
skip "database does not support setting transaction isolation"
|
||||
end
|
||||
|
||||
Tag.establish_connection 'arunit'
|
||||
Tag2.establish_connection 'arunit'
|
||||
Tag.destroy_all
|
||||
end
|
||||
|
||||
# It is impossible to properly test read uncommitted. The SQL standard only
|
||||
# specifies what must not happen at a certain level, not what must happen. At
|
||||
# the read uncommitted level, there is nothing that must not happen.
|
||||
test "read uncommitted" do
|
||||
Tag.transaction(isolation: :read_uncommitted) do
|
||||
assert_equal 0, Tag.count
|
||||
Tag2.create
|
||||
assert_equal 1, Tag.count
|
||||
end
|
||||
end
|
||||
|
||||
# We are testing that a dirty read does not happen
|
||||
test "read committed" do
|
||||
Tag.transaction(isolation: :read_committed) do
|
||||
assert_equal 0, Tag.count
|
||||
|
||||
Tag2.transaction do
|
||||
Tag2.create
|
||||
assert_equal 0, Tag.count
|
||||
end
|
||||
end
|
||||
|
||||
assert_equal 1, Tag.count
|
||||
end
|
||||
|
||||
# We are testing that a nonrepeatable read does not happen
|
||||
test "repeatable read" do
|
||||
tag = Tag.create(name: 'jon')
|
||||
|
||||
Tag.transaction(isolation: :repeatable_read) do
|
||||
tag.reload
|
||||
Tag2.find(tag.id).update_attributes(name: 'emily')
|
||||
|
||||
tag.reload
|
||||
assert_equal 'jon', tag.name
|
||||
end
|
||||
|
||||
tag.reload
|
||||
assert_equal 'emily', tag.name
|
||||
end
|
||||
|
||||
# We are testing that a non-serializable sequence of statements will raise
|
||||
# an error.
|
||||
test "serializable" do
|
||||
if Tag2.connection.adapter_name =~ /mysql/i
|
||||
# Unfortunately it cannot be set to 0
|
||||
Tag2.connection.execute "SET innodb_lock_wait_timeout = 1"
|
||||
end
|
||||
|
||||
assert_raises ActiveRecord::StatementInvalid do
|
||||
Tag.transaction(isolation: :serializable) do
|
||||
Tag.create
|
||||
|
||||
Tag2.transaction(isolation: :serializable) do
|
||||
Tag2.create
|
||||
Tag2.count
|
||||
end
|
||||
|
||||
Tag.count
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "setting isolation when joining a transaction raises an error" do
|
||||
Tag.transaction do
|
||||
assert_raises(ActiveRecord::TransactionIsolationError) do
|
||||
Tag.transaction(isolation: :serializable) { }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "setting isolation when starting a nested transaction raises error" do
|
||||
Tag.transaction do
|
||||
assert_raises(ActiveRecord::TransactionIsolationError) do
|
||||
Tag.transaction(requires_new: true, isolation: :serializable) { }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue