dodo  0.0.1
A C++ library to create containerized Linux services
cache.hpp
Go to the documentation of this file.
1 /*
2  * This file is part of the dodo library (https://github.com/jmspit/dodo).
3  * Copyright (c) 2019 Jan-Marten Spit.
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, version 3.
8  *
9  * This program is distributed in the hope that it will be useful, but
10  * WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12  * General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program. If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 /**
19  * @file cache.hpp
20  * Defines the dodo::common::Cache template class.
21  */
22 
23 #ifndef common_cache_hpp
24 #define common_cache_hpp
25 
26 #include <common/exception.hpp>
27 #include <threads/mutex.hpp>
28 
29 #include <iostream>
30 
31 
32 namespace dodo::common {
33 
34  using namespace std::chrono_literals;
35 
36  /**
37  * Simple thread-safe Cache template for arbitrary Key-Value pairs. A Cache is applicable where the get( const Key& key, Value &value )
38  * that finds the key in the cache, is much faster than the load( const Key& key, Value &value ).
39  * Each CacheEntry tracks the latest hit and load times. In case of pressure on the max_size, entries with the lowest hit_time
40  * are evicted to make room for the new load(). Additionally, if life_time is non-zero and the Value's load time is older
41  * then that, load() is invoked even if get() is a cache hit, updating both the load time and the hit time.
42  *
43  * The Key class must have equality (==) and ordering (<) operators for the template instantiation to compile.
44  *
45  * @tparam Key The unique Key type to the cached entries.
46  * @tparam Value the cached Value type.
47  *
48  * The number of 'loads' is getMisses() + getExpiries().
49  *
50  * The time complexity of the get call is O(N log N) where N is the number of cache entries. The cache and lrumap are backed by
51  * std::map and std::multimap respectively.
52  *
53  * Note that get() receives a copy of the Value in the Cache. If thread A receives Value v1, thread B may cause the Value
54  * to get reloaded if its life_time expired , and that Value v2 might differ from v1. So the Cache does not guarentee that two values
55  * retrurned by get on the same Key are equal no matter how close they are in wallclock time.
56  *
57  * @include cache.cpp
58  *
59  */
60  template <class Key, class Value> class Cache {
61 
62  protected:
63 
64  /**
65  * A CachedEntry holds the Value as well as last load and last hit time.
66  */
67  struct CacheEntry {
68  /** The last time load was called on this Key */
69  std::chrono::steady_clock::time_point load_time;
70  /** The last time get or load was called on this Key. */
71  std::chrono::steady_clock::time_point hit_time;
72  /** The cached Value */
73  Value value;
74  };
75 
76  /** typedef for the Cache map */
77  typedef std::map<Key,CacheEntry> CacheMap;
78  /** typedef for an iterator into the Cache map */
79  typedef typename CacheMap::iterator ICacheMap;
80  /** typedef for a const iterator into the Cache map */
81  typedef typename CacheMap::const_iterator CICacheMap;
82  /** typedef for the LRU map */
83  typedef std::multimap<std::chrono::steady_clock::time_point,Key> LRUMap;
84  /** typedef for an iterator into the LRU map */
85  typedef typename LRUMap::iterator ILRUMap;
86  /** typedef for a const iterator into the LRU map */
87  typedef typename LRUMap::const_iterator CILRUMap;
88 
89  public:
90 
91  /**
92  * Construct a cache with at most max_size entries, and a maximum life_time of the cached entry.
93  * @param max_size The maximum number of entries in the Cache.
94  * @param life_time The maximum number of seconds a cached entry may live before a get hit will issue a load. A life_time
95  * of 0s will disable the aging of cached entries.
96  */
97  Cache( size_t max_size, std::chrono::seconds life_time ) {
98  max_size_ = max_size;
99  life_time_s_ = life_time;
100  }
101 
102  /**
103  * Wipes all cached entries.
104  */
105  void clear() {
106  threads::Mutexer lock( mutex_ );
107  cache_.clear();
108  lrumap_.clear();
109  }
110 
111 
112  /**
113  * Get a copy of the Value idenified by Key.
114  * @param key The Key to get the Value for.
115  * @param value Reference to the Value that will be assigned a copy of the cached entry.
116  * @return false when the key is not loadable (load returns false).
117  * @see load( const Key& key )
118  */
119  bool get( const Key& key, Value &value ) {
120  threads::Mutexer lock( mutex_ );
121  ICacheMap i = cache_.find( key );
122  if ( i != cache_.end() ) {
123  ++hits_;
124  if ( life_time_s_ > 0s && std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now() - i->second.load_time ) > life_time_s_ ) {
125  if ( load( key, i->second.value ) ) {
126  ++expired_;
127  i->second.load_time = std::chrono::steady_clock::now();
128  } else return false;
129  }
130  value = i->second.value;
131  auto prev_hit_time = i->second.hit_time;
132  i->second.hit_time = std::chrono::steady_clock::now();
133  updateLRU( i, prev_hit_time );
134  return true;
135  } else {
136  ++load_;
137  if ( load( key, value ) ) {
138  std::pair<Key,CacheEntry> p = std::pair<Key,CacheEntry>( key, { std::chrono::steady_clock::now(), std::chrono::steady_clock::now(), value } );
139  std::pair<CICacheMap,bool> ret;
140  ret = cache_.insert( p );
141  addLRU( ret.first );
142  if ( cache_.size() > max_size_ ) removeLRU();
143  return true;
144  } else return false;
145  }
146  }
147 
148  /**
149  * Get the number of cache hits, which may include hits on expired entries - which still cause a load() call.
150  * @return The number of times a Key was requested and present in the cache.
151  */
152  size_t getHits() const { return hits_; }
153 
154  /**
155  * Get the number of cache loads - the number of times a Key was requested but not present in the cache.
156  * @return The number of cache loades.
157  */
158  size_t getMisses() const { return load_; }
159 
160  /**
161  * Get the number of times a Key was present in the cache (a hit) but expired so implictly calling a load.
162  * @return The number of times a Key was present in the cache, but expired and cuasing a call to load.
163  */
164  size_t getExpiries() const { return expired_; }
165 
166  /**
167  * Erase the key from the cache. A subsequent get on the same Key will call load. Does nothing if the Key does not
168  * exist in the cache.
169  * @param key The Key to erase.
170  */
171  void erase( const Key& key ) {
172  threads::Mutexer lock( mutex_ );
173  ICacheMap cache_entry = cache_.find( key );
174  if ( cache_entry != cache_.end() ) {
175  auto ret = lrumap_.equal_range( cache_entry->second.hit_time );
176  ILRUMap lru_entry = lrumap_.end();
177  for ( ILRUMap j = ret.first; j != ret.second; ++j ) {
178  if ( key == j->second ) {
179  lru_entry = j;
180  break;
181  }
182  }
183  if ( lru_entry != lrumap_.end() ) {
184  lrumap_.erase(lru_entry);
185  }
186  cache_.erase(cache_entry);
187  }
188  }
189 
190  /**
191  * Set the maximum size / number of entries to cache.
192  * @param max_size The maximum size to set.
193  */
194  void setMaxSize( size_t max_size ) { threads::Mutexer lock(mutex_); max_size_ = max_size;};
195 
196  /**
197  * Set the maximum life time, in seconds, of a cached entry.
198  * @param max_size The maximum size to set.
199  */
200  void setMaxLifeTime( const std::chrono::seconds &seconds ) { threads::Mutexer lock(mutex_); life_time_s_ = seconds;};
201 
202  protected:
203 
204  /**
205  * Implement to laod a value from the slow source. If the key is not loadable, return false.
206  * @param key The Key to load.
207  * @param value To receive the value.
208  * @return False if the key could not be loaded.
209  */
210  virtual bool load( const Key& key, Value &value ) = 0;
211 
212  /**
213  * The Cache map.
214  */
216 
217  private:
218 
219  /**
220  * Add an entry to the lrumap_.
221  * @param iter An iterator to the CacheEntry.
222  */
223  void addLRU( const CICacheMap& iter ) {
224  lrumap_.insert( std::pair<std::chrono::steady_clock::time_point,Key>( (*iter).second.hit_time, (*iter).first ) );
225  }
226 
227  /**
228  * Remove the oldest CachedEntry from cache_ and lrumap_.
229  */
230  void removeLRU() {
231  auto oldest = lrumap_.begin();
232  auto entry = cache_.find( oldest->second );
233  if ( entry != cache_.end() ) cache_.erase( entry );
234  lrumap_.erase( oldest );
235  }
236 
237  /**
238  * Update an entry in the lrumap_.
239  * @param iter An iterator to the CacheEntry.
240  * @param prev_hit_time The previous hit time (the current key into the lrumap_)
241  */
242  void updateLRU( const CICacheMap& iter, std::chrono::steady_clock::time_point prev_hit_time ) {
243  auto ret = lrumap_.equal_range( prev_hit_time );
244  ILRUMap update = lrumap_.end();
245  for ( ILRUMap i = ret.first; i != ret.second; ++i ) {
246  if ( i->second == iter->first ) {
247  update = i;
248  break;
249  }
250  }
251  if ( update != lrumap_.end() ) {
252  lrumap_.erase( update );
253  addLRU( iter );
254  }
255  }
256 
257  /**
258  * The least-recently-used map.
259  */
261 
262  /**
263  * The maximum number of cache entries.
264  */
265  size_t max_size_;
266 
267  /**
268  * The maximum life time of a cached entry.
269  */
270  std::chrono::seconds life_time_s_ = 300s;
271 
272  /**
273  * Mutex to synchronize threads.
274  */
276 
277  /**
278  * The number of hits.
279  */
280  size_t hits_ = 0;
281 
282  /**
283  * The number of loades.
284  */
285  size_t load_ = 0;
286 
287  /**
288  * The number of expiries.
289  */
290  size_t expired_ = 0;
291 
292  };
293 
294 
295 }
296 
297 #endif
dodo::common::Cache::max_size_
size_t max_size_
The maximum number of cache entries.
Definition: cache.hpp:265
dodo::threads::Mutexer
Waits for and locks the Mutex on construction, unlocks the Mutex when this Mutexer is destructed.
Definition: mutex.hpp:100
dodo::common::Cache
Simple thread-safe Cache template for arbitrary Key-Value pairs.
Definition: cache.hpp:60
dodo::common::Cache::LRUMap
std::multimap< std::chrono::steady_clock::time_point, Key > LRUMap
typedef for the LRU map
Definition: cache.hpp:83
dodo::common::Cache::CacheMap
std::map< Key, CacheEntry > CacheMap
typedef for the Cache map
Definition: cache.hpp:77
dodo::common::Cache::getHits
size_t getHits() const
Get the number of cache hits, which may include hits on expired entries - which still cause a load() ...
Definition: cache.hpp:152
dodo::common::Cache::CICacheMap
CacheMap::const_iterator CICacheMap
typedef for a const iterator into the Cache map
Definition: cache.hpp:81
dodo::common::Cache::ILRUMap
LRUMap::iterator ILRUMap
typedef for an iterator into the LRU map
Definition: cache.hpp:85
dodo::common::Cache::CacheEntry::hit_time
std::chrono::steady_clock::time_point hit_time
The last time get or load was called on this Key.
Definition: cache.hpp:71
dodo::common::Cache::Cache
Cache(size_t max_size, std::chrono::seconds life_time)
Construct a cache with at most max_size entries, and a maximum life_time of the cached entry.
Definition: cache.hpp:97
dodo::common::Cache::setMaxSize
void setMaxSize(size_t max_size)
Set the maximum size / number of entries to cache.
Definition: cache.hpp:194
dodo::common::Cache::updateLRU
void updateLRU(const CICacheMap &iter, std::chrono::steady_clock::time_point prev_hit_time)
Update an entry in the lrumap_.
Definition: cache.hpp:242
dodo::common::Cache::CacheEntry::load_time
std::chrono::steady_clock::time_point load_time
The last time load was called on this Key.
Definition: cache.hpp:69
dodo::threads::Mutex
A Mutex to synchronize access to resources between threads.
Definition: mutex.hpp:39
dodo::common::Cache::addLRU
void addLRU(const CICacheMap &iter)
Add an entry to the lrumap_.
Definition: cache.hpp:223
dodo::common::Cache::CILRUMap
LRUMap::const_iterator CILRUMap
typedef for a const iterator into the LRU map
Definition: cache.hpp:87
dodo::common::Cache::getExpiries
size_t getExpiries() const
Get the number of times a Key was present in the cache (a hit) but expired so implictly calling a loa...
Definition: cache.hpp:164
dodo::common::Cache::erase
void erase(const Key &key)
Erase the key from the cache.
Definition: cache.hpp:171
dodo::common
Common and utility interfaces.
Definition: application.hpp:29
dodo::common::Cache::CacheEntry
A CachedEntry holds the Value as well as last load and last hit time.
Definition: cache.hpp:67
dodo::common::Cache::mutex_
threads::Mutex mutex_
Mutex to synchronize threads.
Definition: cache.hpp:275
dodo::common::Cache::lrumap_
LRUMap lrumap_
The least-recently-used map.
Definition: cache.hpp:260
exception.hpp
dodo::common::Cache::getMisses
size_t getMisses() const
Get the number of cache loads - the number of times a Key was requested but not present in the cache.
Definition: cache.hpp:158
dodo::common::Cache::CacheEntry::value
Value value
The cached Value.
Definition: cache.hpp:73
dodo::common::Cache::clear
void clear()
Wipes all cached entries.
Definition: cache.hpp:105
dodo::common::Cache::removeLRU
void removeLRU()
Remove the oldest CachedEntry from cache_ and lrumap_.
Definition: cache.hpp:230
dodo::common::Cache::setMaxLifeTime
void setMaxLifeTime(const std::chrono::seconds &seconds)
Set the maximum life time, in seconds, of a cached entry.
Definition: cache.hpp:200
dodo::common::Cache::ICacheMap
CacheMap::iterator ICacheMap
typedef for an iterator into the Cache map
Definition: cache.hpp:79
dodo::common::Cache::get
bool get(const Key &key, Value &value)
Get a copy of the Value idenified by Key.
Definition: cache.hpp:119
mutex.hpp
dodo::common::Cache::cache_
CacheMap cache_
The Cache map.
Definition: cache.hpp:215