Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

[ANN] Transaction::Simple 1.3.0

0 views
Skip to first unread message

Austin Ziegler

unread,
May 5, 2005, 2:28:26 PM5/5/05
to
I am pleased to announce the release of Transaction::Simple 1.3.0.

It can be downloaded from the trans-simple RubyForge project:

http://rubyforge.org/frs/?group_id=295&release_id=2130

It may also be installed as a RubyGem.

= Transaction::Simple for Ruby
Transaction::Simple provides a generic way to add active transaction
support to objects. The transaction methods added by this module will
work with most objects, excluding those that cannot be Marshal-ed
(bindings, procedure objects, IO instances, or singleton objects).

The transactions supported by Transaction::Simple are not backend
transaction; that is, they are not associated with any sort of data
store. They are "live" transactions occurring in memory and in the
object itself. This is to allow "test" changes to be made to an object
before making the changes permanent.

Transaction::Simple can handle an "infinite" number of transaction
levels (limited only by memory). If I open two transactions, commit the
second, but abort the first, the object will revert to the original
version.

Transaction::Simple supports "named" transactions, so that multiple
levels of transactions can be committed, aborted, or rewound by
referring to the appropriate name of the transaction. Names may be any
object except nil.

Version 1.3.0 of Transaction::Simple adds transaction groups. A
transaction group is an object wrapper that manages a group of objects
as if they were a single object for the purpose of transaction
management. All transactions for this group of objects should be
performed against the transaction group object, not against individual
objects in the group.

Copyright: Copyright (c) 2003 - 2005 by Austin Ziegler
Version: 1.3.0
Licence: MIT-Style

Thanks to David Black and Mauricio Fern?ndez for their help with this
library.

== Usage
include 'transaction/simple'

v = "Hello, you." # -> "Hello, you."
v.extend(Transaction::Simple) # -> "Hello, you."

v.start_transaction # -> ... (a Marshal string)
v.transaction_open? # -> true
v.gsub!(/you/, "world") # -> "Hello, world."

v.rewind_transaction # -> "Hello, you."
v.transaction_open? # -> true

v.gsub!(/you/, "HAL") # -> "Hello, HAL."
v.abort_transaction # -> "Hello, you."
v.transaction_open? # -> false

v.start_transaction # -> ... (a Marshal string)
v.start_transaction # -> ... (a Marshal string)

v.transaction_open? # -> true
v.gsub!(/you/, "HAL") # -> "Hello, HAL."

v.commit_transaction # -> "Hello, HAL."
v.transaction_open? # -> true
v.abort_transaction # -> "Hello, you."
v.transaction_open? # -> false

== Named Transaction Usage
v = "Hello, you." # -> "Hello, you."
v.extend(Transaction::Simple) # -> "Hello, you."

v.start_transaction(:first) # -> ... (a Marshal string)
v.transaction_open? # -> true
v.transaction_open?(:first) # -> true
v.transaction_open?(:second) # -> false
v.gsub!(/you/, "world") # -> "Hello, world."

v.start_transaction(:second) # -> ... (a Marshal string)
v.gsub!(/world/, "HAL") # -> "Hello, HAL."
v.rewind_transaction(:first) # -> "Hello, you."
v.transaction_open? # -> true
v.transaction_open?(:first) # -> true
v.transaction_open?(:second) # -> false

v.gsub!(/you/, "world") # -> "Hello, world."
v.start_transaction(:second) # -> ... (a Marshal string)
v.gsub!(/world/, "HAL") # -> "Hello, HAL."
v.transaction_name # -> :second
v.abort_transaction(:first) # -> "Hello, you."
v.transaction_open? # -> false

v.start_transaction(:first) # -> ... (a Marshal string)
v.gsub!(/you/, "world") # -> "Hello, world."
v.start_transaction(:second) # -> ... (a Marshal string)
v.gsub!(/world/, "HAL") # -> "Hello, HAL."

v.commit_transaction(:first) # -> "Hello, HAL."
v.transaction_open? # -> false

