As I've been catching up on ruby-talk, mentions of Ruby's use of blocks got 
me to thinking about a library that I wrote and fine useful and that it 
might be good to add a "block" API to it.

One of the things that was mentioned as being cool is being able to put a 
database transaction in a block and have the results automatically 
committed. This makes for a nice, neat visualisation of the transaction. 
"Everything done in this block is part of a transaction."

I'm considering adding a block API to Transaction::Simple, but there are 
conceptual problems, and I thought I'd get feedback from the community on 
how to address them -- and determine whether Transaction::Simple *should* 
get a block API.

In specific, I have two ways that I could approach this. The former is 
idiomatic in Ruby; the latter is less so, but I think more useful than the 
former. They exhibit the same problems, though. I can do a block-based 
transaction with either of:

  Transaction::Simple.start(obj) { |tobj| ... }
  obj.transaction_start { |tobj| ... }

As Transaction::Simple supports named transactions, both forms would accept 
an optional parameter for the transaction name.

The main conceptual problem is that the following will result in an error:

  File.open("abc") do |f|
    ...
    f.close
    ...
    f.gets
  end

However, there is only a "conceptual" transaction involved in 
Transaction::Simple. Committing the transaction does not make the object 
unavailable; it simply makes permanent the changes made so far to the 
object. Thus, if I do:

  begin
    obj.value = 0
    Transaction::Simple.start(obj) do |tobj|
      tobj.value = 42
      tobj.transaction_commit
      tobj.value = 43
      raise Foo
    end
  ensure
    puts obj.value
  end

The resulting value will be 43. This is because the transaction is closed 
before you reassign tobj.value the second time. Presumably, the 
implementation of Transaction::Simple would do something like:

  def Transaction::Simple.start(obj, name = nil)
    obj.extend(Transaction::simple)
    obj.transaction_start(name)
    yield obj
  rescue Exception => e
    obj.transaction_abort(name)
    raise e
  ensure
    obj.transaction_commit(name) if obj.transaction_open?
  end

Thus, one would reasonably expect that a transaction would rewind 
appropriately if an exception is raised and not handled during the block; 
unfortunately, this won't be true in the pathological case that I presented.

(Note that an aborted transaction is analogous to a rewound and committed 
transaction.)

Thus, my questions:

0) Do people use Transaction::Simple to suggest this work?
1) Should Transaction::Simple support a block API?
2) If Transaction::Simple supports a block API, should I ignore the obvious
   pathological case? OR
3) Should I do something to protect the user from being an idiot? One
   possibility is to keep track of the current block-transaction level and
   prevent the calling of EITHER abort or commit during a block transaction
   of the same level (or to a transaction named at or above that level).
   This may require some redesign of the internals of Transaction::Simple,
   though.

The thoughts of the community are eagerly desired...

-austin
--
austin ziegler    * austin / halostatue.ca * Toronto, ON, Canada
software designer * pragmatic programmer * 2004.04.24
                                         * 21.42.48