1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 package org.archive.util;
26
27 import java.io.Closeable;
28 import java.io.File;
29 import java.io.Serializable;
30 import java.lang.ref.PhantomReference;
31 import java.lang.ref.Reference;
32 import java.lang.ref.ReferenceQueue;
33 import java.lang.ref.SoftReference;
34 import java.lang.reflect.Field;
35 import java.util.AbstractMap;
36 import java.util.HashMap;
37 import java.util.Iterator;
38 import java.util.LinkedList;
39 import java.util.Map;
40 import java.util.Set;
41 import java.util.concurrent.ConcurrentHashMap;
42 import java.util.concurrent.ConcurrentMap;
43 import java.util.concurrent.CountDownLatch;
44 import java.util.concurrent.TimeUnit;
45 import java.util.concurrent.atomic.AtomicInteger;
46 import java.util.concurrent.atomic.AtomicLong;
47 import java.util.logging.Level;
48 import java.util.logging.Logger;
49
50 import org.archive.crawler.datamodel.CandidateURI;
51 import org.archive.crawler.datamodel.ServerCache;
52 import org.archive.crawler.framework.CrawlController;
53
54 import com.sleepycat.bind.EntryBinding;
55 import com.sleepycat.bind.serial.SerialBinding;
56 import com.sleepycat.bind.serial.StoredClassCatalog;
57 import com.sleepycat.bind.tuple.TupleBinding;
58 import com.sleepycat.collections.StoredSortedMap;
59 import com.sleepycat.je.Database;
60 import com.sleepycat.je.DatabaseConfig;
61 import com.sleepycat.je.DatabaseException;
62 import com.sleepycat.je.DeadlockException;
63 import com.sleepycat.je.Environment;
64 import com.sleepycat.je.EnvironmentConfig;
65
66 /***
67 * A BDB JE backed hashmap. It extends the normal BDB JE map implementation by
68 * holding a cache of soft referenced objects. That is objects are not written
69 * to disk until they are not referenced by any other object and therefore can be
70 * Garbage Collected.
71 * <p/>
72 * BDB Java Edition is actually a btree. Flush to disk can be forced by sync().
73 * <p/>
74 * To ensure that changes/mutations to values in this map are coherent and
75 * consistent at the application level, it is assumed that the application
76 * level only mutates values that are in this map and does not retain references
77 * to values longer than necessary. This allows mappings to be persisted
78 * during GC without explicit transactions or write operations.
79 * <p/>
80 * There are two styles of CachedBdbMap usage:
81 * <p/>
82 * 1. single threaded (or externally synchronized) activity that uses any
83 * Map and ConcurrentMap methods available and specifically requires remove().
84 * <p/>
85 * 2. concurrent, high volume, accretive-only activity that uses
86 * {@link #putIfAbsent}, but not the {@link #put}, the {@link #replace}
87 * or 1 arg {@link #remove} methods.
88 * The concurrent {@link #replace} methods can be used if application level logic
89 * can rule out surprise unmapping of values between threads.
90 * This usage style does not require locking memMap and diskMap together
91 * to guarantee cache coherence and consistency.
92 * <p/>
93 * Both styles rely on an internal expunge operation (or the explicit
94 * {@link #sync()}) to save changes to values.
95 * The single threaded case can also use {@link #put} on top of an
96 * existing entry as long as no thread retains the previous value instance.
97 *
98 * @author John Erik Halse
99 * @author stack
100 * @author gojomo
101 * @author paul baclace (conversion to ConcurrentMap)
102 *
103 * @deprecated use ObjectIdentityBdbCache instead
104 */
105 public class CachedBdbMap<K,V> extends AbstractMap<K,V>
106 implements ConcurrentMap<K,V>, ObjectIdentityCache<K,V>, Serializable, Closeable {
107 private static final long serialVersionUID = -8655539411367047332L;
108
109 private static final Logger logger =
110 Logger.getLogger(CachedBdbMap.class.getName());
111
112 /*** The database name of the class definition catalog.*/
113 private static final String CLASS_CATALOG = "java_class_catalog";
114
115 /*** Number of attempts to tolerate for put().
116 * In BDB, simple write lock contention (non-deadlock) arrives as
117 * DeadlockException even though we are not using explicit transactions.
118 * For details see discussion at
119 * http://forums.oracle.com/forums/thread.jspa?threadID=521898&tstart=-1
120 * Value here should be a function of
121 * setLockTimeout() in {@link CrawlController#setupBdb()}
122 * and CrawlController.getToeCount(); since CrawlController is not
123 * accessible from this util class, a value suitable for average
124 * disk performance and 100 TOE threads is use.
125 * (In practice, {@link #put} contention of statistics is where
126 * timeouts were found.)
127 */
128 private static final int BDB_LOCK_ATTEMPT_TOLERANCE = 24;
129
130 /***
131 * A map of BDB JE Environments so that we reuse the Environment for
132 * databases in the same directory.
133 */
134 private static final Map<String,DbEnvironmentEntry> dbEnvironmentMap =
135 new HashMap<String,DbEnvironmentEntry>();
136
137 /*** The BDB JE environment used for this instance.
138 */
139 private transient DbEnvironmentEntry dbEnvironment;
140
141 /*** The BDB JE database used for this instance. */
142 protected transient Database db;
143
144 /*** The Collection view of the BDB JE database used for this instance. */
145 protected transient StoredSortedMap diskMap;
146
147 /*** The softreferenced cache of diskMap.
148 * Policy is: a memMap value is always correct *if present*.
149 * The diskMap value is only correct if there is no memMap value
150 * (and any phantom memMap values have been persisted.)
151 * That is, if memMap has a value, then that value is the most
152 * up to date. If memMap does not hold a value, then diskMap has the
153 * most up to date value.
154 * <p/>
155 * For diskMap size monitoring and concurrency support, the following
156 * invariant about keys is maintained: If a key is present in memMap,
157 * it must also present in diskMap (although the value in diskMap may
158 * not be the most recent, as above policy describes).
159 * <p/>
160 * The key presence invariant and the value coherence policy are maintained
161 * by using the "happens after" sense in the Java memory model by
162 * transitivity between diskMap and memMap.
163 * <p/>
164 * The clients of this class "only see one value instance" at a time so
165 * that multiple, independent get(k) calls return the same object instance.
166 * <p/>
167 * Strategy Notes about using ConcurrentMap semantics to
168 * implement the desired policy (see method comments for details):
169 * <p/>
170 * <pre>
171 * 1. Swap in/First insert: diskMap,
172 * then memMap // putIfAbsent() assures atomicity.
173 * 2. Value mutation: only one value instance per key in memMap
174 * is maintained so identity compare of value works and all clients
175 * operate on the same object.
176 * This implies that methods which change the instance
177 * (replace() methods) are not compatible with this design and
178 * should only be used if instance of CachedBdbMap is used
179 * by a single thread.
180 * Because content of referent is mutated, mapping is not changed;
181 * 2.1. BDB JE operation assumption: calling get(k) twice returns
182 * values that are equals(), but not necessarily == by identity
183 * ( diskMap.get(k).equals(diskMap.get(k)) is true, but
184 * diskMap.get(k) != diskMap.get(k) (might be, but not guaranteed)).
185 * 3. Swap out/flush: diskMap update, then memMap remove:
186 * diskMap.put(k,v2); // exclusive only if performed when
187 * processing ref queue during expunge;
188 * Avoid expunge races with a countdown latch in SoftEntry;
189 * memMap.remove(k,v2); if memMap race lost, then no harm done
190 * (notice that (k,v2) could match in either diskMap or memMap,
191 * or both, or neither).
192 * 3.1. Only expunge does an update of an existing diskMap entry for
193 * concurrent methods.
194 * 3.2. CachedBdbMap.get() must be able to return referent or otherwise
195 * effect a swap-in if get() is invoked during a swap out of a
196 * particular key-value entry.
197 * 4. sync() to disk: synchronized sync() // fully locked, no races.
198 * </pre>
199 * <p/>
200 * See the "find or create" style cache
201 * {@link ServerCache#getServerFor(CandidateURI)} for the major
202 * high-concurrency client of this class in terms of number of objects
203 * stored and performance impact.
204 */
205 protected transient ConcurrentHashMap<K,SoftEntry<V>> memMap;
206
207 protected transient ReferenceQueue<V> refQueue;
208
209 /*** The number of objects in the diskMap StoredMap.
210 * (Package access for unit testing.) */
211 protected AtomicInteger diskMapSize = new AtomicInteger(0);
212
213
214
215
216
217
218
219
220 /*** count of expunge already done */
221 transient private AtomicInteger expungeStatsNullPhantom;
222
223 /*** count of {@link #putIfAbsent} / {@link #remove} races,
224 * if they occur */
225 transient private AtomicInteger expungeStatsTransientCond;
226
227 /*** count of {@link #putIfAbsent} / {@link #remove} races,
228 * if they occur */
229 transient private AtomicInteger expungeStatsTransientCond2;
230
231 /*** count of {@link #putIfAbsent} / {@link #remove} races,
232 * if they occur */
233 transient private AtomicInteger expungeStatsTransientCond3;
234
235 /*** count of {@link SoftEntry#awaitExpunge()) */
236 transient private AtomicInteger expungeStatsAwaitExpunge;
237
238 /*** count of expunge already done to see if they occur */
239 transient private AtomicInteger expungeStatsNullValue;
240
241 /*** count of expunge of entries not in memMap to see if they occur */
242 transient private AtomicInteger expungeStatsNotInMap;
243
244 /*** static count {@link SoftEntry#awaitExpunge()) timeouts to see if
245 * they occur */
246 final static private AtomicInteger expungeStatsAwaitTimeout
247 = new AtomicInteger(0);
248
249 /*** static count of swap-in timeouts */
250 final static private AtomicInteger expungeStatsSwapIn
251 = new AtomicInteger(0);
252
253 /*** static count of swap-in re-tries timeouts to see if they occur */
254 final static private AtomicInteger expungeStatsSwapInRetry
255 = new AtomicInteger(0);
256
257 /*** static count of swap-in re-tries timeouts to see if they occur */
258 final static private AtomicInteger expungeStatsSwapInRetry2
259 = new AtomicInteger(0);
260
261 /*** static count of expunge put() to BDB (implies disk) */
262 final static private AtomicInteger expungeStatsDiskPut
263 = new AtomicInteger(0);
264
265 /*** transient count of expunge get mem check condition */
266 transient private AtomicInteger expungeStatsGetMemCheck;
267
268 /*** transient count of expunge via poll */
269 transient private AtomicInteger expungeStatsViaPoll;
270
271 /*** count of one arg {@link #remove } use */
272 transient private AtomicInteger useStatsRemove1Used;
273
274 /*** count of two arg {@link #remove} use */
275 transient private AtomicInteger useStatsRemove2Used;
276
277 /*** count of {@link #replace} (2 or 3 arg) use */
278 transient private AtomicInteger useStatsReplaceUsed;
279
280 /*** count of {@link #put} use */
281 transient private AtomicInteger useStatsPutUsed;
282
283 /*** count of {@link #putIfAbsent} use */
284 transient private AtomicInteger useStatsPutIfUsed;
285
286 /*** count of {@link #sync()} use */
287 transient private AtomicInteger useStatsSyncUsed;
288
289 /***
290 * Count of times we got an object from in-memory cache.
291 */
292 private AtomicLong cacheHit = new AtomicLong(0);
293
294 /***
295 * Count of times the {@link CachedBdbMap#get} method was called.
296 */
297 private AtomicLong countOfGets = new AtomicLong(0);
298
299 /***
300 * Count of every time we went to the disk-based map AND we found an
301 * object (Doesn't include accesses that came back null).
302 */
303 private AtomicLong diskHit = new AtomicLong(0);
304
305 /***
306 * Name of bdbje db.
307 */
308 private String dbName = null;
309
310 /***
311 * Reference to the Reference#referent Field.
312 */
313 protected static Field referentField;
314 static {
315
316
317
318
319
320
321 try {
322 referentField = Reference.class.getDeclaredField("referent");
323 referentField.setAccessible(true);
324 } catch (SecurityException e) {
325 throw new RuntimeException(e);
326 } catch (NoSuchFieldException e) {
327 throw new RuntimeException(e);
328 }
329 }
330
331 /*** internal behavior characterization flag.
332 * log a warning when operations are performed that could violate the
333 * "only one reference" design rule. The problem to avoid is
334 * mutating an unmapped value instance that would not be persisted.
335 * (warning is emitted at most once per instance)
336 */
337 final private boolean LOG_ERROR_ON_DESIGN_VIOLATING_METHODS=true;
338
339 /***
340 * Simple structure to keep needed information about a DB Environment.
341 */
342 protected static class DbEnvironmentEntry {
343 Environment environment;
344 StoredClassCatalog classCatalog;
345 int openDbCount = 0;
346 File dbDir;
347 }
348
349 /***
350 * Shutdown default constructor.
351 */
352 private CachedBdbMap() {
353 super();
354 }
355
356 /***
357 * Constructor.
358 *
359 * You must call
360 * {@link #initialize(Environment, Class, Class, StoredClassCatalog)}
361 * to finish construction. Construction is two-stepped to support
362 * reconnecting a deserialized CachedBdbMap with its backing bdbje
363 * database.
364 *
365 * @param dbName Name of the backing db this instance should use.
366 */
367 public CachedBdbMap(final String dbName) {
368 this();
369 this.dbName = dbName;
370 }
371
372 /***
373 * A constructor for creating a new CachedBdbMap.
374 *
375 * Even though the put and get methods conforms to the Collections interface
376 * taking any object as key or value, you have to submit the class of the
377 * allowed key and value objects here and will get an exception if you try
378 * to put anything else in the map.
379 *
380 * <p>This constructor internally calls
381 * {@link #initialize(Environment, Class, Class, StoredClassCatalog)}.
382 * Do not call initialize if you use this constructor.
383 *
384 * @param dbDir The directory where the database will be created.
385 * @param dbName The name of the database to back this map by.
386 * @param keyClass The class of the objects allowed as keys.
387 * @param valueClass The class of the objects allowed as values.
388 *
389 * @throws DatabaseException is thrown if the underlying BDB JE database
390 * throws an exception.
391 */
392 public CachedBdbMap(final File dbDir, final String dbName,
393 final Class<K> keyClass, final Class<V> valueClass)
394 throws DatabaseException {
395 this(dbName);
396 this.dbEnvironment = getDbEnvironment(dbDir);
397 this.dbEnvironment.openDbCount++;
398 initialize(dbEnvironment.environment, valueClass,
399 dbEnvironment.classCatalog);
400 if (logger.isLoggable(Level.INFO)) {
401
402 EnvironmentConfig cfg = this.dbEnvironment.environment.getConfig();
403 logger.info("BdbConfiguration: Cache percentage " +
404 cfg.getCachePercent() + ", cache size " + cfg.getCacheSize() +
405 ", Map size: " + size() + " cfg=" + cfg);
406 }
407 }
408
409 /***
410 * Call this method when you have an instance when you used the
411 * default constructor or when you have a deserialized instance that you
412 * want to reconnect with an extant bdbje environment. Do not
413 * call this method if you used the
414 * {@link #CachedBdbMap(File, String, Class, Class)} constructor.
415 * @param env
416 * @param keyClass
417 * @param valueClass
418 * @param classCatalog
419 * @throws DatabaseException
420 */
421 public synchronized void initialize(final Environment env,
422 final Class<? super V> valueClass, final StoredClassCatalog classCatalog)
423 throws DatabaseException {
424 initializeInstance();
425 this.db = openDatabase(env, this.dbName);
426 this.diskMap = createDiskMap(this.db, classCatalog, String.class,
427 valueClass);
428 }
429
430 /***
431 * Do any instance setup.
432 * This method is used by constructors and when deserializing an instance.
433 */
434 protected void initializeInstance() {
435
436 this.memMap = new ConcurrentHashMap<K,SoftEntry<V>>(
437 8192,
438 0.9f,
439 64
440 );
441 this.refQueue = new ReferenceQueue<V>();
442 initTransientStats();
443 canary = new SoftReference<LowMemoryCanary>(new LowMemoryCanary());
444 }
445
446
447 protected void initTransientStats() {
448 expungeStatsNullPhantom = new AtomicInteger(0);
449 expungeStatsTransientCond = new AtomicInteger(0);
450 expungeStatsTransientCond2 = new AtomicInteger(0);
451 expungeStatsTransientCond3 = new AtomicInteger(0);
452 expungeStatsGetMemCheck = new AtomicInteger(0);
453 expungeStatsViaPoll = new AtomicInteger(0);
454 expungeStatsAwaitExpunge = new AtomicInteger(0);
455 expungeStatsNullValue = new AtomicInteger(0);
456 expungeStatsNotInMap = new AtomicInteger(0);
457 useStatsRemove1Used = new AtomicInteger(0);
458 useStatsRemove2Used = new AtomicInteger(0);
459 useStatsReplaceUsed = new AtomicInteger(0);
460 useStatsPutUsed = new AtomicInteger(0);
461 useStatsPutIfUsed = new AtomicInteger(0);
462 useStatsSyncUsed = new AtomicInteger(0);
463 }
464
465 @SuppressWarnings("unchecked")
466 protected StoredSortedMap createDiskMap(Database database,
467 StoredClassCatalog classCatalog, Class keyClass, Class valueClass) {
468 EntryBinding keyBinding = TupleBinding.getPrimitiveBinding(keyClass);
469 if(keyBinding == null) {
470 keyBinding = new SerialBinding(classCatalog, keyClass);
471 }
472 EntryBinding valueBinding = TupleBinding.getPrimitiveBinding(valueClass);
473 if(valueBinding == null) {
474 valueBinding = new SerialBinding(classCatalog, valueClass);
475 }
476
477 return new StoredSortedMap(database, keyBinding, valueBinding, true);
478 }
479
480 /***
481 * Conditionally await for an entry to be expunged.
482 * If the given entry is in the process of being expunged, this
483 * will block until it is complete and then return true.
484 * If the entry should be expunged, this will try to do the expunge.
485 * If the entry is not being expunged,
486 * then this will return immediately with a value of false.
487 * If this thread is interrupted, the condition is cleared and
488 * the method returns immediately.
489 * <p/>
490 * For transitive "happens before" correctness invariant to be true,
491 * these operations must occur in the following order in one thread:
492 * <pre>
493 * 1. if (entry.get() == null) // cleared by GC
494 * 2. if (entry.startExpunge()) // thread has exclusive expunge
495 * 3. diskMap.put(k, entry.getPhantom().doctoredGet())
496 * 4. memMap.remove(k, entry)
497 * 5. entry.clearPhantom()
498 *
499 * Concurrent threads interested in the same entry will execute:
500 *
501 * 1. if (entry.get() == null) // cleared by GC
502 * 2. if (! entry.startExpunge()) // thread cannot do expunge
503 * 3. if (entry.expungeStarted())
504 * 4. entry.awaitExpunge();
505 * </pre>
506 * @param entry
507 * @return true if entry was expunged, cleared, and no longer in memMap.
508 */
509 private boolean isExpunged(final SoftEntry<V> entry, K key) {
510 boolean ret = false;
511
512 V val = entry.get();
513 if (val == null) {
514 if (entry.startExpunge()) {
515
516 expungeStaleEntry(entry, true);
517 return true;
518 } else if (entry.expungeStarted()) {
519 expungeStatsAwaitExpunge.incrementAndGet();
520 expungeStaleEntries();
521
522 boolean wasInterrupted = Thread.interrupted();
523 try {
524 ret = entry.awaitExpunge();
525 if(ret) {
526 verifyExpunge(entry, key);
527 }
528 return ret;
529 } catch (InterruptedException ex) {
530 return true;
531 } finally {
532 if (wasInterrupted) {
533 Thread.currentThread().interrupt();
534 }
535 }
536 } else {
537 logger.log(
538 Level.WARNING,
539 "could not start expunge, and expunge was never started and val is null",
540 new Exception());
541 return false;
542 }
543 } else {
544 return false;
545 }
546 }
547
548 /***
549 * Verify that expunction has taken place; for debugging. Only
550 * called after awaitExpunge() reports an entry has been expunged.
551 * Detects whether memMap.get() returns the same entry by identity;
552 * (entries are not recycled.)
553 * @param entry entry that should be gone from memMap
554 */
555 private void verifyExpunge(SoftEntry<V> entry, K key) {
556 if(memMap.get(key)==entry) {
557 logger.log(Level.WARNING,"reported-expunged entry at key '"+key+"' still present: "+entry,new Exception());
558 dumpExtraStats();
559
560
561
562
563 memMapRemoveOrWarn(key, entry);
564 }
565 }
566
567 /***
568 * Remove from memMap -- but warn if remove does not have
569 * expected effect. For debugging purposes.
570 *
571 * @param key key to remvoe
572 * @param entry expected item to remove
573 */
574 private void memMapRemoveOrWarn(Object key, SoftEntry<V> entry) {
575 boolean removed = memMap.remove(key, entry);
576 if(!removed) {
577 logger.log(Level.WARNING,"memMap.remove() ineffective",new Exception());
578 } else {
579
580 }
581 }
582
583 /***
584 * Get the database environment for a physical directory where data will be
585 * stored.
586 * <p>
587 * If the environment already exist it will be reused, else a new one will
588 * be created.
589 *
590 * @param dbDir The directory where BDB JE data will be stored.
591 * @return a datastructure containing the environment and a default database
592 * for storing class definitions.
593 */
594 private DbEnvironmentEntry getDbEnvironment(File dbDir) {
595 if (dbEnvironmentMap.containsKey(dbDir.getAbsolutePath())) {
596 return (DbEnvironmentEntry) dbEnvironmentMap.get(dbDir
597 .getAbsolutePath());
598 }
599 EnvironmentConfig envConfig = new EnvironmentConfig();
600 envConfig.setAllowCreate(true);
601 envConfig.setTransactional(false);
602
603
604
605
606 DbEnvironmentEntry env = new DbEnvironmentEntry();
607 try {
608 env.environment = new Environment(dbDir, envConfig);
609 env.dbDir = dbDir;
610 dbEnvironmentMap.put(dbDir.getAbsolutePath(), env);
611
612 DatabaseConfig dbConfig = new DatabaseConfig();
613 dbConfig.setTransactional(false);
614 dbConfig.setAllowCreate(true);
615 dbConfig.setDeferredWrite(true);
616
617 Database catalogDb = env.environment.openDatabase(null,
618 CLASS_CATALOG, dbConfig);
619
620 env.classCatalog = new StoredClassCatalog(catalogDb);
621 } catch (DatabaseException e) {
622 e.printStackTrace();
623
624 }
625 return env;
626 }
627
628 protected Database openDatabase(final Environment environment,
629 final String dbName) throws DatabaseException {
630 DatabaseConfig dbConfig = new DatabaseConfig();
631 dbConfig.setTransactional(false);
632 dbConfig.setAllowCreate(true);
633 dbConfig.setDeferredWrite(true);
634 return environment.openDatabase(null, dbName, dbConfig);
635 }
636
637 public synchronized void close() {
638
639 if (this.db != null) {
640 try {
641 this.db.sync();
642 this.db.close();
643 } catch (DatabaseException e) {
644 e.printStackTrace();
645 } finally {
646 this.db = null;
647 }
648 }
649 if (dbEnvironment != null) {
650 dbEnvironment.openDbCount--;
651 if (dbEnvironment.openDbCount <= 0) {
652 try {
653 dbEnvironment.classCatalog.close();
654 dbEnvironment.environment.close();
655 } catch (DatabaseException de) {
656 de.printStackTrace();
657 }
658 dbEnvironmentMap.remove(dbEnvironment.dbDir.getAbsolutePath());
659 dbEnvironment = null;
660 }
661 }
662 }
663
664 protected void finalize() throws Throwable {
665 close();
666 super.finalize();
667 }
668
669 /***
670 * The keySet of the diskMap is all relevant keys.
671 *
672 * @see java.util.Map#keySet()
673 */
674 @SuppressWarnings("unchecked")
675 public Set<K> keySet() {
676 return diskMap.keySet();
677 }
678
679 public Set<Map.Entry<K,V>> entrySet() {
680
681
682 throw new UnsupportedOperationException();
683 }
684
685 /***
686 * ObjectIdentityCache get-or-atomic-create method.
687 */
688 public V getOrUse(K key, Supplier<V> supplierOrNull) {
689 V val = get(key);
690 if(val!=null || supplierOrNull == null) {
691 return val;
692 }
693 val = supplierOrNull.get();
694 V prevVal = putIfAbsent(key, val);
695 if(prevVal!=null) {
696 return prevVal;
697 }
698 return val;
699 }
700
701 public V get(final Object object) {
702 K key = toKey(object);
703 countOfGets.incrementAndGet();
704 expungeStaleEntries();
705 if (countOfGets.get() % 10000 == 0) {
706 dumpExtraStats();
707 logCacheSummary();
708 }
709
710
711 V val = _getMem(key);
712 if (val != null) {
713 return val;
714 }
715
716
717 V valDisk = _getDisk(key);
718 return valDisk;
719 }
720
721 private V _getDisk(K key) {
722 V v = diskMapGet(key);
723 if (v != null) {
724 diskHit.incrementAndGet();
725
726
727
728 expungeStatsSwapIn.incrementAndGet();
729 SoftEntry<V> newEntry = new SoftEntry<V>(key, v, refQueue);
730 do {
731 SoftEntry<V> existingEntry =
732 (SoftEntry<V>) memMap.putIfAbsent(key, newEntry);
733 if (existingEntry != null) {
734 V prevValue = (V) existingEntry.get();
735 if (prevValue != null) {
736
737 newEntry.clearPhantom();
738 return prevValue;
739 } else {
740
741
742
743
744
745
746
747
748 if (isExpunged(existingEntry, key)) {
749
750 expungeStatsSwapInRetry.incrementAndGet();
751 continue;
752 } else {
753 expungeStatsSwapInRetry2.incrementAndGet();
754 try {
755
756 Thread.sleep(0L, 10000);
757 } catch (InterruptedException ignored) {
758 }
759 logger.log(Level.WARNING,"Swap-In Retry", new Exception());
760 continue;
761 }
762 }
763 }
764 return v;
765 } while (true);
766 }
767 return null;
768 }
769
770 private V _getMem(K key) {
771 do {
772 SoftEntry<V> entry = memMap.get(key);
773 if (entry != null) {
774 V val = entry.get();
775 if (val != null) {
776 cacheHit.incrementAndGet();
777 return val;
778 }
779
780
781
782 if (isExpunged(entry, key)) {
783
784 continue;
785 } else {
786 expungeStatsGetMemCheck.incrementAndGet();
787 logger.log(Level.WARNING,"entry not expunged AND value is null", new Exception());
788 return null;
789 }
790 }
791 return null;
792 } while (true);
793 }
794
795 /***
796 * Info to log, if at FINE level
797 */
798 private void logCacheSummary() {
799 if (logger.isLoggable((Level.FINE))) {
800 logger.fine(composeCacheSummary());
801 }
802 }
803
804 private String composeCacheSummary() {
805 long totalHits = cacheHit.get() + diskHit.get();
806 if (totalHits < 1) {
807 return "";
808 }
809 long cacheHitPercent
810 = (cacheHit.get() * 100) / totalHits;
811 StringBuilder sb = new StringBuilder(120);
812 try {
813 String sname = this.db.getDatabaseName();
814 sb.append("DB name:").append(sname).append(", ");
815 } catch (DatabaseException ignore) {
816 }
817 sb.append(" Cache Hit: ")
818 .append(cacheHitPercent)
819 .append("%, Not in map: ")
820 .append((countOfGets.get() - totalHits))
821 .append(", Total number of gets: ")
822 .append(countOfGets.get());
823 return sb.toString();
824 }
825
826 /*** Map.put() implementation.
827 * <p/>
828 * Warning: This method violates the "only expose one value instance"
829 * design rule for this class. Multiple instances means that it is
830 * undetermined as to which instance will be saved to disk last.
831 * This method can be safely used when only one thread has access to
832 * this instance of CachedBdbMap.
833 * <p/>If possible, use {@link #putIfAbsent} instead.
834 *
835 * <pre>
836 * Preconditions: (diskMap.containsKey(), memMap.containsKey()) are:
837 * (*,*) put() cannot assure "only expose one value instance", so
838 * all cases are the same: put to diskMap then memMap; clear any
839 * pre-existing SoftEntry to prevent expunge of old value.
840 *
841 * PostConditions:
842 * (T,T) both memMap and diskMap will have entries for given key; if
843 * null is returned, then this is the first time the key has an
844 * entry, otherwise the previous value in diskMap will not be
845 * updated until expunge.
846 * </pre>
847 *
848 * @param key
849 * @param value
850 * @return previous value or null.
851 */
852 @SuppressWarnings("unchecked")
853 @Override
854 public V put(K key, V value) {
855 V prevMemValue = null;
856 V prevDiskValue = null;
857 int pu = useStatsPutUsed.incrementAndGet();
858 if (pu == 1 && LOG_ERROR_ON_DESIGN_VIOLATING_METHODS) {
859 logger.log(Level.WARNING,"design violating put() used on dbName=" + dbName, new Exception());
860 }
861 expungeStaleEntries();
862 int attemptTolerance = BDB_LOCK_ATTEMPT_TOLERANCE;
863 while (--attemptTolerance > 0) {
864 try {
865 prevDiskValue = (V)diskMap.put(key,value);
866 break;
867 } catch (Exception e) {
868 if (e instanceof com.sleepycat.util.RuntimeExceptionWrapper
869 && e.getCause() instanceof DeadlockException
870 && attemptTolerance > 0) {
871 if (attemptTolerance == BDB_LOCK_ATTEMPT_TOLERANCE - 1) {
872
873 logger.log(Level.WARNING,"BDB implicit transaction timeout while "
874 + "waiting to acquire lock, retrying; "
875 + " dbName=" + dbName + " key=" + key, new Exception());
876 }
877 continue;
878 } else {
879 logger.warning("unexpected exception: " + e);
880 throw new RuntimeException(e);
881 }
882 }
883 }
884 if (prevDiskValue == null) {
885 diskMapSize.incrementAndGet();
886 }
887 SoftEntry<V> prevEntry = memMap.put(key, new SoftEntry<V>(key, value, refQueue));
888 if (prevEntry != null) {
889 prevMemValue = prevEntry.get();
890
891 prevEntry.clearPhantom();
892 }
893
894 if (prevMemValue != null) {
895 return prevMemValue;
896 }
897 return prevDiskValue;
898 }
899
900 /*** Replace entry for key only if currently mapped to given value.
901 * To maintain the illusion of an invisible cache,
902 * if the memMap has no mapping, the diskMap entry, if any, must
903 * be swapped-in to memMap in order to see if the given oldValue
904 * matches.
905 *
906 * <p/>Possible disk io to swap in from diskMap.
907 * <p/>
908 * Warning: This method violates the "only expose one value instance"
909 * design rule for this class. Multiple instances means that it is
910 * undetermined as to which instance will be saved to disk last.
911 * This method can be safely used when only one thread has access to
912 * this instance of CachedBdbMap.
913 * <pre>
914 * Preconditions: (diskMap.containsKey(), memMap.containsKey()) are:
915 * (F,F) nothing to replace.
916 * PostCondition: (F,F) no replace.
917 * (T,F) value must be swapped-in, do replace(),
918 * PostCondition: (T,T) if
919 * replace occurred or not (value might have been replaced), diskMap
920 * need not be updated because expunge will do that.
921 * (*,T) normal swapped-in condition (we can assume that the method which
922 * put a mapping to memMap took care of creating one for diskMap),
923 * memMap value is most recent and expunge can take care of
924 * updating diskMap, so we only do replace() on memMap;
925 * PostCondition: (*,T).
926 * </pre>
927 * @return true if the replace was peformed (only if the previous mapping
928 * of key was to given oldValue).
929 */
930 @SuppressWarnings("unchecked")
931 public boolean replace(K key, V oldValue, V newValue) {
932 int ru = useStatsReplaceUsed.incrementAndGet();
933 if (ru == 1 && LOG_ERROR_ON_DESIGN_VIOLATING_METHODS) {
934
935 logger.log(Level.WARNING,"design violating replace(,,) used on dbName=" + dbName,new Exception());
936 }
937
938 expungeStaleEntries();
939
940
941 SoftEntry<V> newEntry = new SoftEntry<V>(key, newValue, refQueue);
942 SoftEntry<V> oldEntry = new SoftEntry<V>(key, oldValue, refQueue);
943
944 try {
945 boolean memReplaced = memMap.replace(key, oldEntry, newEntry);
946 if (memReplaced) {
947 newEntry = null;
948 return true;
949 } else {
950 boolean diskReplaced = diskMap.replace(key, oldValue, newValue);
951 if (diskReplaced) {
952
953 memMap.put(key, newEntry);
954 newEntry = null;
955 return true;
956 } else {
957 return false;
958 }
959 }
960 } finally {
961
962 if (oldEntry != null) {
963 oldEntry.clearPhantom();
964 }
965 if (newEntry != null) {
966 newEntry.clearPhantom();
967 }
968 }
969 }
970
971 /*** Replace entry for key only if currently mapped to some value.
972 * To maintain the illusion of an invisible cache,
973 * the diskMap value must be read to see if oldValue
974 * matches. The replace() operation is performed on memCache
975 * and then on diskCache, without synchronizing over the pair of
976 * operations.
977 * <p/>Possible disk io to swap in from diskMap.
978 *
979 * <p/>
980 * Warning: This method violates the "only expose one value instance"
981 * design rule for this class. Multiple instances means that it is
982 * undetermined as to which instance will be saved to disk last.
983 * This method can be safely used when only one thread has access to
984 * this instance of CachedBdbMap.
985 * <pre>
986 * Preconditions: (diskMap.containsKey(), memMap.containsKey()) are:
987 * (F,F) nothing to replace. PostCondition: (F,F) no change.
988 * (T,F) value must be swapped-in, do replace(), PostCondition: (T,T) if
989 * replace occurred or not (value might have been replaced), diskMap
990 * need not be updated because expunge will do that.
991 * (*,T) normal swapped-in condition (we can assume that the method which
992 * put a mapping to memMap took care of creating one for diskMap),
993 * memMap value is most recent and expunge can take care of updating
994 * diskMap, so we only do replace() on memMap.
995 * </pre>
996 * @return previous value if the replace was peformed on either memMap
997 * or diskMap, the previous value in memMap is returned if non-null,
998 * otherwise the previous from diskMap is returned, if no match in
999 * either map, then null is returned.
1000 */
1001 @SuppressWarnings("unchecked")
1002
1003 public V replace(K key, V value) {
1004 int ru = useStatsReplaceUsed.incrementAndGet();
1005 if (ru == 1 && LOG_ERROR_ON_DESIGN_VIOLATING_METHODS) {
1006
1007 logger.log(Level.WARNING,"design violating replace(,) used on dbName=" + dbName, new Exception());
1008 }
1009 SoftEntry<V> newEntry = new SoftEntry<V>(key, value, refQueue);
1010 V prevMemValue = null;
1011 SoftEntry prevEntry = (SoftEntry)memMap.replace(key, newEntry);
1012 if (prevEntry != null) {
1013 prevMemValue = (V)prevEntry.get();
1014
1015
1016
1017 return prevMemValue;
1018 } else {
1019 V prevDiskValue = (V)diskMap.replace(key, value);
1020 if (prevDiskValue == null) {
1021
1022 newEntry.clearPhantom();
1023 return null;
1024 } else {
1025
1026 memMap.put(key, newEntry);
1027 return prevDiskValue;
1028 }
1029 }
1030 }
1031
1032 /*** A composite putIfAbsent() over memMap and diskMap.
1033 * If the specified key is not already associated with any value,
1034 * associate it with the given value.
1035 * By modifying memMap and diskMap.
1036 * <p/>
1037 * diskMap is not updated every time a value is mutated because this
1038 * cached disk store attempts to be invisible to client code. However,
1039 * This method will cause diskMap to be updated if there was no previous
1040 * because this was the case for v1.14.2 and earlier. (It also means
1041 * diskMap and memMap, both ConcurrentMaps, can be coherently maintained
1042 * from the perspective of a one-instance-only style cache with
1043 * diskMap updated at expunge by always modifying diskMap first here.
1044 * The disk map size estimate is also reasonably correct if diskMap
1045 * always has any mapping that is in memMap.)
1046 *
1047 * <p/>
1048 * Swap in/First insert: diskMap.putIfAbsent(),
1049 * then memMap.putIfAbsent(), putIfAbsent() assures atomicity;
1050 * <pre>
1051 * Preconditions: (diskMap.containsKey(), memMap.containsKey(SoftEntry)) are:
1052 * (F,F) initial starting conditions: is absent, put to diskMap then memMap.
1053 * (F,T) transient remove(): await other thread to finish with memMap,
1054 * then proceed as (F,F)
1055 * OR (if remove not used) an unexpected data race occurred.
1056 * (T,F) reloadable from disk or in process of inserting first time:
1057 * not absent.
1058 * (T,T) normal swapped in condition: not absent.
1059 *
1060 * PostConditions:
1061 * (T,T) both memMap and diskMap will have entries for given key (if
1062 * null is returned, then the value given is the one mapped).
1063 * </pre>
1064 * @param key
1065 * @param value the value to which the given key should map, but only
1066 * if there is existing mapping for key.
1067 * @return
1068 */
1069 @SuppressWarnings("unchecked")
1070
1071 public V putIfAbsent(K key, V value) {
1072 expungeStaleEntries();
1073 V existingDiskValue = null;
1074 int attemptTolerance = BDB_LOCK_ATTEMPT_TOLERANCE;
1075 while (--attemptTolerance > 0) {
1076 try {
1077 existingDiskValue = (V)diskMap.putIfAbsent(key,value);
1078 break;
1079 } catch (Exception e) {
1080 if (e instanceof com.sleepycat.util.RuntimeExceptionWrapper
1081 && e.getCause() instanceof DeadlockException
1082 && attemptTolerance > 0) {
1083 if (attemptTolerance == BDB_LOCK_ATTEMPT_TOLERANCE - 1) {
1084
1085 logger.log(Level.WARNING,"BDB implicit transaction timeout while "
1086 + "waiting to acquire lock, retrying; "
1087 + " dbName=" + dbName + " key=" + key, new Exception());
1088 }
1089 continue;
1090 } else {
1091 logger.warning("unexpected exception: " + e);
1092 throw new RuntimeException(e);
1093 }
1094 }
1095 }
1096 if (existingDiskValue == null) {
1097
1098 diskMapSize.incrementAndGet();
1099 useStatsPutIfUsed.incrementAndGet();
1100
1101 SoftEntry<V> newEntry = new SoftEntry<V>(key, value, refQueue);
1102 SoftEntry<V> prevEntry =
1103 (SoftEntry<V>) memMap.putIfAbsent(key, newEntry);
1104 if (prevEntry != null) {
1105
1106
1107
1108
1109
1110 V prevMemValue = prevEntry.get();
1111 expungeStatsTransientCond.getAndIncrement();
1112 if (prevMemValue == null && isExpunged(prevEntry, key)) {
1113 prevEntry = (SoftEntry<V>) memMap.putIfAbsent(key, newEntry);
1114 if (prevEntry == null) {
1115
1116 return null;
1117 } else {
1118 prevMemValue = prevEntry.get();
1119 }
1120 }
1121 newEntry.clearPhantom();
1122 expungeStatsTransientCond2.getAndIncrement();
1123
1124 return prevMemValue;
1125 } else {
1126
1127
1128 return null;
1129 }
1130 } else {
1131
1132 V retValue = get(key);
1133 if (retValue == null) {
1134
1135
1136
1137
1138
1139 expungeStatsTransientCond3.getAndIncrement();
1140 logger.log(Level.WARNING,"unusual put/remove activity for key=" + key,new Exception());
1141 }
1142 return retValue;
1143 }
1144 }
1145
1146 /*** Helper for remove() methods.
1147 * Neutralize a map entry so that diskMap is not updated and remove
1148 * the entry from memMap. This prevents expunge ("swap out") of
1149 * the given entry.
1150 * @param prevEntry SoftEntry to remove from memMap.
1151 * @param key the key to match.
1152 */
1153 private void neutralizeMemMapEntry(SoftEntry<V> prevEntry, final K key) {
1154 do {
1155 if (prevEntry.expungeStarted()) {
1156
1157 isExpunged(prevEntry, key);
1158 memMapRemoveOrWarn(key, prevEntry);
1159 break;
1160 } else {
1161
1162 if (prevEntry.startExpunge()) {
1163 memMapRemoveOrWarn(key, prevEntry);
1164 prevEntry.clearPhantom();
1165 break;
1166 } else {
1167
1168 continue;
1169 }
1170 }
1171 } while (true);
1172 }
1173
1174 /***
1175 * Note that a call to this method CLOSEs the underlying bdbje.
1176 * This instance is no longer of any use. It must be re-initialized.
1177 * We close the db here because if this BigMap is being treated as a plain
1178 * Map, this is only opportunity for cleanup.
1179 */
1180 public synchronized void clear() {
1181 dumpExtraStats();
1182 this.memMap.clear();
1183 this.diskMap.clear();
1184 this.diskMapSize.set(0);
1185 close();
1186 }
1187
1188 /*** Remove mapping for the given key.
1189 * A mapping does not need to be swapped in to be removed.
1190 * Matching entry is removed from memMap and diskMap.
1191 * <pre>
1192 * Preconditions: (diskMap.containsKey(), memMap.containsKey(SoftEntry)) are:
1193 * (F,*) nothing to remove if diskMap has no entry.
1194 * (T,F) remove from diskMap, await expunge in memMap, if in progress.
1195 * (T,T) remove from diskMap, clear phantom to prevent expunge of memMap.
1196 *
1197 * PostConditions:
1198 * (F,F) both memMap and diskMap will NOT have entries for given key (if
1199 * null is returned, then nothing was removed).
1200 * </pre>
1201 * @param key
1202 * @return old value or null if nothing removed.
1203 */
1204 @Override
1205 @SuppressWarnings("unchecked")
1206 public V remove(final Object key) {
1207 V prevDiskValue = null;
1208
1209 SoftEntry<V> prevEntry = (SoftEntry<V>) memMap.get(key);
1210
1211
1212 if ((prevDiskValue = (V) diskMap.remove(key)) != null) {
1213 diskMapSize.decrementAndGet();
1214 int ru = useStatsRemove1Used.incrementAndGet();
1215 if (ru == 1) {
1216
1217 logger.log(Level.WARNING,"remove() used for dbName=" + dbName, new Exception());
1218 }
1219 }
1220
1221
1222
1223 if (prevEntry == null) {
1224
1225 return prevDiskValue;
1226 }
1227 V prevValue = prevEntry.get();
1228 neutralizeMemMapEntry(prevEntry, (K) key);
1229 if (prevValue == null) {
1230 return prevDiskValue;
1231 } else {
1232 return prevValue;
1233 }
1234 }
1235
1236 /*** remove item matching both the key and value.
1237 * Matching entry is removed from memMap and diskMap.
1238 * A mapping does not need to be swapped in to be removed.
1239 * If the entry matching key is swapped in, then value is used
1240 * to find and remove the entry in memMap (the value is of type V),
1241 * and diskMap.remove(K) is used to remove the persistent entry.
1242 *
1243 * Otherwise, a diskMap.remove(K,V) is performed.
1244 * <pre>
1245 * Preconditions: (diskMap.containsKey() AND value.equals(V),
1246 * memMap.containsKey(SoftEntry) AND value.equals(V) AND
1247 * swapped-in) are:
1248 * (F,F) nothing to do.
1249 * (F,T) diskMap.remove(K,V) fails, but memMap.remove(K,V) still possible
1250 * (T,F) remove from diskMap, await expunge in memMap, if in progress.
1251 * (T,T) remove from diskMap, clear phantom to prevent expunge of memMap.
1252 *
1253 * PostConditions:
1254 * (F,F) both memMap and diskMap will NOT have entries for given key (if
1255 * false is returned, then nothing was removed).
1256 * </pre>
1257 * @param key key used for matching mapping to remove.
1258 * @param value value used for matching mapping to remove.
1259 * @return true if entry removed.
1260 */
1261
1262 @SuppressWarnings("unchecked")
1263 public boolean remove(Object key, Object value) {
1264 SoftEntry<V> entry = null;
1265 V memValue = null;
1266
1267
1268
1269 entry = (SoftEntry<V>) memMap.get(key);
1270 if (entry != null) {
1271
1272 memValue = entry.get();
1273 if (memValue != null && memValue.equals(value)) {
1274
1275
1276 int r2u = useStatsRemove2Used.incrementAndGet();
1277 if (r2u == 1 && LOG_ERROR_ON_DESIGN_VIOLATING_METHODS) {
1278 logger.log(
1279 Level.WARNING,
1280 "design violating remove(,) used for dbName="+ dbName,
1281 new Exception());
1282 }
1283
1284 Object obj = diskMap.remove(key);
1285 if (obj != null) {
1286 diskMapSize.decrementAndGet();
1287 }
1288 neutralizeMemMapEntry(entry, (K) key);
1289 return true;
1290 } else {
1291
1292
1293
1294 return false;
1295 }
1296 }
1297
1298
1299 boolean diskMapFound = diskMap.remove(key, value);
1300 if (diskMapFound) {
1301 int r2u = useStatsRemove2Used.incrementAndGet();
1302 if (r2u == 1 && LOG_ERROR_ON_DESIGN_VIOLATING_METHODS) {
1303 logger.log(
1304 Level.WARNING,
1305 "design violating remove(,) used for dbName=" + dbName);
1306 }
1307 diskMapSize.decrementAndGet();
1308 return true;
1309 } else {
1310 return false;
1311 }
1312 }
1313
1314 public synchronized boolean containsKey(Object key) {
1315 if (quickContainsKey(key)) {
1316 return true;
1317 }
1318 return diskMap.containsKey(key);
1319 }
1320
1321 private boolean quickContainsKey(Object key) {
1322 expungeStaleEntries();
1323 return memMap.containsKey(key);
1324 }
1325
1326 /***
1327 * This method is not supported.
1328 * @deprecated
1329 * @param value
1330 * @return
1331 */
1332 public synchronized boolean containsValue(Object value) {
1333 if (quickContainsValue(value)) {
1334 return true;
1335 }
1336 return diskMap.containsValue(value);
1337 }
1338
1339 private boolean quickContainsValue(Object value) {
1340 expungeStaleEntries();
1341
1342 return memMap.containsValue(value);
1343 }
1344
1345 public int size() {
1346 return diskMapSize.get();
1347 }
1348
1349 protected String getDatabaseName() {
1350 String name = "DbName-Lookup-Failed";
1351 try {
1352 if (this.db != null) {
1353 name = this.db.getDatabaseName();
1354 }
1355 } catch (DatabaseException e) {
1356
1357 }
1358 return name;
1359 }
1360
1361 /***
1362 * Sync in-memory map entries to backing disk store.
1363 * When done, the memory map will be cleared and all entries stored
1364 * on disk.
1365 */
1366 @SuppressWarnings("unchecked")
1367 public synchronized void sync() {
1368 String dbName = null;
1369
1370 useStatsSyncUsed.incrementAndGet();
1371 long startTime = 0;
1372 if (logger.isLoggable(Level.INFO)) {
1373 dbName = getDatabaseName();
1374 startTime = System.currentTimeMillis();
1375 logger.info(dbName + " start sizes: disk " + this.diskMapSize.get() +
1376 ", mem " + this.memMap.size());
1377 }
1378 expungeStaleEntries();
1379 LinkedList<SoftEntry<V>> stale = new LinkedList<SoftEntry<V>>();
1380 for (Iterator<K> i = this.memMap.keySet().iterator(); i.hasNext();) {
1381 K key = i.next();
1382 SoftEntry<V> entry = memMap.get(key);
1383 if (entry != null) {
1384
1385 V value = entry.get();
1386 if (value != null) {
1387 expungeStatsDiskPut.incrementAndGet();
1388 this.diskMap.put(key, value);
1389 } else {
1390 stale.add(entry);
1391 }
1392 }
1393 }
1394
1395 for (SoftEntry<V> entry : stale) {
1396 expungeStaleEntry(entry, false);
1397 }
1398
1399
1400 try {
1401 this.db.sync();
1402 } catch (DatabaseException e) {
1403
1404 throw new RuntimeException(e);
1405 }
1406
1407 if (logger.isLoggable(Level.INFO)) {
1408 logger.info(dbName + " sync took " +
1409 (System.currentTimeMillis() - startTime) + "ms. " +
1410 "Finish sizes: disk " +
1411 this.diskMapSize.get() + ", mem " + this.memMap.size());
1412 dumpExtraStats();
1413 }
1414 }
1415
1416 /*** log at INFO level, interesting stats if non-zero. */
1417 private void dumpExtraStats() {
1418 if (logger.isLoggable(Level.INFO)) {
1419
1420 if (logger.isLoggable(Level.FINE) ||
1421 (expungeStatsNullPhantom.get()
1422 + expungeStatsTransientCond.get()
1423 + expungeStatsTransientCond2.get()
1424 + expungeStatsTransientCond3.get()
1425 + expungeStatsAwaitExpunge.get()
1426 + expungeStatsNullValue.get()
1427 + expungeStatsNotInMap.get()
1428 + expungeStatsAwaitTimeout.get()
1429 + expungeStatsSwapIn.get()
1430 + expungeStatsSwapInRetry.get()
1431 + expungeStatsSwapInRetry2.get()
1432 + expungeStatsGetMemCheck.get()
1433 + expungeStatsViaPoll.get()
1434 + useStatsRemove1Used.get()
1435 + useStatsRemove2Used.get()
1436 + useStatsReplaceUsed.get()) > 0) {
1437 logger.info(composeCacheSummary() + " "
1438 + expungeStatsNullPhantom + "=NullPhantom "
1439 + expungeStatsTransientCond + "=TransientCond "
1440 + expungeStatsTransientCond2 + "=TransientCond2 "
1441 + expungeStatsTransientCond3 + "=TransientCond3 "
1442 + expungeStatsAwaitExpunge + "=AwaitExpunge "
1443 + expungeStatsNullValue + "=NullValue "
1444 + expungeStatsNotInMap+ "=NotInMap "
1445 + expungeStatsAwaitTimeout+ "=AwaitTimeout "
1446 + expungeStatsSwapIn+ "=SwapIn "
1447 + diskHit+ "=diskHit "
1448 + cacheHit+ "=cacheHit "
1449 + countOfGets+ "=countOfGets "
1450 + expungeStatsSwapInRetry+ "=SwapInRetry "
1451 + expungeStatsSwapInRetry2+ "=SwapInRetry2 "
1452 + expungeStatsDiskPut + "=DiskPut "
1453 + expungeStatsGetMemCheck + "=GetMemCheck "
1454 + expungeStatsViaPoll + "=ViaPoll "
1455 + useStatsRemove1Used + "=Remove1Used "
1456 + useStatsRemove2Used + "=Remove2Used "
1457 + useStatsReplaceUsed + "=ReplaceUsed "
1458 + useStatsPutUsed + "=PutUsed "
1459 + useStatsPutIfUsed+ "=PutIfUsed "
1460 + useStatsSyncUsed+ "=SyncUsed "
1461 + diskMapSize.get()+ "=diskMapSize "
1462 + memMap.size()+ "=memMap.size "
1463 );
1464 }
1465 }
1466 }
1467
1468 /*** An incremental, poll-based expunger.
1469 * See #Expunger for dedicated thread expunger.
1470 */
1471 @SuppressWarnings("unchecked")
1472 private void expungeStaleEntries() {
1473 int c = 0;
1474 long startTime = System.currentTimeMillis();
1475 for(SoftEntry<V> entry; (entry = (SoftEntry<V>)refQueuePoll()) != null;) {
1476 expungeStaleEntry(entry, false);
1477 expungeStatsViaPoll.incrementAndGet();
1478 c++;
1479 }
1480 if (c > 0 && logger.isLoggable(Level.FINER)) {
1481 long endTime = System.currentTimeMillis();
1482 try {
1483 logger.finer("DB: " + db.getDatabaseName() + ", Expunged: "
1484 + c + ", Diskmap size: " + diskMapSize.get()
1485 + ", Cache size: " + memMap.size()
1486 + ", in "+(endTime-startTime)+"ms");
1487 } catch (DatabaseException e) {
1488 logger.log(Level.FINER,"exception while logging",e);
1489 }
1490 }
1491 }
1492
1493 /*** Expunge an entry from memMap while updating diskMap.
1494 * Concurrent implementation.
1495 *
1496 * This thread is the only thread performing an expunge of the given
1497 * entry (if it or the calling thread sees a true SoftEntry.startExpunge()),
1498 * so we can assume exclusive access to the given entry.
1499 *
1500 * 3. Swap out/flush: diskMap update, then memMap remove:
1501 * diskMap.put(k,v2); // exclusive only if performed when processing ref queue
1502 * memMap.remove(k,v2); if memMap race lost, then no harm done;
1503 * 3.1. Only expunge does an update of an existing diskMap entry (except
1504 * for sync()).
1505 * Possible disk io as data is written to BDB map.
1506 * <pre>
1507 * Preconditions: (diskMap.containsKey(K), memMap.containsKey(SoftEntry)) are:
1508 * (F,F) remove() or replace was used.
1509 * (F,T) transient while remove() or replace in progress.
1510 * (T,F) normal condition, memMap value is most recent
1511 * (T,T) normal condition, memMap value is cleared SoftReference and
1512 * the attached PhantomEntry doctoredGet() has most recent value.
1513 *
1514 * PostConditions:
1515 * (T,F) for K key, memMap will not have the given mapping and diskMap
1516 * will have a mapping to the value held by the phantom value
1517 * held by given entry, if entry was not already expunged.
1518 * </pre>
1519 * The PostCondition only applies if exclusive expunge access is obtained.
1520 * If another thread is already doing the expunge of given entry, this
1521 * will not wait for that to finish.
1522 *
1523 * @param entry a SoftEntry<V> obtained from refQueuePoll().
1524 * @param alreadyExclusive if true, then assume this thread already has
1525 * SoftEntry.startExpunge()==true.
1526 */
1527 @SuppressWarnings("unchecked")
1528 private void expungeStaleEntry(SoftEntry<V> entry, final boolean alreadyExclusive) {
1529
1530
1531
1532
1533
1534
1535
1536 V phantomValue = null;
1537
1538 PhantomEntry<V> phantom = (PhantomEntry<V>)entry.getPhantom();
1539 if (phantom == null) {
1540 expungeStatsNullPhantom.incrementAndGet();
1541 entry.clearPhantom();
1542 return;
1543 } else {
1544
1545 phantomValue = (V)phantom.doctoredGet();
1546 }
1547 if (alreadyExclusive || entry.startExpunge()) {
1548 try {
1549
1550
1551
1552 if (memMap.get(phantom.getKey())
1553 == entry) {
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564 if (phantomValue != null) {
1565 diskMap.put(phantom.getKey(), phantomValue);
1566 expungeStatsDiskPut.incrementAndGet();
1567 } else {
1568 expungeStatsNullValue.incrementAndGet();
1569 }
1570
1571
1572
1573
1574 memMapRemoveOrWarn(phantom.getKey(), entry);
1575 } else {
1576 this.expungeStatsNotInMap.incrementAndGet();
1577 }
1578 } finally {
1579
1580 entry.clearPhantom();
1581 }
1582 }
1583 }
1584
1585 private static class PhantomEntry<T> extends PhantomReference<T> {
1586 private final Object key;
1587
1588 public PhantomEntry(Object key, T referent) {
1589 super(referent, null);
1590 this.key = key;
1591 }
1592
1593 /***
1594 * @return Return the referent. The contract for {@link #get()}
1595 * always returns a null referent. We've cheated and doctored
1596 * PhantomReference to return the actual referent value. See notes
1597 * at {@link #referentField};
1598 */
1599 final public Object doctoredGet() {
1600 try {
1601
1602
1603
1604 return referentField.get(this);
1605 } catch (IllegalAccessException e) {
1606 throw new RuntimeException(e);
1607 }
1608 }
1609
1610 /***
1611 * @return Returns the key.
1612 */
1613 final public Object getKey() {
1614 return this.key;
1615 }
1616
1617 /***
1618 * @param obj
1619 * @return true if the referents are equals().
1620 */
1621 @Override
1622 @SuppressWarnings("unchecked")
1623 final public boolean equals(Object obj) {
1624 if (obj == null) {
1625 return false;
1626 }
1627 if (getClass() != obj.getClass()) {
1628 return false;
1629 }
1630 final PhantomEntry<T> other = (PhantomEntry<T>) obj;
1631 if (this.doctoredGet() != other.doctoredGet()
1632 && (this.doctoredGet() == null
1633 || !this.doctoredGet().equals(other.doctoredGet()))) {
1634 return false;
1635 }
1636 return true;
1637 }
1638
1639 @Override
1640 final public int hashCode() {
1641 return this.key != null ? this.key.hashCode() : 0;
1642 }
1643
1644 }
1645
1646 /***
1647 * SoftReference cache entry.
1648 * "Expunge" is the process of flushing entries in the cache (memMap)
1649 * to diskMap after GC determines they are no longer references.
1650 * A PhantomReference is used to hold the key and value as a last
1651 * chance before GC hook that can effect the update of diskMap.
1652 * <p/>
1653 * The coordinated process to do expunge is:
1654 * <pre>
1655 * SoftEntry se = map.get();
1656 * if (se.startExpunge()) {
1657 * ;// update diskMap here
1658 * map.remove(); // remove SoftEntry after diskMap update
1659 * se.clearPhantom(); // clear after remove mapping
1660 * } else { // another thread does expunge
1661 * }
1662 * </pre>
1663 * The coordinated process to wait for another thread to do expunge:
1664 * <pre>
1665 * SoftEntry se = map.get();
1666 * if (se.awaitExpunge()) {
1667 * ;// wait occurred (if you care)
1668 * }
1669 * ;// postcondition: se was removed from map
1670 * </pre>
1671 * Entries are not recycled.
1672 */
1673 private static class SoftEntry<T> extends SoftReference<T> {
1674 private PhantomEntry phantom;
1675
1676 /*** count down to expunged; if null, then expunge has not
1677 * been started. See {@link #startExpunge()} and
1678 * {@link #expungeStarted()}.
1679 * Effectively final (never goes back to null).
1680 */
1681 private CountDownLatch expunged = null;
1682
1683 @SuppressWarnings("unchecked")
1684 public SoftEntry(Object key, T referent, ReferenceQueue<T> q) {
1685 super(referent, q);
1686 this.phantom = new PhantomEntry(key, referent);
1687 }
1688
1689 /***
1690 * Synchronized so this can be atomic and have
1691 * proper happens-before (inter-thread data sync)
1692 * w.r.t. other methods of this instance regardless of
1693 * what thread is the caller.
1694 * @return referent.
1695 */
1696 public T get() {
1697
1698 synchronized (this) {
1699 return super.get();
1700 }
1701 }
1702
1703 /***
1704 * Get the phantom.
1705 *
1706 * Synchronized so this can be atomic and have
1707 * proper happens-before (inter-thread data sync)
1708 * w.r.t. other methods of this instance regardless of
1709 * what thread is the caller.
1710 * @return Returns the phantom reference.
1711 */
1712 final public synchronized PhantomEntry getPhantom() {
1713 return this.phantom;
1714 }
1715
1716 /***
1717 * @return true if this entry is at end of lifecycle.
1718 */
1719 final public boolean isCleared() {
1720 return getPhantom() == null;
1721 }
1722
1723 /*** clear referent and end-of-life-cycle this instance.
1724 * Do not clearPhantom() until after diskMap updated and after
1725 * this entry is removed from memMap (because threads waiting
1726 * for expunge to finish will likely do a memMap.get()
1727 * or putIfAbsent() and
1728 * there should be no residue so a fresh entry can be created
1729 * when the value is swapped in from diskMap.
1730 * <p/>Causes any threads that called or will later
1731 * call awaitExpunge() on this
1732 * instance to continue
1733 *
1734 * <p/>Idempotent.
1735 *
1736 * After this method returns,
1737 * {@link #clearPhantom()} will return true.
1738 *
1739 * Synchronized so this can be atomic and have
1740 * proper happens-before (inter-thread data sync)
1741 * w.r.t. other methods of this instance regardless of
1742 * what thread is the caller.
1743 */
1744 final public synchronized void clearPhantom() {
1745 if (this.phantom != null) {
1746 this.phantom.clear();
1747 this.phantom = null;
1748 }
1749 super.clear();
1750 if (expungeStarted()) {
1751 expunged.countDown();
1752 }
1753 }
1754
1755 /***
1756 * Was expunge ever started on this instance?
1757 * (If instance was temporary and thrown away,
1758 * we pretend it was expunged.)
1759 *
1760 * Synchronized so this can be atomic and have
1761 * proper happens-before (inter-thread data sync)
1762 * w.r.t. other methods of this instance regardless of
1763 * what thread is the caller.
1764 * No side-effects.
1765 * @return true if expunge was ever started on this entry,
1766 * expunge may or may not be finished and #isCleared()
1767 * may or may not be true,
1768 * see {@link #awaitExpunge()}
1769 * @see {@link CachedBdbMap#isExpunged(SoftEntry)}
1770 */
1771 final public synchronized boolean expungeStarted() {
1772 if (expunged != null) {
1773 return true;
1774 } else {
1775 return false;
1776 }
1777 }
1778
1779 /*** Can the calling thread perform the expunge on this?
1780 * Side Effect: if true is returned, sets up latch for any
1781 * threads that want to await for expunge.
1782 *
1783 * Synchronized so this can be atomic and have
1784 * proper happens-before (inter-thread data sync)
1785 * w.r.t. other methods of this instance regardless of
1786 * what thread is the caller.
1787 * @return true if calling thread has exclusive access to
1788 * expunge this instance; do not call this unless
1789 * going to do the expunge; true is only returned
1790 * once per instance.
1791 */
1792 final public synchronized boolean startExpunge() {
1793 if (isCleared() || expungeStarted()) {
1794 return false;
1795 } else {
1796 expunged = new CountDownLatch(1);
1797 return true;
1798 }
1799 }
1800
1801 /*** If this entry is in the process of being expunged
1802 * (when {@link #expungeStarted()} is true), then this will wait
1803 * until the expunge is finished. If expunge was not started
1804 * or was finished, this method returns immediately.
1805 *
1806 * To ensure liveliness, a multi sec timeout is used that
1807 * if triggered, throws InterruptedException.
1808 *
1809 * Effectively synchronized so this can be atomic and have
1810 * proper happens-before (inter-thread data sync)
1811 * w.r.t. other methods of this instance regardless of
1812 * what thread is the caller.
1813 *
1814 * @throws java.lang.InterruptedException if timeout occurred.
1815 * @return true if expunge is completed (or timeout) or this
1816 * entry was never used, false if expunge is not happening.
1817 */
1818 final public boolean awaitExpunge() throws InterruptedException {
1819 if (isCleared()) {
1820 return true;
1821 }
1822 if (expungeStarted()) {
1823
1824 long plentyOfWriteLatencySec = 6L;
1825 if (expunged.await(plentyOfWriteLatencySec, TimeUnit.SECONDS)) {
1826 return true;
1827 } else {
1828 expungeStatsAwaitTimeout.incrementAndGet();
1829 logger.log(Level.WARNING, "timeout at "
1830 + plentyOfWriteLatencySec + " sec", new Exception());
1831 throw new InterruptedException("timeout");
1832 }
1833 } else {
1834 return false;
1835 }
1836 }
1837
1838 /***
1839 *
1840 * @param obj
1841 * @return true if the references are equal, and if references differ,
1842 * if phantoms are == or equals(), return true.
1843 */
1844 @Override
1845 @SuppressWarnings("unchecked")
1846 public boolean equals(Object obj) {
1847 if (obj == null) {
1848 return false;
1849 }
1850 if (getClass() != obj.getClass()) {
1851 return false;
1852 }
1853 if (this == obj) {
1854 return true;
1855 }
1856 final SoftEntry<T> other = (SoftEntry<T>) obj;
1857 if (this.phantom != other.phantom
1858 && (this.phantom == null
1859 || !this.phantom.equals(other.phantom))) {
1860 return false;
1861 }
1862 return true;
1863 }
1864
1865 @Override
1866 public int hashCode() {
1867 return this.phantom != null ? this.phantom.hashCode() : 0;
1868 }
1869
1870 public String toString() {
1871 if (phantom != null) {
1872 return "SoftEntry(key=" + phantom.getKey().toString() + ")";
1873 } else {
1874 return "SoftEntry()";
1875 }
1876 }
1877 }
1878
1879
1880
1881
1882 @SuppressWarnings("unchecked")
1883 private K toKey(Object o) {
1884 return (K)o;
1885 }
1886
1887 @SuppressWarnings("unchecked")
1888 private V diskMapGet(K k) {
1889 return (V)diskMap.get(k);
1890 }
1891
1892 @SuppressWarnings("unchecked")
1893 private SoftEntry<?> refQueuePoll() {
1894 return (SoftEntry<?>)refQueue.poll();
1895 }
1896
1897
1898
1899
1900
1901
1902
1903 protected transient SoftReference<LowMemoryCanary> canary;
1904 protected class LowMemoryCanary {
1905 /*** When collected/finalized -- as should be expected in
1906 * low-memory conditions -- trigger an expunge and a
1907 * new 'canary' insertion. */
1908 public void finalize() {
1909 CachedBdbMap.this.expungeStaleEntries();
1910
1911 if(CachedBdbMap.this.db !=null) {
1912 CachedBdbMap.this.canary =
1913 new SoftReference<LowMemoryCanary>(new LowMemoryCanary());
1914 } else {
1915 CachedBdbMap.this.canary = null;
1916 }
1917 }
1918 }
1919 }