#ifndef CRUCIBLE_CACHE_H #define CRUCIBLE_CACHE_H #include "crucible/lockset.h" #include #include #include #include #include namespace crucible { using namespace std; template class LRUCache { public: using Key = tuple; using Func = function; private: struct Value { Key key; Return ret; }; using ListIter = typename list::iterator; Func m_fn; list m_list; map m_map; LockSet m_lockset; size_t m_max_size; mutex m_mutex; void check_overflow(); void recent_use(ListIter vp); void erase_item(ListIter vp); void erase_key(const Key &k); Return insert_item(Func fn, Arguments... args); public: LRUCache(Func f = Func(), size_t max_size = 100); void func(Func f); void max_size(size_t new_max_size); Return operator()(Arguments... args); Return refresh(Arguments... args); void expire(Arguments... args); void insert(const Return &r, Arguments... args); void clear(); }; template LRUCache::LRUCache(Func f, size_t max_size) : m_fn(f), m_max_size(max_size) { } template Return LRUCache::insert_item(Func fn, Arguments... args) { Key k(args...); // Do we have it cached? unique_lock lock(m_mutex); auto found = m_map.find(k); if (found == m_map.end()) { // No, release cache lock and acquire key lock lock.unlock(); auto key_lock = m_lockset.make_lock(k); // Did item appear in cache while we were waiting for key? lock.lock(); found = m_map.find(k); if (found == m_map.end()) { // No, we now hold key and cache locks, but item not in cache. // Release cache lock and call the function lock.unlock(); // Create new value Value v { .key = k, .ret = fn(args...), }; // Reacquire cache lock lock.lock(); // Make room check_overflow(); // Insert return value at back of LRU list (hot end) auto new_item = m_list.insert(m_list.end(), v); // Insert return value in map bool inserted = false; tie(found, inserted) = m_map.insert(make_pair(v.key, new_item)); // We (should be) holding a lock on this key so we are the ones to insert it THROW_CHECK0(runtime_error, inserted); } // Item should be in cache now THROW_CHECK0(runtime_error, found != m_map.end()); } else { // Move to end of LRU recent_use(found->second); } // Return cached object return found->second->ret; } template void LRUCache::erase_item(ListIter vp) { if (vp != m_list.end()) { m_map.erase(vp->key); m_list.erase(vp); } } template void LRUCache::erase_key(const Key &k) { auto map_item = m_map.find(k); if (map_item != m_map.end()) { auto list_item = map_item->second; m_map.erase(map_item); m_list.erase(list_item); } } template void LRUCache::check_overflow() { // Erase items at front of LRU list (cold end) until max size reached or list empty while (m_map.size() >= m_max_size && !m_list.empty()) { erase_item(m_list.begin()); } } template void LRUCache::recent_use(ListIter vp) { // Splice existing items at back of LRU list (hot end) auto next_vp = vp; ++next_vp; m_list.splice(m_list.end(), m_list, vp, next_vp); } template void LRUCache::max_size(size_t new_max_size) { unique_lock lock(m_mutex); m_max_size = new_max_size; // FIXME: this really reduces the cache size to new_max_size - 1 // because every other time we call this method, it is immediately // followed by insert. check_overflow(); } template void LRUCache::func(Func func) { unique_lock lock(m_mutex); m_fn = func; } template void LRUCache::clear() { // Move the map and list onto the stack, then destroy it after we've released the lock // so that we don't block other threads if the list's destructors are expensive decltype(m_list) new_list; decltype(m_map) new_map; unique_lock lock(m_mutex); m_list.swap(new_list); m_map.swap(new_map); lock.unlock(); } template Return LRUCache::operator()(Arguments... args) { return insert_item(m_fn, args...); } template void LRUCache::expire(Arguments... args) { unique_lock lock(m_mutex); erase_key(Key(args...)); } template Return LRUCache::refresh(Arguments... args) { expire(args...); return operator()(args...); } template void LRUCache::insert(const Return &r, Arguments... args) { insert_item([&](Arguments...) -> Return { return r; }, args...); } } #endif // CRUCIBLE_CACHE_H