Commit, Recover & Transactions in Ruby

The spec says 'transactions', we immediately think 'InnoDB'. But what if you're not using a database, working on a light-weight application, or simply want to ensure a graceful recovery process? In the past that usually meant layers of additional logic, variables to keep and capture intermediate state, and all the other associated headaches. That is, until Gregory Brown and Austin Ziegler published Transaction::Simple - a gem, both literally and figuratively, which makes transaction support in Ruby a walk in the park.

Transaction::Simple provides a generic way to inject transaction support into any object - that's right, any Ruby object. All the magic happens directly in memory, and thus there are no dependencies on additional backends. Your only theoretical limit is the memory of your machine, as object versions are created transparently and on the fly. In addition, the gem offers nested transactions, named transactions, transaction groups, commit and recovery procedures. Talk about fully featured! An example is due:

require 'rubygems'
require 'transaction/simple'

# Create a Ruby hash to store the balance
bank = {"Bob" => 100, "Jon" => 50}

# Add tranasction support to our Ruby hash
bank.extend(Transaction::Simple)
bank.start_transaction(:deposit)
bank['Bob'] += 25                 # -> 125
bank.commit_transaction(:deposit)

# block-form usage
Transaction::Simple.start(bank) do |t|
    # v has been extended with Transaction::Simple and an unnamed
    t.transaction_open?          # -> true
    bank['Bob'] -= 25            # -> 100

    # Hmm, I think we made an error, let's abort!
    t.rewind_transaction
    t.transaction_open?         # -> true

    bank['Bob'] -= 25           # -> 100

    puts "Bob's balance: #{bank['Bob']}, expecting: 100"

    # Break out of the transaction block and abort the transaction
    t.abort_transaction
end

puts "Bob's balance: #{bank['Bob']}, expecting: 125"

# -> Bob's balance: 100, expecting: 100
# -> Bob's balance: 125, expecting: 125

Advanced Commit and Recovery

Applying similar patterns as we saw above, we can greatly simplify our recovery procedures in almost any Ruby program with minimal intervention:

# applying to arbitrary objects
obj = SomeObject.new
obj.extend(Transaction::Simple)

# Recover, rewind, and retry use-case
begin
  v.start_transaction

    if v.do_work
      v.commit_transaction
    else
      v.abort_transaction

rescue Exception
  v.rewind_transaction
  v.recover
  retry
end

And as if this is not enough, make sure to explore transaction groups once you dive into the documentation. Named transactions, in conjunction with transaction groups will allow you to easily manage a group of objects as if they were a single object. Man, I love Ruby. What is your (most recent) favorite gem?


Ilya Grigorik

Ilya Grigorik is a web performance engineer and developer advocate at Google, where his focus is on making the web fast and driving adoption of performance best practices at Google and beyond.