001package org.apache.commons.jcs3.auxiliary.disk.indexed;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *   http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import java.io.File;
023import java.io.IOException;
024import java.io.Serializable;
025import java.nio.file.Files;
026import java.util.ArrayList;
027import java.util.Collections;
028import java.util.Comparator;
029import java.util.HashMap;
030import java.util.HashSet;
031import java.util.LinkedList;
032import java.util.List;
033import java.util.Map;
034import java.util.Set;
035import java.util.concurrent.ConcurrentSkipListSet;
036import java.util.concurrent.atomic.AtomicInteger;
037import java.util.concurrent.atomic.AtomicLong;
038import java.util.concurrent.locks.ReentrantReadWriteLock;
039
040import org.apache.commons.jcs3.auxiliary.AuxiliaryCacheAttributes;
041import org.apache.commons.jcs3.auxiliary.disk.AbstractDiskCache;
042import org.apache.commons.jcs3.auxiliary.disk.behavior.IDiskCacheAttributes.DiskLimitType;
043import org.apache.commons.jcs3.engine.behavior.ICacheElement;
044import org.apache.commons.jcs3.engine.behavior.IElementSerializer;
045import org.apache.commons.jcs3.engine.control.group.GroupAttrName;
046import org.apache.commons.jcs3.engine.control.group.GroupId;
047import org.apache.commons.jcs3.engine.logging.behavior.ICacheEvent;
048import org.apache.commons.jcs3.engine.logging.behavior.ICacheEventLogger;
049import org.apache.commons.jcs3.engine.stats.StatElement;
050import org.apache.commons.jcs3.engine.stats.Stats;
051import org.apache.commons.jcs3.engine.stats.behavior.IStatElement;
052import org.apache.commons.jcs3.engine.stats.behavior.IStats;
053import org.apache.commons.jcs3.log.Log;
054import org.apache.commons.jcs3.log.LogManager;
055import org.apache.commons.jcs3.utils.struct.AbstractLRUMap;
056import org.apache.commons.jcs3.utils.struct.LRUMap;
057import org.apache.commons.jcs3.utils.timing.ElapsedTimer;
058
059/**
060 * Disk cache that uses a RandomAccessFile with keys stored in memory. The maximum number of keys
061 * stored in memory is configurable. The disk cache tries to recycle spots on disk to limit file
062 * expansion.
063 */
064public class IndexedDiskCache<K, V> extends AbstractDiskCache<K, V>
065{
066    /** The logger */
067    private static final Log log = LogManager.getLog(IndexedDiskCache.class);
068
069    /** Cache name used in log messages */
070    protected final String logCacheName;
071
072    /** The name of the file where the data is stored */
073    private final String fileName;
074
075    /** The IndexedDisk manages reads and writes to the data file. */
076    private IndexedDisk dataFile;
077
078    /** The IndexedDisk manages reads and writes to the key file. */
079    private IndexedDisk keyFile;
080
081    /** Map containing the keys and disk offsets. */
082    private final Map<K, IndexedDiskElementDescriptor> keyHash;
083
084    /** The maximum number of keys that we will keep in memory. */
085    private final int maxKeySize;
086
087    /** A handle on the data file. */
088    private File rafDir;
089
090    /** Should we keep adding to the recycle bin. False during optimization. */
091    private boolean doRecycle = true;
092
093    /** Should we optimize real time */
094    private boolean isRealTimeOptimizationEnabled = true;
095
096    /** Should we optimize on shutdown. */
097    private boolean isShutdownOptimizationEnabled = true;
098
099    /** are we currently optimizing the files */
100    private boolean isOptimizing;
101
102    /** The number of times the file has been optimized. */
103    private int timesOptimized;
104
105    /** The thread optimizing the file. */
106    private volatile Thread currentOptimizationThread;
107
108    /** used for counting the number of requests */
109    private int removeCount;
110
111    /** Should we queue puts. True when optimizing. We write the queue post optimization. */
112    private boolean queueInput;
113
114    /** list where puts made during optimization are made */
115    private final ConcurrentSkipListSet<IndexedDiskElementDescriptor> queuedPutList;
116
117    /** RECYCLE BIN -- array of empty spots */
118    private final ConcurrentSkipListSet<IndexedDiskElementDescriptor> recycle;
119
120    /** User configurable parameters */
121    private final IndexedDiskCacheAttributes cattr;
122
123    /** How many slots have we recycled. */
124    private int recycleCnt;
125
126    /** How many items were there on startup. */
127    private int startupSize;
128
129    /** the number of bytes free on disk. */
130    private final AtomicLong bytesFree = new AtomicLong();
131
132    /** mode we are working on (size or count limited **/
133    private DiskLimitType diskLimitType = DiskLimitType.COUNT;
134
135    /** simple stat */
136    private final AtomicInteger hitCount = new AtomicInteger(0);
137
138    /**
139     * Use this lock to synchronize reads and writes to the underlying storage mechanism.
140     */
141    protected ReentrantReadWriteLock storageLock = new ReentrantReadWriteLock();
142
143    /**
144     * Constructor for the DiskCache object.
145     * <p>
146     *
147     * @param cacheAttributes
148     */
149    public IndexedDiskCache(final IndexedDiskCacheAttributes cacheAttributes)
150    {
151        this(cacheAttributes, null);
152    }
153
154    /**
155     * Constructor for the DiskCache object.
156     * <p>
157     *
158     * @param cattr
159     * @param elementSerializer
160     *            used if supplied, the super's super will not set a null
161     */
162    public IndexedDiskCache(final IndexedDiskCacheAttributes cattr, final IElementSerializer elementSerializer)
163    {
164        super(cattr);
165
166        setElementSerializer(elementSerializer);
167
168        this.cattr = cattr;
169        this.maxKeySize = cattr.getMaxKeySize();
170        this.isRealTimeOptimizationEnabled = cattr.getOptimizeAtRemoveCount() > 0;
171        this.isShutdownOptimizationEnabled = cattr.isOptimizeOnShutdown();
172        this.logCacheName = "Region [" + getCacheName() + "] ";
173        this.diskLimitType = cattr.getDiskLimitType();
174        // Make a clean file name
175        this.fileName = getCacheName().replaceAll("[^a-zA-Z0-9-_\\.]", "_");
176        this.keyHash = createInitialKeyMap();
177        this.queuedPutList = new ConcurrentSkipListSet<>(new PositionComparator());
178        this.recycle = new ConcurrentSkipListSet<>();
179
180        try
181        {
182            initializeFileSystem(cattr);
183            initializeKeysAndData(cattr);
184
185            // Initialization finished successfully, so set alive to true.
186            setAlive(true);
187            log.info("{0}: Indexed Disk Cache is alive.", logCacheName);
188
189            // TODO: Should we improve detection of whether or not the file should be optimized.
190            if (isRealTimeOptimizationEnabled && !keyHash.isEmpty())
191            {
192                // Kick off a real time optimization, in case we didn't do a final optimization.
193                doOptimizeRealTime();
194            }
195        }
196        catch (final IOException e)
197        {
198            log.error("{0}: Failure initializing for fileName: {1} and directory: {2}",
199                    logCacheName, fileName, this.rafDir.getAbsolutePath(), e);
200        }
201    }
202
203    /**
204     * Tries to create the root directory if it does not already exist.
205     * <p>
206     *
207     * @param cattr
208     */
209    private void initializeFileSystem(final IndexedDiskCacheAttributes cattr)
210    {
211        this.rafDir = cattr.getDiskPath();
212        log.info("{0}: Cache file root directory: {1}", logCacheName, rafDir);
213    }
214
215    /**
216     * Creates the key and data disk caches.
217     * <p>
218     * Loads any keys if they are present and ClearDiskOnStartup is false.
219     * <p>
220     *
221     * @param cattr
222     * @throws IOException
223     */
224    private void initializeKeysAndData(final IndexedDiskCacheAttributes cattr) throws IOException
225    {
226        this.dataFile = new IndexedDisk(new File(rafDir, fileName + ".data"), getElementSerializer());
227        this.keyFile = new IndexedDisk(new File(rafDir, fileName + ".key"), getElementSerializer());
228
229        if (cattr.isClearDiskOnStartup())
230        {
231            log.info("{0}: ClearDiskOnStartup is set to true. Ignoring any persisted data.",
232                    logCacheName);
233            initializeEmptyStore();
234        }
235        else if (!keyFile.isEmpty())
236        {
237            // If the key file has contents, try to initialize the keys
238            // from it. In no keys are loaded reset the data file.
239            initializeStoreFromPersistedData();
240        }
241        else
242        {
243            // Otherwise start with a new empty map for the keys, and reset
244            // the data file if it has contents.
245            initializeEmptyStore();
246        }
247    }
248
249    /**
250     * Initializes an empty disk cache.
251     * <p>
252     *
253     * @throws IOException
254     */
255    private void initializeEmptyStore() throws IOException
256    {
257        this.keyHash.clear();
258
259        if (!dataFile.isEmpty())
260        {
261            dataFile.reset();
262        }
263    }
264
265    /**
266     * Loads any persisted data and checks for consistency. If there is a consistency issue, the
267     * files are cleared.
268     * <p>
269     *
270     * @throws IOException
271     */
272    private void initializeStoreFromPersistedData() throws IOException
273    {
274        loadKeys();
275
276        if (keyHash.isEmpty())
277        {
278            dataFile.reset();
279        }
280        else
281        {
282            final boolean isOk = checkKeyDataConsistency(false);
283            if (!isOk)
284            {
285                keyHash.clear();
286                keyFile.reset();
287                dataFile.reset();
288                log.warn("{0}: Corruption detected. Resetting data and keys files.", logCacheName);
289            }
290            else
291            {
292                synchronized (this)
293                {
294                    startupSize = keyHash.size();
295                }
296            }
297        }
298    }
299
300    /**
301     * Loads the keys from the .key file. The keys are stored in a HashMap on disk. This is
302     * converted into a LRUMap.
303     */
304    protected void loadKeys()
305    {
306        log.debug("{0}: Loading keys for {1}", () -> logCacheName, () -> keyFile.toString());
307
308        storageLock.writeLock().lock();
309
310        try
311        {
312            // clear a key map to use.
313            keyHash.clear();
314
315            final HashMap<K, IndexedDiskElementDescriptor> keys = keyFile.readObject(
316                new IndexedDiskElementDescriptor(0, (int) keyFile.length() - IndexedDisk.HEADER_SIZE_BYTES));
317
318            if (keys != null)
319            {
320                log.debug("{0}: Found {1} in keys file.", logCacheName, keys.size());
321
322                keyHash.putAll(keys);
323
324                log.info("{0}: Loaded keys from [{1}], key count: {2}; up to {3} will be available.",
325                        () -> logCacheName, () -> fileName, keyHash::size, () -> maxKeySize);
326            }
327
328            if (log.isTraceEnabled())
329            {
330                dump(false);
331            }
332        }
333        catch (final Exception e)
334        {
335            log.error("{0}: Problem loading keys for file {1}", logCacheName, fileName, e);
336        }
337        finally
338        {
339            storageLock.writeLock().unlock();
340        }
341    }
342
343    /**
344     * Check for minimal consistency between the keys and the datafile. Makes sure no starting
345     * positions in the keys exceed the file length.
346     * <p>
347     * The caller should take the appropriate action if the keys and data are not consistent.
348     *
349     * @param checkForDedOverlaps
350     *            if {@code true}, do a more thorough check by checking for
351     *            data overlap
352     * @return {@code true} if the test passes
353     */
354    private boolean checkKeyDataConsistency(final boolean checkForDedOverlaps)
355    {
356        final ElapsedTimer timer = new ElapsedTimer();
357        log.debug("{0}: Performing inital consistency check", logCacheName);
358
359        boolean isOk = true;
360        try
361        {
362            final long fileLength = dataFile.length();
363
364            final IndexedDiskElementDescriptor corruptDed = keyHash.values().stream()
365                .filter(ded -> ded.pos + IndexedDisk.HEADER_SIZE_BYTES + ded.len > fileLength)
366                .findFirst()
367                .orElse(null);
368
369            if (corruptDed != null)
370            {
371                isOk = false;
372                log.warn("{0}: The dataFile is corrupted!\n raf.length() = {1}\n ded.pos = {2}",
373                        logCacheName, fileLength, corruptDed.pos);
374            }
375            else if (checkForDedOverlaps)
376            {
377                isOk = checkForDedOverlaps(createPositionSortedDescriptorList());
378            }
379        }
380        catch (final IOException e)
381        {
382            log.error(e);
383            isOk = false;
384        }
385
386        log.info("{0}: Finished inital consistency check, isOk = {1} in {2}",
387                logCacheName, isOk, timer.getElapsedTimeString());
388
389        return isOk;
390    }
391
392    /**
393     * Detects any overlapping elements. This expects a sorted list.
394     * <p>
395     * The total length of an item is IndexedDisk.RECORD_HEADER + ded.len.
396     * <p>
397     *
398     * @param sortedDescriptors
399     * @return false if there are overlaps.
400     */
401    protected boolean checkForDedOverlaps(final IndexedDiskElementDescriptor[] sortedDescriptors)
402    {
403        final ElapsedTimer timer = new ElapsedTimer();
404        boolean isOk = true;
405        long expectedNextPos = 0;
406        for (final IndexedDiskElementDescriptor ded : sortedDescriptors) {
407            if (expectedNextPos > ded.pos)
408            {
409                log.error("{0}: Corrupt file: overlapping deds {1}", logCacheName, ded);
410                isOk = false;
411                break;
412            }
413            expectedNextPos = ded.pos + IndexedDisk.HEADER_SIZE_BYTES + ded.len;
414        }
415        log.debug("{0}: Check for DED overlaps took {1} ms.", () -> logCacheName,
416                timer::getElapsedTime);
417
418        return isOk;
419    }
420
421    /**
422     * Saves key file to disk. This converts the LRUMap to a HashMap for deserialization.
423     */
424    protected void saveKeys()
425    {
426        try
427        {
428            log.info("{0}: Saving keys to: {1}, key count: {2}",
429                    () -> logCacheName, () -> fileName, keyHash::size);
430
431            keyFile.reset();
432
433            final HashMap<K, IndexedDiskElementDescriptor> keys = new HashMap<>(keyHash);
434            if (!keys.isEmpty())
435            {
436                keyFile.writeObject(keys, 0);
437            }
438
439            log.info("{0}: Finished saving keys.", logCacheName);
440        }
441        catch (final IOException e)
442        {
443            log.error("{0}: Problem storing keys.", logCacheName, e);
444        }
445    }
446
447    /**
448     * Update the disk cache. Called from the Queue. Makes sure the Item has not been retrieved from
449     * purgatory while in queue for disk. Remove items from purgatory when they go to disk.
450     * <p>
451     *
452     * @param ce
453     *            The ICacheElement&lt;K, V&gt; to put to disk.
454     */
455    @Override
456    protected void processUpdate(final ICacheElement<K, V> ce)
457    {
458        if (!isAlive())
459        {
460            log.error("{0}: No longer alive; aborting put of key = {1}",
461                    () -> logCacheName, ce::getKey);
462            return;
463        }
464
465        log.debug("{0}: Storing element on disk, key: {1}",
466                () -> logCacheName, ce::getKey);
467
468        // old element with same key
469        IndexedDiskElementDescriptor old = null;
470
471        try
472        {
473            IndexedDiskElementDescriptor ded = null;
474            final byte[] data = getElementSerializer().serialize(ce);
475
476            // make sure this only locks for one particular cache region
477            storageLock.writeLock().lock();
478            try
479            {
480                old = keyHash.get(ce.getKey());
481
482                // Item with the same key already exists in file.
483                // Try to reuse the location if possible.
484                if (old != null && data.length <= old.len)
485                {
486                    // Reuse the old ded. The defrag relies on ded updates by reference, not
487                    // replacement.
488                    ded = old;
489                    ded.len = data.length;
490                }
491                else
492                {
493                    // we need this to compare in the recycle bin
494                    ded = new IndexedDiskElementDescriptor(dataFile.length(), data.length);
495
496                    if (doRecycle)
497                    {
498                        final IndexedDiskElementDescriptor rep = recycle.ceiling(ded);
499                        if (rep != null)
500                        {
501                            // remove element from recycle bin
502                            recycle.remove(rep);
503                            ded = rep;
504                            ded.len = data.length;
505                            recycleCnt++;
506                            this.adjustBytesFree(ded, false);
507                            log.debug("{0}: using recycled ded {1} rep.len = {2} ded.len = {3}",
508                                    logCacheName, ded.pos, rep.len, ded.len);
509                        }
510                    }
511
512                    // Put it in the map
513                    keyHash.put(ce.getKey(), ded);
514
515                    if (queueInput)
516                    {
517                        queuedPutList.add(ded);
518                        log.debug("{0}: added to queued put list. {1}",
519                                () -> logCacheName, queuedPutList::size);
520                    }
521
522                    // add the old slot to the recycle bin
523                    if (old != null)
524                    {
525                        addToRecycleBin(old);
526                    }
527                }
528
529                dataFile.write(ded, data);
530            }
531            finally
532            {
533                storageLock.writeLock().unlock();
534            }
535
536            log.debug("{0}: Put to file: {1}, key: {2}, position: {3}, size: {4}",
537                    logCacheName, fileName, ce.getKey(), ded.pos, ded.len);
538        }
539        catch (final IOException e)
540        {
541            log.error("{0}: Failure updating element, key: {1} old: {2}",
542                    logCacheName, ce.getKey(), old, e);
543        }
544    }
545
546    /**
547     * Gets the key, then goes to disk to get the object.
548     * <p>
549     *
550     * @param key
551     * @return ICacheElement&lt;K, V&gt; or null
552     * @see AbstractDiskCache#doGet
553     */
554    @Override
555    protected ICacheElement<K, V> processGet(final K key)
556    {
557        if (!isAlive())
558        {
559            log.error("{0}: No longer alive so returning null for key = {1}",
560                    logCacheName, key);
561            return null;
562        }
563
564        log.debug("{0}: Trying to get from disk: {1}", logCacheName, key);
565
566        ICacheElement<K, V> object = null;
567        try
568        {
569            storageLock.readLock().lock();
570            try
571            {
572                object = readElement(key);
573            }
574            finally
575            {
576                storageLock.readLock().unlock();
577            }
578
579            if (object != null)
580            {
581                hitCount.incrementAndGet();
582            }
583        }
584        catch (final IOException ioe)
585        {
586            log.error("{0}: Failure getting from disk, key = {1}", logCacheName, key, ioe);
587            reset();
588        }
589        return object;
590    }
591
592    /**
593     * Gets matching items from the cache.
594     * <p>
595     *
596     * @param pattern
597     * @return a map of K key to ICacheElement&lt;K, V&gt; element, or an empty map if there is no
598     *         data in cache matching keys
599     */
600    @Override
601    public Map<K, ICacheElement<K, V>> processGetMatching(final String pattern)
602    {
603        final Map<K, ICacheElement<K, V>> elements = new HashMap<>();
604        Set<K> keyArray = null;
605        storageLock.readLock().lock();
606        try
607        {
608            keyArray = new HashSet<>(keyHash.keySet());
609        }
610        finally
611        {
612            storageLock.readLock().unlock();
613        }
614
615        final Set<K> matchingKeys = getKeyMatcher().getMatchingKeysFromArray(pattern, keyArray);
616
617        for (final K key : matchingKeys)
618        {
619            final ICacheElement<K, V> element = processGet(key);
620            if (element != null)
621            {
622                elements.put(key, element);
623            }
624        }
625        return elements;
626    }
627
628    /**
629     * Reads the item from disk.
630     * <p>
631     *
632     * @param key
633     * @return ICacheElement
634     * @throws IOException
635     */
636    private ICacheElement<K, V> readElement(final K key) throws IOException
637    {
638        final IndexedDiskElementDescriptor ded = keyHash.get(key);
639
640        if (ded != null)
641        {
642            log.debug("{0}: Found on disk, key: ", logCacheName, key);
643
644            try
645            {
646                return dataFile.readObject(ded);
647                // TODO consider checking key equality and throwing if there is a failure
648            }
649            catch (final IOException e)
650            {
651                log.error("{0}: IO Exception, Problem reading object from file", logCacheName, e);
652                throw e;
653            }
654            catch (final Exception e)
655            {
656                log.error("{0}: Exception, Problem reading object from file", logCacheName, e);
657                throw new IOException(logCacheName + "Problem reading object from disk.", e);
658            }
659        }
660
661        return null;
662    }
663
664    /**
665     * Return the keys in this cache.
666     * <p>
667     *
668     * @see org.apache.commons.jcs3.auxiliary.disk.AbstractDiskCache#getKeySet()
669     */
670    @Override
671    public Set<K> getKeySet() throws IOException
672    {
673        final HashSet<K> keys = new HashSet<>();
674
675        storageLock.readLock().lock();
676
677        try
678        {
679            keys.addAll(this.keyHash.keySet());
680        }
681        finally
682        {
683            storageLock.readLock().unlock();
684        }
685
686        return keys;
687    }
688
689    /**
690     * Returns true if the removal was successful; or false if there is nothing to remove. Current
691     * implementation always result in a disk orphan.
692     * <p>
693     *
694     * @return true if at least one item was removed.
695     * @param key
696     */
697    @Override
698    protected boolean processRemove(final K key)
699    {
700        if (!isAlive())
701        {
702            log.error("{0}: No longer alive so returning false for key = {1}", logCacheName, key);
703            return false;
704        }
705
706        if (key == null)
707        {
708            return false;
709        }
710
711        boolean removed = false;
712        try
713        {
714            storageLock.writeLock().lock();
715
716            if (key instanceof String && key.toString().endsWith(NAME_COMPONENT_DELIMITER))
717            {
718                removed = performPartialKeyRemoval((String) key);
719            }
720            else if (key instanceof GroupAttrName && ((GroupAttrName<?>) key).attrName == null)
721            {
722                removed = performGroupRemoval(((GroupAttrName<?>) key).groupId);
723            }
724            else
725            {
726                removed = performSingleKeyRemoval(key);
727            }
728        }
729        finally
730        {
731            storageLock.writeLock().unlock();
732        }
733
734        // this increments the remove count.
735        // there is no reason to call this if an item was not removed.
736        if (removed)
737        {
738            doOptimizeRealTime();
739        }
740
741        return removed;
742    }
743
744    /**
745     * Iterates over the keyset. Builds a list of matches. Removes all the keys in the list. Does
746     * not remove via the iterator, since the map impl may not support it.
747     * <p>
748     * This operates under a lock obtained in doRemove().
749     * <p>
750     *
751     * @param key
752     * @return true if there was a match
753     */
754    private boolean performPartialKeyRemoval(final String key)
755    {
756        boolean removed = false;
757
758        // remove all keys of the same name hierarchy.
759        final List<K> itemsToRemove = new LinkedList<>();
760
761        for (final K k : keyHash.keySet())
762        {
763            if (k instanceof String && k.toString().startsWith(key))
764            {
765                itemsToRemove.add(k);
766            }
767        }
768
769        // remove matches.
770        for (final K fullKey : itemsToRemove)
771        {
772            // Don't add to recycle bin here
773            // https://issues.apache.org/jira/browse/JCS-67
774            performSingleKeyRemoval(fullKey);
775            removed = true;
776            // TODO this needs to update the remove count separately
777        }
778
779        return removed;
780    }
781
782    /**
783     * Remove all elements from the group. This does not use the iterator to remove. It builds a
784     * list of group elements and then removes them one by one.
785     * <p>
786     * This operates under a lock obtained in doRemove().
787     * <p>
788     *
789     * @param key
790     * @return true if an element was removed
791     */
792    private boolean performGroupRemoval(final GroupId key)
793    {
794        boolean removed = false;
795
796        // remove all keys of the same name group.
797        final List<K> itemsToRemove = new LinkedList<>();
798
799        // remove all keys of the same name hierarchy.
800        for (final K k : keyHash.keySet())
801        {
802            if (k instanceof GroupAttrName && ((GroupAttrName<?>) k).groupId.equals(key))
803            {
804                itemsToRemove.add(k);
805            }
806        }
807
808        // remove matches.
809        for (final K fullKey : itemsToRemove)
810        {
811            // Don't add to recycle bin here
812            // https://issues.apache.org/jira/browse/JCS-67
813            performSingleKeyRemoval(fullKey);
814            removed = true;
815            // TODO this needs to update the remove count separately
816        }
817
818        return removed;
819    }
820
821    /**
822     * Removes an individual key from the cache.
823     * <p>
824     * This operates under a lock obtained in doRemove().
825     * <p>
826     *
827     * @param key
828     * @return true if an item was removed.
829     */
830    private boolean performSingleKeyRemoval(final K key)
831    {
832        final boolean removed;
833        // remove single item.
834        final IndexedDiskElementDescriptor ded = keyHash.remove(key);
835        removed = ded != null;
836        addToRecycleBin(ded);
837
838        log.debug("{0}: Disk removal: Removed from key hash, key [{1}] removed = {2}",
839                logCacheName, key, removed);
840        return removed;
841    }
842
843    /**
844     * Remove all the items from the disk cache by resetting everything.
845     */
846    @Override
847    public void processRemoveAll()
848    {
849        final ICacheEvent<String> cacheEvent =
850                createICacheEvent(getCacheName(), "all", ICacheEventLogger.REMOVEALL_EVENT);
851        try
852        {
853            reset();
854        }
855        finally
856        {
857            logICacheEvent(cacheEvent);
858        }
859    }
860
861    /**
862     * Reset effectively clears the disk cache, creating new files, recycle bins, and keymaps.
863     * <p>
864     * It can be used to handle errors by last resort, force content update, or removeall.
865     */
866    private void reset()
867    {
868        log.info("{0}: Resetting cache", logCacheName);
869
870        try
871        {
872            storageLock.writeLock().lock();
873
874            if (dataFile != null)
875            {
876                dataFile.close();
877            }
878
879            final File dataFileTemp = new File(rafDir, fileName + ".data");
880            Files.delete(dataFileTemp.toPath());
881
882            if (keyFile != null)
883            {
884                keyFile.close();
885            }
886            final File keyFileTemp = new File(rafDir, fileName + ".key");
887            Files.delete(keyFileTemp.toPath());
888
889            dataFile = new IndexedDisk(dataFileTemp, getElementSerializer());
890            keyFile = new IndexedDisk(keyFileTemp, getElementSerializer());
891
892            this.recycle.clear();
893            this.keyHash.clear();
894        }
895        catch (final IOException e)
896        {
897            log.error("{0}: Failure resetting state", logCacheName, e);
898        }
899        finally
900        {
901            storageLock.writeLock().unlock();
902        }
903    }
904
905    /**
906     * Create the map for keys that contain the index position on disk.
907     *
908     * @return a new empty Map for keys and IndexedDiskElementDescriptors
909     */
910    private Map<K, IndexedDiskElementDescriptor> createInitialKeyMap()
911    {
912        Map<K, IndexedDiskElementDescriptor> keyMap = null;
913        if (maxKeySize >= 0)
914        {
915            if (this.diskLimitType == DiskLimitType.COUNT)
916            {
917                keyMap = new LRUMapCountLimited(maxKeySize);
918            }
919            else
920            {
921                keyMap = new LRUMapSizeLimited(maxKeySize);
922            }
923
924            log.info("{0}: Set maxKeySize to: \"{1}\"", logCacheName, maxKeySize);
925        }
926        else
927        {
928            // If no max size, use a plain map for memory and processing efficiency.
929            keyMap = new HashMap<>();
930            // keyHash = Collections.synchronizedMap( new HashMap() );
931            log.info("{0}: Set maxKeySize to unlimited", logCacheName);
932        }
933
934        return keyMap;
935    }
936
937    /**
938     * Dispose of the disk cache in a background thread. Joins against this thread to put a cap on
939     * the disposal time.
940     * <p>
941     * TODO make dispose window configurable.
942     */
943    @Override
944    public void processDispose()
945    {
946        final ICacheEvent<String> cacheEvent = createICacheEvent(getCacheName(), "none", ICacheEventLogger.DISPOSE_EVENT);
947        try
948        {
949            final Thread t = new Thread(this::disposeInternal, "IndexedDiskCache-DisposalThread");
950            t.start();
951            // wait up to 60 seconds for dispose and then quit if not done.
952            try
953            {
954                t.join(60 * 1000);
955            }
956            catch (final InterruptedException ex)
957            {
958                log.error("{0}: Interrupted while waiting for disposal thread to finish.",
959                        logCacheName, ex);
960            }
961        }
962        finally
963        {
964            logICacheEvent(cacheEvent);
965        }
966    }
967
968    /**
969     * Internal method that handles the disposal.
970     */
971    protected void disposeInternal()
972    {
973        if (!isAlive())
974        {
975            log.error("{0}: Not alive and dispose was called, filename: {1}",
976                    logCacheName, fileName);
977            return;
978        }
979
980        // Prevents any interaction with the cache while we're shutting down.
981        setAlive(false);
982
983        final Thread optimizationThread = currentOptimizationThread;
984        if (isRealTimeOptimizationEnabled && optimizationThread != null)
985        {
986            // Join with the current optimization thread.
987            log.debug("{0}: In dispose, optimization already in progress; waiting for completion.",
988                    logCacheName);
989
990            try
991            {
992                optimizationThread.join();
993            }
994            catch (final InterruptedException e)
995            {
996                log.error("{0}: Unable to join current optimization thread.",
997                        logCacheName, e);
998            }
999        }
1000        else if (isShutdownOptimizationEnabled && this.getBytesFree() > 0)
1001        {
1002            optimizeFile();
1003        }
1004
1005        saveKeys();
1006
1007        try
1008        {
1009            log.debug("{0}: Closing files, base filename: {1}", logCacheName,
1010                    fileName);
1011            dataFile.close();
1012            dataFile = null;
1013            keyFile.close();
1014            keyFile = null;
1015        }
1016        catch (final IOException e)
1017        {
1018            log.error("{0}: Failure closing files in dispose, filename: {1}",
1019                    logCacheName, fileName, e);
1020        }
1021
1022        log.info("{0}: Shutdown complete.", logCacheName);
1023    }
1024
1025    /**
1026     * Add descriptor to recycle bin if it is not null. Adds the length of the item to the bytes
1027     * free.
1028     * <p>
1029     * This is called in three places: (1) When an item is removed. All item removals funnel down to the removeSingleItem method.
1030     * (2) When an item on disk is updated with a value that will not fit in the previous slot. (3) When the max key size is
1031     * reached, the freed slot will be added.
1032     * <p>
1033     *
1034     * @param ded
1035     */
1036    protected void addToRecycleBin(final IndexedDiskElementDescriptor ded)
1037    {
1038        // reuse the spot
1039        if (ded != null)
1040        {
1041            storageLock.readLock().lock();
1042
1043            try
1044            {
1045                adjustBytesFree(ded, true);
1046
1047                if (doRecycle)
1048                {
1049                    recycle.add(ded);
1050                    log.debug("{0}: recycled ded {1}", logCacheName, ded);
1051                }
1052            }
1053            finally
1054            {
1055                storageLock.readLock().unlock();
1056            }
1057        }
1058    }
1059
1060    /**
1061     * Performs the check for optimization, and if it is required, do it.
1062     */
1063    protected void doOptimizeRealTime()
1064    {
1065        if (isRealTimeOptimizationEnabled && !isOptimizing
1066            && removeCount++ >= cattr.getOptimizeAtRemoveCount())
1067        {
1068            isOptimizing = true;
1069
1070            log.info("{0}: Optimizing file. removeCount [{1}] OptimizeAtRemoveCount [{2}]",
1071                    logCacheName, removeCount, cattr.getOptimizeAtRemoveCount());
1072
1073            if (currentOptimizationThread == null)
1074            {
1075                storageLock.writeLock().lock();
1076
1077                try
1078                {
1079                    if (currentOptimizationThread == null)
1080                    {
1081                        currentOptimizationThread = new Thread(() -> {
1082                            optimizeFile();
1083                            currentOptimizationThread = null;
1084                        }, "IndexedDiskCache-OptimizationThread");
1085                    }
1086                }
1087                finally
1088                {
1089                    storageLock.writeLock().unlock();
1090                }
1091
1092                if (currentOptimizationThread != null)
1093                {
1094                    currentOptimizationThread.start();
1095                }
1096            }
1097        }
1098    }
1099
1100    /**
1101     * File optimization is handled by this method. It works as follows:
1102     * <ol>
1103     * <li>Shutdown recycling and turn on queuing of puts.</li>
1104     * <li>Take a snapshot of the current descriptors. If there are any removes, ignore them, as they will be compacted during the
1105     * next optimization.</li>
1106     * <li>Optimize the snapshot. For each descriptor:
1107     * <ol>
1108     * <li>Obtain the write-lock.</li>
1109     * <li>Shift the element on the disk, in order to compact out the free space.</li>
1110     * <li>Release the write-lock. This allows elements to still be accessible during optimization.</li>
1111     * </ol>
1112     * </li>
1113     * <li>Obtain the write-lock.</li>
1114     * <li>All queued puts are made at the end of the file. Optimize these under a single write-lock.</li>
1115     * <li>Truncate the file.</li>
1116     * <li>Release the write-lock.</li>
1117     * <li>Restore system to standard operation.</li>
1118     * </ol>
1119     */
1120    protected void optimizeFile()
1121    {
1122        final ElapsedTimer timer = new ElapsedTimer();
1123        timesOptimized++;
1124        log.info("{0}: Beginning Optimization #{1}", logCacheName, timesOptimized);
1125
1126        // CREATE SNAPSHOT
1127        IndexedDiskElementDescriptor[] defragList = null;
1128
1129        storageLock.writeLock().lock();
1130
1131        try
1132        {
1133            queueInput = true;
1134            // shut off recycle while we're optimizing,
1135            doRecycle = false;
1136            defragList = createPositionSortedDescriptorList();
1137        }
1138        finally
1139        {
1140            storageLock.writeLock().unlock();
1141        }
1142
1143        // Defrag the file outside of the write lock. This allows a move to be made,
1144        // and yet have the element still accessible for reading or writing.
1145        long expectedNextPos = defragFile(defragList, 0);
1146
1147        // ADD THE QUEUED ITEMS to the end and then truncate
1148        storageLock.writeLock().lock();
1149
1150        try
1151        {
1152            try
1153            {
1154                if (!queuedPutList.isEmpty())
1155                {
1156                    defragList = queuedPutList.toArray(new IndexedDiskElementDescriptor[0]);
1157
1158                    // pack them at the end
1159                    expectedNextPos = defragFile(defragList, expectedNextPos);
1160                }
1161                // TRUNCATE THE FILE
1162                dataFile.truncate(expectedNextPos);
1163            }
1164            catch (final IOException e)
1165            {
1166                log.error("{0}: Error optimizing queued puts.", logCacheName, e);
1167            }
1168
1169            // RESTORE NORMAL OPERATION
1170            removeCount = 0;
1171            resetBytesFree();
1172            this.recycle.clear();
1173            queuedPutList.clear();
1174            queueInput = false;
1175            // turn recycle back on.
1176            doRecycle = true;
1177            isOptimizing = false;
1178        }
1179        finally
1180        {
1181            storageLock.writeLock().unlock();
1182        }
1183
1184        log.info("{0}: Finished #{1}, Optimization took {2}",
1185                logCacheName, timesOptimized, timer.getElapsedTimeString());
1186    }
1187
1188    /**
1189     * Defragments the file in place by compacting out the free space (i.e., moving records
1190     * forward). If there were no gaps the resulting file would be the same size as the previous
1191     * file. This must be supplied an ordered defragList.
1192     * <p>
1193     *
1194     * @param defragList
1195     *            sorted list of descriptors for optimization
1196     * @param startingPos
1197     *            the start position in the file
1198     * @return this is the potential new file end
1199     */
1200    private long defragFile(final IndexedDiskElementDescriptor[] defragList, final long startingPos)
1201    {
1202        final ElapsedTimer timer = new ElapsedTimer();
1203        long preFileSize = 0;
1204        long postFileSize = 0;
1205        long expectedNextPos = 0;
1206        try
1207        {
1208            preFileSize = this.dataFile.length();
1209            // find the first gap in the disk and start defragging.
1210            expectedNextPos = startingPos;
1211            for (final IndexedDiskElementDescriptor element : defragList) {
1212                storageLock.writeLock().lock();
1213                try
1214                {
1215                    if (expectedNextPos != element.pos)
1216                    {
1217                        dataFile.move(element, expectedNextPos);
1218                    }
1219                    expectedNextPos = element.pos + IndexedDisk.HEADER_SIZE_BYTES + element.len;
1220                }
1221                finally
1222                {
1223                    storageLock.writeLock().unlock();
1224                }
1225            }
1226
1227            postFileSize = this.dataFile.length();
1228
1229            // this is the potential new file end
1230            return expectedNextPos;
1231        }
1232        catch (final IOException e)
1233        {
1234            log.error("{0}: Error occurred during defragmentation.", logCacheName, e);
1235        }
1236        finally
1237        {
1238            log.info("{0}: Defragmentation took {1}. File Size (before={2}) (after={3}) (truncating to {4})",
1239                    logCacheName, timer.getElapsedTimeString(), preFileSize, postFileSize, expectedNextPos);
1240        }
1241
1242        return 0;
1243    }
1244
1245    /**
1246     * Creates a snapshot of the IndexedDiskElementDescriptors in the keyHash and returns them
1247     * sorted by position in the dataFile.
1248     * <p>
1249     *
1250     * @return IndexedDiskElementDescriptor[]
1251     */
1252    private IndexedDiskElementDescriptor[] createPositionSortedDescriptorList()
1253    {
1254        final List<IndexedDiskElementDescriptor> defragList = new ArrayList<>(keyHash.values());
1255        Collections.sort(defragList, (ded1, ded2) -> Long.compare(ded1.pos, ded2.pos));
1256
1257        return defragList.toArray(new IndexedDiskElementDescriptor[0]);
1258    }
1259
1260    /**
1261     * Returns the current cache size.
1262     * <p>
1263     *
1264     * @return The size value
1265     */
1266    @Override
1267    public int getSize()
1268    {
1269        return keyHash.size();
1270    }
1271
1272    /**
1273     * Returns the size of the recycle bin in number of elements.
1274     * <p>
1275     *
1276     * @return The number of items in the bin.
1277     */
1278    protected int getRecyleBinSize()
1279    {
1280        return this.recycle.size();
1281    }
1282
1283    /**
1284     * Returns the number of times we have used spots from the recycle bin.
1285     * <p>
1286     *
1287     * @return The number of spots used.
1288     */
1289    protected int getRecyleCount()
1290    {
1291        return this.recycleCnt;
1292    }
1293
1294    /**
1295     * Returns the number of bytes that are free. When an item is removed, its length is recorded.
1296     * When a spot is used form the recycle bin, the length of the item stored is recorded.
1297     * <p>
1298     *
1299     * @return The number bytes free on the disk file.
1300     */
1301    protected long getBytesFree()
1302    {
1303        return this.bytesFree.get();
1304    }
1305
1306    /**
1307     * Resets the number of bytes that are free.
1308     */
1309    private void resetBytesFree()
1310    {
1311        this.bytesFree.set(0);
1312    }
1313
1314    /**
1315     * To subtract you can pass in false for add..
1316     * <p>
1317     *
1318     * @param ded
1319     * @param add
1320     */
1321    private void adjustBytesFree(final IndexedDiskElementDescriptor ded, final boolean add)
1322    {
1323        if (ded != null)
1324        {
1325            final int amount = ded.len + IndexedDisk.HEADER_SIZE_BYTES;
1326
1327            if (add)
1328            {
1329                this.bytesFree.addAndGet(amount);
1330            }
1331            else
1332            {
1333                this.bytesFree.addAndGet(-amount);
1334            }
1335        }
1336    }
1337
1338    /**
1339     * This is for debugging and testing.
1340     * <p>
1341     *
1342     * @return the length of the data file.
1343     * @throws IOException
1344     */
1345    protected long getDataFileSize() throws IOException
1346    {
1347        long size = 0;
1348
1349        storageLock.readLock().lock();
1350
1351        try
1352        {
1353            if (dataFile != null)
1354            {
1355                size = dataFile.length();
1356            }
1357        }
1358        finally
1359        {
1360            storageLock.readLock().unlock();
1361        }
1362
1363        return size;
1364    }
1365
1366    /**
1367     * For debugging. This dumps the values by default.
1368     */
1369    public void dump()
1370    {
1371        dump(true);
1372    }
1373
1374    /**
1375     * For debugging.
1376     * <p>
1377     *
1378     * @param dumpValues
1379     *            A boolean indicating if values should be dumped.
1380     */
1381    public void dump(final boolean dumpValues)
1382    {
1383        if (log.isTraceEnabled())
1384        {
1385            log.trace("{0}: [dump] Number of keys: {1}", logCacheName, keyHash.size());
1386
1387            for (final Map.Entry<K, IndexedDiskElementDescriptor> e : keyHash.entrySet())
1388            {
1389                final K key = e.getKey();
1390                final IndexedDiskElementDescriptor ded = e.getValue();
1391
1392                log.trace("{0}: [dump] Disk element, key: {1}, pos: {2}, len: {3}" +
1393                        (dumpValues ? ", val: " + get(key) : ""),
1394                        logCacheName, key, ded.pos, ded.len);
1395            }
1396        }
1397    }
1398
1399    /**
1400     * @return Returns the AuxiliaryCacheAttributes.
1401     */
1402    @Override
1403    public AuxiliaryCacheAttributes getAuxiliaryCacheAttributes()
1404    {
1405        return this.cattr;
1406    }
1407
1408    /**
1409     * Returns info about the disk cache.
1410     * <p>
1411     *
1412     * @see org.apache.commons.jcs3.auxiliary.AuxiliaryCache#getStatistics()
1413     */
1414    @Override
1415    public synchronized IStats getStatistics()
1416    {
1417        final IStats stats = new Stats();
1418        stats.setTypeName("Indexed Disk Cache");
1419
1420        final ArrayList<IStatElement<?>> elems = new ArrayList<>();
1421
1422        elems.add(new StatElement<>("Is Alive", Boolean.valueOf(isAlive())));
1423        elems.add(new StatElement<>("Key Map Size", Integer.valueOf(this.keyHash != null ? this.keyHash.size() : -1)));
1424        try
1425        {
1426            elems.add(
1427                    new StatElement<>("Data File Length", Long.valueOf(this.dataFile != null ? this.dataFile.length() : -1L)));
1428        }
1429        catch (final IOException e)
1430        {
1431            log.error(e);
1432        }
1433        elems.add(new StatElement<>("Max Key Size", this.maxKeySize));
1434        elems.add(new StatElement<>("Hit Count", this.hitCount));
1435        elems.add(new StatElement<>("Bytes Free", this.bytesFree));
1436        elems.add(new StatElement<>("Optimize Operation Count", Integer.valueOf(this.removeCount)));
1437        elems.add(new StatElement<>("Times Optimized", Integer.valueOf(this.timesOptimized)));
1438        elems.add(new StatElement<>("Recycle Count", Integer.valueOf(this.recycleCnt)));
1439        elems.add(new StatElement<>("Recycle Bin Size", Integer.valueOf(this.recycle.size())));
1440        elems.add(new StatElement<>("Startup Size", Integer.valueOf(this.startupSize)));
1441
1442        // get the stats from the super too
1443        final IStats sStats = super.getStatistics();
1444        elems.addAll(sStats.getStatElements());
1445
1446        stats.setStatElements(elems);
1447
1448        return stats;
1449    }
1450
1451    /**
1452     * This is exposed for testing.
1453     * <p>
1454     *
1455     * @return Returns the timesOptimized.
1456     */
1457    protected int getTimesOptimized()
1458    {
1459        return timesOptimized;
1460    }
1461
1462    /**
1463     * This is used by the event logging.
1464     * <p>
1465     *
1466     * @return the location of the disk, either path or ip.
1467     */
1468    @Override
1469    protected String getDiskLocation()
1470    {
1471        return dataFile.getFilePath();
1472    }
1473
1474    /**
1475     * Compares IndexedDiskElementDescriptor based on their position.
1476     * <p>
1477     * @deprecated Use lambda instead
1478     */
1479    @Deprecated
1480    protected static final class PositionComparator implements Comparator<IndexedDiskElementDescriptor>, Serializable
1481    {
1482        /** serialVersionUID */
1483        private static final long serialVersionUID = -8387365338590814113L;
1484
1485        /**
1486         * Compares two descriptors based on position.
1487         * <p>
1488         *
1489         * @see java.util.Comparator#compare(Object, Object)
1490         */
1491        @Override
1492        public int compare(final IndexedDiskElementDescriptor ded1, final IndexedDiskElementDescriptor ded2)
1493        {
1494            return Long.compare(ded1.pos, ded2.pos);
1495        }
1496    }
1497
1498    /**
1499     * Class for recycling and lru. This implements the LRU overflow callback, so we can add items
1500     * to the recycle bin. This class counts the size element to decide, when to throw away an element
1501     */
1502    public class LRUMapSizeLimited extends AbstractLRUMap<K, IndexedDiskElementDescriptor>
1503    {
1504        /**
1505         * <code>tag</code> tells us which map we are working on.
1506         */
1507        public static final String TAG = "orig";
1508
1509        // size of the content in kB
1510        private final AtomicInteger contentSize;
1511        private final int maxSize;
1512
1513        /**
1514         * Default
1515         */
1516        public LRUMapSizeLimited()
1517        {
1518            this(-1);
1519        }
1520
1521        /**
1522         * @param maxKeySize
1523         */
1524        public LRUMapSizeLimited(final int maxKeySize)
1525        {
1526            this.maxSize = maxKeySize;
1527            this.contentSize = new AtomicInteger(0);
1528        }
1529
1530        // keep the content size in kB, so 2^31 kB is reasonable value
1531        private void subLengthFromCacheSize(final IndexedDiskElementDescriptor value)
1532        {
1533            contentSize.addAndGet((value.len + IndexedDisk.HEADER_SIZE_BYTES) / -1024 - 1);
1534        }
1535
1536        // keep the content size in kB, so 2^31 kB is reasonable value
1537        private void addLengthToCacheSize(final IndexedDiskElementDescriptor value)
1538        {
1539            contentSize.addAndGet((value.len + IndexedDisk.HEADER_SIZE_BYTES) / 1024 + 1);
1540        }
1541
1542        @Override
1543        public IndexedDiskElementDescriptor put(final K key, final IndexedDiskElementDescriptor value)
1544        {
1545            IndexedDiskElementDescriptor oldValue = null;
1546
1547            try
1548            {
1549                oldValue = super.put(key, value);
1550            }
1551            finally
1552            {
1553                // keep the content size in kB, so 2^31 kB is reasonable value
1554                if (value != null)
1555                {
1556                    addLengthToCacheSize(value);
1557                }
1558                if (oldValue != null)
1559                {
1560                    subLengthFromCacheSize(oldValue);
1561                }
1562            }
1563
1564            return oldValue;
1565        }
1566
1567        @Override
1568        public IndexedDiskElementDescriptor remove(final Object key)
1569        {
1570            IndexedDiskElementDescriptor value = null;
1571
1572            try
1573            {
1574                value = super.remove(key);
1575                return value;
1576            }
1577            finally
1578            {
1579                if (value != null)
1580                {
1581                    subLengthFromCacheSize(value);
1582                }
1583            }
1584        }
1585
1586        /**
1587         * This is called when the may key size is reached. The least recently used item will be
1588         * passed here. We will store the position and size of the spot on disk in the recycle bin.
1589         * <p>
1590         *
1591         * @param key
1592         * @param value
1593         */
1594        @Override
1595        protected void processRemovedLRU(final K key, final IndexedDiskElementDescriptor value)
1596        {
1597            if (value != null)
1598            {
1599                subLengthFromCacheSize(value);
1600            }
1601
1602            addToRecycleBin(value);
1603
1604            log.debug("{0}: Removing key: [{1}] from key store.", logCacheName, key);
1605            log.debug("{0}: Key store size: [{1}].", logCacheName, this.size());
1606
1607            doOptimizeRealTime();
1608        }
1609
1610        @Override
1611        protected boolean shouldRemove()
1612        {
1613            return maxSize > 0 && contentSize.get() > maxSize && !this.isEmpty();
1614        }
1615    }
1616
1617    /**
1618     * Class for recycling and lru. This implements the LRU overflow callback, so we can add items
1619     * to the recycle bin. This class counts the elements to decide, when to throw away an element
1620     */
1621
1622    public class LRUMapCountLimited extends LRUMap<K, IndexedDiskElementDescriptor>
1623    // implements Serializable
1624    {
1625        public LRUMapCountLimited(final int maxKeySize)
1626        {
1627            super(maxKeySize);
1628        }
1629
1630        /**
1631         * This is called when the may key size is reached. The least recently used item will be
1632         * passed here. We will store the position and size of the spot on disk in the recycle bin.
1633         * <p>
1634         *
1635         * @param key
1636         * @param value
1637         */
1638        @Override
1639        protected void processRemovedLRU(final K key, final IndexedDiskElementDescriptor value)
1640        {
1641            addToRecycleBin(value);
1642            log.debug("{0}: Removing key: [{1}] from key store.", logCacheName, key);
1643            log.debug("{0}: Key store size: [{1}].", logCacheName, this.size());
1644
1645            doOptimizeRealTime();
1646        }
1647    }
1648}