49 Validation of MarkAsDirty
A boolean macro CEDA_VALIDATE_MARK_AS_DIRTY is defined in IPersistable.h.
@def bool CEDA_VALIDATE_MARK_AS_DIRTY = false
This determines whether calls to MarkAsDirty() on IPersistable objects are validated.
Normally it is set to false
CRC validation is expensive so it should only be enabled for debugging purposes. It is used to
check whether persistent objects have been marked as dirty as required. This is worthwhile because
it is all too easy to forget a call to MarkAsDirty(). We try to identify which object had a
missing call, and we try to trip the assertion at the end of the very transaction in which the call was
missing, rather than some time later.
This is achieved by recording a CRC32 hash on the serialised state of each IPersistable object.
This is calculated whenever an object is loaded from disk or written to disk.
At the end of a transaction, an assertion error trips if an object is found in the ROT which is clean and yet the CRC hash of its current serialisable state doesn't match the recorded CRC hash. That implies the serialised state has changed, so the object should have been marked as dirty.
Typically IPersistable objects are marked as dirty for an extended period (e.g. 1000 milliseconds)
before they are written to the LSS.
This relates to the m_sleepTime member of the DosWriter.
If an object is already dirty when a transaction begins, then a failure to call MarkAsDirty() won't
be detected.
That's not great if we want the validation code to reliably detect these programming errors.
In an earlier version of CEDA the solution was to process the PSpaceDos in a PSpace
after each transaction, so that all objects resident in memory are cleaned. However this greatly increases
the amount of disk writing while running tests.
Instead a second dirty bit flag DBP_PO_MARKED_DIRTY_CRC in the IObject flags is
used to solve this problem.
// Persistent object is marked as dirty and has been added to the DOS
// Cleared when the dirty object set is processed
DBP_PO_DIRTY
// Used in DEBUG to validate calls to MarkAsDirty() using a CRC32 hash on the serialised state.
// Set when an object is changed (so its CRC is likely to change), and cleared at the end of
// every transaction when the CRCs are updated.
DBP_PO_MARKED_DIRTY_CRC
When an IPersistable object is modified and MarkAsDirty() is called, both the
DBP_PO_DIRTY and DBP_PO_MARKED_DIRTY_CRC bit flags are set.
However they are cleared at different times:
DBP_PO_DIRTYis cleared when the dirty object is written to the LSS.DBP_PO_MARKED_DIRTY_CRCis cleared at the end of every transaction, when the CRC hash is updated.
Changes made by setting the macro to true
Calculate CRC on a given IPersistable object
The following code in ROT.cpp is used to calculate the CRC on the serialised state of a given
IPersistable object.
@if (CEDA_VALIDATE_MARK_AS_DIRTY)
{
@println(*** Warning: This build is validating calls to MarkAsDirty())
class StreamCrcCalculator : public IOutputStream
{
public:
// Implementation of IOutputStream
virtual void WriteStream(const void* buffer, ssize_t numBytes)
{
m_crc.Append(buffer,numBytes);
}
virtual void FlushStream() {}
CRC32 GetCRC() const { return m_crc(); }
private:
Crc32 m_crc;
};
uint32 CalculateCrc(ptr<IPersistable> po)
{
cxAssert(po);
StreamCrcCalculator c;
{
Archive ar(&c);
po->Serialise(ar);
}
return c.GetCRC();
}
}
Record CRC in each IPersistable object
A 32 bit CRC is added to every IPersistable object:
$struct+ PersistObjState
{
@if (CEDA_VALIDATE_MARK_AS_DIRTY)
{
uint32 m_crc = 0; // CRC of serialised state used to validate calls to MarkAsDirty()
}
};
@if (CEDA_VALIDATE_MARK_AS_DIRTY)
{
inline uint32 GetCrc32(ptr<const IPersistable> po) { return po->GetPersistObjState().m_crc; }
inline void SetCrc32(ptr<const IPersistable> po, uint32 crc) { po->GetPersistObjState().m_crc = crc; }
}
Calculate CRC in each IPersistable object when it's loaded from the LSS
This is regardless of whether the object is loaded synchronously or asynchronously.
bool DGIndepNodeForAsyncPref::HandleAsyncLoad(ptr<IPersistable> po)
{
@if (CEDA_VALIDATE_MARK_AS_DIRTY)
{
if (po) SetCrc32(po, CalculateCrc(po));
}
...
}
ptr<IPersistable> ROT::SyncLoadObject(OID oid)
{
ptr<IPersistable> po = pspace_.TryLoadPOGivenOid(oid);
@if (CEDA_VALIDATE_MARK_AS_DIRTY)
{
if (po) SetCrc32(po, CalculateCrc(po));
}
return po;
}
Validate CRCs of all clean objects in the ROT when close a transaction
If an object is clean then it should have the correct CRC.
If not then it must have been modified without calling MarkAsDirty().
When a transaction ends on a PSpace the CRCs are validated:
void PSpace::OnEndTransaction(CSpace* cspace)
{
@if (CEDA_VALIDATE_MARK_AS_DIRTY)
{
rot_.ValidateCRCs();
}
}
The ROT validates the CRCs of all clean objects in memory as follows:
@if (CEDA_VALIDATE_MARK_AS_DIRTY)
{
void ROT::ValidateCRCs()
{
for (auto& i : map_)
{
ptr<IPersistable> po = i.second;
if (po.m_table)
{
bool isDirty = GetIObjectFlag(po,DBP_PO_MARKED_DIRTY_CRC);
uint32 newCrc = CalculateCrc(po);
uint32 prevCrc = GetCrc32(po);
bool valid = (isDirty || newCrc == prevCrc);
cxAlwaysAssert(valid); // Trips if there was a missing call to MarkAsDirty() on po.
// Update the CRC on the object and mark the object as no longer having a dirty CRC
SetCrc32(po,newCrc);
SetIObjectFlag(po, DBP_PO_MARKED_DIRTY_CRC, false);
}
}
}
}
Proposal: allow this check to be enabled at run time, rather than use a compile time macro
We want to be able to enable this validation at run time with minimal performance penalty when it is turned off, rather than using a macro. This is so that application programmers built on top of CEDA can enable it as well.
Rather than record the CRCs in the IPersistable objects, they could be recorded in a separate map in the ROT.
class ROT
{
...
std::map<OID,uint32> crcMap_;
};