{i} This page does not meet our wiki style guidelines. Please help improve this page by cleaning up its formatting.

Objects in many languages have methods called destructors that are typically used to automatically clean up resources held by an object when it is freed, typically when it goes out of scope. Mercurial uses such objects for keeping track of held locks, transactions, and the like, automatically cleaning them up when trouble happens.

Unfortunately, Python destructors are not immediately called in some instances involving exceptions, which is where we most need them to fire. This is because Python doesn't call destructors when propagating the exception up the call chain, because it doesn't destroy the individual function scopes as it goes. Thus, an exception can potentially hold a reference to a lock or transaction indefinitely.

Consider the following function:

def commit(self):
    # do a bunch of setup work
    lock = self.lock()
    wlock = self.wlock()
    # do some more work
    tr = self.transaction()
    # do work
    tr.close()
    # report some status

Exceptions can occur at multiple points in this function and multiple resources may need to be released.

There are a bunch of different ways to attempt to address the problem. The first that's often suggested is Python decorators. The idea is to wrap each function with decorators to acquire and release each resource. There are several problems with this approach. First, the locks may not be grabbed immediately upon entering the function which makes using decorators difficult. Second, a lock may sometimes be passed in from an outside function, which confuses decorators further. Third, decorators are ugly and confusing syntax. And finally, they're not available in Python 2.3! So, decorators are out.

Python 2.5 has a "with:" syntax that's meant to tackle this, but it's almost as bad. And again, not available in Python 2.3.

Another approach is to use try: and finally:. The obvious approach is problematic:

def commit(self):
    # do a bunch of setup work
    lock = self.lock()
    wlock = self.wlock()
    try:
        # do some more work
        tr = self.transaction()
        try:
            # do work
            tr.close()
        finally:
            tr.abort()
        # report some status
    finally:
        lock.release()
        wlock.release()

For starters, consider the call to self.wlock() raising an exception. The first lock would then never get released. Now consider the added complexity if lock and wlock get passed in from an external function.

Here's a much simpler method:

def commit(self, lock=None):
    wlock = tr = None
    try:
        # do a bunch of setup work
        if not lock:
            lock = self.lock()
        wlock = self.wlock()
        # do some more work
        tr = self.transaction()
        # do work
        tr.close()
        # report some status
    finally:
        del lock, wlock, tr

Here, any point in the code can fail and we'll clean up correctly. At all points, lock, wlock, and tr are things we can 'del', even if a lock is passed in (the caller's lock is correctly not released).

If we wish to release a lock, the preferred way to do it is:

def commit(self, lock=None):
    wlock = tr = None
    try:
        # do a bunch of setup work
        if not lock:
            lock = self.lock()
        wlock = self.wlock()
        # do some more work

        tr = self.transaction()
        # do work
        tr.close()
        lock = None # local release
        # finalize
        wlock = None # another local release
        # report some status
    finally:
        del lock, wlock, tr

Setting lock to None here will cause the lock to get released by destructor only if we created it ourselves! Note that we don't want to 'del' it here as that could cause an exception in the finally clause.

Also note that rather than calling 'del' in the 'finally:' clause, we could simply set them all to None as we did in the preamble. But that would be more typing...


CategoryHowTo

DealingWithDestructors (last edited 2012-05-13 14:01:46 by 62)