[Persistence-sig] "Straw Man" transaction API

Jeremy Hylton jeremy@alum.mit.edu
Mon, 29 Jul 2002 20:36:51 -0400


Last week, I worked out a revised transaction API for user code and
for data managers.  It's implemented in ZODB4, but is fairly
preliminary code.  I imagine we'll revise it further, but I'd like to
describe the changes briefly.

Here's a short summary from the ZODB4/Doc/changes.txt document:

    The Transaction implementation has been completely overhauled.
    There are four significant changes that users may need to cope
    with.  The first is that a transaction that fails because of an
    uncaught exception is not aborted.  The user code should
    explicitly call get_transaction().abort().  The second is that
    commit() does not take an optional argument to flag subtransaction
    commits.  Instead, call the savepoint() method.  ZODB will return
    a rollback object from savepoint().  If the rollback object's
    rollback() method is called, it will abort the savepoint() --
    rolling back changes to the previous savepoint or the start of the
    transaction.
    
    The other changes to the Transaction implementation affect
    implementors of resource / data managers.  The ZODB Connection
    object is an example of a data manager.  When the persistence
    machinery detects that an object has been modified, the register()
    method is called on its data manager.  It's up to the data manager
    to register with the transaction.  The manager is registered with
    the transaction, not the individual objects.  The interface the
    data manager implements (IDataManager) has changed.  It should
    implement four methods: prepare(), abort(), commit(), and
    savepoint().  Here is how they correspond to the odl API:
    prepare() is roughly equivalent to tpc_begin() through tpc_vote().
    abort() and commit() are roughly equivalent to tpc_abort() and
    tpc_finish().  savepoint() is used for subtransactions.
    
The APIs look like this:

class ITransaction(Interface):
    """Transaction objects."""

    def abort():
        """Abort the current transaction."""

    def begin():
        """Begin a transaction."""

    def commit():
        """Commit a transaction."""

    def join(resource):
        """Join a resource manager to the current transaction."""

    def status():
        """Return status of the current transaction."""

class IDataManager(Interface):
    """Data management interface for storing objects transactionally."""

    def prepare(transaction):
        """Begin two-phase commit of a transaction.

        DataManager should return True or False.
        """
        
    def abort(transaction):
        """Abort changes made by transaction."""
    
    def commit(transaction):
        """Commit changes made by transaction."""

    def savepoint(transaction):
        """Do tentative commit of changes to this point.

        Should return an object implementing IRollback
        """
        
class IRollback(Interface):
    
    def rollback():
        """Rollback changes since savepoint."""

I think the rollback mechanism will work well enough.  Gray and Reuter
explain that it can be used to simulate a nested transaction
architecture.  Thus, I think it's a reasonable building block for the
nested transaction API.

I think I'm also in favor of the new abort semantics.  ZODB3 would
abort the transactions -- call abort() on all the data managers -- if
an error occurred during a commit.  The new code requires that the
user do this instead.  I think that's better, because it leaves the
state of the objects intact if the code wants to analyze what went
wrong before retrying the transaction.

Note that a Transaction doesn't have a register method.  Instead, a
modified object calls register() on its data manager.  The data
manager can join() that transaction if that's the right thing to do.
The ZODB Connection joins on the first register call of the
transaction.  However, I currently have join() on the transaction, not
the Transaction.Manager (aka TP monitor).

I'm in favor of sticking with register() as the persistent method,
although notify() would be okay, too.  I imagine that some data
managers would want to be notified when an object is read or written.
In that case, I'm not sure if notify() is enough; we might want a
notify method for each kind of event or a notify() method with the
event as an argument.

(The need for notify-on-read, BTW, is to support higher isolation
levels than ZODB currently supports.)

Jeremy