1
0
Fork 0
arangodb/arangod/Aql/QueryCache.cpp

549 lines
15 KiB
C++

////////////////////////////////////////////////////////////////////////////////
/// 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/tri-strings.h"
#include "Basics/WriteLocker.h"
#include "VocBase/vocbase.h"
#include <velocypack/Builder.h>
#include <velocypack/velocypack-aliases.h>
using namespace arangodb::aql;
/// @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<arangodb::aql::QueryCacheMode> Mode(CACHE_ON_DEMAND);
/// @brief create a cache entry
QueryCacheResultEntry::QueryCacheResultEntry(
uint64_t hash, QueryString const& queryString,
std::shared_ptr<VPackBuilder> queryResult, std::vector<std::string> const& collections)
: _hash(hash),
_queryString(queryString.data(), queryString.size()),
_queryResult(queryResult),
_collections(collections),
_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(),
_entriesByCollection(),
_head(nullptr),
_tail(nullptr),
_numElements(0) {
_entriesByHash.reserve(128);
_entriesByCollection.reserve(16);
}
/// @brief destroy a database-specific cache
QueryCacheDatabaseEntry::~QueryCacheDatabaseEntry() {
for (auto& it : _entriesByHash) {
tryDelete(it.second);
}
_entriesByHash.clear();
_entriesByCollection.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->_collections) {
auto it2 = _entriesByCollection.find(it);
if (it2 == _entriesByCollection.end()) {
// no entry found for collection. now create it
_entriesByCollection.emplace(it, std::unordered_set<uint64_t>{hash});
} else {
// there already was an entry for this collection
(*it2).second.emplace(hash);
}
}
} catch (...) {
// rollback
// remove from collections
for (auto const& it : entry->_collections) {
auto it2 = _entriesByCollection.find(it);
if (it2 != _entriesByCollection.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 collections in the
/// database-specific cache
void QueryCacheDatabaseEntry::invalidate(
std::vector<std::string> const& collections) {
for (auto const& it : collections) {
invalidate(it);
}
}
/// @brief invalidate all entries for a collection in the database-specific
/// cache
void QueryCacheDatabaseEntry::invalidate(std::string const& collection) {
auto it = _entriesByCollection.find(collection);
if (it == _entriesByCollection.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);
}
}
_entriesByCollection.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<std::string, size_t>& result) {
MUTEX_LOCKER(mutexLocker, _propertiesLock);
result.first = modeString(mode());
result.second = MaxResults;
}
/// @brief set the cache properties
void QueryCache::setProperties(
std::pair<std::string, size_t> 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<VPackBuilder> result,
std::vector<std::string> const& collections) {
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<QueryCacheResultEntry>(
hash, queryString, result, collections);
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<QueryCacheDatabaseEntry>();
it = _entries[part].emplace(vocbase, db.get()).first;
db.release();
}
// store cache entry
(*it).second->store(hash, entry.get());
return entry.release();
}
/// @brief invalidate all queries for the given collections
void QueryCache::invalidate(TRI_vocbase_t* vocbase,
std::vector<std::string> const& collections) {
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(collections);
}
/// @brief invalidate all queries for a particular collection
void QueryCache::invalidate(TRI_vocbase_t* vocbase, std::string const& collection) {
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(collection);
}
/// @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<int>(
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);
}
}