50 Historical transaction proposal
Status: This chapter records a historical transaction proposal and does not define the current interface.
declare locks or transactions on CSpaces, PSpaces and Workingsets discusses fundamental issues that needs to be fixed, hence this proposal.
We assume the proposal that every CSpace has a pointer to a IOperationCallBacks is implemented.
A PSpace implements IOperationCallBacks.
cxObject changes
Under this proposal the following will be removed from the public API for the cxObject library:
- ECSpaceLockMode
- LockInfo
- CSpaceLockMode
- MultipleCSpaceLock2
CSpaceLock supports locking any number of CSpaces. It is templatised on the number of CSpaces to lock and it uses variadic templates. There is no concept of locking modes.
OnBeginTransaction(CSpace*) and OnEndTransaction(CSpace*) are called for
each CSpace as it is locked and unlocked and we don't need special notifications for locking
multiple CSpaces.
cxPersistStore changes
The cxPersistStore and cxOperation libraries don't change the API defined by the cxObject library for locking CSpaces.
A CSpace has a pointer to a IOperationCallBacks object, and this provides the
OnBeginTransaction() and OnEndTransaction() hooks needed to modify what is done
when a transaction on a PSpace is opened and closed.
This is a fully general solution that supports locking one or multiple CSpaces using the API in cxObject
(i.e. using CSpaceLock and perhaps MultipleCSpaceLock if variadics don't work).
We provide free functions to Open, Destroy and Transfer PSpaces. These assume the calling thread is inside a PersistStore transaction.
Tracking the set of PSpaces that have been locked by a given thread
The overlapped locking of PSpaces by a thread is tracked so that the PSpace transaction groups are merged as required.
RecordedLocks has
demonstrated that it is quite easy to record the set of CSpaces that are currently
locked by a given thread. It uses a std::set<CSpace*> in TLS and simply inserts into
the set when a CSpace is locked and erases from the set when a CSpace is unlocked.
We can do pretty much the same thing. We have a PersistStoreTxn in TLS for the thread:
struct PersistStoreTxn
{
// One of the PSpaces in the group of PSpaces on which a transaction is taking place
PSpace* pspaceInGroup = nullptr;
ssize_t numPSpacesLocked = 0;
};
thread_local PersistStoreTxn tls_persistStoreTxn;
class PSpace : public IOperationCallBacks
{
virtual void OnBeginTransaction(CSpace* cspace)
{
PSpaceTxnGroupMgr& gm = m_ps.GetPSpaceTxnGroupMgr();
auto& txn = tls_persistStoreTxn;
if (txn.numPSpacesLocked++ == 0)
{
// We have locked the first PSpace in the group
cxAssert(txn.pspaceInGroup == nullptr);
txn.pspaceInGroup = this;
}
else
{
// We have locked another PSpace in the group
cxAssert(txn.pspaceInGroup != nullptr);
gm.MergeTwoPSpaceTxnGroups(txn.pspaceInGroup, this);
}
}
virtual void OnEndTransaction(CSpace* cspace)
{
auto& txn = tls_persistStoreTxn;
cxAssert(txn.numPSpacesLocked > 0);
if (--txn.numPSpacesLocked == 0)
{
// Transaction on a group of PSpaces ended
txn.pspaceInGroup = nullptr;
}
}
PersistStore& m_ps;
};
There is no need to deal specially with freshly opened PSpaces.
We just need to make sure that when a new PSpace is created or opened at least
one CSpaceTxn is obtained on it.
That will mean OnBeginTransaction()/OnEndTransaction() will be called
on that new PSpace.
That's exactly what we need to ensure it is merged into the PSpace transaction
group exactly as required.
Opening a PSpace
The following public API function is used to open or create a PSpace. The given CSpace is used if provided, if it's null then the PSpace creates its own CSpace.
$function+ PSpace* OpenPSpace(PersistStore* ps, ConstStringZ name, EOpenMode openMode, CSpace* cspace)
{
cxAssert(ps);
return ps->OpenPSpace(name, openMode, cspace);
}
This is implemented by the PersistStore as follows:
PSpace* PersistStore::OpenPSpace(ConstStringZ name, EOpenMode openMode, CSpace* cspace)
{
return m_spaceMap.OpenPSpace(*m_lss, m_rootObject.m_oidHighForOidAllocations, name, openMode, cspace);
}
This is in turn implemented by the PSpaceMap as follows:
PSpace* PSpaceMap::OpenPSpace(ILogStructuredStore& lss, OidHigh oidHigh, ConstStringZ name, EOpenMode openMode, CSpace* cspace)
{
cxAssert(name);
std::lock_guard<std::mutex> lock(mutex_);
PSpace* pspace = nullptr;
MAP::iterator i = map_.find(name);
if (i == map_.end())
{
if (openMode == OM_OPEN_EXISTING || openMode == OM_DELETE_EXISTING)
{
throw OpenPSpaceException(openMode, name);
}
Entry e;
e.m_oid.high_ = oidHigh;
e.m_oid.low_ = 0x00000000;
cxVerify(lss.AllocateAffiliateSeid(e.m_oid));
e.m_ptr = pspace = new PSpace(m_ps,name,e.m_oid,true,cspace);
e.m_committed = false;
map_[name] = e;
}
else
{
if (openMode == OM_CREATE_NEW)
{
throw OpenPSpaceException(openMode, name);
}
Entry& e = i->second;
cxAssert(e.m_ptr == nullptr); // Only allow a PSpace to be opened once
cxAssert(e.m_oid);
e.m_ptr = pspace = new PSpace(m_ps,name,e.m_oid,false,cspace); // Create PSpace
}
cxAssert(pspace);
m_ps.m_dosWriter.OnOpenPSpace(pspace);
return pspace;
}
Note that the PSpace constructor uses a CSpaceLock.
PSpace::PSpace(PersistStore& ps, ConstStringZ name, OID oid, bool creating, CSpace* cspace) :
operationCallbackTxn_(nullptr),
m_ps(ps),
m_roots(oid,creating),
m_dq(creating),
m_name(name),
rot_(*this),
m_useIncrementalOidAllocations(false),
createdNew_(creating),
m_oidHighForOidAllocations(ps.GetOidHighForOidAllocations()),
m_affiliateOidLow(oid.low_),
m_ensureCommitted(false),
m_dos(*this),
m_cspace(cspace),
m_nextInTxnGrp(this),
m_inGrp(false)
{
cxAssert(name);
cxAssert(strlen(name) > 0);
if (!m_cspace)
{
m_cspace = CreateCSpace();
}
cxAssert(!GetPtrSlot(m_cspace,PTR_SLOT_PSpace));
SetPtrSlot(m_cspace, PTR_SLOT_PSpace, this);
// Needed to avoid assertion when call SetOid(). Also needed to allow any prefs to be dereferenced
DeclareThreadPSpace dtp(this);
CSpaceLock lock(m_cspace);
PSpaceRoot* r = new PSpaceRoot(*this);
RegisterGcObject(m_cspace,r);
AddGcRoot(m_cspace,r);
if (creating)
{
cxAssert(!m_roots.m_dqOid);
m_roots.m_dqOid = m_ps.AllocateAffiliateOid(m_affiliateOidLow); // Allocate an OID for the DQ
// We do not write the roots or DQ to the LSS here because creation of the PSpace is tied
// to the enclosing transaction. I.e. we wait until the PSpace DOS is processed.
}
else
{
m_roots.ReadFromLss(ps.GetLss());
m_dq.ReadFromLss(ps.GetLss(), m_roots.m_dqOid);
}
}