== Block Transaction Usage
v = "Hello, you." # -> "Hello, you."
Transaction::Simple.start(v) do |tv|
# v has been extended with Transaction::Simple and an unnamed
# transaction has been started.
tv.transaction_open? # -> true
tv.gsub!(/you/, "world") # -> "Hello, world."

tv.rewind_transaction # -> "Hello, you."
tv.transaction_open? # -> true

tv.gsub!(/you/, "HAL") # -> "Hello, HAL."
# The following breaks out of the transaction block after
# aborting the transaction.
tv.abort_transaction # -> "Hello, you."
end
# v still has Transaction::Simple applied from here on out.
v.transaction_open? # -> false

Transaction::Simple.start(v) do |tv|
tv.start_transaction # -> ... (a Marshal string)

tv.transaction_open? # -> true
tv.gsub!(/you/, "HAL") # -> "Hello, HAL."

# If #commit_transaction were called without having started a
# second transaction, then it would break out of the transaction
# block after committing the transaction.
tv.commit_transaction # -> "Hello, HAL."
tv.transaction_open? # -> true
tv.abort_transaction # -> "Hello, you."
end
v.transaction_open? # -> false

== Transaction Groups
require 'transaction/simple/group'

x = "Hello, you."
y = "And you, too."

g = Transaction::Simple::Group.new(x, y)
g.start_transaction(:first) # -> [ x, y ]
g.transaction_open?(:first) # -> true
x.transaction_open?(:first) # -> true
y.transaction_open?(:first) # -> true

x.gsub!(/you/, "world") # -> "Hello, world."
y.gsub!(/you/, "me") # -> "And me, too."

g.start_transaction(:second) # -> [ x, y ]
x.gsub!(/world/, "HAL") # -> "Hello, HAL."
y.gsub!(/me/, "Dave") # -> "And Dave, too."

g.rewind_transaction(:second) # -> [ x, y ]
x # -> "Hello, world."
y # -> "And me, too."

x.gsub!(/world/, "HAL") # -> "Hello, HAL."
y.gsub!(/me/, "Dave") # -> "And Dave, too."

g.commit_transaction(:second) # -> [ x, y ]
x # -> "Hello, HAL."
y # -> "And Dave, too."

g.abort_transaction(:first) # -> [ x, y ]
x = -> "Hello, you."
y = -> "And you, too."

== Thread Safety
Threadsafe version of Transaction::Simple and Transaction::Simple::Group
exist; these are loaded from 'transaction/simple/threadsafe' and
'transaction/simple/threadsafe/group', respectively, and are represented
in Ruby code as Transaction::Simple::ThreadSafe and
Transaction::Simple::ThreadSafe::Group, respectively.

== Contraindications
While Transaction::Simple is very useful, it has some severe limitations
that must be understood. Transaction::Simple:

