View Javadoc

1   /* CachedBdbMap
2    * 
3    * $Id: CachedBdbMap.java 6816 2010-04-12 21:49:16Z gojomo $
4    * 
5    * Created on Mar 24, 2004
6    *
7    * Copyright (C) 2009 Internet Archive.
8    *
9    * This file is part of the Heritrix web crawler (crawler.archive.org).
10   *
11   * Heritrix is free software; you can redistribute it and/or modify
12   * it under the terms of the GNU Lesser Public License as published by
13   * the Free Software Foundation; either version 2.1 of the License, or
14   * any later version.
15   *
16   * Heritrix is distributed in the hope that it will be useful,
17   * but WITHOUT ANY WARRANTY; without even the implied warranty of
18   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19   * GNU Lesser Public License for more details.
20   *
21   * You should have received a copy of the GNU Lesser Public License
22   * along with Heritrix; if not, write to the Free Software
23   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
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     // The following atomic counters are used for reporting 
214     //  code branches that can occur.
215     // Some of these are frequent while others  are rare or depend on
216     //  application level and GC behavior.
217     // If problems occur, these provide diagnostics such as whether usage went
218     //  outside the bounds of the above described expected "two styles".
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         // We need access to the referent field in the PhantomReference.
316         // For more on this trick, see
317         //
318         // http://www.javaspecialists.co.za/archive/Issue098.html and for
319         // discussion:
320         // http://www.theserverside.com/tss?service=direct/0/NewsThread/threadViewer.markNoisy.link&sp=l29865&sp=l146901
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             // Write out the bdb configuration.
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         // ToDo: initial capacity should be related to number of seeds, max depth, max docs
436         this.memMap = new ConcurrentHashMap<K,SoftEntry<V>>(
437                                                             8192, // initial capacity
438                                                             0.9f, // acceptable load factor
439                                                             64 // est. number of concurrent threads
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         // BDB JE is btree only, so Sorted Map is no additional work
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()) { // try do the expunge now, if needed
515                 // got exclusive expunge access
516                 expungeStaleEntry(entry, true);
517                 return true;
518             } else if (entry.expungeStarted()) {  // might need to wait for expunge
519                 expungeStatsAwaitExpunge.incrementAndGet();
520                 expungeStaleEntries();
521                 // preserve interrupted status
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) { // timeout (stats already incr.)
530                     return true; // be lively 
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 { // entry not collected, not expunged
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             // try fixup
560             // FIXME: this is just papering over/recovering from a state
561             // which should not be reached; remove this code when true bug
562             // discovered
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             // at this point: memMap no longer contains the given entry
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 //        // We're doing the caching ourselves so setting these at the lowest
604 //        // possible level.
605 //        envConfig.setCachePercent(1);
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             //throw new FatalConfigurationException(e.getMessage());
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         // Close out my bdb db.
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         // Would require complicated implementation to 
681         // maintain identity guarantees, so skipping
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         // check mem cache
711         V val = _getMem(key);
712         if (val != null) { // cache hit
713             return val;
714         }
715         
716         // check backing diskMap
717         V valDisk = _getDisk(key); // disk io can occur here
718         return valDisk;
719     }
720     
721     private V _getDisk(K key) {
722         V v = diskMapGet(key); // disk io can occur here
723         if (v != null) {
724             diskHit.incrementAndGet();
725             //
726             //     cache in memMap (swap in)
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); // unchecked cast
733                 if (existingEntry != null) {
734                     V prevValue = (V) existingEntry.get();
735                     if (prevValue != null) {
736                         // another thread beat this thread to memMap insert for swap-in
737                         newEntry.clearPhantom(); // instance not needed
738                         return prevValue;
739                     } else { // SoftEntry referent has been collected
740                         // another thread beat this thread in memMap insert for swap-in
741                         //   AND existingEntry was cleared before we could see it.
742                         // This condition requires 2 competing threads working on
743                         //   the same entry, and a full GC to be interleaved.
744                         // This is like a simultaneous collision between 
745                         //   2 cars and an airplane.  
746                         // Wait for expunge to finish
747                         
748                         if (isExpunged(existingEntry, key)) {
749                             // existingEntry is no longer in memMap
750                             expungeStatsSwapInRetry.incrementAndGet();
751                             continue;  // re-try insert into memMap
752                         } else {
753                             expungeStatsSwapInRetry2.incrementAndGet();
754                             try {
755                                 // stuck memMap entry
756                                 Thread.sleep(0L, 10000);
757                             } catch (InterruptedException ignored) {
758                             }
759                             logger.log(Level.WARNING,"Swap-In Retry", new Exception());
760                             continue;  // re-try insert into memMap
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(); // get & hold, so not cleared pre-return
775                 if (val != null) {
776                     cacheHit.incrementAndGet();
777                     return val;
778                 }
779                 // null SoftEntry.get() means entry is being GC-ed
780                 //  need to wait for expunge to finish and then retry
781                 //  which will lead to a diskMap.get()
782                 if (isExpunged(entry, key)) {
783                     // entry has been removed, lookup again
784                     continue; // tail recursion optimization
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);// unchecked cast, disk io
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                         // emit once on first retry
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             // prevent previous SoftEntry from being expunged
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             // warn on non-accretive use
935             logger.log(Level.WARNING,"design violating replace(,,) used on dbName=" + dbName,new Exception());
936         }
937         
938         expungeStaleEntries();
939         
940         //  make the ref wrappers
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) { // case (*,T)
947                 newEntry = null; // skip neutralize in finally
948                 return true;
949             } else { // case (?,F), does diskMap have a mapping?
950                 boolean diskReplaced = diskMap.replace(key, oldValue, newValue);
951                 if (diskReplaced) { // case (T,F)
952                     // swap-in 
953                     memMap.put(key, newEntry);
954                     newEntry = null; // skip neutralize in finally
955                     return true;
956                 } else { // case (F,F)
957                     return false;  // nothing to replace
958                 }
959             }
960         } finally {
961             // neutralize any unused ref wrappers
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     //@Override
1003     public V replace(K key, V value) {
1004         int ru = useStatsReplaceUsed.incrementAndGet();
1005         if (ru == 1 && LOG_ERROR_ON_DESIGN_VIOLATING_METHODS) {
1006             // warn on non-accretive use
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); // unchecked cast
1012         if (prevEntry != null) { // case (*,T) AND matched
1013             prevMemValue = (V)prevEntry.get();
1014             // at this point an old softEntry and phantom is ready to GC but 
1015             //      it has an old value that expunge() will write to disk;
1016             //   hence the warning in comments above.
1017             return prevMemValue;
1018         } else { // case (?,F), no match in memMap, but does diskMap match?
1019             V prevDiskValue = (V)diskMap.replace(key, value); // unchecked cast
1020             if (prevDiskValue == null) { // case (F,F)
1021                 // key not mapped previously
1022                 newEntry.clearPhantom(); // instance not used
1023                 return null; // nothing to replace
1024             } else { // case (T,F)
1025                 // swap in 
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     //@Override
1071     public V putIfAbsent(K key, V value) {
1072         expungeStaleEntries(); // possible disk io
1073         V existingDiskValue = null;
1074         int attemptTolerance = BDB_LOCK_ATTEMPT_TOLERANCE;
1075         while (--attemptTolerance > 0) {
1076             try {
1077                 existingDiskValue = (V)diskMap.putIfAbsent(key,value);// unchecked cast, disk io
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                         // emit once on first retry
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) { // insert to diskMap succeeded
1097             // case (F,*)
1098             diskMapSize.incrementAndGet();
1099             useStatsPutIfUsed.incrementAndGet();
1100             // next, put mapping into cache, if not already there
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                 // case (F,T) transient remove(): await other thread to finish, 
1106                 //      then proceed as (F,F).
1107                 // diskMap insert succeeded but insert to memMap was skipped.  
1108                 //  This branch occurs when a simultaneous 
1109                 //  remove() is occurring.
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                         // a concurrent expunge or remove() "happens before" this put
1116                         return null; // put succeeded
1117                     } else {
1118                         prevMemValue = prevEntry.get();
1119                     }
1120                 }
1121                 newEntry.clearPhantom(); // unused instance
1122                 expungeStatsTransientCond2.getAndIncrement();
1123                 // other thread won, return value it set
1124                 return prevMemValue;
1125             } else {
1126                 // case (F,F) 
1127                 // both inserts to diskMap and then memMap succeeded
1128                 return null; // put succeeded
1129             }
1130         } else { // case (T,*)  already exists in diskMap, so not absent.
1131             //  No new entry inserted, but current value must be returned.
1132             V retValue = get(key); // swap in, if necessary
1133             if (retValue == null) {
1134                 // this can only  happen if there is a race with this.remove()
1135                 // that just removed the key we inserted into diskMap.
1136                 //   It is always possible that another thread could remove
1137                 //  an entry before the calling client sees it, but this is 
1138                 //  unlikely by design.  
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                 // GC or expunge already in progress.
1157                 isExpunged(prevEntry, key);
1158                 memMapRemoveOrWarn(key, prevEntry); // possibly redundant
1159                 break;
1160             } else {
1161                 // reserve expunge, then clear it
1162                 if (prevEntry.startExpunge()) { // get exclusive access
1163                     memMapRemoveOrWarn(key, prevEntry);
1164                     prevEntry.clearPhantom(); // prevent expunge on old entry
1165                     break;
1166                 } else {
1167                     // another thread started expunge
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         // lookup memMap so we can use remove(K,V) later
1209         SoftEntry<V> prevEntry = (SoftEntry<V>) memMap.get(key); // unchecked
1210 
1211         // map mutating operations always change diskMap first
1212         if ((prevDiskValue = (V) diskMap.remove(key)) != null) { // unchecked
1213             diskMapSize.decrementAndGet();
1214             int ru = useStatsRemove1Used.incrementAndGet();
1215             if (ru == 1) {
1216                 // warn on non-accretive use
1217                 logger.log(Level.WARNING,"remove() used for dbName=" + dbName, new Exception());
1218             }
1219         }
1220         // regardless of whether prevDiskValue is null, memMap remove
1221         //   must occur to ensure consistency.
1222 
1223         if (prevEntry == null) {
1224             // nothing to remove from memMap
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     //@Override
1262     @SuppressWarnings("unchecked")
1263     public boolean remove(Object key, Object value) { // ConcurrentMap version
1264         SoftEntry<V> entry = null;
1265         V memValue = null;
1266         
1267         // first get cache entry since we want to inhibit expunge
1268         //   if the remove succeeds on memMap
1269         entry = (SoftEntry<V>) memMap.get(key); // unchecked cast
1270         if (entry != null) {
1271             // entry found for key
1272             memValue = entry.get();
1273             if (memValue != null && memValue.equals(value)) {
1274                 // case (*,T) AND swapped in
1275                 // swapped-in value matches
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                 // map mutating operations always change diskMap first
1284                 Object obj = diskMap.remove(key);
1285                 if (obj != null) {
1286                     diskMapSize.decrementAndGet();
1287                 }
1288                 neutralizeMemMapEntry(entry, (K) key); // also removes entry from memMap
1289                 return true;
1290             } else { // case (*,F) AND swapped in
1291                 // if swapped in and value does not match, then
1292                 //     diskMap is *not* checked since it's value is
1293                 //     not necessarily up to date (when swapped in)
1294                 return false;
1295             }
1296         }
1297         // already covered cases: swapped in
1298         //  cases: (*,F)  AND swapped out
1299         boolean diskMapFound = diskMap.remove(key, value);
1300         if (diskMapFound) { // case (T,F) AND swapped out
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; // case (F,F) AND swapped out
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(); // note: expunge is not quick
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(); // note: expunge is not quick
1341         // FIXME this isn't really right, as memMap is of SoftEntries
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             // Ignore.
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         // Sync. memory and disk.
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                 // Get & hold so not cleared pre-return.
1385                 V value = entry.get();
1386                 if (value != null) {
1387                     expungeStatsDiskPut.incrementAndGet();
1388                     this.diskMap.put(key, value); // unchecked cast
1389                 } else {
1390                     stale.add(entry);
1391                 }
1392             }
1393         }
1394         // for any entries above that had been cleared, ensure expunged
1395         for (SoftEntry<V> entry : stale) {
1396             expungeStaleEntry(entry, false);
1397         }   
1398         
1399         // force sync of deferred-writes
1400         try {
1401             this.db.sync();
1402         } catch (DatabaseException e) {
1403             // TODO Auto-generated catch block
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             // emit if FINE or has interesting values
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) { // concurrent version
1529         // If phantom already null, its already expunged -- probably
1530         // because it was purged directly first from inside in
1531         // {@link #get(String)} and then it went on the poll queue and
1532         // when it came off inside in expungeStaleEntries, this method
1533         // was called again.
1534         //  OR, could be http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6837858
1535         //
1536         V phantomValue = null;
1537         // keep this ref on stack so it cannot be nulled in mid-operation
1538         PhantomEntry<V> phantom = (PhantomEntry<V>)entry.getPhantom(); // unchecked cast
1539         if (phantom == null) {
1540             expungeStatsNullPhantom.incrementAndGet(); 
1541             entry.clearPhantom(); // free up waiting threads, if any
1542             return; // nothing to do
1543         } else {
1544             // recover hidden value
1545             phantomValue = (V)phantom.doctoredGet(); // unchecked cast
1546         }
1547         if (alreadyExclusive || entry.startExpunge()) { // exclusive expunge
1548             try {
1549                 // Still in memMap? 
1550                 //  If the object that is in memMap is not the one passed here, then
1551                 // memMap has been changed -- probably by a put on top of this entry.]
1552                 if (memMap.get(phantom.getKey()) 
1553                         == entry) { // NOTE: intentional identity compare
1554                     // given instance entry still in memMap;
1555                     //   as long as CachedBDBMap.remove(), CachedBDBMap.put(), 
1556                     //   and CachedBDBMap.replace() are not used
1557                     //   by application, then only this thread is doing the expunge.
1558                     
1559                     // we have the key and if we have a phantom Value, then
1560                     //   the diskMap can be updated.
1561                     // diskMap MUST be updated *before* entry.clearPhantom().
1562                     //  Because this thread is the only one expunging the given entry.getKey() 
1563                     //    we can use diskMap.put().
1564                     if (phantomValue != null) {
1565                         diskMap.put(phantom.getKey(), phantomValue); // unchecked cast
1566                         expungeStatsDiskPut.incrementAndGet();
1567                     } else {
1568                         expungeStatsNullValue.incrementAndGet();
1569                     }
1570                     //  remove entry that matches key and value; 
1571                     //    since only this thread has the exclusive expunge job
1572                     //    for entry, we will either remove the exact object
1573                     //    or remove no object (non-interference).
1574                     memMapRemoveOrWarn(phantom.getKey(), entry);
1575                 } else { // not in map (a soft assert failure)
1576                     this.expungeStatsNotInMap.incrementAndGet();
1577                 }
1578             } finally { // an exclusive expunge must clear entry
1579                 //  but only after diskMap and memMap are updated
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                 // Here we use the referentField saved off on static
1602                 // initialization of this class to get at this References'
1603                 // private referent field.
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; // unchecked cast
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); // unchecked cast
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             // ensure visibility
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; // expunge completed, or entry was unused
1821             }
1822             if (expungeStarted()) {
1823                 // much longer than time for diskMap.putIfAbsent(key,value)
1824                 long plentyOfWriteLatencySec = 6L;
1825                 if (expunged.await(plentyOfWriteLatencySec, TimeUnit.SECONDS)) {
1826                     return true; // finished
1827                 } else { // timeout, just in case
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; // not started
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) { // same reference
1854                 return true; 
1855             }
1856             final SoftEntry<T> other = (SoftEntry<T>) obj; // unchecked cast
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     // Crude, probably unreliable/fragile but harmless mechanism to 
1899     // trigger expunge of cleared SoftReferences in low-memory 
1900     // conditions even without any of the other get/put triggers. 
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             // only install new canary if map still 'open' with db reference
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 }