View Javadoc

1   /**
2    *  Copyright 2003-2007 Greg Luck
3    *
4    *  Licensed under the Apache License, Version 2.0 (the "License");
5    *  you may not use this file except in compliance with the License.
6    *  You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *  Unless required by applicable law or agreed to in writing, software
11   *  distributed under the License is distributed on an "AS IS" BASIS,
12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *  See the License for the specific language governing permissions and
14   *  limitations under the License.
15   */
16  
17  
18  package net.sf.ehcache.store;
19  
20  import net.sf.ehcache.CacheException;
21  import net.sf.ehcache.Ehcache;
22  import net.sf.ehcache.Element;
23  import net.sf.ehcache.Status;
24  import net.sf.ehcache.event.RegisteredEventListeners;
25  import org.apache.commons.logging.Log;
26  import org.apache.commons.logging.LogFactory;
27  
28  import java.io.ByteArrayInputStream;
29  import java.io.ByteArrayOutputStream;
30  import java.io.File;
31  import java.io.FileInputStream;
32  import java.io.FileOutputStream;
33  import java.io.IOException;
34  import java.io.ObjectInputStream;
35  import java.io.ObjectOutputStream;
36  import java.io.ObjectStreamClass;
37  import java.io.RandomAccessFile;
38  import java.io.Serializable;
39  import java.io.StreamCorruptedException;
40  import java.util.ArrayList;
41  import java.util.Collections;
42  import java.util.HashMap;
43  import java.util.HashSet;
44  import java.util.Iterator;
45  import java.util.List;
46  import java.util.Map;
47  import java.util.Set;
48  
49  /**
50   * A disk store implementation.
51   * <p/>
52   * As of ehcache-1.2 (v1.41 of this file) DiskStore has been changed to a mix of finer grained locking using synchronized collections
53   * and synchronizing on the whole instance, as was the case with earlier versions.
54   * <p/>
55   * The DiskStore, as of ehcache-1.2.4, supports eviction using an LFU policy, if a maximum disk
56   * store size is set. LFU uses statistics held at the Element level which survive moving between
57   * maps in the MemoryStore and DiskStores.
58   *
59   * todo doco for buffer
60   * @author Adam Murdoch
61   * @author Greg Luck
62   * @author patches contributed: Ben Houston
63   * @version $Id: DiskStore.java 512 2007-07-10 09:18:45Z gregluck $
64   */
65  public class DiskStore implements Store {
66  
67      /**
68       * If the CacheManager needs to resolve a conflict with the disk path, it will create a
69       * subdirectory in the given disk path with this prefix followed by a number. The presence of this
70       * name is used to determined whether it makes sense for a persistent DiskStore to be loaded. Loading
71       * persistent DiskStores will only have useful semantics where the diskStore path has not changed.
72       */
73      public static final String AUTO_DISK_PATH_DIRECTORY_PREFIX = "ehcache_auto_created";
74  
75      private static final Log LOG = LogFactory.getLog(DiskStore.class.getName());
76      private static final int MS_PER_SECOND = 1000;
77      private static final int SPOOL_THREAD_INTERVAL = 200;
78      private static final int ESTIMATED_MINIMUM_PAYLOAD_SIZE = 512;
79      private static final int ONE_MEGABYTE = 1048576;
80  
81      private long expiryThreadInterval;
82  
83      private final String name;
84      private boolean active;
85      private RandomAccessFile randomAccessFile;
86  
87      private Map diskElements = Collections.synchronizedMap(new HashMap());
88      private List freeSpace = Collections.synchronizedList(new ArrayList());
89      private Map spool = new HashMap();
90      private Object spoolLock = new Object();
91  
92      private Thread spoolAndExpiryThread;
93  
94      private Ehcache cache;
95  
96      /**
97       * If persistent, the disk file will be kept
98       * and reused on next startup. In addition the
99       * memory store will flush all contents to spool,
100      * and spool will flush all to disk.
101      */
102     private final boolean persistent;
103 
104     private final String diskPath;
105 
106     private File dataFile;
107 
108 
109     /**
110      * Used to persist elements
111      */
112     private File indexFile;
113 
114     private Status status;
115 
116     /**
117      * The size in bytes of the disk elements
118      */
119     private long totalSize;
120 
121     /**
122      * The maximum elements to allow in the disk file.
123      */
124     private final long maxElementsOnDisk;
125     /**
126      * Whether the cache is eternal
127      */
128     private boolean eternal;
129     private int lastElementSize;
130     private int diskSpoolBufferSizeBytes;
131 
132 
133     /**
134      * Creates a disk store.
135      *
136      * @param cache    the {@link net.sf.ehcache.Cache} that the store is part of
137      * @param diskPath the directory in which to create data and index files
138      */
139     public DiskStore(Ehcache cache, String diskPath) {
140         status = Status.STATUS_UNINITIALISED;
141         this.cache = cache;
142         name = cache.getName();
143         this.diskPath = diskPath;
144         expiryThreadInterval = cache.getDiskExpiryThreadIntervalSeconds();
145         persistent = cache.isDiskPersistent();
146         maxElementsOnDisk = cache.getMaxElementsOnDisk();
147         eternal = cache.isEternal();
148         diskSpoolBufferSizeBytes = cache.getCacheConfiguration().getDiskSpoolBufferSizeMB() * ONE_MEGABYTE;
149 
150 
151 
152         try {
153             initialiseFiles();
154 
155             active = true;
156 
157             // Always start up the spool thread
158             spoolAndExpiryThread = new SpoolAndExpiryThread();
159             spoolAndExpiryThread.start();
160 
161             status = Status.STATUS_ALIVE;
162         } catch (final Exception e) {
163             // Cleanup on error
164             dispose();
165             LOG.error(name + "Cache: Could not create disk store. Initial cause was " + e.getMessage(), e);
166         }
167     }
168 
169 
170     private void initialiseFiles() throws Exception {
171         // Make sure the cache directory exists
172         final File diskDir = new File(diskPath);
173         if (diskDir.exists() && !diskDir.isDirectory()) {
174             throw new Exception("Store directory \"" + diskDir.getCanonicalPath() + "\" exists and is not a directory.");
175         }
176         if (!diskDir.exists() && !diskDir.mkdirs()) {
177             throw new Exception("Could not create cache directory \"" + diskDir.getCanonicalPath() + "\".");
178         }
179 
180         dataFile = new File(diskDir, getDataFileName());
181         indexFile = new File(diskDir, getIndexFileName());
182 
183         deleteIndexIfNoData();
184 
185         if (persistent) {
186             //if diskpath contains auto generated string
187             if (diskPath.indexOf(AUTO_DISK_PATH_DIRECTORY_PREFIX) != -1) {
188                 LOG.warn("Data in persistent disk stores is ignored for stores from automatically created directories"
189                         + " (they start with " + AUTO_DISK_PATH_DIRECTORY_PREFIX + ").\n"
190                         + "Remove diskPersistent or resolve the conflicting disk paths in cache configuration.\n"
191                         + "Deleting data file " + getDataFileName());
192                 dataFile.delete();
193             } else if (!readIndex()) {
194                 if (LOG.isDebugEnabled()) {
195                     LOG.debug("Index file dirty or empty. Deleting data file " + getDataFileName());
196                 }
197                 dataFile.delete();
198             }
199         } else {
200             if (LOG.isDebugEnabled()) {
201                 LOG.debug("Deleting data file " + getDataFileName());
202             }
203             dataFile.delete();
204             indexFile = null;
205         }
206 
207         // Open the data file as random access. The dataFile is created if necessary.
208         randomAccessFile = new RandomAccessFile(dataFile, "rw");
209     }
210 
211     private void deleteIndexIfNoData() {
212         boolean dataFileExists = dataFile.exists();
213         boolean indexFileExists = indexFile.exists();
214         if (!dataFileExists && indexFileExists) {
215             if (LOG.isDebugEnabled()) {
216                 LOG.debug("Matching data file missing for index file. Deleting index file " + getIndexFileName());
217             }
218             indexFile.delete();
219         }
220     }
221 
222     /**
223      * Asserts that the store is active.
224      */
225     private void checkActive() throws CacheException {
226         if (!active) {
227             throw new CacheException(name + " Cache: The Disk store is not active.");
228         }
229     }
230 
231     /**
232      * Gets an {@link Element} from the Disk Store.
233      *
234      * @return The element
235      */
236     public final synchronized Element get(final Object key) {
237         try {
238             checkActive();
239 
240             // Check in the spool.  Remove if present
241             Element element;
242             synchronized (spoolLock) {
243                 element = (Element) spool.remove(key);
244             }
245             if (element != null) {
246                 element.updateAccessStatistics();
247                 return element;
248             }
249 
250             // Check if the element is on disk
251             final DiskElement diskElement = (DiskElement) diskElements.get(key);
252             if (diskElement == null) {
253                 // Not on disk
254                 return null;
255             }
256 
257             element = loadElementFromDiskElement(diskElement);
258             element.updateAccessStatistics();
259             return element;
260         } catch (Exception exception) {
261             LOG.error(name + "Cache: Could not read disk store element for key " + key + ". Error was "
262                     + exception.getMessage(), exception);
263         }
264         return null;
265     }
266 
267     /**
268      * An unsynchronized and very low cost check to see if a key is in the Store. No check is made to see if the Element is expired.
269      *
270      * @param key The Element key
271      * @return true if found. If this method return false, it means that an Element with the given key is definitely not in the MemoryStore.
272      *         If it returns true, there is an Element there. An attempt to get it may return null if the Element has expired.
273      */
274     public final boolean containsKey(Object key) {
275         return diskElements.containsKey(key) || spool.containsKey(key);
276     }
277 
278     private Element loadElementFromDiskElement(DiskElement diskElement) throws IOException, ClassNotFoundException {
279         Element element;
280         // Load the element
281         randomAccessFile.seek(diskElement.position);
282         final byte[] buffer = new byte[diskElement.payloadSize];
283         randomAccessFile.readFully(buffer);
284         final ByteArrayInputStream instr = new ByteArrayInputStream(buffer);
285 
286         final ObjectInputStream objstr = new ObjectInputStream(instr) {
287             /**
288              * Overridden because of:
289              * Bug 1324221 ehcache DiskStore has issues when used in Tomcat
290              */
291             protected Class resolveClass(ObjectStreamClass clazz) throws ClassNotFoundException, IOException {
292                 try {
293                     ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
294                     return Class.forName(clazz.getName(), false, classLoader);
295                 } catch (ClassNotFoundException e) {
296                     // Use the default as a fallback because of
297                     // bug 1517565 - DiskStore loadElementFromDiskElement
298                     return super.resolveClass(clazz);
299                 }
300             }
301         };
302         element = (Element) objstr.readObject();
303         objstr.close();
304         return element;
305     }
306 
307     /**
308      * Gets an {@link Element} from the Disk Store, without updating statistics
309      *
310      * @return The element
311      */
312     public final synchronized Element getQuiet(final Object key) {
313         try {
314             checkActive();
315 
316             // Check in the spool.  Remove if present
317             Element element;
318             synchronized (spoolLock) {
319                 element = (Element) spool.remove(key);
320             }
321             if (element != null) {
322                 //element.updateAccessStatistics(); Don't update statistics
323                 return element;
324             }
325 
326             // Check if the element is on disk
327             final DiskElement diskElement = (DiskElement) diskElements.get(key);
328             if (diskElement == null) {
329                 // Not on disk
330                 return null;
331             }
332 
333             element = loadElementFromDiskElement(diskElement);
334             //element.updateAccessStatistics(); Don't update statistics
335             return element;
336         } catch (Exception e) {
337             LOG.error(name + "Cache: Could not read disk store element for key " + key
338                     + ". Initial cause was " + e.getMessage(), e);
339         }
340         return null;
341     }
342 
343 
344     /**
345      * Gets an Array of the keys for all elements in the disk store.
346      *
347      * @return An Object[] of {@link Serializable} keys
348      * @noinspection SynchronizeOnNonFinalField
349      */
350     public final synchronized Object[] getKeyArray() {
351         Set elementKeySet;
352         synchronized (diskElements) {
353             elementKeySet = diskElements.keySet();
354         }
355         Set spoolKeySet;
356         synchronized (spoolLock) {
357             spoolKeySet = spool.keySet();
358         }
359         Set allKeysSet = new HashSet(elementKeySet.size() + spoolKeySet.size());
360         allKeysSet.addAll(elementKeySet);
361         allKeysSet.addAll(spoolKeySet);
362         return allKeysSet.toArray();
363     }
364 
365     /**
366      * Returns the current store size.
367      *
368      * @noinspection SynchronizeOnNonFinalField
369      */
370     public final synchronized int getSize() {
371         try {
372             checkActive();
373             int spoolSize;
374             synchronized (spoolLock) {
375                 spoolSize = spool.size();
376             }
377             int diskSize;
378             synchronized (diskElements) {
379                 diskSize = diskElements.size();
380             }
381             return spoolSize + diskSize;
382         } catch (Exception e) {
383             LOG.error(name + "Cache: Could not determine size of disk store.. Initial cause was " + e.getMessage(), e);
384             return 0;
385         }
386     }
387 
388     /**
389      * Returns the store status.
390      */
391     public final Status getStatus() {
392         return status;
393     }
394 
395     /**
396      * Puts an element into the disk store.
397      * <p/>
398      * This method is not synchronized. It is however threadsafe. It uses fine-grained
399      * synchronization on the spool.
400      */
401     public final void put(final Element element) {
402         try {
403             checkActive();
404 
405             // Spool the element
406             if (spoolAndExpiryThread.isAlive()) {
407                 synchronized (spoolLock) {
408                     spool.put(element.getObjectKey(), element);
409                 }
410             } else {
411                 LOG.error(name + "Cache: Elements cannot be written to disk store because the" +
412                         " spool thread has died.");
413                 synchronized (spoolLock) {
414                     spool.clear();
415                 }
416             }
417 
418         } catch (Exception e) {
419             LOG.error(name + "Cache: Could not write disk store element for " + element.getObjectKey()
420                     + ". Initial cause was " + e.getMessage(), e);
421         }
422     }
423 
424     /**
425      * In some circumstances data can be written so quickly to the spool that the VM runs out of memory
426      * while waiting for the spooling to disk.
427      * <p/>
428      * This is a very simple and quick test which estimates the spool size based on the last element's written size.
429      *
430      * @return
431      */
432     public boolean backedUp() {
433         long estimatedSpoolSize = spool.size() * lastElementSize;
434         boolean backedUp = estimatedSpoolSize > diskSpoolBufferSizeBytes;
435         if (backedUp && LOG.isTraceEnabled()) {
436             LOG.trace("A back up on cache puts occurred. Consider increasing diskSpoolBufferSizeMB for cache " + name);
437         }
438         return backedUp;
439 
440     }
441 
442     /**
443      * Removes an item from the disk store.
444      *
445      * @noinspection SynchronizeOnNonFinalField
446      */
447     public final synchronized Element remove(final Object key) {
448         Element element;
449         try {
450             checkActive();
451 
452             // Remove the entry from the spool
453             synchronized (spoolLock) {
454                 element = (Element) spool.remove(key);
455             }
456 
457             // Remove the entry from the file. Could be in both places.
458             synchronized (diskElements) {
459                 final DiskElement diskElement = (DiskElement) diskElements.remove(key);
460                 if (diskElement != null) {
461                     element = loadElementFromDiskElement(diskElement);
462                     freeBlock(diskElement);
463                 }
464             }
465         } catch (Exception exception) {
466             String message = name + "Cache: Could not remove disk store entry for " + key
467                     + ". Error was " + exception.getMessage();
468             LOG.error(message, exception);
469             throw new CacheException(message);
470         }
471         return element;
472     }
473 
474     /**
475      * Marks a block as free.
476      *
477      * @param diskElement the DiskElement to move to the free space list
478      */
479     private void freeBlock(final DiskElement diskElement) {
480         totalSize -= diskElement.payloadSize;
481         diskElement.payloadSize = 0;
482 
483         //reset Element meta data
484         diskElement.key = null;
485         diskElement.hitcount = 0;
486         diskElement.expiryTime = 0;
487 
488         freeSpace.add(diskElement);
489     }
490 
491     /**
492      * Remove all of the elements from the store.
493      * <p/>
494      * If there are registered <code>CacheEventListener</code>s they are notified of the expiry or removal
495      * of the <code>Element</code> as each is removed.
496      */
497     public final synchronized void removeAll() {
498         try {
499             checkActive();
500 
501             // Ditch all the elements, and truncate the file
502             spool = Collections.synchronizedMap(new HashMap());
503             diskElements = Collections.synchronizedMap(new HashMap());
504             freeSpace = Collections.synchronizedList(new ArrayList());
505             totalSize = 0;
506             randomAccessFile.setLength(0);
507             if (persistent) {
508                 indexFile.delete();
509                 indexFile.createNewFile();
510             }
511         } catch (Exception e) {
512             // Clean up
513             LOG.error(name + " Cache: Could not rebuild disk store. Initial cause was " + e.getMessage(), e);
514             dispose();
515         }
516     }
517 
518     /**
519      * Shuts down the disk store in preparation for cache shutdown
520      * <p/>
521      * If a VM crash happens, the shutdown hook will not run. The data file and the index file
522      * will be out of synchronisation. At initialisation we always delete the index file
523      * after we have read the elements, so that it has a zero length. On a dirty restart, it still will have
524      * and the data file will automatically be deleted, thus preserving safety.
525      */
526     public final synchronized void dispose() {
527 
528         if (!active) {
529             return;
530         }
531 
532         // Close the cache
533         try {
534 
535             flush();
536 
537             //stop the spool thread
538             if (spoolAndExpiryThread != null) {
539                 spoolAndExpiryThread.interrupt();
540             }
541 
542             //Clear in-memory data structures
543             spool.clear();
544             diskElements.clear();
545             freeSpace.clear();
546             if (randomAccessFile != null) {
547                 randomAccessFile.close();
548             }
549             if (!persistent) {
550                 LOG.debug("Deleting file " + dataFile.getName());
551                 dataFile.delete();
552             }
553         } catch (Exception e) {
554             LOG.error(name + "Cache: Could not shut down disk cache. Initial cause was " + e.getMessage(), e);
555         } finally {
556             active = false;
557             randomAccessFile = null;
558             notifyAll();
559 
560             //release reference to cache
561             cache = null;
562         }
563     }
564 
565     /**
566      * Flush the spool if persistent, so we don't lose any data.
567      *
568      * @throws IOException
569      */
570     public final void flush() throws IOException {
571         if (persistent) {
572             flushSpool();
573             writeIndex();
574         }
575     }
576 
577     /**
578      * both flushing and expiring contend for the same lock on diskElement, so
579      * might as well do them sequentially in the one thread.
580      * <p/>
581      * This thread is protected from Throwables by only calling methods that guard
582      * against these.
583      */
584     private void spoolAndExpiryThreadMain() {
585         long nextExpiryTime = System.currentTimeMillis();
586         while (true) {
587 
588             try {
589                 Thread.sleep(SPOOL_THREAD_INTERVAL);
590             } catch (InterruptedException e) {
591                 LOG.debug("Spool Thread interrupted.");
592                 return;
593             }
594 
595             if (!active) {
596                 return;
597             }
598 
599             throwableSafeFlushSpoolIfRequired();
600 
601             if (!active) {
602                 return;
603             }
604             nextExpiryTime = throwableSafeExpireElementsIfRequired(nextExpiryTime);
605 
606 
607         }
608     }
609 
610     private long throwableSafeExpireElementsIfRequired(long nextExpiryTime) {
611 
612         long updatedNextExpiryTime = nextExpiryTime;
613 
614         // Expire elements
615         if (!eternal && System.currentTimeMillis() > nextExpiryTime) {
616             try {
617                 updatedNextExpiryTime += expiryThreadInterval * MS_PER_SECOND;
618                 expireElements();
619             } catch (Throwable e) {
620                 LOG.error(name + " Cache: Could not expire elements from disk due to "
621                         + e.getMessage() + ". Continuing...", e);
622             }
623         }
624         return updatedNextExpiryTime;
625     }
626 
627     private void throwableSafeFlushSpoolIfRequired() {
628         if (spool != null && spool.size() != 0) {
629             // Write elements to disk
630             try {
631                 flushSpool();
632             } catch (Throwable e) {
633                 LOG.error(name + " Cache: Could not flush elements to disk due to " + e.getMessage() + ". Continuing...", e);
634             }
635         }
636     }
637 
638 
639     /**
640      * Flushes all spooled elements to disk.
641      * Note that the cache is locked for the entire time that the spool is being flushed.
642      *
643      * @noinspection SynchronizeOnNonFinalField
644      */
645     private synchronized void flushSpool() throws IOException {
646         if (spool.size() == 0) {
647             return;
648         }
649 
650         Map copyOfSpool = swapSpoolReference();
651 
652         //does not guarantee insertion order
653         Iterator valuesIterator = copyOfSpool.values().iterator();
654         while (valuesIterator.hasNext()) {
655             writeOrReplaceEntry(valuesIterator.next());
656             valuesIterator.remove();
657         }
658     }
659 
660     private Map swapSpoolReference() {
661         Map copyOfSpool = null;
662         synchronized (spoolLock) {
663             // Copy the reference of the old spool, not the contents. Avoid potential spike in memory usage
664             copyOfSpool = spool;
665 
666             // use a new map making the reference swap above SAFE
667             spool = Collections.synchronizedMap(new HashMap());
668         }
669         return copyOfSpool;
670     }
671 
672 
673     private void writeOrReplaceEntry(Object object) throws IOException {
674         Element element = (Element) object;
675         if (element == null) {
676             return;
677         }
678         final Serializable key = (Serializable) element.getObjectKey();
679         removeOldEntryIfAny(key);
680         if (maxElementsOnDisk > 0 && diskElements.size() >= maxElementsOnDisk) {
681             evictLfuDiskElement();
682         }
683         writeElement(element, key);
684     }
685 
686 
687     private void writeElement(Element element, Serializable key) throws IOException {
688         try {
689             int bufferLength;
690             long expirationTime = element.getExpirationTime();
691 
692             MemoryEfficientByteArrayOutputStream buffer = null;
693             try {
694                 buffer = serializeEntry(element);
695 
696                 bufferLength = buffer.size();
697                 DiskElement diskElement = checkForFreeBlock(bufferLength);
698 
699                 // Write the record
700                 randomAccessFile.seek(diskElement.position);
701                 randomAccessFile.write(buffer.toByteArray(), 0, bufferLength);
702                 buffer = null;
703 
704                 // Add to index, update stats
705                 diskElement.payloadSize = bufferLength;
706                 diskElement.key = key;
707                 diskElement.expiryTime = expirationTime;
708                 diskElement.hitcount = element.getHitCount();
709                 totalSize += bufferLength;
710                 lastElementSize = bufferLength;
711                 synchronized (diskElements) {
712                     diskElements.put(key, diskElement);
713                 }
714             } catch (OutOfMemoryError e) {
715                 LOG.error("OutOfMemoryError on serialize: " + key);
716 
717             }
718 
719         } catch (Exception e) {
720             // Catch any exception that occurs during serialization
721             LOG.error(name + "Cache: Failed to write element to disk '" + key
722                     + "'. Initial cause was " + e.getMessage(), e);
723         }
724 
725     }
726 
727     /**
728      * This class is designed to minimse the number of System.arraycopy(); methods
729      * required to complete.
730      */
731     class MemoryEfficientByteArrayOutputStream extends ByteArrayOutputStream {
732 
733 
734         /**
735          * Creates a new byte array output stream, with a buffer capacity of
736          * the specified size, in bytes.
737          *
738          * @param size the initial size.
739          */
740         public MemoryEfficientByteArrayOutputStream(int size) {
741             super(size);
742         }
743 
744         /**
745          * Gets the bytes. Not all may be valid. Use only up to getSize()
746          *
747          * @return the underlying byte[]
748          */
749         public synchronized byte getBytes()[] {
750             return buf;
751         }
752     }
753 
754     private MemoryEfficientByteArrayOutputStream serializeEntry(Element element) throws IOException {
755         MemoryEfficientByteArrayOutputStream outstr = new MemoryEfficientByteArrayOutputStream(estimatedPayloadSize());
756         ObjectOutputStream objstr = new ObjectOutputStream(outstr);
757         objstr.writeObject(element);
758         objstr.close();
759         return outstr;
760     }
761 
762 
763     private int estimatedPayloadSize() {
764         int size = 0;
765         try {
766             size = (int) (totalSize / diskElements.size());
767         } catch (Exception e) {
768             //
769         }
770         if (size <= 0) {
771             size = ESTIMATED_MINIMUM_PAYLOAD_SIZE;
772         }
773         return size;
774     }
775 
776     /**
777      * Remove the old entry, if any
778      *
779      * @param key
780      */
781     private void removeOldEntryIfAny(Serializable key) {
782 
783         final DiskElement oldBlock;
784         synchronized (diskElements) {
785             oldBlock = (DiskElement) diskElements.remove(key);
786         }
787         if (oldBlock != null) {
788             freeBlock(oldBlock);
789         }
790     }
791 
792     private DiskElement checkForFreeBlock(int bufferLength) throws IOException {
793         DiskElement diskElement = findFreeBlock(bufferLength);
794         if (diskElement == null) {
795             diskElement = new DiskElement();
796             diskElement.position = randomAccessFile.length();
797             diskElement.blockSize = bufferLength;
798         }
799         return diskElement;
800     }
801 
802     /**
803      * Writes the Index to disk on shutdown
804      * <p/>
805      * The index consists of the elements Map and the freeSpace List
806      * <p/>
807      * Note that the cache is locked for the entire time that the index is being written
808      */
809     private synchronized void writeIndex() throws IOException {
810 
811         ObjectOutputStream objectOutputStream = null;
812         try {
813             FileOutputStream fout = new FileOutputStream(indexFile);
814             objectOutputStream = new ObjectOutputStream(fout);
815             objectOutputStream.writeObject(diskElements);
816             objectOutputStream.writeObject(freeSpace);
817         } finally {
818             if (objectOutputStream != null) {
819                 objectOutputStream.close();
820             }
821         }
822     }
823 
824     /**
825      * Reads Index to disk on startup.
826      * <p/>
827      * if the index file does not exist, it creates a new one.
828      * <p/>
829      * Note that the cache is locked for the entire time that the index is being written
830      *
831      * @return True if the index was read successfully, false otherwise
832      */
833     private synchronized boolean readIndex() throws IOException {
834         ObjectInputStream objectInputStream = null;
835         FileInputStream fin = null;
836         boolean success = false;
837         if (indexFile.exists()) {
838             try {
839                 fin = new FileInputStream(indexFile);
840                 objectInputStream = new ObjectInputStream(fin);
841                 diskElements = (Map) objectInputStream.readObject();
842                 freeSpace = (List) objectInputStream.readObject();
843                 success = true;
844             } catch (StreamCorruptedException e) {
845                 LOG.error("Corrupt index file. Creating new index.");
846             } catch (IOException e) {
847                 //normal when creating the cache for the first time
848                 if (LOG.isDebugEnabled()) {
849                     LOG.debug("IOException reading index. Creating new index. ");
850                 }
851             } catch (ClassNotFoundException e) {
852                 LOG.error("Class loading problem reading index. Creating new index. Initial cause was " + e.getMessage(), e);
853             } finally {
854                 try {
855                     if (objectInputStream != null) {
856                         objectInputStream.close();
857                     } else if (fin != null) {
858                         fin.close();
859                     }
860                 } catch (IOException e) {
861                     LOG.error("Problem closing the index file.");
862                 }
863 
864                 //Always zero out file. That way if there is a dirty shutdown, the file will still be empty
865                 //the next time we start up and readIndex will automatically fail.
866                 //If there was a problem reading the index this time we also want to zero it out.
867                 createNewIndexFile();
868             }
869         } else {
870             createNewIndexFile();
871         }
872 
873         //Return the success flag
874         return success;
875 
876     }
877 
878     private void createNewIndexFile() throws IOException {
879         if (indexFile.exists()) {
880             indexFile.delete();
881             if (LOG.isDebugEnabled()) {
882                 LOG.debug("Index file " + indexFile + " deleted.");
883             }
884         }
885         if (indexFile.createNewFile()) {
886             if (LOG.isDebugEnabled()) {
887                 LOG.debug("Index file " + indexFile + " created successfully");
888             }
889         } else {
890             throw new IOException("Index file " + indexFile + " could not created.");
891         }
892     }
893 
894     /**
895      * Removes expired elements.
896      * <p/>
897      * Note that the DiskStore cannot efficiently expire based on TTI. It does it on TTL. However any gets out
898      * of the DiskStore are check for both before return.
899      *
900      * @noinspection SynchronizeOnNonFinalField
901      */
902     public void expireElements() {
903         final long now = System.currentTimeMillis();
904 
905         // Clean up the spool
906         synchronized (spoolLock) {
907             for (Iterator iterator = spool.values().iterator(); iterator.hasNext();) {
908                 final Element element = (Element) iterator.next();
909                 if (element.isExpired()) {
910                     // An expired element
911                     if (LOG.isDebugEnabled()) {
912                         LOG.debug(name + "Cache: Removing expired spool element " + element.getObjectKey());
913                     }
914                     iterator.remove();
915                     notifyExpiryListeners(element);
916                 }
917             }
918         }
919 
920         Element element = null;
921         RegisteredEventListeners listeners = cache.getCacheEventNotificationService();
922         synchronized (diskElements) {
923             // Clean up disk elements
924             for (Iterator iterator = diskElements.entrySet().iterator(); iterator.hasNext();) {
925                 final Map.Entry entry = (Map.Entry) iterator.next();
926                 final DiskElement diskElement = (DiskElement) entry.getValue();
927 
928                 if (now >= diskElement.expiryTime) {
929                     // An expired element
930                     if (LOG.isDebugEnabled()) {
931                         LOG.debug(name + "Cache: Removing expired spool element " + entry.getKey() + " from Disk Store");
932                     }
933 
934                     iterator.remove();
935 
936                     // only load the element from the file if there is a listener interested in hearing about its expiration 
937                     if (listeners.hasCacheEventListeners()) {
938                         try {
939                             element = loadElementFromDiskElement(diskElement);
940                             notifyExpiryListeners(element);
941                         } catch (Exception exception) {
942                             LOG.error(name + "Cache: Could not remove disk store entry for " + entry.getKey()
943                                     + ". Error was " + exception.getMessage(), exception);
944                         }
945                     }
946                     freeBlock(diskElement);
947                 }
948             }
949         }
950     }
951 
952     /**
953      * It is enough that an element is expiring here. Notify even though there might be another
954      * element with the same key elsewhere in the stores.
955      *
956      * @param element
957      */
958     private void notifyExpiryListeners(Element element) {
959         cache.getCacheEventNotificationService().notifyElementExpiry(element, false);
960     }
961 
962     /**
963      * Allocates a free block.
964      */
965     private DiskElement findFreeBlock(final int length) {
966         for (int i = 0; i < freeSpace.size(); i++) {
967             final DiskElement element = (DiskElement) freeSpace.get(i);
968             if (element.blockSize >= length) {
969                 freeSpace.remove(i);
970                 return element;
971             }
972         }
973         return null;
974     }
975 
976     /**
977      * Returns a {@link String} representation of the {@link DiskStore}
978      */
979     public final String toString() {
980         StringBuffer sb = new StringBuffer();
981         sb.append("[ dataFile = ").append(dataFile.getAbsolutePath())
982                 .append(", active=").append(active)
983                 .append(", totalSize=").append(totalSize)
984                 .append(", status=").append(status)
985                 .append(", expiryThreadInterval = ").append(expiryThreadInterval)
986                 .append(" ]");
987         return sb.toString();
988     }
989 
990     /**
991      * Generates a unique directory name for use in automatically creating a diskStorePath where there is a conflict.
992      *
993      * @return a path consisting of {@link #AUTO_DISK_PATH_DIRECTORY_PREFIX} followed by "_" followed by the current
994      *         time as a long e.g. ehcache_auto_created_1149389837006
995      */
996     public static String generateUniqueDirectory() {
997         return DiskStore.AUTO_DISK_PATH_DIRECTORY_PREFIX + "_" + System.currentTimeMillis();
998     }
999 
1000 
1001     /**
1002      * A reference to an on-disk elements.
1003      * <p/>
1004      * Copies of expiryTime and hitcount are held here as a performance optimisation, so
1005      * that we do not need to load the data from Disk to get this often used information.
1006      *
1007      * @noinspection SerializableHasSerializationMethods
1008      */
1009     private static final class DiskElement implements Serializable, LfuPolicy.Metadata {
1010 
1011         private static final long serialVersionUID = -717310932566592289L;
1012 
1013         /**
1014          * the file pointer
1015          */
1016         private long position;
1017 
1018         /**
1019          * The size used for data.
1020          */
1021         private int payloadSize;
1022 
1023         /**
1024          * the size of this element.
1025          */
1026         private int blockSize;
1027 
1028         /**
1029          * The key this element is mapped with in DiskElements. This is only a reference
1030          * to the key. It is used in DiskElements and therefore the only memory cost is the
1031          * reference.
1032          */
1033         private Object key;
1034 
1035         /**
1036          * The expiry time in milliseconds
1037          */
1038         private long expiryTime;
1039 
1040         /**
1041          * The numbe of times the element has been requested and found in the cache.
1042          */
1043         private long hitcount;
1044 
1045 
1046         /**
1047          * @return the key of this object
1048          */
1049         public Object getKey() {
1050             return key;
1051         }
1052 
1053         /**
1054          * @return the hit count for the element
1055          */
1056         public long getHitCount() {
1057             return hitcount;
1058         }
1059     }
1060 
1061     /**
1062      * A background daemon thread that writes objects to the file.
1063      */
1064     private final class SpoolAndExpiryThread extends Thread {
1065         public SpoolAndExpiryThread() {
1066             super("Store " + name + " Spool Thread");
1067             setDaemon(true);
1068             setPriority(Thread.NORM_PRIORITY);
1069         }
1070 
1071         /**
1072          * RemoteDebugger thread method.
1073          */
1074         public final void run() {
1075             spoolAndExpiryThreadMain();
1076         }
1077     }
1078 
1079     /**
1080      * @return the total size of the data file and the index file, in bytes.
1081      */
1082     public final long getTotalFileSize() {
1083         return getDataFileSize() + getIndexFileSize();
1084     }
1085 
1086     /**
1087      * @return the size of the data file in bytes.
1088      */
1089     public final long getDataFileSize() {
1090         return dataFile.length();
1091     }
1092 
1093     /**
1094      * The design of the layout on the data file means that there will be small gaps created when DiskElements
1095      * are reused.
1096      *
1097      * @return the sparseness, measured as the percentage of space in the Data File not used for holding data
1098      */
1099     public final float calculateDataFileSparseness() {
1100         return 1 - ((float) getUsedDataSize() / (float) getDataFileSize());
1101     }
1102 
1103     /**
1104      * When elements are deleted, spaces are left in the file. These spaces are tracked and are reused
1105      * when new elements need to be written.
1106      * <p/>
1107      * This method indicates the actual size used for data, excluding holes. It can be compared with
1108      * {@link #getDataFileSize()} as a measure of fragmentation.
1109      */
1110     public final long getUsedDataSize() {
1111         return totalSize;
1112     }
1113 
1114     /**
1115      * @return the size of the index file, in bytes.
1116      */
1117     public final long getIndexFileSize() {
1118         if (indexFile == null) {
1119             return 0;
1120         } else {
1121             return indexFile.length();
1122         }
1123     }
1124 
1125     /**
1126      * @return the file name of the data file where the disk store stores data, without any path information.
1127      */
1128     public final String getDataFileName() {
1129         return name + ".data";
1130     }
1131 
1132     /**
1133      * @return the disk path, which will be dependent on the operating system
1134      */
1135     public final String getDataFilePath() {
1136         return diskPath;
1137     }
1138 
1139     /**
1140      * @return the file name of the index file, which maintains a record of elements and their addresses
1141      *         on the data file, without any path information.
1142      */
1143     public final String getIndexFileName() {
1144         return name + ".index";
1145     }
1146 
1147     /**
1148      * The spool thread is started when the disk store is created.
1149      * <p/>
1150      * It will continue to run until the {@link #dispose()} method is called,
1151      * at which time it should be interrupted and then die.
1152      *
1153      * @return true if the spoolThread is still alive.
1154      */
1155     public final boolean isSpoolThreadAlive() {
1156         if (spoolAndExpiryThread == null) {
1157             return false;
1158         } else {
1159             return spoolAndExpiryThread.isAlive();
1160         }
1161     }
1162 
1163     private void evictLfuDiskElement() {
1164         synchronized (diskElements) {
1165             DiskElement diskElement = findRelativelyUnused();
1166             diskElements.remove(diskElement.key);
1167             notifyEvictionListeners(diskElement);
1168             freeBlock(diskElement);
1169         }
1170     }
1171 
1172     /**
1173      * Find a "relatively" unused disk element, but not the element just added.
1174      */
1175     private DiskElement findRelativelyUnused() {
1176         LfuPolicy.Metadata[] elements = sampleElements(diskElements);
1177         LfuPolicy.Metadata metadata = LfuPolicy.leastHit(elements, null);
1178         return (DiskElement) metadata;
1179     }
1180 
1181     /**
1182      * Uses random numbers to sample the entire map.
1183      *
1184      * @return an array of sampled elements
1185      */
1186     private LfuPolicy.Metadata[] sampleElements(Map map) {
1187         int[] offsets = LfuPolicy.generateRandomSample(map.size());
1188         DiskElement[] elements = new DiskElement[offsets.length];
1189         Iterator iterator = map.values().iterator();
1190         for (int i = 0; i < offsets.length; i++) {
1191             for (int j = 0; j < offsets[i]; j++) {
1192                 iterator.next();
1193             }
1194             elements[i] = (DiskElement) iterator.next();
1195         }
1196         return elements;
1197     }
1198 
1199     private void notifyEvictionListeners(DiskElement diskElement) {
1200         RegisteredEventListeners listeners = cache.getCacheEventNotificationService();
1201         // only load the element from the file if there is a listener interested in hearing about its expiration
1202         if (listeners.hasCacheEventListeners()) {
1203             Element element = null;
1204             try {
1205                 element = loadElementFromDiskElement(diskElement);
1206                 cache.getCacheEventNotificationService().notifyElementEvicted(element, false);
1207             } catch (Exception exception) {
1208                 LOG.error(name + "Cache: Could not notify disk store eviction of " + element.getObjectKey() +
1209                         ". Error was " + exception.getMessage(), exception);
1210             }
1211         }
1212 
1213     }
1214 
1215 
1216 }