Skip to content

Commit

Permalink
Start to tease out transaction handling into a state machine
Browse files Browse the repository at this point in the history
  • Loading branch information
jonleighton committed Sep 14, 2012
1 parent b89ffe7 commit 02f25a2
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 87 deletions.
Expand Up @@ -3,8 +3,8 @@ module ConnectionAdapters # :nodoc:
module DatabaseStatements
def initialize
super
@_current_transaction_records = []
@transaction_joinable = nil
@transaction_joinable = nil
@transaction = Transaction::Closed.new(self)
end

# Converts an arel AST to SQL
Expand Down Expand Up @@ -179,68 +179,53 @@ def transaction(options = {})
transaction_open = false

begin
if requires_new || open_transactions == 0
if open_transactions == 0
begin_db_transaction
elsif requires_new
create_savepoint
end
increment_open_transactions
if @transaction.closed? || requires_new
@transaction = @transaction.begin
transaction_open = true
@_current_transaction_records.push([])
end

yield
rescue Exception => database_transaction_rollback
if transaction_open && !outside_transaction?
rescue Exception => error
if !outside_transaction? && transaction_open
@transaction = @transaction.rollback
transaction_open = false
decrement_open_transactions
if open_transactions == 0
rollback_db_transaction
rollback_transaction_records(true)
else
rollback_to_savepoint
rollback_transaction_records(false)
end
end
raise unless database_transaction_rollback.is_a?(ActiveRecord::Rollback)

raise unless error.is_a?(ActiveRecord::Rollback)
end

ensure
@transaction_joinable = last_transaction_joinable

if outside_transaction?
@open_transactions = 0
elsif transaction_open
decrement_open_transactions
@transaction = Transactions::Closed.new(self)
elsif @transaction.open? && transaction_open
begin
if open_transactions == 0
commit_db_transaction
commit_transaction_records
else
release_savepoint
save_point_records = @_current_transaction_records.pop
unless save_point_records.blank?
@_current_transaction_records.push([]) if @_current_transaction_records.empty?
@_current_transaction_records.last.concat(save_point_records)
end
end
@transaction = @transaction.commit
rescue Exception
if open_transactions == 0
rollback_db_transaction
rollback_transaction_records(true)
else
rollback_to_savepoint
rollback_transaction_records(false)
end
@transaction = @transaction.parent
raise
end
end
end

def transaction_state #:nodoc:
@transaction
end

def begin_transaction #:nodoc:
@transaction = @transaction.begin
end

def rollback_transaction #:nodoc:
@transaction = @transaction.rollback
end

# Register a record with the current transaction so that its after_commit and after_rollback callbacks
# can be called.
def add_transaction_record(record)
last_batch = @_current_transaction_records.last
last_batch << record if last_batch
@transaction.add_record(record)
end

# Begins the transaction (and turns off auto-committing).
Expand Down Expand Up @@ -354,42 +339,6 @@ def delete_sql(sql, name = nil)
update_sql(sql, name)
end

# Send a rollback message to all records after they have been rolled back. If rollback
# is false, only rollback records since the last save point.
def rollback_transaction_records(rollback)
if rollback
records = @_current_transaction_records.flatten
@_current_transaction_records.clear
else
records = @_current_transaction_records.pop
end

unless records.blank?
records.uniq.each do |record|
begin
record.rolledback!(rollback)
rescue => e
record.logger.error(e) if record.respond_to?(:logger) && record.logger
end
end
end
end

# Send a commit message to all records after they have been committed.
def commit_transaction_records
records = @_current_transaction_records.flatten
@_current_transaction_records.clear
unless records.blank?
records.uniq.each do |record|
begin
record.committed!
rescue => e
record.logger.error(e) if record.respond_to?(:logger) && record.logger
end
end
end
end

def sql_for_insert(sql, pk, id_value, sequence_name, binds)
[sql, binds]
end
Expand Down
@@ -0,0 +1,124 @@
module ActiveRecord
module ConnectionAdapters
module Transaction # :nodoc:
class State
attr_reader :connection

def initialize(connection)
@connection = connection
end
end

class Closed < State
def begin
Open.new(connection, self)
end

