Exceptions thrown while lock a CSpace

Unfortunately when exceptions occur it's all to easy to corrupt a persistent store and cause permanent data loss.

It is not generally a good idea to catch exceptions in a try - catch(...) block because that will hide a lack of proper exception handling under the carpet.

In fact one should only catch exceptions that you expect are reasonable to occur and that can be safely handled.

We certainly don't want to catch access violations, division by zero etc.

The problem of uncaught exceptions

A CSpaceLock defines an atomic unit of work for the persistent store - i.e. it is associated with a transaction. A CSpaceLock also tells the system to update the display as required. So the atomic unit of work relates both to changes on disk as well as changes on the screen.

Consider the following example


void foo()
{
    // Acquire a lock on the CSpace for the scope of this function
    CSpaceLock lock;

    MakeChangesToPersistentObjects();
}

It is all too easy for an exception to be thrown within MakeChangesToPersistentObjects(), perhaps part way through its execution and leaving persistent objects in an invalid state. It would be very bad to propagate these changes to disk. Of course a correct design would not allow this to happen in the first place, but it is not uncommon for a programmer to forget to account for exceptions.

The usual behaviour for an RAII class used to lock the CSpace would be to automatically and silently commit the transaction when an exception is thrown and the stack unwinds.

Automatically aborting the process if an uncaught exception is thrown inside a CSpaceLock

std::uncaught_exceptions() can be used to detect whether a CSpaceLock is destructing because an exception was thrown in the scope in which it was declared and the stack is unwinding. We can call std::terminate() when uncaught exceptions are thrown inside a CSpaceLock.


class CSpaceLock
{
public:
    ~CSpaceLock()
    {
        if (uncaughtExceptionCount_ != std::uncaught_exceptions()) 
        {
            // Stack is unwinding, we assume it is too risky to allow
            // a transaction to commit, and instead terminate the process
            cxAlwaysAssert( false );
            std::terminate();
        }
    }
    
private:
    int uncaughtExceptionCount_ = std::uncaught_exceptions();
};

The calls to std::uncaught_exceptions() impact the rate at which a CSpaceLock can be used to lock a CSpace. The rate drops from about 34 MHz to 14 MHz on a windows-64 build with AMD Ryzen 9 3900X 12-core machine.

Explicitly commiting a CSpaceLock

[please ignore: this is the old approach and is no longer the case]

To force the programmer to be conscientious with exception programming, it is a requirement that a CSpaceLock be explicitly committed before it destructs. So the above code must instead be written as follows


void foo()
{
    CSpaceLock lock;
    MakeChangesToPersistentObjects();

    // Every CSpaceLock must be explicitly committed before it is allowed to destruct
    lock.Commit();
}

Now if an exception is thrown by MakeChangesToPersistentObjects(), the CSpaceLock will be destroyed (as the stack unwinds) without it being committed. This is regarded as a design error, and the system disables further writing to the persistent store and aborts the process!

This forces the programmer to handle exceptions. Of course it is reasonable for exceptions to be thrown in a deeply nested function call, and the exception is caught and handled a long way from where it was thrown.