Nesting ActiveRecord transactions
Published by peter January 24th, 2008 in active_record, ruby, transactions.I was experimenting with transactions in Rails/ActiveRecord to see what they can and what they can't do. At first it seemed I couldn't get transactions to work at all. I made sure I was using the InnoDB storage engine. I rechecked my code. But the test kept failing.
This, by the way, is the test:
-
require File.dirname(__FILE__) + '/../test_helper'
-
-
class BlogTest <ActiveSupport::TestCase
-
-
def test_transaction_rollback
-
b = Blog.find(2);
-
assert_equal(b.title, "Title 2")
-
-
assert_raise RuntimeError do
-
Blog.transaction do
-
b.title = "changed title"
-
b.save
-
raise "forced error" # force exception
-
end
-
end
-
b.reload # get the current state from the database
-
-
assert_equal(b.title, "Title 2")
-
end
-
end
Luckily a colleague pointed out that it was probably related to Rails' limited support for nested transactions; tests run within a transaction for performance reasons... which makes it impossible to roll back a transaction within a test.
Consider the following code:
-
Account.transaction do
-
User.transaction do
-
# do some stuff
-
end
-
-
# the previous transaction has already been committed, if we
-
# raise an exception now it can not be rolled back
-
raise
-
end
The inner transaction won't be rolled back if the outer transaction fails after it's execution!
Googling around I found a simple fix for my test. Turn off the transactional behavior of the fixtures in the test:
-
class BlogTest <ActiveSupport::TestCase
-
self.use_transactional_fixtures = false
-
...
-
end
Which works, but will slow down the execution of all tests. And it actually doesn't really solve the problem, it is a work-around.
I decided to see why nested transactions where such a big issue, and how it could be solved and found the ActiveRecord NestedTransactions (ARNE) pluging. ARNE is a plugin which makes ActiveRecord use SAVEPOINTS to achieve nested transactions functionality.
Savepoints save the state within a transaction to provide the ability to jump back to this state when something goes wrong later on. In SQL it looks something like this:
-
BEGIN; # start transaction
-
SAVEPOINT my_first_savepoint # capture the current state
-
-
# do some work here
-
-
ROLLBACK TO SAVEPOINT my_first_savepoint # optionally roll back to the specified state.
-
RELEASE SAVEPOINT my_first_savepoint # clean up
-
END; # end the transaction
The ARNE plugin wraps transaction blocks in savepoints to provide nesting functionality and actually works beautifully; my test succeeded. I did however find a small bug for which I've submitted a patch. The name of a safepoint must be unique for nesting to work without problems. If a name is used twice the initial savepoint is overwritten. Since the name was originally generated using a random generator this was in no way guaranteed:
-
# method to generate savepoint name
-
def generate_savepoint_name
-
# name must start with letters
-
"SP#{MD5.md5(rand.to_s)}"
-
end
A better solution would (IMHO) be to use a UUID generator:
-
# method to generate savepoint name
-
def generate_savepoint_name
-
# name must start with letters
-
"SP#{UUID.random_create.to_s.gsub('-','_')}"
-
end
I used the UUID generator from the uuidtools Gem. Note the random_create (a random generator is used to seeded the UUID algoritm) this guarantees that the above will also work when there is no standard MAC address (I heard about VPS solutions with this problem).
I learned a lot by looking at the way transaction handling is implemented in ActiveRecord. The code is clean, compact and readable. It made me like Ruby even more; although some people will probably say (and I tend to agree) that nesting should have been part of ActiveRecord all along.




















0 Responses to “Nesting ActiveRecord transactions”
Please Wait
Leave a Reply