//////////////////////////////////////////////////////////////////////////////// /// DISCLAIMER /// /// Copyright 2014-2016 ArangoDB GmbH, Cologne, Germany /// Copyright 2004-2014 triAGENS GmbH, Cologne, Germany /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. /// You may obtain a copy of the License at /// /// http://www.apache.org/licenses/LICENSE-2.0 /// /// Unless required by applicable law or agreed to in writing, software /// distributed under the License is distributed on an "AS IS" BASIS, /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. /// See the License for the specific language governing permissions and /// limitations under the License. /// /// Copyright holder is ArangoDB GmbH, Cologne, Germany /// /// @author Jan Steemann //////////////////////////////////////////////////////////////////////////////// #include "QueryCache.h" #include "Basics/fasthash.h" #include "Basics/Exceptions.h" #include "Basics/MutexLocker.h" #include "Basics/ReadLocker.h" #include "Basics/WriteLocker.h" #include "VocBase/vocbase.h" #include #include using namespace arangodb::aql; namespace { /// @brief singleton instance of the query cache static arangodb::aql::QueryCache Instance; /// @brief maximum number of results in each per-database cache static size_t MaxResults = 128; // default value. can be changed later /// @brief whether or not the cache is enabled static std::atomic Mode(CACHE_ON_DEMAND); } /// @brief create a cache entry QueryCacheResultEntry::QueryCacheResultEntry( uint64_t hash, QueryString const& queryString, std::shared_ptr const& queryResult, std::vector const& dataSources) : _hash(hash), _queryString(queryString.data(), queryString.size()), _queryResult(queryResult), _dataSources(dataSources), _prev(nullptr), _next(nullptr), _refCount(0), _deletionRequested(0) { } /// @brief check whether the element can be destroyed, and delete it if yes void QueryCacheResultEntry::tryDelete() { _deletionRequested = 1; if (_refCount == 0) { delete this; } } /// @brief use the element, so it cannot be deleted meanwhile void QueryCacheResultEntry::use() { ++_refCount; } /// @brief unuse the element, so it can be deleted if required void QueryCacheResultEntry::unuse() { TRI_ASSERT(_refCount > 0); if (--_refCount == 0) { if (_deletionRequested == 1) { // trigger the deletion delete this; } } } /// @brief create a database-specific cache QueryCacheDatabaseEntry::QueryCacheDatabaseEntry() : _entriesByHash(), _head(nullptr), _tail(nullptr), _numElements(0) { _entriesByHash.reserve(128); _entriesByDataSource.reserve(16); } /// @brief destroy a database-specific cache QueryCacheDatabaseEntry::~QueryCacheDatabaseEntry() { for (auto& it : _entriesByHash) { tryDelete(it.second); } _entriesByHash.clear(); _entriesByDataSource.clear(); } /// @brief lookup a query result in the database-specific cache QueryCacheResultEntry* QueryCacheDatabaseEntry::lookup( uint64_t hash, QueryString const& queryString) { auto it = _entriesByHash.find(hash); if (it == _entriesByHash.end()) { // not found in cache return nullptr; } // found some result in cache if (queryString.size() != (*it).second->_queryString.size() || memcmp(queryString.data(), (*it).second->_queryString.c_str(), queryString.size()) != 0) { // found something, but obviously the result of a different query with the // same hash return nullptr; } // found an entry auto entry = (*it).second; // mark the entry as being used so noone else can delete it while it is in use entry->use(); return entry; } /// @brief store a query result in the database-specific cache void QueryCacheDatabaseEntry::store(uint64_t hash, QueryCacheResultEntry* entry) { // insert entry into the cache if (!_entriesByHash.emplace(hash, entry).second) { // remove previous entry auto it = _entriesByHash.find(hash); TRI_ASSERT(it != _entriesByHash.end()); auto previous = (*it).second; unlink(previous); _entriesByHash.erase(it); tryDelete(previous); // and insert again _entriesByHash.emplace(hash, entry); } try { for (auto const& it : entry->_dataSources) { auto it2 = _entriesByDataSource.find(it); if (it2 == _entriesByDataSource.end()) { // no entry found for data source. now create it _entriesByDataSource.emplace(it, std::unordered_set{hash}); } else { // there already was an entry for this data source (*it2).second.emplace(hash); } } } catch (...) { // rollback // remove from data sources for (auto const& it : entry->_dataSources) { auto it2 = _entriesByDataSource.find(it); if (it2 != _entriesByDataSource.end()) { (*it2).second.erase(hash); } } // finally remove entry itself from hash table auto it = _entriesByHash.find(hash); TRI_ASSERT(it != _entriesByHash.end()); auto previous = (*it).second; _entriesByHash.erase(it); unlink(previous); tryDelete(previous); throw; } link(entry); enforceMaxResults(::MaxResults); TRI_ASSERT(_numElements <= ::MaxResults); TRI_ASSERT(_head != nullptr); TRI_ASSERT(_tail != nullptr); TRI_ASSERT(_tail == entry); TRI_ASSERT(entry->_next == nullptr); } /// @brief invalidate all entries for the given data sources in the /// database-specific cache void QueryCacheDatabaseEntry::invalidate( std::vector const& dataSources) { for (auto const& it : dataSources) { invalidate(it); } } /// @brief invalidate all entries for a data source in the database-specific /// cache void QueryCacheDatabaseEntry::invalidate(std::string const& dataSource) { auto it = _entriesByDataSource.find(dataSource); if (it == _entriesByDataSource.end()) { return; } for (auto& it2 : (*it).second) { auto it3 = _entriesByHash.find(it2); if (it3 != _entriesByHash.end()) { // remove entry from the linked list auto entry = (*it3).second; unlink(entry); // erase it from hash table _entriesByHash.erase(it3); // delete the object itself tryDelete(entry); } } _entriesByDataSource.erase(it); } /// @brief enforce maximum number of results void QueryCacheDatabaseEntry::enforceMaxResults(size_t value) { while (_numElements > value) { // too many elements. now wipe the first element from the list // copy old _head value as unlink() will change it... auto head = _head; unlink(head); auto it = _entriesByHash.find(head->_hash); TRI_ASSERT(it != _entriesByHash.end()); _entriesByHash.erase(it); tryDelete(head); } } /// @brief check whether the element can be destroyed, and delete it if yes void QueryCacheDatabaseEntry::tryDelete(QueryCacheResultEntry* e) { e->tryDelete(); } /// @brief unlink the result entry from the list void QueryCacheDatabaseEntry::unlink(QueryCacheResultEntry* e) { if (e->_prev != nullptr) { e->_prev->_next = e->_next; } if (e->_next != nullptr) { e->_next->_prev = e->_prev; } if (_head == e) { _head = e->_next; } if (_tail == e) { _tail = e->_prev; } e->_prev = nullptr; e->_next = nullptr; TRI_ASSERT(_numElements > 0); --_numElements; } /// @brief link the result entry to the end of the list void QueryCacheDatabaseEntry::link(QueryCacheResultEntry* e) { ++_numElements; if (_head == nullptr) { // list is empty TRI_ASSERT(_tail == nullptr); // set list head and tail to the element _head = e; _tail = e; return; } if (_tail != nullptr) { // adjust list tail _tail->_next = e; } e->_prev = _tail; _tail = e; } /// @brief create the query cache QueryCache::QueryCache() : _propertiesLock(), _entriesLock(), _entries() {} /// @brief destroy the query cache QueryCache::~QueryCache() { for (unsigned int i = 0; i < numberOfParts; ++i) { invalidate(i); } } /// @brief return the query cache properties VPackBuilder QueryCache::properties() { MUTEX_LOCKER(mutexLocker, _propertiesLock); VPackBuilder builder; builder.openObject(); builder.add("mode", VPackValue(modeString(mode()))); builder.add("maxResults", VPackValue(::MaxResults)); builder.close(); return builder; } /// @brief return the cache properties void QueryCache::properties(std::pair& result) { MUTEX_LOCKER(mutexLocker, _propertiesLock); result.first = modeString(mode()); result.second = ::MaxResults; } /// @brief set the cache properties void QueryCache::setProperties( std::pair const& properties) { MUTEX_LOCKER(mutexLocker, _propertiesLock); setMode(properties.first); setMaxResults(properties.second); } /// @brief test whether the cache might be active /// this is a quick test that may save the caller from further bothering /// about the query cache if case it returns `false` bool QueryCache::mayBeActive() const { return (mode() != CACHE_ALWAYS_OFF); } /// @brief return whether or not the query cache is enabled QueryCacheMode QueryCache::mode() const { return ::Mode.load(std::memory_order_relaxed); } /// @brief return a string version of the mode std::string QueryCache::modeString(QueryCacheMode mode) { switch (mode) { case CACHE_ALWAYS_OFF: return "off"; case CACHE_ALWAYS_ON: return "on"; case CACHE_ON_DEMAND: return "demand"; } TRI_ASSERT(false); return "off"; } /// @brief lookup a query result in the cache QueryCacheResultEntry* QueryCache::lookup(TRI_vocbase_t* vocbase, uint64_t hash, QueryString const& queryString) { auto const part = getPart(vocbase); READ_LOCKER(readLocker, _entriesLock[part]); auto it = _entries[part].find(vocbase); if (it == _entries[part].end()) { // no entry found for the requested database return nullptr; } return (*it).second->lookup(hash, queryString); } /// @brief store a query in the cache /// if the call is successful, the cache has taken over ownership for the /// query result! QueryCacheResultEntry* QueryCache::store( TRI_vocbase_t* vocbase, uint64_t hash, QueryString const& queryString, std::shared_ptr const& result, std::shared_ptr const& stats, std::vector&& dataSources) { if (!result->slice().isArray()) { return nullptr; } // get the right part of the cache to store the result in auto const part = getPart(vocbase); // create the cache entry outside the lock auto entry = std::make_unique( hash, queryString, result, std::move(dataSources)); WRITE_LOCKER(writeLocker, _entriesLock[part]); auto it = _entries[part].find(vocbase); if (it == _entries[part].end()) { // create entry for the current database auto db = std::make_unique(); it = _entries[part].emplace(vocbase, db.get()).first; db.release(); } // store cache entry (*it).second->store(hash, entry.get()); return entry.release(); } /// @brief store a query in the cache void QueryCache::store(TRI_vocbase_t* vocbase, std::unique_ptr entry) { // get the right part of the cache to store the result in auto const part = getPart(vocbase); WRITE_LOCKER(writeLocker, _entriesLock[part]); auto it = _entries[part].find(vocbase); if (it == _entries[part].end()) { // create entry for the current database auto db = std::make_unique(); it = _entries[part].emplace(vocbase, db.get()).first; db.release(); } // store cache entry (*it).second->store(entry->_hash, entry.get()); entry.release(); } /// @brief invalidate all queries for the given data sources void QueryCache::invalidate(TRI_vocbase_t* vocbase, std::vector const& dataSources) { auto const part = getPart(vocbase); WRITE_LOCKER(writeLocker, _entriesLock[part]); auto it = _entries[part].find(vocbase); if (it == _entries[part].end()) { return; } // invalidate while holding the lock (*it).second->invalidate(dataSources); } /// @brief invalidate all queries for a particular data source void QueryCache::invalidate(TRI_vocbase_t* vocbase, std::string const& dataSource) { auto const part = getPart(vocbase); WRITE_LOCKER(writeLocker, _entriesLock[part]); auto it = _entries[part].find(vocbase); if (it == _entries[part].end()) { return; } // invalidate while holding the lock (*it).second->invalidate(dataSource); } /// @brief invalidate all queries for a particular database void QueryCache::invalidate(TRI_vocbase_t* vocbase) { QueryCacheDatabaseEntry* databaseQueryCache = nullptr; { auto const part = getPart(vocbase); WRITE_LOCKER(writeLocker, _entriesLock[part]); auto it = _entries[part].find(vocbase); if (it == _entries[part].end()) { return; } databaseQueryCache = (*it).second; _entries[part].erase(it); } // delete without holding the lock TRI_ASSERT(databaseQueryCache != nullptr); delete databaseQueryCache; } /// @brief invalidate all queries void QueryCache::invalidate() { for (unsigned int i = 0; i < numberOfParts; ++i) { WRITE_LOCKER(writeLocker, _entriesLock[i]); // must invalidate all entries now because disabling the cache will turn off // cache invalidation when modifying data. turning on the cache later would // then // lead to invalid results being returned. this can all be prevented by // fully // clearing the cache invalidate(i); } } /// @brief get the query cache instance QueryCache* QueryCache::instance() { return &::Instance; } /// @brief enforce maximum number of elements in each database-specific cache void QueryCache::enforceMaxResults(size_t value) { for (unsigned int i = 0; i < numberOfParts; ++i) { WRITE_LOCKER(writeLocker, _entriesLock[i]); for (auto& it : _entries[i]) { it.second->enforceMaxResults(value); } } } /// @brief determine which lock to use for the cache entries unsigned int QueryCache::getPart(TRI_vocbase_t const* vocbase) const { return static_cast( fasthash64(&vocbase, sizeof(TRI_vocbase_t const*), 0xf12345678abcdef) % numberOfParts); } /// @brief invalidate all entries in the cache part /// note that the caller of this method must hold the write lock void QueryCache::invalidate(unsigned int part) { for (auto& it : _entries[part]) { delete it.second; } _entries[part].clear(); } /// @brief sets the maximum number of results in each per-database cache void QueryCache::setMaxResults(size_t value) { if (value == 0) { return; } if (value > ::MaxResults) { enforceMaxResults(value); } ::MaxResults = value; } /// @brief sets the caching mode void QueryCache::setMode(QueryCacheMode value) { if (value == mode()) { // actually no mode change return; } invalidate(); ::Mode.store(value, std::memory_order_release); } /// @brief enable or disable the query cache void QueryCache::setMode(std::string const& value) { if (value == "demand") { setMode(CACHE_ON_DEMAND); } else if (value == "on") { setMode(CACHE_ALWAYS_ON); } else { setMode(CACHE_ALWAYS_OFF); } }