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 }