* uses Marshal. Thus, any object which cannot be Marshal-ed cannot use
Transaction::Simple. In my experience, this affects singleton objects
more often than any other object. It may be that Ruby 2.0 will solve
this problem.
* does not manage resources. Resources external to the object and its
instance variables are not managed at all. However, all instance
variables and objects "belonging" to those instance variables are
managed. If there are object reference counts to be handled,
Transaction::Simple will probably cause problems.
* is not thread-safe. In the ACID ("atomic, consistent, isolated,
durable") test, Transaction::Simple provides C and D, but it is up to
the user of Transaction::Simple to provide isolation. Transactions
should be considered "critical sections" in multi-threaded
applications. Thread safety can be ensured with
Transaction::Simple::ThreadSafe. With transaction groups, some level
of atomicity is assured.
* does not maintain Object#__id__ values on rewind or abort. This may
change for future versions.

== Transaction::simple 1.3.0
* Updated to fix a lot of warnings.
* Added a per-transaction-object list of excluded instance variables.
* Moved Transaction::simple::ThreadSafe to transaction/simple/threadsafe.
* Added transaction groups. Transaction groups are wrapper objects to allow
the coordination of transactions with a group of objects. There are both
normal and threadsafe versions of transaction groups.
* Fixed a long-standing problem where instance variables that were added to an
object after a transaction was started would remain.
* Reorganised unit tests.

-austin
--
Austin Ziegler * halos...@gmail.com
* Alternate: aus...@halostatue.ca

John Lam

unread,
May 5, 2005, 2:43:53 PM5/5/05
to
Wow wow wow! This is so massively cool. Thanks for creating this. I can't wait to spend some quality time with this.

Do you snapshot state as the objects are modified? If so, does this avoid the problem of requiring two phase commit in your transaction groups?

Cheers,
-John

________________________________

Austin Ziegler

unread,
May 5, 2005, 3:17:06 PM5/5/05
to
On 5/5/05, John Lam <jl...@iunknown.com> wrote:
> Wow wow wow! This is so massively cool. Thanks for creating this.
> I can't wait to spend some quality time with this.
>
> Do you snapshot state as the objects are modified? If so, does
> this avoid the problem of requiring two phase commit in your
> transaction groups?

I'm not quite sure what you're saying here. Transaction snapshots
are taken when you start the transaction:

require 'transaction/simple/group'

x = "Hello, you."
y = "And you, too."

g = Transaction::Simple::Group.new(x, y)
g.start_transaction(:first) # -> [ x, y ]
g.transaction_open?(:first) # -> true
x.transaction_open?(:first) # -> true
y.transaction_open?(:first) # -> true

At this point, the transaction state of x and y is set. 'g' doesn't
maintain any transaction state; x and y do independently. Strictly
speaking, the transaction group is just a synchronizer.



x.gsub!(/you/, "world") # -> "Hello, world."
y.gsub!(/you/, "me") # -> "And me, too."

Here, we've modified both x and y. No new snapshot has yet been
made.

g.start_transaction(:second) # -> [ x, y ]

There. That takes another snapshot and gives it the transaction name
of :second.

x.gsub!(/world/, "HAL") # -> "Hello, HAL."
y.gsub!(/me/, "Dave") # -> "And Dave, too."

Another modification.



g.rewind_transaction(:second) # -> [ x, y ]
x # -> "Hello, world."
y # -> "And me, too."

We've rewound the transaction on each object to its original state
here.

x.gsub!(/world/, "HAL") # -> "Hello, HAL."
y.gsub!(/me/, "Dave") # -> "And Dave, too."

g.commit_transaction(:second) # -> [ x, y ]
x # -> "Hello, HAL."
y # -> "And Dave, too."

Changes are made and the second transaction is committed on both
objects. Note: if something goes wrong in the committing of the
first object, then the second object won't be committed.



g.abort_transaction(:first) # -> [ x, y ]
x = -> "Hello, you."
y = -> "And you, too."

This aborts the transaction and restores the state of the objects to
their original value. Except that they are now able to do
transactions.

Make sense?

Most of this code is based on what I did for block transactions:

Transaction::Simple.start_named(:foo, x, y) do |tx, ty|
...
end

I may go through at some point and unify the implementations here
and provide block forms of the transaction group methods.

Jeff Barczewski

unread,
May 5, 2005, 3:35:35 PM5/5/05
to
Could one use this as a simple way to provide dialog state across
discrete web pages in a wizard type scenario? Like what some people
try to do with continuations, although this seems to get complicated
quickly. Maybe this could provide an easier mechanism??

For instance if you can start a transaction when you come into the
wizard, commiting and rolling back inner transactions as you change
pages, then finally commiting the final wrapper transaction when you
complete the wizard and then use the data.

One would have to store this somewhere that it could be retrieved by
any thread, but it seems like maybe this might work with a little
thought.

Jeff

John Lam

unread,
May 5, 2005, 3:40:34 PM5/5/05
to
Yes - what you're saying does make sense.

I was thinking about cases where you're performing transactions against large objects, say a hash table.

Since the target objects don't know anything about transactions (and you don't know anything about their internal state) then this would wind up duplicating a lot of state.

However, if an object were Tx aware, then it would know how to manage its state in face of a transaction manager. Adding a single entry to a hash table wouldn't require copying the entire table first - you'd simply record the fact that you are adding a record and add it at commit-time.

