/************************************************************************ * EOS - the CERN Disk Storage System * * Copyright (C) 2022 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_NSOBJECTLOCKER_HH #define EOS_NSOBJECTLOCKER_HH #include "namespace/Namespace.hh" #include #include #include EOSNSNAMESPACE_BEGIN typedef std::unique_lock MDWriteLock; typedef std::shared_lock MDReadLock; class LockableNSObjMD { public: template friend class NSObjectMDLocker; LockableNSObjMD(){} LockableNSObjMD(const LockableNSObjMD& other) = delete; LockableNSObjMD& operator=(const LockableNSObjMD&) = delete; virtual ~LockableNSObjMD() = default; protected: /** * Runs a write operation where the logic is located on the functor passed in parameter. * * If this instance already has a write-lock registered, no lock will be taken before running the functor, * if not, a write-lock will be taken before running the functor * @tparam Functor the function type to pass * @param functor the function to run * @return the return value of the functor */ template auto runWriteOp(Functor && functor) const -> decltype(functor()){ if(!isLockRegisteredByThisThread(MDWriteLock())){ //Object mutex is not locked, lock it and run the functor MDWriteLock lock(getMutex()); return functor(); } else { //Object mutex is locked by this thread, run the functor return functor(); } } template auto runWriteOp(Functor && functor) -> decltype(functor()){ return const_cast(this)->runWriteOp(functor); } /** * Runs a read operation where the logic is located on the functor passed in parameter. * * If this instance already has a read-lock (or write-lock) registered, no lock will be taken before running the functor, * if not, a read-lock will be taken before running the functor * @tparam Functor the function type to pass * @param functor the function to run * @return the return value of the functor */ template auto runReadOp(Functor && functor) const -> decltype(functor()) { if(!isLockRegisteredByThisThread(MDReadLock())){ //Object mutex is not locked, lock it and run the functor MDReadLock lock(getMutex()); return functor(); } else { //Object mutex is locked by this thread, run the functor return functor(); } } template auto runReadOp(Functor && functor) -> decltype(functor()) { return const_cast(this)->runReadOp(functor); } //Assumes read lock is taken for the map bool isThisThreadInLockMap(const std::map & threadIdLockMap) const { return (threadIdLockMap.find(std::this_thread::get_id()) != threadIdLockMap.end()); } //Assumes lock is taken for the map void registerLock(std::map & threadIdLockMap) { auto threadId = std::this_thread::get_id(); auto threadIdLockMapItor = threadIdLockMap.find(threadId); if(threadIdLockMapItor == threadIdLockMap.end()) { threadIdLockMap[threadId] = 0; } threadIdLockMap[threadId] += 1; } //Assumes lock is taken for the map void unregisterLock(std::map & threadIdLockMap) { auto threadId = std::this_thread::get_id(); auto threadIdLockMapItor = threadIdLockMap.find(threadId); if(threadIdLockMapItor != threadIdLockMap.end()) { threadIdLockMap[threadId] -= 1; if(threadIdLockMapItor->second == 0) { threadIdLockMap.erase(threadId); } } } template void lock(LockType & lock) { //Lock the object only if it is not already read-locked or write-locked if(!isLockRegisteredByThisThread(lock)) { lock.lock(); } registerLock(lock); } /** * This method contains the logic that will check * wether a lock is already taken by this thread before acquiring * a read-lock * @param mdLock the lock allowing the overloading to work, it is actually not used * @return true if this thread already has the lock allowing the read operation to * be performed, false otherwise */ bool isLockRegisteredByThisThread(const MDReadLock & mdLock) const { std::unique_lock lock(mThreadIdLockMapMutex); // In case of a read, if this object is already locked by a write lock we consider it to be read-locked as well // otherwise a deadlock will happen if the object is write locked and a getter method that will try to // read lock the object is called... return (isThisThreadInLockMap(mThreadIdWriteLockMap) || isThisThreadInLockMap(mThreadIdReadLockMap)); } /** * This method contains the logic that will check * wether a lock is already taken by this thread before acquiring * a write-lock * @param mdLock the lock allowing the overloading to work, it is actually not used * @return true if this thread already has the lock allowing the write operation to * be performed, false otherwise */ bool isLockRegisteredByThisThread(const MDWriteLock & mdLock) const { std::unique_lock lock(mThreadIdLockMapMutex); return isThisThreadInLockMap(mThreadIdWriteLockMap); } /** * Registers the read lock * @param mdLock the lock allowing the overloading of this member function to work. It * is actually not used */ virtual void registerLock(MDReadLock & mdLock) { std::unique_lock lock(mThreadIdLockMapMutex); registerLock(mThreadIdReadLockMap); } /** * Registers the write lock * @param mdLock the lock allowing the overloading of this member function to work. It * is actually not used */ virtual void registerLock(MDWriteLock & mdLock) { std::unique_lock lock(mThreadIdLockMapMutex); registerLock(mThreadIdWriteLockMap); //A Write lock is also a readlock. If one tries to read //lock after a write lock on the same thread, a deadlock will happen registerLock(mThreadIdReadLockMap); } /** * Unregisters the read lock * @param mdLock the lock allowing the overloading of this member function to work. It * is actually not used */ virtual void unregisterLock(MDReadLock & mdLock) { std::unique_lock lock(mThreadIdLockMapMutex); unregisterLock(mThreadIdReadLockMap); } /** * Unregisters the write lock * @param mdLock the lock allowing the overloading of this member function to work. It * is actually not used */ virtual void unregisterLock(MDWriteLock & mdLock) { std::unique_lock lock(mThreadIdLockMapMutex); unregisterLock(mThreadIdWriteLockMap); unregisterLock(mThreadIdReadLockMap); } std::shared_timed_mutex & getMutex() { return const_cast(this)->getMutex(); } virtual std::shared_timed_mutex & getMutex() const = 0; private: //Mutex to protect the map that keeps track of the threads that are locking this MD object mutable std::mutex mThreadIdLockMapMutex; //Map that keeps track of the threads that already have a lock //on this MD object. This map is only filled when the MDLocker object //is used. mutable std::map mThreadIdWriteLockMap; mutable std::map mThreadIdReadLockMap; }; template class NSObjectMDLocker { public: //Constructor that defers the locking of the mutex and will delegate the locking logic to the objectMD NSObjectMDLocker(ObjectMDPtr objectMDPtr){ if(objectMDPtr) { mLock = LockType(objectMDPtr->getMutex(),std::defer_lock); mObjectMDPtr = objectMDPtr; mObjectMDPtr->lock(mLock); } else { // We should normally never reach that code in production // if the file/container does not exist, a MDException will // be thrown. throw_mdexception(ENOENT,"file/container does not exist"); } } ObjectMDPtr operator->() { return mObjectMDPtr; } ObjectMDPtr getUnderlyingPtr() { return operator->(); } virtual ~NSObjectMDLocker(){ if(mObjectMDPtr) { mObjectMDPtr->unregisterLock(mLock); } } private: //! KEEP THIS ORDER, THE SHARED_PTR NEEDS TO BE DESTROYED AFTER THE LOCK... //! Otherwise you will have a deadlock! ObjectMDPtr mObjectMDPtr; LockType mLock; }; EOSNSNAMESPACE_END #endif // EOS_NSOBJECTLOCKER_HH