Exception safety and persistence

A database using pessimistic concurrency control will typically use strict two phase locking which means that shared resources are locked by a transaction as it proceeds, and all locks are released when the transaction commits or aborts. This guarantees that (concurrent) transactions are serialisable - i.e. their execution effect is equivalent to them having been applied one after the other. However, inevitably there are opportunities for dead-lock. The system needs to detect dead-lock (such as by detecting a cycle in the wait-for graph), and pick a "victim" transaction, abort it and start it again from scratch.

This approach is at odds with the ideology of orthogonal persistence, because it means that access to persistent objects can fail, and these objects "magically" roll back to an earlier state, whereas any transient objects do not.

Ceda instead uses operational transform and data replication - which could be regarded as a form of optimistic concurrency control. The idea is for each user to work on a local copy of the data, and propagate changes as operations that undergo transformation in order to be executed in different orders at different sites.

This approach means that objects in a Ceda persistent store can be regarded as private to a single process, and there is no concept of granting access to other processes. This allows for a purer form of orthogonal persistence - because the process can treat transient and persistent objects as private data, and there is no concept of dead-lock and rolling back of objects to earlier states.

The C++ programmer must deal with exceptions correctly, without treating transient and persistent objects differently. It is never reasonable to allow an exception to leave data structures in a garbled state. There is no magical mechanism that rolls objects back to previous states before the transaction began. Consider the following example


class X
{
    void ReadFromFile(const xstring& filename)
    {
        < open file >
        < parse file and read into members of X >
        < close file >
    }
};

This is an example where lots of errors can occur. Eg there may be I/O errors, or format errors in the file. It is not uncommon for designs to use exceptions for these errors.

When such an error occurs, we should document the fact that X may be left in a garbled state - say because half of the members are in the original state and half are in the new state. This design is tolerable as long as the subsequent destructor of X doesn't crash or leak, and clients that call X::ReadFromFile() deal with exceptions correctly. A useful approach in these cases is create a temporary X on the frame and read into the temporary, and only if that succeeds do we assign into the "real" one.