A
Austin Ziegler
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
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