1 module cachetools.cachelru; 2 3 import std.typecons; 4 import std.exception; 5 import core.stdc.time; 6 7 private import std.experimental.allocator; 8 private import std.experimental.allocator.mallocator : Mallocator; 9 10 private import cachetools.internal; 11 private import cachetools.interfaces; 12 private import cachetools.containers.hashmap; 13 private import cachetools.containers.lists; 14 15 /// 16 /// CacheLRU contains maximum `size` items 17 /// Eviction policy: 18 /// 1. evict TTL-ed entry (if TTL enabled), otherwise 19 /// 2. if oldest entry not expired - evict oldes accessed (LRU) 20 /// 21 /// User can be informed about evicted entries via cache event list. 22 /// 23 24 class CacheLRU(K, V, Allocator = Mallocator) { 25 /// 26 /// Implemented as HashMap and multi-dlist. 27 /// 28 /// HashMap (by key) keep 29 /// 1. cached value 30 /// 2. pointer to dlist element. 31 /// 3. creation time (to check expiration and purge expired entry on get() without access to dlist) 32 /// 4. hits counter 33 /// 34 /// dlist keep key, cteation timestamp (to check expiration) 35 /// 1. key, so that we can remove entries from hashmap for lists heads (AccessIndex and TimeIndex) 36 /// 2. creation time, so that we can check expiration for 'TimeIndex' 37 /// 38 /// Each element in dlist have two sets of double-links - first set create order by access time, second set 39 /// for creation time. 40 /// 41 private { 42 enum size_t AccessIndex = 0; 43 enum size_t TimeIndex = 1; 44 struct ListElement { 45 K key; // we keep key here so we can remove element from map when we evict with LRU or TTL) 46 time_t ts; // creation (we keep it here to check expiration for oldest element) 47 } 48 49 struct MapElement { 50 StoredType!V value; // value 51 //ushort hits; // accesses 52 time_t ts; // creation (wee keep it here to check expiration on access) 53 ListElementPtr list_element_ptr; 54 } 55 56 alias allocator = Allocator.instance; 57 alias ListElementPtr = __elements.Node*; 58 59 MultiDList!(ListElement, 2, Allocator) __elements; // keeps order by creation time and by access time 60 HashMap!(K, MapElement, Allocator) __map; // keeps MapElement by key 61 SList!(CacheEvent!(K, V), Allocator) __events; // unbounded list of cache events 62 63 // configuration 64 size_t __size = 1024; // limit num of elements in cache 65 uint __ttl; // use TTL if __ttl > 0 66 bool __reportCacheEvents; // will user read cache events? 67 } 68 struct CacheEventRange(K, V) { 69 SList!(CacheEvent!(K, V), Allocator) __events; 70 this(ref SList!(CacheEvent!(K, V), Allocator) events) { 71 import std.algorithm.mutation : swap; 72 73 swap(__events, events); 74 } 75 76 bool empty() const nothrow { 77 return __events.empty(); 78 } 79 80 void popFront() nothrow { 81 __events.popFront(); 82 } 83 84 auto front() { 85 return __events.front(); 86 } 87 88 auto length() pure const nothrow @safe { 89 return __events.length; 90 } 91 } 92 93 final Nullable!V get(K k) @safe { 94 debug (cachetools) 95 safe_tracef("get %s", k); 96 auto store_ptr = k in __map; 97 if (!store_ptr) { 98 return Nullable!V(); 99 } 100 if (__ttl > 0 && time(null) - store_ptr.ts >= __ttl) { 101 // remove expired entry 102 if (__reportCacheEvents) { 103 // store in event list 104 CacheEvent!(K, V) cache_event = { 105 EventType.Expired, k, store_ptr.value 106 }; 107 __events.insertBack(cache_event); 108 } 109 // and remove from storage and list 110 __map.remove(k); 111 __elements.remove(store_ptr.list_element_ptr); 112 return Nullable!V(); 113 } 114 //store_ptr.hits++; 115 auto order_p = store_ptr.list_element_ptr; 116 __elements.move_to_tail(order_p, AccessIndex); 117 return Nullable!V(store_ptr.value); 118 } 119 120 final PutResult put(K k, V v) @safe 121 out { 122 assert(__result != PutResult(PutResultFlag.None)); 123 } 124 do { 125 time_t ts = time(null); 126 PutResult result; 127 auto store_ptr = k in __map; 128 if (!store_ptr) // insert element 129 { 130 result = PutResultFlag.Inserted; 131 if (__elements.length >= __size) { 132 ListElementPtr e; 133 // we have to purge 134 // 1. check if oldest element is ttled 135 if (__ttl > 0 && ts - __elements.head(TimeIndex).ts >= __ttl) { 136 // purge ttl-ed element 137 e = __elements.head(TimeIndex); 138 debug (cachetools) 139 safe_tracef("purging ttled %s", *e); 140 } else { 141 // purge lru element 142 e = __elements.head(AccessIndex); 143 debug (cachetools) 144 safe_tracef("purging lru %s", *e); 145 } 146 assert(e !is null); 147 if (__reportCacheEvents) { 148 auto value_ptr = e.key in __map; 149 CacheEvent!(K, V) cache_event = { 150 EventType.Evicted, e.key, value_ptr.value 151 }; 152 __events.insertBack(cache_event); 153 } 154 __map.remove(e.key); 155 __elements.remove(e); 156 result |= PutResultFlag.Evicted; 157 } 158 auto order_node = __elements.insert_last(ListElement(k, ts)); 159 MapElement e = {value: 160 v, ts : ts, list_element_ptr : order_node}; 161 __map.put(k, e); 162 } else // update element 163 { 164 result = PutResultFlag.Replaced; 165 debug (cachetools) 166 safe_tracef("update %s", *store_ptr); 167 ListElementPtr e = store_ptr.list_element_ptr; 168 e.ts = ts; 169 __elements.move_to_tail(e, TimeIndex); 170 if (__reportCacheEvents) { 171 auto v_ptr = e.key in __map; 172 CacheEvent!(K, V) cache_event = { 173 EventType.Updated, e.key, v_ptr.value 174 }; 175 __events.insertBack(cache_event); 176 } 177 store_ptr.value = v; 178 store_ptr.ts = ts; 179 } 180 return result; 181 } 182 183 final bool remove(K k) @safe { 184 debug (cachetools) 185 safe_tracef("remove from cache %s", k); 186 auto map_ptr = k in __map; 187 if (!map_ptr) // do nothing 188 { 189 return false; 190 } 191 ListElementPtr e = map_ptr.list_element_ptr; 192 if (__reportCacheEvents) { 193 auto v_ptr = e.key in __map; 194 CacheEvent!(K, V) cache_event = { 195 EventType.Removed, e.key, v_ptr.value 196 }; 197 __events.insertBack(cache_event); 198 } 199 __map.remove(e.key); 200 __elements.remove(e); 201 return true; 202 } 203 204 final void clear() @safe { 205 if (__reportCacheEvents) { 206 foreach (pair; __map.byPair) { 207 CacheEvent!(K, V) cache_event = { 208 EventType.Removed, pair.key, pair.value.value 209 }; 210 __events.insertBack(cache_event); 211 } 212 } 213 __map.clear(); 214 __elements.clear(); 215 } 216 217 size_t length() pure nothrow const @safe @nogc { 218 return __elements.length; 219 } 220 221 auto size(size_t s) pure nothrow @safe @nogc { 222 __size = s; 223 return this; 224 } 225 226 size_t size() pure nothrow const @safe @nogc { 227 return __size; 228 } 229 230 auto ttl(uint d) pure nothrow @safe @nogc { 231 __ttl = d; 232 return this; 233 } 234 235 uint ttl() pure nothrow const @safe @nogc { 236 return __ttl; 237 } 238 239 auto enableCacheEvents() pure nothrow @safe @nogc { 240 __reportCacheEvents = true; 241 return this; 242 } 243 244 final auto cacheEvents() @safe { 245 return CacheEventRange!(K, V)(__events); 246 } 247 } 248 249 @safe unittest { 250 import std.stdio; 251 import std.datetime; 252 import core.thread; 253 import std.algorithm; 254 import std.experimental.logger; 255 256 globalLogLevel = LogLevel.info; 257 info("Testing LRU"); 258 PutResult r; 259 260 auto lru = new CacheLRU!(int, string); 261 lru.size(4).ttl(1).enableCacheEvents(); 262 assert(lru.size == 4); 263 assert(lru.ttl == 1); 264 265 assert(lru.length == 0); 266 r = lru.put(1, "one"); 267 assert(r == PutResult(PutResultFlag.Inserted)); 268 r = lru.put(2, "two"); 269 assert(r == PutResult(PutResultFlag.Inserted)); 270 auto v = lru.get(1); 271 assert(v == "one"); 272 r = lru.put(3, "three"); 273 assert(r & PutResultFlag.Inserted); 274 r = lru.put(4, "four"); 275 assert(r & PutResultFlag.Inserted); 276 assert(lru.length == 4); 277 // next put should purge... 278 r = lru.put(5, "five"); 279 assert(r == PutResult(PutResultFlag.Evicted, PutResultFlag.Inserted)); 280 () @trusted { Thread.sleep(2.seconds); }(); 281 v = lru.get(1); // it must be expired by ttl 282 assert(v.isNull); 283 assert(lru.length == 3); 284 r = lru.put(6, "six"); 285 assert(r == PutResult(PutResultFlag.Inserted)); 286 assert(lru.length == 4); 287 r = lru.put(7, "seven"); 288 assert(r == PutResult(PutResultFlag.Evicted, PutResultFlag.Inserted)); 289 assert(lru.length == 4); 290 lru.put(7, "7"); 291 assert(lru.length == 4); 292 assert(lru.get(7) == "7"); 293 lru.clear(); 294 assert(lru.length == 0); 295 assert(lru.get(7).isNull); 296 auto events = lru.cacheEvents(); 297 assert(!events.empty); 298 assert(events.length() == 8); 299 assert(lru.__events.empty); 300 assert(equal(events.map!"a.key", [2, 1, 3, 7, 6, 7, 5, 4])); 301 assert(equal(events.map!"a.val", ["two", "one", "three", "seven", "six", "7", "five", "four"])); 302 } 303 304 // check if we can cache with immutable struct keys 305 @safe unittest { 306 struct S { 307 int s; 308 } 309 310 CacheLRU!(immutable S, string) cache = new CacheLRU!(immutable S, string); 311 immutable S s1 = immutable S(1); 312 cache.put(s1, "one"); 313 auto s11 = cache.get(s1); 314 assert(s11 == "one"); 315 assert(cache.remove(s1)); 316 assert(!cache.remove(S(2))); 317 } 318 319 // check if we can cache with immutable struct values 320 @safe unittest { 321 struct S { 322 int s; 323 } 324 325 auto cache = new CacheLRU!(string, immutable S); 326 immutable S s1 = immutable S(1); 327 cache.put("one", s1); 328 auto s11 = cache.get("one"); 329 assert(s11 == s1); 330 immutable S s12 = immutable S(12); 331 cache.put("one", s12); 332 auto s121 = cache.get("one"); 333 assert(s121 == s12); 334 } 335 336 // check if we can cache with immutable class keys and values 337 @safe unittest { 338 import cachetools.hash : hash_function; 339 340 class C { 341 int s; 342 this(int v) { 343 s = v; 344 } 345 346 override hash_t toHash() const { 347 return hash_function(s); 348 } 349 350 bool opEquals(const C other) pure const @safe { 351 auto i = [1, 2]; 352 return s == other.s; 353 } 354 } 355 356 CacheLRU!(immutable C, string) ick = new CacheLRU!(immutable C, string); 357 immutable C s1 = new immutable C(1); 358 ick.put(s1, "one"); 359 auto s11 = ick.get(s1); 360 assert(s11 == "one"); 361 362 CacheLRU!(string, immutable C) icv = new CacheLRU!(string, immutable C); 363 immutable C s1v = new immutable C(1); 364 icv.put("one", s1v); 365 auto s11v = icv.get("one"); 366 assert(s11v is s1v); 367 } 368 369 /// 370 @nogc nothrow unittest { 371 import std.experimental.allocator.mallocator; 372 373 alias allocator = Mallocator.instance; 374 alias Cache = CacheLRU!(int, string); 375 376 auto lru = make!(Cache)(allocator); 377 378 lru.size = 2048; // keep 2048 elements in cache 379 lru.ttl = 60; // set 60 seconds TTL for items in cache 380 381 lru.put(1, "one"); 382 auto v = lru.get(0); 383 assert(v.isNull); // no such item in cache 384 v = lru.get(1); 385 assert(v == "one"); // 1 is in cache 386 lru.remove(1); 387 } 388 389 @safe nothrow unittest { 390 auto lru = new CacheLRU!(int, string); 391 () @nogc { 392 lru.enableCacheEvents(); 393 lru.put(1, "one"); 394 lru.put(1, "next one"); 395 assert(lru.get(1) == "next one"); 396 auto events = lru.cacheEvents(); 397 assert(events.length == 1); 398 lru.clear(); 399 }(); 400 }