From: Austin Ziegler Date: 2004-04-25T11:08:35+09:00 Subject: Block APIs and Transaction::Simple 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