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...