45 CidMap

PersistStore uses a CidMap to associate a non-zero integer Class Identifier (CID) with each fully qualified class name for the IPersistable objects in the store. These classes are always concrete, implement interface IPersistable, have a registered ReflectedClass and have a reflected default constructor. Note the implication: changing the fully qualified name of an IPersistable class introduces schema evolution issues for a PersistStore.

Note that a fully qualified class name could possiblly include angled brackets for template classes.

As an example one of the txPersistStore unit tests produces the following mapping between CID and fully qualified class name:

CIDfully qualified class name
1RootWithBTree
2ceda::BPlusTree
3ceda::LeafNode

A CID is 1 plus the index into an array of strings.

In memory a CID is represented using the platform dependent type ssize_t. It is serialised using variable length serialisation. That means it is often serialised using only 1 or 2 bytes in each IPersistable object on disk.

This allows IPersistable objects to store just the CID to uniquely identify their type. The CID is used to create an object instance of the required type during deserialisation.

If an invalid CID integer is read then a BadCidException exception is thrown. This is never supposed to happen, if it does then the store is corrupt.

Note that the CidMap is not itself an IPersistable object with an OID, because that is too incestuous a design! [The CidMap would be needed to help deserialise itself].

Together with the TypeOpsMap, the CidMap persists in a single serial element in a PersistStore, with OID = 00000001.00000000.

When a PersistStore is opened the CidMap is read into memory. There is support for applying a name translation map. This maps old fully qualified class names to new fully qualified class names. If it is found that a name has been repeated then a RepeatedClassNameInCidMapException exception is thrown. This is never supposed to happen, if it does then the store is corrupt.

Serialising an object

When an IPersistable object is to be written to the store, we need to first write the CID. That suggests we might want to cache the CID in the ReflectedClass. However there are three reasons why we don't:

  • It breaks the layering - because ReflectedClass is in cxObject and doesn't know anything about cxPersistStore.
  • A completely read only ReflectedClass can go into the code segment rather than the data segment. This will reduce bloat and allow DLLs to load more quickly.
  • A process can open multiple persistent stores at the same time, and persistent stores can assign different CIDs to a given ReflectedClass. Therefore most generally there is a one to many mapping between ReflectedClass and CID.

Therefore we choose to store a map from ReflectedClass* to CID in the CidMap. It is unlikely that this will create a bottleneck, even when writing lots of small objects to disk.

Deserialising an object

After reading the CID from the archive, we need to find the ReflectedClass in order to create an instance.

This is very fast because we simply index with the CID into a std::deque<Entry> and an Entry caches a pointer to the ReflectedClass.

Thread safety

In order to support independent reading and writing of objects in the one persistent store, the CidMap is protected by a std::mutex.

GetCid()

This function is called each time a persistent object is to be written to disk. Therefore it should be reasonably efficient.

GetCid() looks up m_rcToCid which is a map from ReflectedClass* to CID. If the given reflected class is not found then a new entry must be created in both m_cidToReflectedClass and m_rcToCid.

Lazy binding of name to reflected class

Previouly there was a more complex implementation of the CidMap using a std::deque where an Entry only looks up the ReflectedClass on demand.

struct Entry
{
    Entry(const ReflectedClass& rc) : m_name(rc.m_name), m_rc(&rc) {}
    Entry() : m_rc(nullptr) {}

    // Fully qualified class name
    xstring m_name;

    // Cache the lookup into the ReflectedClassRegistry
    mutable const ReflectedClass* m_rc;
};

// Indexed by CID, and provides the fully qualified class name
std::deque<Entry> m_entrys;

In theory that might allow for a store to be opened before some of the DLLs that are required to read some of the objects in the store. However that seems rather complex, so for simplicity it is now assumed that all required libraries are opened before a store is opened. That allows the mapping from CID to ReflectedClass to be calculated at the time the CidMap is first deserialised.

Using a class name translation table

The following declarations in IPersistStore.h are relevant to a form of schema evolution of a PersistStore involving renaming of classes.

$struct+ ClassNameTranslation
{
    ConstStringZ oldName;
    ConstStringZ newName;
};

$struct+ ClassNameTranslationTable
{
    const ClassNameTranslation* entries;
    ssize_t count;
};

A ClassNameTranslationTable can optionally be passed to CidMap::Deserialise() in order to allow for the class names read from disk to be translated before they are looked up in the ReflectedClass registry.

todo: It would be preferable to provide support for renaming of classes in the xc++ language and this information is reflected. We could allow for alternative names to be specified where a class is defined, and this causes the ReflectedClass to be registered under all those names. That would eliminate the need for class name translation tables.

Removal of redundant CidMap entries?

Can we afford to perform instance counting of all the objects in the store? The downside is that the map will be marked as dirty all the time. However it could actually be nice to have the usage statistics. If not then we can't afford to ever delete entries in the CidMap.

CidMap.h

Source: Ceda/cxPersistStore/src/CidMap.h

CidMap.cpp

Source: Ceda/cxPersistStore/src/CidMap.cpp