001package org.apache.commons.jcs3.auxiliary.disk.block;
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.util.ArrayList;
025import java.util.Arrays;
026import java.util.HashSet;
027import java.util.List;
028import java.util.Map;
029import java.util.Map.Entry;
030import java.util.Set;
031import java.util.concurrent.ScheduledExecutorService;
032import java.util.concurrent.ScheduledFuture;
033import java.util.concurrent.TimeUnit;
034import java.util.concurrent.locks.ReentrantReadWriteLock;
035import java.util.stream.Collectors;
036
037import org.apache.commons.jcs3.auxiliary.AuxiliaryCacheAttributes;
038import org.apache.commons.jcs3.auxiliary.disk.AbstractDiskCache;
039import org.apache.commons.jcs3.engine.behavior.ICacheElement;
040import org.apache.commons.jcs3.engine.behavior.IElementSerializer;
041import org.apache.commons.jcs3.engine.behavior.IRequireScheduler;
042import org.apache.commons.jcs3.engine.control.group.GroupAttrName;
043import org.apache.commons.jcs3.engine.control.group.GroupId;
044import org.apache.commons.jcs3.engine.stats.StatElement;
045import org.apache.commons.jcs3.engine.stats.Stats;
046import org.apache.commons.jcs3.engine.stats.behavior.IStatElement;
047import org.apache.commons.jcs3.engine.stats.behavior.IStats;
048import org.apache.commons.jcs3.log.Log;
049import org.apache.commons.jcs3.log.LogManager;
050import org.apache.commons.jcs3.utils.serialization.StandardSerializer;
051
052/**
053 * There is one BlockDiskCache per region. It manages the key and data store.
054 */
055public class BlockDiskCache<K, V>
056    extends AbstractDiskCache<K, V>
057    implements IRequireScheduler
058{
059    /** The logger. */
060    private static final Log log = LogManager.getLog( BlockDiskCache.class );
061
062    /** The name to prefix all log messages with. */
063    private final String logCacheName;
064
065    /** The name of the file to store data. */
066    private final String fileName;
067
068    /** The data access object */
069    private BlockDisk dataFile;
070
071    /** Attributes governing the behavior of the block disk cache. */
072    private final BlockDiskCacheAttributes blockDiskCacheAttributes;
073
074    /** The root directory for keys and data. */
075    private final File rootDirectory;
076
077    /** Store, loads, and persists the keys */
078    private BlockDiskKeyStore<K> keyStore;
079
080    /**
081     * Use this lock to synchronize reads and writes to the underlying storage mechanism. We don't
082     * need a reentrant lock, since we only lock one level.
083     */
084    private final ReentrantReadWriteLock storageLock = new ReentrantReadWriteLock();
085
086    private ScheduledFuture<?> future;
087
088    /**
089     * Constructs the BlockDisk after setting up the root directory.
090     * <p>
091     * @param cacheAttributes
092     */
093    public BlockDiskCache( final BlockDiskCacheAttributes cacheAttributes )
094    {
095        this( cacheAttributes, new StandardSerializer() );
096    }
097
098    /**
099     * Constructs the BlockDisk after setting up the root directory.
100     * <p>
101     * @param cacheAttributes
102     * @param elementSerializer used if supplied, the super's super will not set a null
103     */
104    public BlockDiskCache( final BlockDiskCacheAttributes cacheAttributes, final IElementSerializer elementSerializer )
105    {
106        super( cacheAttributes );
107        setElementSerializer( elementSerializer );
108
109        this.blockDiskCacheAttributes = cacheAttributes;
110        this.logCacheName = "Region [" + getCacheName() + "] ";
111
112        log.info("{0}: Constructing BlockDiskCache with attributes {1}", logCacheName, cacheAttributes );
113
114        // Make a clean file name
115        this.fileName = getCacheName().replaceAll("[^a-zA-Z0-9-_\\.]", "_");
116        this.rootDirectory = cacheAttributes.getDiskPath();
117
118        log.info("{0}: Cache file root directory: [{1}]", logCacheName, rootDirectory);
119
120        try
121        {
122            if ( this.blockDiskCacheAttributes.getBlockSizeBytes() > 0 )
123            {
124                this.dataFile = new BlockDisk( new File( rootDirectory, fileName + ".data" ),
125                                               this.blockDiskCacheAttributes.getBlockSizeBytes(),
126                                               getElementSerializer() );
127            }
128            else
129            {
130                this.dataFile = new BlockDisk( new File( rootDirectory, fileName + ".data" ),
131                                               getElementSerializer() );
132            }
133
134            keyStore = new BlockDiskKeyStore<>( this.blockDiskCacheAttributes, this );
135
136            final boolean alright = verifyDisk();
137
138            if ( keyStore.isEmpty() || !alright )
139            {
140                this.reset();
141            }
142
143            // Initialization finished successfully, so set alive to true.
144            setAlive(true);
145            log.info("{0}: Block Disk Cache is alive.", logCacheName);
146        }
147        catch ( final IOException e )
148        {
149            log.error("{0}: Failure initializing for fileName: {1} and root directory: {2}",
150                    logCacheName, fileName, rootDirectory, e);
151        }
152    }
153
154    /**
155     * @see org.apache.commons.jcs3.engine.behavior.IRequireScheduler#setScheduledExecutorService(java.util.concurrent.ScheduledExecutorService)
156     */
157    @Override
158    public void setScheduledExecutorService(final ScheduledExecutorService scheduledExecutor)
159    {
160        // add this region to the persistence thread.
161        // TODO we might need to stagger this a bit.
162        if ( this.blockDiskCacheAttributes.getKeyPersistenceIntervalSeconds() > 0 )
163        {
164            future = scheduledExecutor.scheduleAtFixedRate(keyStore::saveKeys,
165                    this.blockDiskCacheAttributes.getKeyPersistenceIntervalSeconds(),
166                    this.blockDiskCacheAttributes.getKeyPersistenceIntervalSeconds(),
167                    TimeUnit.SECONDS);
168        }
169    }
170
171    /**
172     * We need to verify that the file on disk uses the same block size and that the file is the
173     * proper size.
174     * <p>
175     * @return true if it looks ok
176     */
177    protected boolean verifyDisk()
178    {
179        boolean alright = false;
180        // simply try to read a few. If it works, then the file is probably ok.
181        // TODO add more.
182
183        storageLock.readLock().lock();
184
185        try
186        {
187            this.keyStore.entrySet().stream()
188                .limit(100)
189                .forEach(entry -> {
190                    try
191                    {
192                        final Object data = this.dataFile.read(entry.getValue());
193                        if ( data == null )
194                        {
195                            throw new IOException("Data is null");
196                        }
197                    }
198                    catch (final IOException | ClassNotFoundException e)
199                    {
200                        throw new RuntimeException(logCacheName
201                                + " Couldn't find data for key [" + entry.getKey() + "]", e);
202                    }
203                });
204            alright = true;
205        }
206        catch ( final Exception e )
207        {
208            log.warn("{0}: Problem verifying disk.", logCacheName, e);
209            alright = false;
210        }
211        finally
212        {
213            storageLock.readLock().unlock();
214        }
215
216        return alright;
217    }
218
219    /**
220     * Return the keys in this cache.
221     * <p>
222     * @see org.apache.commons.jcs3.auxiliary.disk.AbstractDiskCache#getKeySet()
223     */
224    @Override
225    public Set<K> getKeySet() throws IOException
226    {
227        final HashSet<K> keys = new HashSet<>();
228
229        storageLock.readLock().lock();
230
231        try
232        {
233            keys.addAll(this.keyStore.keySet());
234        }
235        finally
236        {
237            storageLock.readLock().unlock();
238        }
239
240        return keys;
241    }
242
243    /**
244     * Gets matching items from the cache.
245     * <p>
246     * @param pattern
247     * @return a map of K key to ICacheElement&lt;K, V&gt; element, or an empty map if there is no
248     *         data in cache matching keys
249     */
250    @Override
251    public Map<K, ICacheElement<K, V>> processGetMatching( final String pattern )
252    {
253        Set<K> keyArray = null;
254        storageLock.readLock().lock();
255        try
256        {
257            keyArray = new HashSet<>(keyStore.keySet());
258        }
259        finally
260        {
261            storageLock.readLock().unlock();
262        }
263
264        final Set<K> matchingKeys = getKeyMatcher().getMatchingKeysFromArray( pattern, keyArray );
265
266        return matchingKeys.stream()
267            .collect(Collectors.toMap(
268                    key -> key,
269                    this::processGet)).entrySet().stream()
270                .filter(entry -> entry.getValue() != null)
271                .collect(Collectors.toMap(
272                        Entry::getKey,
273                        Entry::getValue));
274    }
275
276    /**
277     * Returns the number of keys.
278     * <p>
279     * (non-Javadoc)
280     * @see org.apache.commons.jcs3.auxiliary.disk.AbstractDiskCache#getSize()
281     */
282    @Override
283    public int getSize()
284    {
285        return this.keyStore.size();
286    }
287
288    /**
289     * Gets the ICacheElement&lt;K, V&gt; for the key if it is in the cache. The program flow is as follows:
290     * <ol>
291     * <li>Make sure the disk cache is alive.</li> <li>Get a read lock.</li> <li>See if the key is
292     * in the key store.</li> <li>If we found a key, ask the BlockDisk for the object at the
293     * blocks..</li> <li>Release the lock.</li>
294     * </ol>
295     * @param key
296     * @return ICacheElement
297     * @see org.apache.commons.jcs3.auxiliary.disk.AbstractDiskCache#get(Object)
298     */
299    @Override
300    protected ICacheElement<K, V> processGet( final K key )
301    {
302        if ( !isAlive() )
303        {
304            log.debug("{0}: No longer alive so returning null for key = {1}", logCacheName, key );
305            return null;
306        }
307
308        log.debug("{0}: Trying to get from disk: {1}", logCacheName, key );
309
310        ICacheElement<K, V> object = null;
311
312
313        try
314        {
315            storageLock.readLock().lock();
316            try {
317                final int[] ded = this.keyStore.get( key );
318                if ( ded != null )
319                {
320                    object = this.dataFile.read( ded );
321                }
322            } finally {
323                storageLock.readLock().unlock();
324            }
325
326        }
327        catch ( final IOException ioe )
328        {
329            log.error("{0}: Failure getting from disk--IOException, key = {1}", logCacheName, key, ioe );
330            reset();
331        }
332        catch ( final Exception e )
333        {
334            log.error("{0}: Failure getting from disk, key = {1}", logCacheName, key, e );
335        }
336        return object;
337    }
338
339    /**
340     * Writes an element to disk. The program flow is as follows:
341     * <ol>
342     * <li>Acquire write lock.</li> <li>See id an item exists for this key.</li> <li>If an item
343     * already exists, add its blocks to the remove list.</li> <li>Have the Block disk write the
344     * item.</li> <li>Create a descriptor and add it to the key map.</li> <li>Release the write
345     * lock.</li>
346     * </ol>
347     * @param element
348     * @see org.apache.commons.jcs3.auxiliary.disk.AbstractDiskCache#update(ICacheElement)
349     */
350    @Override
351    protected void processUpdate( final ICacheElement<K, V> element )
352    {
353        if ( !isAlive() )
354        {
355            log.debug("{0}: No longer alive; aborting put of key = {1}",
356                    () -> logCacheName, element::getKey);
357            return;
358        }
359
360        int[] old = null;
361
362        // make sure this only locks for one particular cache region
363        storageLock.writeLock().lock();
364
365        try
366        {
367            old = this.keyStore.get( element.getKey() );
368
369            if ( old != null )
370            {
371                this.dataFile.freeBlocks( old );
372            }
373
374            final int[] blocks = this.dataFile.write( element );
375
376            this.keyStore.put( element.getKey(), blocks );
377
378            log.debug("{0}: Put to file [{1}] key [{2}]", () -> logCacheName,
379                    () -> fileName, element::getKey);
380        }
381        catch ( final IOException e )
382        {
383            log.error("{0}: Failure updating element, key: {1} old: {2}",
384                    logCacheName, element.getKey(), Arrays.toString(old), e);
385        }
386        finally
387        {
388            storageLock.writeLock().unlock();
389        }
390
391        log.debug("{0}: Storing element on disk, key: {1}", () -> logCacheName,
392                element::getKey);
393    }
394
395    /**
396     * Returns true if the removal was successful; or false if there is nothing to remove. Current
397     * implementation always result in a disk orphan.
398     * <p>
399     * @param key
400     * @return true if removed anything
401     * @see org.apache.commons.jcs3.auxiliary.disk.AbstractDiskCache#remove(Object)
402     */
403    @Override
404    protected boolean processRemove( final K key )
405    {
406        if ( !isAlive() )
407        {
408            log.debug("{0}: No longer alive so returning false for key = {1}", logCacheName, key );
409            return false;
410        }
411
412        boolean reset = false;
413        boolean removed = false;
414
415        storageLock.writeLock().lock();
416
417        try
418        {
419            if (key instanceof String && key.toString().endsWith(NAME_COMPONENT_DELIMITER))
420            {
421                removed = performPartialKeyRemoval((String) key);
422            }
423            else if (key instanceof GroupAttrName && ((GroupAttrName<?>) key).attrName == null)
424            {
425                removed = performGroupRemoval(((GroupAttrName<?>) key).groupId);
426            }
427            else
428            {
429                removed = performSingleKeyRemoval(key);
430            }
431        }
432        catch ( final Exception e )
433        {
434            log.error("{0}: Problem removing element.", logCacheName, e );
435            reset = true;
436        }
437        finally
438        {
439            storageLock.writeLock().unlock();
440        }
441
442        if ( reset )
443        {
444            reset();
445        }
446
447        return removed;
448    }
449
450    /**
451     * Remove all elements from the group. This does not use the iterator to remove. It builds a
452     * list of group elements and then removes them one by one.
453     * <p>
454     * This operates under a lock obtained in doRemove().
455     * <p>
456     *
457     * @param key
458     * @return true if an element was removed
459     */
460    private boolean performGroupRemoval(final GroupId key)
461    {
462        // remove all keys of the same name group.
463        final List<K> itemsToRemove = keyStore.keySet()
464                .stream()
465                .filter(k -> k instanceof GroupAttrName && ((GroupAttrName<?>) k).groupId.equals(key))
466                .collect(Collectors.toList());
467
468        // remove matches.
469        // Don't add to recycle bin here
470        // https://issues.apache.org/jira/browse/JCS-67
471        itemsToRemove.forEach(this::performSingleKeyRemoval);
472        // TODO this needs to update the remove count separately
473
474        return !itemsToRemove.isEmpty();
475    }
476
477    /**
478     * Iterates over the keyset. Builds a list of matches. Removes all the keys in the list. Does
479     * not remove via the iterator, since the map impl may not support it.
480     * <p>
481     * This operates under a lock obtained in doRemove().
482     * <p>
483     *
484     * @param key
485     * @return true if there was a match
486     */
487    private boolean performPartialKeyRemoval(final String key)
488    {
489        // remove all keys of the same name hierarchy.
490        final List<K> itemsToRemove = keyStore.keySet()
491                .stream()
492                .filter(k -> k instanceof String && k.toString().startsWith(key))
493                .collect(Collectors.toList());
494
495        // remove matches.
496        // Don't add to recycle bin here
497        // https://issues.apache.org/jira/browse/JCS-67
498        itemsToRemove.forEach(this::performSingleKeyRemoval);
499        // TODO this needs to update the remove count separately
500
501        return !itemsToRemove.isEmpty();
502    }
503
504
505        private boolean performSingleKeyRemoval(final K key) {
506                final boolean removed;
507                // remove single item.
508                final int[] ded = this.keyStore.remove( key );
509                removed = ded != null;
510                if ( removed )
511                {
512                    this.dataFile.freeBlocks( ded );
513                }
514
515            log.debug("{0}: Disk removal: Removed from key hash, key [{1}] removed = {2}",
516                    logCacheName, key, removed);
517                return removed;
518        }
519
520    /**
521     * Resets the keyfile, the disk file, and the memory key map.
522     * <p>
523     * @see org.apache.commons.jcs3.auxiliary.disk.AbstractDiskCache#removeAll()
524     */
525    @Override
526    protected void processRemoveAll()
527    {
528        reset();
529    }
530
531    /**
532     * Dispose of the disk cache in a background thread. Joins against this thread to put a cap on
533     * the disposal time.
534     * <p>
535     * TODO make dispose window configurable.
536     */
537    @Override
538    public void processDispose()
539    {
540        final Thread t = new Thread(this::disposeInternal, "BlockDiskCache-DisposalThread" );
541        t.start();
542        // wait up to 60 seconds for dispose and then quit if not done.
543        try
544        {
545            t.join( 60 * 1000 );
546        }
547        catch ( final InterruptedException ex )
548        {
549            log.error("{0}: Interrupted while waiting for disposal thread to finish.",
550                    logCacheName, ex );
551        }
552    }
553
554    /**
555     * Internal method that handles the disposal.
556     */
557    protected void disposeInternal()
558    {
559        if ( !isAlive() )
560        {
561            log.error("{0}: Not alive and dispose was called, filename: {1}", logCacheName, fileName);
562            return;
563        }
564        storageLock.writeLock().lock();
565        try
566        {
567            // Prevents any interaction with the cache while we're shutting down.
568            setAlive(false);
569            this.keyStore.saveKeys();
570
571            if (future != null)
572            {
573                future.cancel(true);
574            }
575
576            try
577            {
578                log.debug("{0}: Closing files, base filename: {1}", logCacheName, fileName );
579                dataFile.close();
580                // dataFile = null;
581            }
582            catch ( final IOException e )
583            {
584                log.error("{0}: Failure closing files in dispose, filename: {1}",
585                        logCacheName, fileName, e );
586            }
587        }
588        finally
589        {
590            storageLock.writeLock().unlock();
591        }
592
593        log.info("{0}: Shutdown complete.", logCacheName);
594    }
595
596    /**
597     * Returns the attributes.
598     * <p>
599     * @see org.apache.commons.jcs3.auxiliary.AuxiliaryCache#getAuxiliaryCacheAttributes()
600     */
601    @Override
602    public AuxiliaryCacheAttributes getAuxiliaryCacheAttributes()
603    {
604        return this.blockDiskCacheAttributes;
605    }
606
607    /**
608     * Reset effectively clears the disk cache, creating new files, recycle bins, and keymaps.
609     * <p>
610     * It can be used to handle errors by last resort, force content update, or remove all.
611     */
612    private void reset()
613    {
614        log.info("{0}: Resetting cache", logCacheName);
615
616        try
617        {
618            storageLock.writeLock().lock();
619
620            this.keyStore.reset();
621
622            if ( dataFile != null )
623            {
624                dataFile.reset();
625            }
626        }
627        catch ( final IOException e )
628        {
629            log.error("{0}: Failure resetting state", logCacheName, e );
630        }
631        finally
632        {
633            storageLock.writeLock().unlock();
634        }
635    }
636
637    /**
638     * Add these blocks to the emptyBlock list.
639     * <p>
640     * @param blocksToFree
641     */
642    protected void freeBlocks( final int[] blocksToFree )
643    {
644        this.dataFile.freeBlocks( blocksToFree );
645    }
646
647    /**
648     * Returns info about the disk cache.
649     * <p>
650     * @see org.apache.commons.jcs3.auxiliary.AuxiliaryCache#getStatistics()
651     */
652    @Override
653    public IStats getStatistics()
654    {
655        final IStats stats = new Stats();
656        stats.setTypeName( "Block Disk Cache" );
657
658        final ArrayList<IStatElement<?>> elems = new ArrayList<>();
659
660        elems.add(new StatElement<>( "Is Alive", Boolean.valueOf(isAlive()) ) );
661        elems.add(new StatElement<>( "Key Map Size", Integer.valueOf(this.keyStore.size()) ) );
662
663        if (this.dataFile != null)
664        {
665            try
666            {
667                elems.add(new StatElement<>( "Data File Length", Long.valueOf(this.dataFile.length()) ) );
668            }
669            catch ( final IOException e )
670            {
671                log.error( e );
672            }
673
674            elems.add(new StatElement<>( "Block Size Bytes",
675                    Integer.valueOf(this.dataFile.getBlockSizeBytes()) ) );
676            elems.add(new StatElement<>( "Number Of Blocks",
677                    Integer.valueOf(this.dataFile.getNumberOfBlocks()) ) );
678            elems.add(new StatElement<>( "Average Put Size Bytes",
679                    Long.valueOf(this.dataFile.getAveragePutSizeBytes()) ) );
680            elems.add(new StatElement<>( "Empty Blocks",
681                    Integer.valueOf(this.dataFile.getEmptyBlocks()) ) );
682        }
683
684        // get the stats from the super too
685        final IStats sStats = super.getStatistics();
686        elems.addAll(sStats.getStatElements());
687
688        stats.setStatElements( elems );
689
690        return stats;
691    }
692
693    /**
694     * This is used by the event logging.
695     * <p>
696     * @return the location of the disk, either path or ip.
697     */
698    @Override
699    protected String getDiskLocation()
700    {
701        return dataFile.getFilePath();
702    }
703}