At this point you'll need to have 2-phase commit protocols - telling each object to prepare to commit before actually committing. Any object that fails to prepare to commit aborts the entire Tx.

This is probably a lot more complicated than what you have envisioned (after all these are *Simple* transactions). I think this will still be very useful for many situations where you aren't modifying very large objects (or are not doing many such Tx in parallel).

-John

________________________________

From: Austin Ziegler [mailto:halos...@gmail.com]
Sent: Thu 5/5/2005 3:17 PM
To: ruby-talk ML

Austin Ziegler

unread,
May 5, 2005, 3:44:45 PM5/5/05
to
On 5/5/05, Jeff Barczewski <jeff.ba...@gmail.com> wrote:
> Could one use this as a simple way to provide dialog state across
> discrete web pages in a wizard type scenario? Like what some
> people try to do with continuations, although this seems to get
> complicated quickly. Maybe this could provide an easier
> mechanism??

Mmmm. Maybe, but it would depend on the architecture of your program
more than anything. In a stateless situation, then really not. In a
stateful situation, then easily.

These items are for live, in-memory transactions. As long as your
program is running, you should be okay with this.

Austin Ziegler

unread,
May 5, 2005, 3:49:15 PM5/5/05
to
On 5/5/05, John Lam <jl...@iunknown.com> wrote:
> Yes - what you're saying does make sense.
>
> I was thinking about cases where you're performing transactions
> against large objects, say a hash table.

Well, I do these against large objects. Transaction::Simple is a
fundamental support library for PDF::Writer. Just about the only
time it sees update is when I need to update it for PDF::Writer ;).

> Since the target objects don't know anything about transactions
> (and you don't know anything about their internal state) then this
> would wind up duplicating a lot of state.

Well, that's not really true. With Transaction::Simple, the target
object does become transaction-aware -- but it has nothing to do
with a "transaction manager" or database transactions.

> However, if an object were Tx aware, then it would know how to
> manage its state in face of a transaction manager. Adding a single
> entry to a hash table wouldn't require copying the entire table
> first - you'd simply record the fact that you are adding a record
> and add it at commit-time.

Right. This isn't a transaction log that can be replayed. This does
the simplest possible thing (which is why there are a number of
limitatiosn): it takes a Marshal.dump of the object.

> At this point you'll need to have 2-phase commit protocols -
> telling each object to prepare to commit before actually
> committing. Any object that fails to prepare to commit aborts the
> entire Tx.

Well, I don't know if it's currently used, but DHH added
Transaction::Simple support to ActiveRecord a while back so that
when you do a transaction with ActiveRecord, if the database
transaction fails, it also causes the object transaction to fail.

> This is probably a lot more complicated than what you have
> envisioned (after all these are *Simple* transactions). I think
> this will still be very useful for many situations where you
> aren't modifying very large objects (or are not doing many such Tx
> in parallel).

Perhaps. Something like this might be possible with a Builder
object, but it would require a level of wrapping that I'm not -- at
this point -- interested in dealing with.

Logan Capaldo

unread,
May 5, 2005, 7:34:59 PM5/5/05
to

This is cool. Would things like this work?

v = SomeObject.new
v.extend(Transaction::Simple)

begin
v.start_transaction
v.someMethodThatMightRaiseAnExceptionLeavingTheObjectInAnInvalidState
v.commit_transaction
rescue SaidException
v.abort_transaction
end

Austin Ziegler

unread,
May 5, 2005, 9:46:07 PM5/5/05
to
On 5/5/05, Logan Capaldo <loganc...@gmail.com> wrote:
> This is cool. Would things like this work?
>
> v = SomeObject.new
> v.extend(Transaction::Simple)
>
> begin
> v.start_transaction
v.bad_method

> v.commit_transaction
> rescue SaidException
> v.abort_transaction
> end

That, Logan, is one of the very reasons that this package exists.

You can also go:

begin
v.start_transaction
if v.bad_method
v.commit_transaction
else
v.abort_transaction
rescue SillyException
v.rewind_transaction
v.recover
retry
end

0 new messages