// ---------------------------------------------------------------------- // File: common/ExpiryCache.cc // Author: Jozsef Makai - CERN // ---------------------------------------------------------------------- /************************************************************************ * EOS - the CERN Disk Storage System * * Copyright (C) 2017 CERN/Switzerland * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see .* ************************************************************************/ #ifndef EOS_EXPIRYCACHE_HH #define EOS_EXPIRYCACHE_HH #include "common/Namespace.hh" #include "common/RWMutex.hh" #include #include EOSCOMMONNAMESPACE_BEGIN class UpdateException : public std::runtime_error { public: explicit UpdateException(const std::string& message) : runtime_error(message) {}; }; //! @brief Configurable cache for a single object with an expiry time frame. It works with expiry and invalidity time frames. //! Before expiry, the data in cache is not updated and request is served from cache instantly. //! After expiry and before invalidity, data is served from cache instantly and asynchronous update is issued. //! After invalidity (or in case of a forced update), update is synchronous, and client waits for it to happen. //! @tparam T type of the object to be stored in the cache template class ExpiryCache { private: eos::common::RWMutex mObjectLock; eos::common::RWMutex mUpdatePromiseLock; std::atomic_bool mIsUpdatePending{false}; std::atomic mExpiredAfter; std::atomic mInvalidAfter; std::chrono::time_point mUpdatedAt = std::chrono::steady_clock::now(); std::shared_future mUpdateFuture; std::unique_ptr mCachedObject = nullptr; //! @brief Check if the contained data needs an update and became invalid //! @param forceUpdate forced update is needed ignoring the state of the cache, data is invalidated //! @param isInvalidRet return value to tell whether the data is invalid //! @return whether update is needed or not bool IsUpdateNeeded(bool forceUpdate, bool& isInvalidRet) { bool isInvalid = false; std::chrono::seconds elapsedSinceUpdate; { eos::common::RWMutexReadLock lock(mObjectLock); auto now = std::chrono::steady_clock::now(); elapsedSinceUpdate = std::chrono::duration_cast(now - mUpdatedAt); isInvalid = !mCachedObject || forceUpdate || elapsedSinceUpdate >= mInvalidAfter.load(); isInvalidRet = isInvalid; } if(isInvalid || elapsedSinceUpdate >= mExpiredAfter.load()) { if(mIsUpdatePending) { return false; } return true; } return false; } public: //! @brief Construct a cache object //! @param expiredAfter expiry time, after this data is served from cache instantly and asynchronous update is issued //! @param invalidAfter invalidity time, after this the data is no longer served from cache, the client will wait for a synchronous update, never by default explicit ExpiryCache(std::chrono::seconds expiredAfter, std::chrono::seconds invalidAfter = std::chrono::seconds::max()) : mExpiredAfter(expiredAfter), mInvalidAfter(invalidAfter > expiredAfter ? invalidAfter : std::chrono::seconds::max()) {} //! @brief Set the expiry time, only changed if expiry < invalidity relation remains //! @param expiredAfter expiry time void SetExpiredAfter(std::chrono::seconds expiredAfter) { if (mInvalidAfter.load() > expiredAfter) mExpiredAfter.store(expiredAfter); } //! @brief Set the invalidity time, only changed if expiry < invalidity relation remains //! @param invalidAfter invalidity time void SetInvalidAfter(std::chrono::seconds invalidAfter) { if (invalidAfter > mExpiredAfter.load()) mInvalidAfter.store(invalidAfter); } //! @brief Request for the cached data. //! @tparam Functor type of the updating function object //! @tparam ARGS variadic template type for the arguments //! @param forceUpdate tells whether it should be a forced update //! @param produceObject function object to call for an update, it has to return a pointer to the new object //! @param params variadic parameters, will be perfect forwarded to the updating function //! @return the object in the cache template typename std::remove_reference::type getCachedObject(bool forceUpdate, Functor&& produceObject, ARGS&&... params) { bool isInvalid = false; if (IsUpdateNeeded(forceUpdate, isInvalid)) { eos::common::RWMutexWriteLock promiseLock(mUpdatePromiseLock); if (IsUpdateNeeded(forceUpdate, isInvalid)) { mIsUpdatePending = true; mUpdateFuture = std::async( isInvalid ? std::launch::deferred : std::launch::async, [&](ARGS&& ... params) { try { T* updatedObject = produceObject(std::forward(params)...); if (updatedObject != nullptr) { eos::common::RWMutexWriteLock lock(mObjectLock); mCachedObject.reset(updatedObject); mUpdatedAt = std::chrono::steady_clock::now(); } } catch (...) {} mIsUpdatePending = false; }, std::forward(params)... ); } } if(isInvalid) { eos::common::RWMutexReadLock promiseLock(mUpdatePromiseLock); mUpdateFuture.get(); } eos::common::RWMutexReadLock lock(mObjectLock); return mCachedObject ? *mCachedObject : throw UpdateException("Could not update the data, no valid data is present."); } }; EOSCOMMONNAMESPACE_END #endif //EOS_EXPIRYCACHE_HH