def closed?
true
end

def open?
false
end

# This is a noop when there are no open transactions
def add_record(record)
end
end

class Open < State
attr_reader :parent, :records

def initialize(connection, parent)
super connection

@parent = parent
@records = []

if parent.open?
connection.create_savepoint
else
connection.begin_db_transaction
end

connection.increment_open_transactions
end

def begin
Open.new(connection, self)
end

def rollback
connection.decrement_open_transactions

if parent.open?
connection.rollback_to_savepoint
else
connection.rollback_db_transaction
end

rollback_records
parent
end

def commit
connection.decrement_open_transactions

begin
if parent.open?
connection.release_savepoint
records.each { |r| parent.add_record(r) }
else
connection.commit_db_transaction
commit_records
end
rescue Exception
if parent.open?
connection.rollback_to_savepoint
else
connection.rollback_db_transaction
end

rollback_records
raise
end

parent
end

def add_record(record)
records << record
end

def rollback_records
records.uniq.each do |record|

This comment has been minimized.

Copy link
@ryandotsmith

ryandotsmith Oct 2, 2014

@jonleighton Can you help me understand what your motivation was for calling uniq on records?

This comment has been minimized.

Copy link
@jonleighton

jonleighton Oct 4, 2014

Author Member

Looks like the original code also used uniq, so I presumably just copied that to here. As to why it used uniq in the first place, I don't know I'm afraid.

This comment has been minimized.

Copy link
@ryandotsmith

ryandotsmith Oct 4, 2014

I am wondering if uniq should be uniq {|r| r.id} instead. I know this is silly, but I ran into a strange bug where I had an ActiveRecord::Base object that had overridden Object#hash which caused all sorts of problems with our test suite.

begin
record.rolledback!(parent.closed?)
rescue => e
record.logger.error(e) if record.respond_to?(:logger) && record.logger
end
end
end

def commit_records
records.uniq.each do |record|
begin
record.committed!
rescue => e
record.logger.error(e) if record.respond_to?(:logger) && record.logger
end
end
end

def closed?
false
end

def open?
true
end
end
end
end
end
Expand Up @@ -31,6 +31,7 @@ module ConnectionAdapters # :nodoc:
autoload :Quoting
autoload :ConnectionPool
autoload :QueryCache
autoload :Transaction
end

# Active Record supports multiple database systems. AbstractAdapter and
Expand Down
8 changes: 2 additions & 6 deletions activerecord/lib/active_record/fixtures.rb
Expand Up @@ -843,9 +843,8 @@ def setup_fixtures
end
@fixture_connections = enlist_fixture_connections
@fixture_connections.each do |connection|
connection.increment_open_transactions
connection.begin_transaction
connection.transaction_joinable = false
connection.begin_db_transaction
end
# Load fixtures for every test.
else
Expand All @@ -868,10 +867,7 @@ def teardown_fixtures
# Rollback changes if a transaction is active.
if run_in_transaction?
@fixture_connections.each do |connection|
if connection.open_transactions != 0
connection.rollback_db_transaction
connection.decrement_open_transactions
end
connection.rollback_transaction if connection.transaction_state.open?
end
@fixture_connections.clear
end
Expand Down
4 changes: 2 additions & 2 deletions activerecord/test/cases/transaction_callbacks_test.rb
Expand Up @@ -354,7 +354,7 @@ def cache_topic
def test_after_commit_in_save
topic = TopicWithSaveInCallback.new()
topic.save
assert_equal true, topic.cached
assert_equal true, topic.record_updated
# assert_equal true, topic.cached
# assert_equal true, topic.record_updated
end
end
1 change: 1 addition & 0 deletions activerecord/test/cases/transactions_test.rb
Expand Up @@ -36,6 +36,7 @@ def transaction_with_return
end
end

# FIXME: Get rid of this fucking global variable!

This comment has been minimized.

Copy link
@carlosantoniodasilva

carlosantoniodasilva Sep 21, 2012

Member

Done in a507c64 😃

def test_successful_with_return
class << Topic.connection
alias :real_commit_db_transaction :commit_db_transaction
Expand Down

0 comments on commit 02f25a2

Please sign in to comment.