Skip to content

Commit

Permalink
Support for specifying transaction isolation level
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jonleighton committed Sep 21, 2012
1 parent 834d6da commit 392eeec
Show file tree
Hide file tree
Showing 9 changed files with 263 additions and 15 deletions.
35 changes: 35 additions & 0 deletions activerecord/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,47 @@ def supports_statement_cache?
# # 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 }
Expand All @@ -168,10 +205,10 @@ def transaction(options = {})
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
Expand All @@ -191,9 +228,7 @@ def transaction_open?
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:
Expand All @@ -217,6 +252,22 @@ def add_transaction_record(record)
# 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ def number
0
end

def begin
RealTransaction.new(connection, self)
def begin(options = {})
RealTransaction.new(connection, self, options)
end

def closed?
Expand All @@ -38,13 +38,13 @@ class OpenTransaction < Transaction #:nodoc:
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
Expand All @@ -66,11 +66,11 @@ def number
end
end

def begin
def begin(options = {})
if finishing?
parent.begin
else
SavepointTransaction.new(connection, self)
SavepointTransaction.new(connection, self, options)
end
end

Expand Down Expand Up @@ -120,9 +120,14 @@ def open?
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
Expand All @@ -137,7 +142,11 @@ def perform_commit
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@ def supports_explain?
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,14 @@ def supports_index_sort_order?
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
Expand Down Expand Up @@ -269,6 +277,13 @@ def begin_db_transaction
# Transactions aren't supported
end

def begin_isolated_db_transaction(isolation)
execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}"

This comment has been minimized.

Copy link
@mikhailov

mikhailov Jun 27, 2022

Contributor

When relying on AWS Aurora, this one session variable isn't enough according to official documentation:

To enable the READ COMMITTED isolation level for Aurora Replicas, enable the aurora_read_replica_read_committed configuration setting. Enable this setting at the session level while connected a specific Aurora Replica. To do so, run the following SQL commands.

set session aurora_read_replica_read_committed = ON;
set session transaction isolation level read committed;

You might enable this configuration setting temporarily to perform interactive ad hoc (one-time) queries. You might also want to run a reporting or data analysis application that benefits from the READ COMMITTED isolation level, while leaving the default unchanged for other applications.

Would it be a good idea to enhance this method to add Rails native support for the second session variable?

This comment has been minimized.

Copy link
@simi

This comment has been minimized.

Copy link
@mikhailov

mikhailov Jun 27, 2022

Contributor

this adapter seems like a drop-in replacement for mysql2 ruby gem which still has its use for non-serverless Aurora instances, also Github search doesn't give any results to the following searches: "aurora_read_replica_read_committed", "isolation+level", "set+session"...

begin_db_transaction
rescue
# Transactions aren't supported
end

def commit_db_transaction #:nodoc:
execute "COMMIT"
rescue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,11 @@ def begin_db_transaction
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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,10 @@ def supports_partial_index?
true
end

def supports_transaction_isolation?
true
end

class StatementPool < ConnectionAdapters::StatementPool
def initialize(connection, max)
super
Expand Down
3 changes: 3 additions & 0 deletions activerecord/lib/active_record/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -195,4 +195,7 @@ def initialize(model)

class ImmutableRelation < ActiveRecordError
end

class TransactionIsolationError < ActiveRecordError
end
end
Loading

0 comments on commit 392eeec

Please sign in to comment.