1 module hunt.security.util.Cache;
2 
3 import hunt.security.util.ReferenceQueue;
4 
5 import hunt.collection;
6 import hunt.Exceptions;
7 import hunt.logging;
8 import hunt.system.Memory;
9 
10 import std.conv;
11 import std.datetime;
12 
13 
14 /**
15  * Abstract base class and factory for caches. A cache is a key-value mapping.
16  * It has properties that make it more suitable for caching than a Map.
17  *
18  * The factory methods can be used to obtain two different implementations.
19  * They have the following properties:
20  *
21  *  . keys and values reside in memory
22  *
23  *  . keys and values must be non-null
24  *
25  *  . maximum size. Replacements are made in LRU order.
26  *
27  *  . optional lifetime, specified in seconds.
28  *
29  *  . safe for concurrent use by multiple threads
30  *
31  *  . values are held by either standard references or via SoftReferences.
32  *    SoftReferences have the advantage that they are automatically cleared
33  *    by the garbage collector in response to memory demand. This makes it
34  *    possible to simple set the maximum size to a very large value and let
35  *    the GC automatically size the cache dynamically depending on the
36  *    amount of available memory.
37  *
38  * However, note that because of the way SoftReferences are implemented in
39  * HotSpot at the moment, this may not work perfectly as it clears them fairly
40  * eagerly. Performance may be improved if the Java heap size is set to larger
41  * value using e.g. java -ms64M -mx128M foo.Test
42  *
43  * Cache sizing: the memory cache is implemented on top of a LinkedHashMap.
44  * In its current implementation, the number of buckets (NOT entries) in
45  * (Linked)HashMaps is always a power of two. It is recommended to set the
46  * maximum cache size to value that uses those buckets fully. For example,
47  * if a cache with somewhere between 500 and 1000 entries is desired, a
48  * maximum size of 750 would be a good choice: try 1024 buckets, with a
49  * load factor of 0.75f, the number of entries can be calculated as
50  * buckets / 4 * 3. As mentioned above, with a SoftReference cache, it is
51  * generally reasonable to set the size to a fairly large value.
52  *
53  * @author Andreas Sterbenz
54  */
55 abstract class Cache(K,V) {
56 
57     protected this() {
58         // empty
59     }
60 
61     /**
62      * Return the number of currently valid entries in the cache.
63      */
64     abstract int size();
65 
66     /**
67      * Remove all entries from the cache.
68      */
69     abstract void clear();
70 
71     /**
72      * Add an entry to the cache.
73      */
74     abstract void put(K key, V value);
75 
76     /**
77      * Get a value from the cache.
78      */
79     abstract V get(K key);
80 
81     /**
82      * Remove an entry from the cache.
83      */
84     abstract void remove(K key);
85 
86     /**
87      * Set the maximum size.
88      */
89     abstract void setCapacity(int size);
90 
91     /**
92      * Set the timeout(in seconds).
93      */
94     abstract void setTimeout(int timeout);
95 
96     /**
97      * accept a visitor
98      */
99     abstract void accept(CacheVisitor!(K, V) visitor);
100 
101     interface CacheVisitor(K, V) {
102         void visit(Map!(K, V) map);
103     }
104 
105 }
106 
107 /**
108 * Utility class that wraps a byte array and implements the equals()
109 * and hashCode() contract in a way suitable for Maps and caches.
110 */
111 static class EqualByteArray {
112 
113     private byte[] b;
114     private size_t hash;
115 
116     this(byte[] b) {
117         this.b = b;
118         hash = 0;
119     }
120 
121     override size_t toHash() @trusted nothrow {
122         size_t h = hash;
123         if (h == 0) {
124             h = b.length + 1;
125             for (size_t i = 0; i < b.length; i++) {
126                 h += (b[i] & 0xff) * 37;
127             }
128             hash = h;
129         }
130         return h;
131     }
132 
133     override bool opEquals(Object obj) {
134         if (this is obj) {
135             return true;
136         }
137         if (obj is null) {
138             return false;
139         }
140         EqualByteArray other = cast(EqualByteArray)obj;
141         if(other is null)
142             return false;
143         return this.b == other.b;
144     }
145 }
146 
147 
148 class NullCache(K, V) : Cache!(K, V) {
149 
150     __gshared static Cach!(K, V) INSTANCE;
151 
152     shared static this()
153     {
154         INSTANCE = new NullCache!(K, V)();
155     }
156 
157     private this() {
158         // empty
159     }
160 
161     int size() {
162         return 0;
163     }
164 
165     void clear() {
166         // empty
167     }
168 
169     void put(K key, V value) {
170         // empty
171     }
172 
173     V get(K key) {
174         return null;
175     }
176 
177     void remove(K key) {
178         // empty
179     }
180 
181     void setCapacity(int size) {
182         // empty
183     }
184 
185     void setTimeout(int timeout) {
186         // empty
187     }
188 
189     void accept(CacheVisitor!(K, V) visitor) {
190         // empty
191     }
192 
193 }
194 
195 
196 /**
197 */
198 class MemoryCache(K, V) : Cache!(K, V) {
199 
200     private enum float LOAD_FACTOR = 0.75f;
201 
202     // XXXX
203     // private final static bool DEBUG = false;
204 
205     private Map!(K, CacheEntry!(K, V)) cacheMap;
206     private int maxSize;
207     private long lifetime;
208 
209     // ReferenceQueue is of type V instead of Cache!(K, V)
210     // to allow SoftCacheEntry to extend SoftReference<V>
211     private ReferenceQueue!V queue;
212 
213     this(bool soft, int maxSize) {
214         this(soft, maxSize, 0);
215     }
216 
217     this(bool soft, int maxSize, int lifetime) {
218         this.maxSize = maxSize;
219         this.lifetime = lifetime * 1000;
220         // if (soft)
221         //     this.queue = new ReferenceQueue<>();
222         // else
223             this.queue = null;
224 
225         int buckets = cast(int)(maxSize / LOAD_FACTOR) + 1;
226         cacheMap = new LinkedHashMap!(K, CacheEntry!(K, V))(buckets, LOAD_FACTOR, true);
227     }
228 
229     /**
230      * Empty the reference queue and remove all corresponding entries
231      * from the cache.
232      *
233      * This method should be called at the beginning of each public
234      * method.
235      */
236     private void emptyQueue() {
237         // if (queue is null) {
238         //     return;
239         // }
240         // int startSize = cacheMap.size();
241         // while (true) {
242         //     CacheEntry!(K, V) entry = (CacheEntry!(K, V))queue.poll();
243         //     if (entry is null) {
244         //         break;
245         //     }
246         //     K key = entry.getKey();
247         //     if (key is null) {
248         //         // key is null, entry has already been removed
249         //         continue;
250         //     }
251         //     CacheEntry!(K, V) currentEntry = cacheMap.remove(key);
252         //     // check if the entry in the map corresponds to the expired
253         //     // entry. If not, readd the entry
254         //     if ((currentEntry !is null) && (entry != currentEntry)) {
255         //         cacheMap.put(key, currentEntry);
256         //     }
257         // }
258         // version(HuntDebugMode) {
259         //     int endSize = cacheMap.size();
260         //     if (startSize != endSize) {
261         //         trace("*** Expunged " ~ to!string(startSize - endSize)
262         //                 ~ " entries, " ~ endSize.to!string() ~ " entries left");
263         //     }
264         // }
265     }
266 
267     /**
268      * Scan all entries and remove all expired ones.
269      */
270     private void expungeExpiredEntries() {
271         emptyQueue();
272         if (lifetime == 0) {
273             return;
274         }
275         int cnt = 0;
276         long time = Clock.currStdTime;
277         // foreach (CacheEntry!(K, V) entry; cacheMap.values()) {
278         //     if (entry.isValid(time) == false) {
279         //         t.remove();
280         //         cnt++;
281         //     }
282         // }
283         implementationMissing();
284         version(HuntDebugMode)  {
285             if (cnt != 0) {
286                 trace("Removed " ~ cnt.to!string()
287                         ~ " expired entries, remaining " ~ cacheMap.size().to!string());
288             }
289         }
290     }
291 
292     override int size() {
293         expungeExpiredEntries();
294         return cacheMap.size();
295     }
296 
297     override void clear() {
298         if (queue !is null) {
299             // if this is a SoftReference cache, first invalidate() all
300             // entries so that GC does not have to enqueue them
301             foreach (CacheEntry!(K, V) entry ; cacheMap.values()) {
302                 entry.invalidate();
303             }
304             // while (queue.poll() !is null) {
305             //     // empty
306             // }
307         }
308         cacheMap.clear();
309     }
310 
311     override void put(K key, V value) {
312         emptyQueue();
313         long expirationTime = (lifetime == 0) ? 0 :
314                                        Clock.currStdTime + lifetime;
315         CacheEntry!(K, V) newEntry = newEntry(key, value, expirationTime, queue);
316         CacheEntry!(K, V) oldEntry = cacheMap.put(key, newEntry);
317         if (oldEntry !is null) {
318             oldEntry.invalidate();
319             return;
320         }
321         if (maxSize > 0 && cacheMap.size() > maxSize) {
322             expungeExpiredEntries();
323             if (cacheMap.size() > maxSize) { // still too large?
324                 // Iterator<CacheEntry!(K, V)> t = cacheMap.values().iterator();
325                 // CacheEntry!(K, V) lruEntry = t.next();
326                 // foreach(CacheEntry!(K, V) lruEntry; cacheMap.values())
327                 // {
328                 //     version(HuntDebugMode)  {
329                 //         tracef("** Overflow removal "
330                 //             + lruEntry.getKey() ~ " | " ~ lruEntry.getValue());
331                 //     }
332                 //     t.remove();
333                 //     lruEntry.invalidate();
334                 //     break;
335                 // }
336 
337         implementationMissing();
338             }
339         }
340     }
341 
342     override V get(K key) {
343         emptyQueue();
344         CacheEntry!(K, V) entry = cacheMap.get(key);
345         if (entry is null) {
346             return null;
347         }
348         long time = (lifetime == 0) ? 0 :Clock.currStdTime;
349         if (entry.isValid(time) == false) {
350             version(HuntDebugMode)  {
351                 tracef("Ignoring expired entry");
352             }
353             cacheMap.remove(key);
354             return null;
355         }
356         return entry.getValue();
357     }
358 
359     override void remove(K key) {
360         emptyQueue();
361         CacheEntry!(K, V) entry = cacheMap.remove(key);
362         if (entry !is null) {
363             entry.invalidate();
364         }
365     }
366 
367     override void setCapacity(int size) {
368         expungeExpiredEntries();
369         // TODO: Tasks pending completion -@zxp at 8/10/2018, 4:50:39 PM
370         // 
371         implementationMissing();
372         // if (size > 0 && cacheMap.size() > size) {
373         //     // Iterator<CacheEntry!(K, V)> t = cacheMap.values().iterator();
374         //     // for (int i = cacheMap.size() - size; i > 0; i--) {
375         //     int i = cacheMap.size() - size;
376         //     foreach(CacheEntry!(K, V) lruEntry; cacheMap.values())
377         //         version(HuntDebugMode)  {
378         //             tracef("** capacity reset removal "
379         //                 + lruEntry.getKey() ~ " | " ~ lruEntry.getValue());
380         //         }
381         //         // t.remove();
382         //         cacheMap.remove()
383         //         lruEntry.invalidate();
384         //         i--; 
385         //         if(i <= 0)  break;
386         //     }
387         // }
388 
389         maxSize = size > 0 ? size : 0;
390 
391         version(HuntDebugMode)  {
392             tracef("** capacity reset to " ~ size.to!string());
393         }
394     }
395 
396     override void setTimeout(int timeout) {
397         emptyQueue();
398         lifetime = timeout > 0 ? timeout * 1000L : 0L;
399 
400         version(HuntDebugMode)  {
401             tracef("** lifetime reset to " ~ timeout.to!string());
402         }
403     }
404 
405     // it is a heavyweight method.
406     override void accept(CacheVisitor!(K, V) visitor) {
407         expungeExpiredEntries();
408         Map!(K, V) cached = getCachedEntries();
409 
410         visitor.visit(cached);
411     }
412 
413     private Map!(K, V) getCachedEntries() {
414         Map!(K, V) kvmap = new HashMap!(K, V)(cacheMap.size());
415 
416         foreach (CacheEntry!(K, V) entry ; cacheMap.values()) {
417             kvmap.put(entry.getKey(), entry.getValue());
418         }
419 
420         return kvmap;
421     }
422 
423     protected CacheEntry!(K, V) newEntry(K key, V value,
424             long expirationTime, ReferenceQueue!V queue) {
425         // if (queue !is null) {
426         //     return new SoftCacheEntry!V(key, value, expirationTime, queue);
427         // } else {
428             return new HardCacheEntry!(K, V)(key, value, expirationTime);
429         // }
430     }
431 
432     private static interface CacheEntry(K, V) {
433 
434         bool isValid(long currentTime);
435 
436         void invalidate();
437 
438         K getKey();
439 
440         V getValue();
441 
442     }
443 
444     private static class HardCacheEntry(K, V) : CacheEntry!(K, V) {
445 
446         private K key;
447         private V value;
448         private long expirationTime;
449 
450         this(K key, V value, long expirationTime) {
451             this.key = key;
452             this.value = value;
453             this.expirationTime = expirationTime;
454         }
455 
456         K getKey() {
457             return key;
458         }
459 
460         V getValue() {
461             return value;
462         }
463 
464         bool isValid(long currentTime) {
465             bool valid = (currentTime <= expirationTime);
466             if (valid == false) {
467                 invalidate();
468             }
469             return valid;
470         }
471 
472         void invalidate() {
473             key = null;
474             value = null;
475             expirationTime = -1;
476         }
477     }
478 
479     // private static class SoftCacheEntry(K, V)
480     //         : //SoftReference<V>
481     //         CacheEntry!(K, V) {
482 
483     //     private K key;
484     //     private long expirationTime;
485 
486     //     this(K key, V value, long expirationTime,
487     //             ReferenceQueue<V> queue) {
488     //         super(value, queue);
489     //         this.key = key;
490     //         this.expirationTime = expirationTime;
491     //     }
492 
493     //     K getKey() {
494     //         return key;
495     //     }
496 
497     //     V getValue() {
498     //         return get();
499     //     }
500 
501     //     bool isValid(long currentTime) {
502     //         bool valid = (currentTime <= expirationTime) && (get() !is null);
503     //         if (valid == false) {
504     //             invalidate();
505     //         }
506     //         return valid;
507     //     }
508 
509     //     void invalidate() {
510     //         clear();
511     //         key = null;
512     //         expirationTime = -1;
513     //     }
514     // }
515 
516 }
517 
518 
519 
520     /**
521      * Return a new memory cache with the specified maximum size, unlimited
522      * lifetime for entries, with the values held by SoftReferences.
523      */
524     static Cache!(K, V) newSoftMemoryCache(K, V)(int size) {
525         return new MemoryCache!(K, V)(true, size);
526     }
527 
528     /**
529      * Return a new memory cache with the specified maximum size, the
530      * specified maximum lifetime (in seconds), with the values held
531      * by SoftReferences.
532      */
533     static Cache!(K, V) newSoftMemoryCache(K, V)(int size, int timeout) {
534         return new MemoryCache!(K, V)(true, size, timeout);
535     }
536 
537     /**
538      * Return a new memory cache with the specified maximum size, unlimited
539      * lifetime for entries, with the values held by standard references.
540      */
541     static Cache!(K, V) newHardMemoryCache(K, V)(int size) {
542         return new MemoryCache!(K, V)(false, size);
543     }
544 
545     /**
546      * Return a dummy cache that does nothing.
547      */
548     static Cache!(K, V) newNullCache(K, V)() {
549         return NullCache!(K, V).INSTANCE;
550     }
551 
552     /**
553      * Return a new memory cache with the specified maximum size, the
554      * specified maximum lifetime (in seconds), with the values held
555      * by standard references.
556      */
557     static Cache!(K, V) newHardMemoryCache(K, V)(int size, int timeout) {
558         return new MemoryCache!(K, V)(false, size, timeout);
559     }