/* * Copyright (C) 2007-2019 Xagasoft, All rights reserved. * * This file is part of the libbu++ library and is released under the * terms of the license contained in the file LICENSE. */ #ifndef BU_CACHE_BASE_H #define BU_CACHE_BASE_H #include "bu/string.h" #include "bu/archive.h" #include "bu/hash.h" #include "bu/readwritemutex.h" #include "bu/mutexlocker.h" #include "bu/cacheobject.h" #include "bu/sio.h" namespace Bu { template class CacheBase; template class CacheEntry; template class CacheEntry { friend class CacheBase; private: CacheEntry( obtype *pObject ) : iRefCount( 0 ), bDeleted( false ), pObject( pObject ) { } public: int getRefCount() const { mEntry.lock(); int ret = iRefCount; mEntry.unlock(); return ret; } obtype *getPtr() const { return pObject; } void lock() const { mObject.lock(); } void unlock() const { mObject.unlock(); } Bu::Mutex &getMutex() { return mObject; } void incRef() { mEntry.lock(); iRefCount++; mEntry.unlock(); } bool decRef() { mEntry.lock(); iRefCount--; bool bRet = iRefCount > 0; mEntry.unlock(); return bRet; } bool isDeleted() { mEntry.lock(); bool bRet = bDeleted; mEntry.unlock(); return bRet; } // Indicates that the entry was both deleted // and has a refcount of zero bool isReadyForCleanup() { mEntry.lock(); bool bRet = bDeleted && iRefCount == 0; mEntry.unlock(); return bRet; } private: mutable Bu::Mutex mEntry; mutable Bu::Mutex mObject; int iRefCount; bool bDeleted; obtype *pObject; }; template class CachePtrInterface { protected: CachePtrInterface() { } virtual ~CachePtrInterface() { } template void checkRef( CacheBase *pCache, const keytype &kId, CacheEntry * &rpEnt, obtype * &rpData ) const { if( pCache == NULL ) throw Bu::ExceptionBase("Invalid cache pointer"); if( !rpData ) { rpEnt = pCache->getRef( kId ); rpEnt->incRef(); rpData = dynamic_cast(rpEnt->getPtr()); if( rpData == NULL ) { rpEnt->decRef(); rpEnt = NULL; throw std::bad_cast(); } } } template void releaseRef( CacheBase *pCache, CacheEntry * &rpEnt, obtype * &rpData ) const { if( pCache == NULL ) return; if( rpData == NULL ) return; rpData = NULL; rpEnt->decRef(); pCache->releaseRef( rpEnt ); rpEnt = NULL; } }; template class CachePtr : protected CachePtrInterface { friend class CacheBase; private: typedef CachePtr MyType; CachePtr( CacheBase *pCache, CacheEntry *pEnt, const keytype &kId ) : pCache( pCache ), kId( kId ), pEnt( pEnt ), pData( NULL ) { pEnt->incRef(); pData = dynamic_cast( pEnt->getPtr() ); if( pData == NULL ) { pEnt->decRef(); throw std::bad_cast(); } } CachePtr( CacheBase *pCache, const keytype &kId ) : pCache( pCache ), kId( kId ), pEnt( NULL ), pData( NULL ) { } public: typedef keytype Key; typedef obtype Value; CachePtr() : pCache( NULL ), pEnt( NULL ), pData( NULL ) { } CachePtr( const MyType &rhs ) : pCache( rhs.pCache ), kId( rhs.kId ), pEnt( rhs.pEnt ), pData( rhs.pData ) { if( pEnt ) pEnt->incRef(); } virtual ~CachePtr() { unbind(); } const keytype &getKey() const { return kId; } obtype &operator*() { bind(); return pData; } const obtype &operator*() const { bind(); return pData; } obtype *operator->() { bind(); return pData; } const obtype *operator->() const { bind(); return pData; } MyType operator=( const MyType &rhs ) { unbind(); pCache = rhs.pCache; kId = rhs.kId; pEnt = rhs.pEnt; pData = rhs.pData; if( pEnt ) pEnt->incRef(); return *this; } template CachePtr cast() { return pCache->template cast( *this ); } template CachePtr cast() const { return pCache->template cast( *this ); } bool operator==( const MyType &rhs ) const { return pCache == rhs.pCache && kId == rhs.kId; } bool operator!=( const MyType &rhs ) const { return pCache != rhs.pCache || kId != rhs.kId; } void bind() { CachePtrInterface::checkRef( pCache, kId, pEnt, pData ); } void unbind() { CachePtrInterface::releaseRef( pCache, pEnt, pData ); } void bind() const { CachePtrInterface::checkRef( pCache, kId, pEnt, pData ); } void unbind() const { CachePtrInterface::releaseRef( pCache, pEnt, pData ); } bool isSet() const { if( pCache ) return true; return false; } void clear() { unbind(); pCache = NULL; pEnt = NULL; pData = NULL; } void lock() { bind(); if( pEnt ) pEnt->lock(); } void unlock() { bind(); if( pEnt ) pEnt->unlock(); } class Locker { public: Locker( MyType &rPtr ) : rPtr( rPtr ), bLocked( true ) { rPtr.lock(); } ~Locker() { unlock(); } void unlock() { if( !bLocked ) return; rPtr.unlock(); bLocked = false; } void lock() { if( bLocked ) return; rPtr.lock(); bLocked = true; } private: MyType &rPtr; bool bLocked; }; private: CacheBase *pCache; mutable keytype kId; mutable CacheEntry *pEnt; mutable obtype *pData; }; template class CacheBase { friend class CachePtrInterface; friend class CacheObject; public: CacheBase() { } virtual ~CacheBase() { } typedef CacheEntry Entry; typedef Bu::List KeyList; typedef CacheObject ObjectType; CachePtr insert( obtype *pObject ) { Entry *pEnt = addEntry( pObject ); return CachePtr( this, pEnt, pObject->getKey() ); } template CachePtr insert( supertype *pObject ) { obtype *pCast = dynamic_cast( pObject ); if( pCast == NULL ) throw std::bad_cast(); Entry *pEnt = addEntry( pCast ); return CachePtr( this, pEnt, pObject->getKey() ); } bool has( const keytype &key ) { Bu::ReadWriteMutex::ReadLocker rl( mCacheEntry ); if( hCacheEntry.has( key ) ) return true; return _has( key ); } CachePtr get( const keytype &key ) { Entry *pEnt = getEntry( key ); return CachePtr( this, pEnt, key ); } template CachePtr get( const keytype &key ) { Entry *pEnt = getEntry( key ); return CachePtr( this, pEnt, key ); } CachePtr getLazy( const keytype &key ) { return CachePtr( this, key ); } template CachePtr getLazy( const keytype &key ) { return CachePtr( this, key ); } template CachePtr cast( CachePtr &ptr ) { if( ptr.pEnt ) return CachePtr( this, ptr.pEnt, ptr.kId ); else return CachePtr( this, ptr.kId ); } template CachePtr cast( const CachePtr &ptr ) { if( ptr.pEnt ) return CachePtr( this, ptr.pEnt, ptr.kId ); else return CachePtr( this, ptr.kId ); } /** * Removes an item from the cache. * The object in question can have references to it, and those * references will still work after the erase is called. When erase * is called the object is immediately removed from the backing store * as well as from the main index. At that point existing references * can be used as normal, but when the last reference is released * the memory will be automatically cleaned up. */ void erase( const keytype &key ) { Bu::ReadWriteMutex::WriteLocker wl( mCacheEntry ); if( hCacheEntry.has( key ) ) { Entry *pEnt = hCacheEntry.get( key ); pEnt->mEntry.lock(); if( pEnt->iRefCount == 0 ) { pEnt->mEntry.unlock(); delete pEnt->pObject; delete pEnt; } else { pEnt->bDeleted = true; // Bu::println("Deleted item, ref count = %1").arg( pEnt->iRefCount ); pEnt->mEntry.unlock(); } hCacheEntry.erase( key ); } _erase( key ); } /** * Removes an item from the cache and frees memory immediately. * The object in question cannot have any active references in order * for this to succeed, otherwise it will throw an exception. */ void eraseNow( const keytype &key ) { Bu::ReadWriteMutex::WriteLocker wl( mCacheEntry ); if( hCacheEntry.has( key ) ) { Entry *pEnt = hCacheEntry.get( key ); pEnt->mEntry.lock(); if( pEnt->iRefCount > 0 ) { int iCount = pEnt->iRefCount; pEnt->mEntry.unlock(); throw Bu::ExceptionBase( Bu::String("Cache entry %1 cannot be erased, there are %2 active references.").arg( key ).arg( iCount ).end().getStr() ); } delete pEnt->pObject; delete pEnt; hCacheEntry.erase( key ); } _erase( key ); } virtual KeyList getKeys() const=0; virtual int getSize() const=0; void sync() { { Bu::ReadWriteMutex::WriteLocker wl( mCacheEntry ); _sync(); } syncChanges(); } protected: Entry *getRef( const keytype &k ) { Entry *pEnt = getEntry( k ); return pEnt; } void releaseRef( Entry *pEnt ) { if( pEnt->isReadyForCleanup() ) { delete pEnt->pObject; delete pEnt; } } void objectChanged( const keytype &k ) { Bu::ReadWriteMutex::WriteLocker wl( mCacheEntry ); hChanged.insert( k, true ); } protected: virtual bool _has( const keytype &key )=0; virtual void _create( const obtype *o )=0; virtual void _erase( const keytype &k )=0; virtual obtype *_load( typename Bu::CacheObject::Initializer &initObj, const keytype &k )=0; virtual void _save( const obtype *o )=0; virtual void _sync()=0; private: Entry *addEntry( obtype *pObject ) { Entry *pEnt = new Entry( pObject ); mCacheEntry.lockWrite(); hCacheEntry.insert( pObject->getKey(), pEnt ); if( pObject->hasChanged() ) hChanged.insert( pObject->getKey(), true ); mCacheEntry.unlockWrite(); _create( pObject ); // I'm not sold on the ordering here...or the fact we're not // using th initilizer pObject->setCache( this ); pObject->setCacheEntry( pEnt ); return pEnt; } Entry *getEntry( const keytype &k ) { Entry *pEnt = NULL; try { Bu::ReadWriteMutex::ReadLocker rl( mCacheEntry ); pEnt = hCacheEntry.get( k ); } catch(...) { // try to load the object from the backing store typename ObjectType::Initializer initObj( this ); obtype *pObject = _load( initObj, k ); pEnt = new Entry( pObject ); pObject->setCacheEntry( pEnt ); Bu::ReadWriteMutex::WriteLocker wl( mCacheEntry ); hCacheEntry.insert( k, pEnt ); } return pEnt; } void syncChanges() { Bu::List lKeys; mCacheEntry.lockWrite(); for( typename CacheKeySet::iterator i = hChanged.begin(); i; i++ ) { lKeys.append( i.getKey() ); } hChanged.clear(); mCacheEntry.unlockWrite(); for( typename List::iterator i = lKeys.begin(); i; i++ ) { Entry *pEnt = NULL; try { Bu::ReadWriteMutex::ReadLocker rl( mCacheEntry ); pEnt = hCacheEntry.get( *i ); } catch( Bu::HashException & ) { // The entry wasn't there, most likely because it // was deleted. That's ok. } // This isn't inside of the above because we want to be // able to handle these exceptions differently. if( pEnt ) { Bu::MutexLocker ml( pEnt->getMutex() ); _save( pEnt->getPtr() ); pEnt->getPtr()->changed( false ); } } } private: typedef Bu::Hash CacheEntryHash; typedef Bu::Hash CacheKeySet; CacheEntryHash hCacheEntry; CacheKeySet hChanged; Bu::ReadWriteMutex mCacheEntry; }; template void _cacheObjectSave( Bu::Stream &s, obtype *pObject ) { Bu::Archive ar( s, Bu::Archive::save ); ar << *pObject; } template obtype *_cacheObjectLoad( typename Bu::CacheObject::Initializer &initObject, const keytype & /*rKey*/, Bu::Stream &s ) { Bu::Archive ar( s, Bu::Archive::load ); obtype *ret = initObject(new obtype()); ar >> *ret; return ret; } } #endif