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:

RUBY:
  1. require File.dirname(__FILE__) + '/../test_helper'
  2.  
  3. class BlogTest <ActiveSupport::TestCase
  4.  
  5.   def test_transaction_rollback
  6.     b = Blog.find(2);
  7.     assert_equal(b.title, "Title 2")
  8.    
  9.     assert_raise RuntimeError do
  10.       Blog.transaction do
  11.         b.title = "changed title"
  12.         b.save
  13.         raise "forced error" # force exception
  14.       end
  15.     end
  16.     b.reload # get the current state from the database
  17.  
  18.     assert_equal(b.title, "Title 2")
  19.   end
  20. 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:

RUBY:
  1. Account.transaction do
  2.   User.transaction do
  3.     # do some stuff
  4.   end
  5.  
  6.   # the previous transaction has already been committed, if we
  7.   # raise an exception now it can not be rolled back
  8.   raise
  9. 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:

RUBY:
  1. class BlogTest <ActiveSupport::TestCase
  2.   self.use_transactional_fixtures = false
  3.   ...
  4. 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:

SQL:
  1. BEGIN; # start transaction
  2. SAVEPOINT my_first_savepoint # capture the current state
  3.  
  4. # do some work here
  5.  
  6. ROLLBACK TO SAVEPOINT my_first_savepoint  # optionally roll back to the specified state.
  7. RELEASE SAVEPOINT my_first_savepoint # clean up
  8. 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:

RUBY:
  1. # method to generate savepoint name
  2. def generate_savepoint_name
  3.   # name must start with letters
  4.   "SP#{MD5.md5(rand.to_s)}"
  5. end

A better solution would (IMHO) be to use a UUID generator:

RUBY:
  1. # method to generate savepoint name
  2. def generate_savepoint_name
  3.   # name must start with letters
  4.   "SP#{UUID.random_create.to_s.gsub('-','_')}"
  5. 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”

  1. No Comments

Leave a Reply





About

Welcome to the weblog of Peter Maas. Here you'll find various posts related to stuff I like (like my kids and espresso) and stuff I do (like developing software).

JavaOne 2008 Pictures


Greenland sea_lion Acme Anvile at CommunityOne Keynote Tim Bray introducing the (J)Ruby panel Community One Keynote nearby hotel Moscone Center - JavaOne Stage being build in the nearby park pub Okke en Rudie Java + You on a cab Scribbled Sun Logo javaone 2008 goodybag Hotel room Golden Gate alcatraz Charles Nutter & Guillaume Laforge Stretched Limo golden_gate_warning_sign javaone2008 keynote
View more photos >

Categories



Meld u aan voor PayPal en begin direct met het accepteren van creditcardbetalingen.