diff options
Diffstat (limited to 'src/mongo/util/read_through_cache.h')
-rw-r--r-- | src/mongo/util/read_through_cache.h | 330 |
1 files changed, 330 insertions, 0 deletions
diff --git a/src/mongo/util/read_through_cache.h b/src/mongo/util/read_through_cache.h new file mode 100644 index 00000000000..83790bd9605 --- /dev/null +++ b/src/mongo/util/read_through_cache.h @@ -0,0 +1,330 @@ +/** + * Copyright (C) 2019-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include <boost/optional.hpp> + +#include "mongo/bson/oid.h" +#include "mongo/platform/mutex.h" +#include "mongo/stdx/condition_variable.h" +#include "mongo/util/invalidating_lru_cache.h" + +namespace mongo { + +class OperationContext; + +/** + * Serves as a container of the non-templatised parts of the ReadThroughCache class below. + */ +class ReadThroughCacheBase { + ReadThroughCacheBase(const ReadThroughCacheBase&) = delete; + ReadThroughCacheBase& operator=(const ReadThroughCacheBase&) = delete; + +public: + /** + * Returns the cache generation identifier. + */ + OID getCacheGeneration() const; + +protected: + ReadThroughCacheBase(Mutex& mutex); + + virtual ~ReadThroughCacheBase(); + + /** + * Type used to guard accesses and updates to the cache. + * + * Guard object for synchronizing accesses to data cached in ReadThroughCache instances. + * This guard allows one thread to access the cache at a time, and provides an exception-safe + * mechanism for a thread to release the cache mutex while performing network or disk operations + * while allowing other readers to proceed. + * + * There are two ways to use this guard. One may simply instantiate the guard like a + * std::lock_guard, and perform reads or writes of the cache. + * + * Alternatively, one may instantiate the guard, examine the cache, and then enter into an + * update mode by first wait()ing until otherUpdateInFetchPhase() is false, and then + * calling beginFetchPhase(). At this point, other threads may acquire the guard in the simple + * manner and do reads, but other threads may not enter into a fetch phase. During the fetch + * phase, the thread should perform required network or disk activity to determine what update + * it will make to the cache. Then, it should call endFetchPhase(), to reacquire the cache + * mutex. At that point, the thread can make its modifications to the cache and let the guard + * go out of scope. + * + * All updates by guards using a fetch-phase are totally ordered with respect to one another, + * and all guards using no fetch phase are totally ordered with respect to one another, but + * there is not a total ordering among all guard objects. + * + * The cached data has an associated counter, called the cache generation. If the cache + * generation changes while a guard is in fetch phase, the fetched data should not be stored + * into the cache, because some invalidation event occurred during the fetch phase. + */ + class CacheGuard { + CacheGuard(const CacheGuard&) = delete; + CacheGuard& operator=(const CacheGuard&) = delete; + + public: + /** + * Constructs a cache guard, locking the mutex that synchronizes ReadThroughCache accesses. + */ + explicit CacheGuard(ReadThroughCacheBase* distCache) + : _distCache(distCache), _cacheLock(distCache->_cacheWriteMutex) {} + + /** + * Releases the mutex that synchronizes cache access, if held, and notifies any threads + * waiting for their own opportunity to update the cache. + */ + ~CacheGuard() { + if (!_cacheLock.owns_lock()) { + _cacheLock.lock(); + } + + if (_isThisGuardInFetchPhase) { + invariant(otherUpdateInFetchPhase()); + _distCache->_isFetchPhaseBusy = false; + _distCache->_fetchPhaseIsReady.notify_all(); + } + } + + /** + * Returns true if the distCache reports that it is in fetch phase. + */ + bool otherUpdateInFetchPhase() const { + return _distCache->_isFetchPhaseBusy; + } + + /** + * Waits on the _distCache->_fetchPhaseIsReady condition. + */ + void wait() { + invariant(!_isThisGuardInFetchPhase); + _distCache->_fetchPhaseIsReady.wait(_cacheLock, + [&] { return !otherUpdateInFetchPhase(); }); + } + + /** + * Enters fetch phase, releasing the _distCache->_cacheMutex after recording the current + * cache generation. + */ + void beginFetchPhase() { + invariant(!otherUpdateInFetchPhase()); + _isThisGuardInFetchPhase = true; + _distCache->_isFetchPhaseBusy = true; + _distCacheFetchGenerationAtFetchBegin = _distCache->_fetchGeneration; + _cacheLock.unlock(); + } + + /** + * Exits the fetch phase, reacquiring the _distCache->_cacheMutex. + */ + void endFetchPhase() { + _cacheLock.lock(); + // We do not clear _distCache->_isFetchPhaseBusy or notify waiters until + // ~CacheGuard(), for two reasons. First, there's no value to notifying the waiters + // before you're ready to release the mutex, because they'll just go to sleep on the + // mutex. Second, in order to meaningfully check the preconditions of + // isSameCacheGeneration(), we need a state that means "fetch phase was entered and now + // has been exited." That state is _isThisGuardInFetchPhase == true and + // _lock.owns_lock() == true. + } + + /** + * Returns true if _distCache->_fetchGeneration remained the same while this guard was + * in fetch phase. Behavior is undefined if this guard never entered fetch phase. + * + * If this returns true, do not update the cached data with this + */ + bool isSameCacheGeneration() const { + invariant(_isThisGuardInFetchPhase); + invariant(_cacheLock.owns_lock()); + return _distCacheFetchGenerationAtFetchBegin == _distCache->_fetchGeneration; + } + + private: + ReadThroughCacheBase* const _distCache; + + stdx::unique_lock<Latch> _cacheLock; + + bool _isThisGuardInFetchPhase{false}; + OID _distCacheFetchGenerationAtFetchBegin; + }; + + friend class ReadThroughCacheBase::CacheGuard; + + /** + * Updates _fetchGeneration to a new OID + */ + void _updateCacheGeneration(const CacheGuard&); + + /** + * Protects _fetchGeneration and _isFetchPhaseBusy. Manipulated via CacheGuard. + */ + Mutex& _cacheWriteMutex; + + /** + * Current generation of cached data. Updated every time part of the cache gets + * invalidated. Protected by CacheGuard. + */ + OID _fetchGeneration{OID::gen()}; + + /** + * True if there is an update to the _cache in progress, and that update is currently in + * the "fetch phase", during which it does not hold the _cacheMutex. + * + * Manipulated via CacheGuard. + */ + bool _isFetchPhaseBusy{false}; + + /** + * Condition used to signal that it is OK for another CacheGuard to enter a fetch phase. + * Manipulated via CacheGuard. + */ + stdx::condition_variable _fetchPhaseIsReady; +}; + +/** + * Implements a generic read-through cache built on top of InvalidatingLRUCache. + */ +template <typename Key, typename Value> +class ReadThroughCache : public ReadThroughCacheBase { +public: + using Cache = InvalidatingLRUCache<Key, Value>; + using ValueHandle = typename Cache::ValueHandle; + using LookupFn = std::function<boost::optional<Value>(OperationContext*, const Key&)>; + + /** + * If 'key' is found in the cache, returns a ValidHandle, otherwise invokes the blocking + * 'lookup' method below to fetch the 'key' from the backing store. If the key is not found in + * the backing store, returns a ValueHandle which defaults to not-set (it's bool operator is + * false). + * + * NOTES: + * This is a potentially blocking method. + * The returned value may be invalid by the time the caller gets access to it. + */ + ValueHandle acquire(OperationContext* opCtx, const Key& key) { + while (true) { + auto cachedValue = _cache.get(key); + if (cachedValue) + return cachedValue; + + // Otherwise make sure we have the locks we need and check whether and wait on another + // thread is fetching into the cache + CacheGuard guard(this); + + while (!(cachedValue = _cache.get(key)) && guard.otherUpdateInFetchPhase()) { + guard.wait(); + } + + if (cachedValue) + return cachedValue; + + // If there's still no value in the cache, then we need to go and get it. Take the slow + // path. + guard.beginFetchPhase(); + + auto value = lookup(opCtx, key); + if (!value) + return cachedValue; + + // All this does is re-acquire the _cacheWriteMutex if we don't hold it already - a + // caller may also call endFetchPhase() after this returns. + guard.endFetchPhase(); + + if (guard.isSameCacheGeneration()) + return _cache.insertOrAssignAndGet(key, std::move(*value)); + + // If the cache generation changed while this thread was in fetch mode, the data + // associated with the value may now be invalid, so we will throw out the fetched value + // and retry. + } + } + + /** + * Invalidates the given 'key' and immediately replaces it with a new value. + */ + ValueHandle insertOrAssignAndGet(const Key& key, Value&& newValue) { + CacheGuard guard(this); + _updateCacheGeneration(guard); + return _cache.insertOrAssignAndGet(key, std::move(newValue)); + } + + /** + * The invalidate methods below all marks the given value(s) as invalid and remove them from + * cache, which means that a subsequent call to acquire will invoke 'lookup'. + */ + void invalidate(const Key& key) { + CacheGuard guard(this); + _updateCacheGeneration(guard); + _cache.invalidate(key); + } + + template <typename Pred> + void invalidateIf(const Pred& predicate) { + CacheGuard guard(this); + _updateCacheGeneration(guard); + _cache.invalidateIf( + [&](const Key& key, const Value* value) { return predicate(key, value); }); + } + + void invalidateAll() { + invalidateIf([](const Key&, const Value*) { return true; }); + } + + /** + * Returns statistics information about the cache for reporting purposes. + */ + std::vector<typename Cache::CachedItemInfo> getCacheInfo() const { + return _cache.getCacheInfo(); + } + +protected: + /** + * ReadThroughCache constructor, to be called by sub-classes. Accepts the initial size of the + * cache, and a reference to a Mutex. The Mutex is for the exclusive use of the + * ReadThroughCache, the sub-class should never actually use it (apart from passing it to this + * constructor). Having the Mutex stored by the sub-class allows latch diagnostics to be + * correctly associated with the sub-class (not the generic ReadThroughCache class). + */ + ReadThroughCache(int cacheSize, Mutex& mutex) + : ReadThroughCacheBase(mutex), _cache(cacheSize) {} + +private: + /** + * Provide the value for a key when there is a cache miss. Sub-classes must implement this + * function appropriately. Throw a uassertion to indicate an error while looking up the value, + * or return value for this key, or boost::none if this key has no value. + */ + virtual boost::optional<Value> lookup(OperationContext* opCtx, const Key& key) = 0; + + Cache _cache; +}; + +} // namespace mongo |