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 }