org.apache.jcs.auxiliary.disk.block.BlockDiskCache.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.jcs.auxiliary.disk.block.BlockDiskCache.java

Source

package org.apache.jcs.auxiliary.disk.block;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.jcs.auxiliary.AuxiliaryCacheAttributes;
import org.apache.jcs.auxiliary.disk.AbstractDiskCache;
import org.apache.jcs.engine.CacheConstants;
import org.apache.jcs.engine.behavior.ICacheElement;
import org.apache.jcs.engine.control.group.GroupAttrName;
import org.apache.jcs.engine.control.group.GroupId;
import org.apache.jcs.engine.stats.StatElement;
import org.apache.jcs.engine.stats.Stats;
import org.apache.jcs.engine.stats.behavior.IStatElement;
import org.apache.jcs.engine.stats.behavior.IStats;

import EDU.oswego.cs.dl.util.concurrent.WriterPreferenceReadWriteLock;

/**
 * There is one BlockDiskCache per region. It manages the key and data store.
 * <p>
 * @author Aaron Smuts
 */
public class BlockDiskCache extends AbstractDiskCache {
    /** Don't change */
    private static final long serialVersionUID = 1L;

    /** The logger. */
    private static final Log log = LogFactory.getLog(BlockDiskCache.class);

    /** The name to prefix all log messages with. */
    private final String logCacheName;

    /** The name of the file to store data. */
    private String fileName;

    /** The data access object */
    private BlockDisk dataFile;

    /** Attributes governing the behavior of the block disk cache. */
    private BlockDiskCacheAttributes blockDiskCacheAttributes;

    /** The root directory for keys and data. */
    private File rootDirectory;

    /** Store, loads, and persists the keys */
    private BlockDiskKeyStore keyStore;

    /**
     * Use this lock to synchronize reads and writes to the underlying storage mechansism. We don't
     * need a reentrant lock, since we only lock one level.
     */
    // private ReentrantWriterPreferenceReadWriteLock storageLock = new
    // ReentrantWriterPreferenceReadWriteLock();
    private WriterPreferenceReadWriteLock storageLock = new WriterPreferenceReadWriteLock();

    /**
     * Constructs the BlockDisk after setting up the root directory.
     * <p>
     * @param cacheAttributes
     */
    public BlockDiskCache(BlockDiskCacheAttributes cacheAttributes) {
        super(cacheAttributes);

        this.blockDiskCacheAttributes = cacheAttributes;
        this.logCacheName = "Region [" + getCacheName() + "] ";

        if (log.isInfoEnabled()) {
            log.info(logCacheName + "Constructing BlockDiskCache with attributes " + cacheAttributes);
        }

        this.fileName = getCacheName();
        String rootDirName = cacheAttributes.getDiskPath();
        this.rootDirectory = new File(rootDirName);
        this.rootDirectory.mkdirs();

        if (log.isInfoEnabled()) {
            log.info(logCacheName + "Cache file root directory: [" + rootDirName + "]");
        }

        try {
            if (this.blockDiskCacheAttributes.getBlockSizeBytes() > 0) {
                this.dataFile = new BlockDisk(new File(rootDirectory, fileName + ".data"),
                        this.blockDiskCacheAttributes.getBlockSizeBytes());
            } else {
                this.dataFile = new BlockDisk(new File(rootDirectory, fileName + ".data"));
            }

            keyStore = new BlockDiskKeyStore(this.blockDiskCacheAttributes, this);

            boolean alright = verifyDisk();

            if (keyStore.size() == 0 || !alright) {
                this.reset();
            }

            // Initialization finished successfully, so set alive to true.
            alive = true;
            if (log.isInfoEnabled()) {
                log.info(logCacheName + "Block Disk Cache is alive.");
            }
        } catch (Exception e) {
            log.error(logCacheName + "Failure initializing for fileName: " + fileName + " and root directory: "
                    + rootDirName, e);
        }
        ShutdownHook shutdownHook = new ShutdownHook();
        Runtime.getRuntime().addShutdownHook(shutdownHook);
    }

    /**
     * We need to verify that the file on disk uses the same block size and that the file is the
     * proper size.
     * <p>
     * @return true if it looks ok
     */
    protected boolean verifyDisk() {
        boolean alright = false;
        // simply try to read a few. If it works, then the file is probably ok.
        // TODO add more.
        try {
            int maxToTest = 100;
            int count = 0;
            Set keySet = this.keyStore.entrySet();
            Iterator it = keySet.iterator();
            while (it.hasNext() && count < maxToTest) {
                count++;
                Map.Entry entry = (Map.Entry) it.next();
                Object data = this.dataFile.read((int[]) entry.getValue());
                if (data == null) {
                    throw new Exception("Couldn't find data for key [" + entry.getKey() + "]");
                }
            }
            alright = true;
        } catch (Exception e) {
            log.warn("Problem verifying disk.  Message [" + e.getMessage() + "]");
            alright = false;
        }
        return alright;
    }

    /**
     * This requires a full iteration through the keys.
     * <p>
     * (non-Javadoc)
     * @see org.apache.jcs.auxiliary.disk.AbstractDiskCache#getGroupKeys(java.lang.String)
     */
    public Set getGroupKeys(String groupName) {
        GroupId groupId = new GroupId(cacheName, groupName);
        HashSet keys = new HashSet();
        try {
            storageLock.readLock().acquire();

            for (Iterator itr = this.keyStore.keySet().iterator(); itr.hasNext();) {
                Object k = itr.next();
                if (k instanceof GroupAttrName && ((GroupAttrName) k).groupId.equals(groupId)) {
                    keys.add(((GroupAttrName) k).attrName);
                }
            }
        } catch (Exception e) {
            log.error(logCacheName + "Failure getting from disk, group = " + groupName, e);
        } finally {
            storageLock.readLock().release();
        }

        return keys;
    }

    /**
     * Returns the number of keys.
     * <p>
     * (non-Javadoc)
     * @see org.apache.jcs.auxiliary.disk.AbstractDiskCache#getSize()
     */
    public int getSize() {
        return this.keyStore.size();
    }

    /**
     * Gets the ICacheElement for the key if it is in the cache. The program flow is as follows:
     * <ol>
     * <li>Make sure the disk cache is alive.</li>
     * <li>Get a read lock.</li>
     * <li>See if the key is in the key store.</li>
     * <li>If we found a key, ask the BlockDisk for the object at the blocks..</li>
     * <li>Release the lock.</li>
     * </ol>
     * (non-Javadoc)
     * @see org.apache.jcs.auxiliary.disk.AbstractDiskCache#doGet(java.io.Serializable)
     */
    protected ICacheElement doGet(Serializable key) {
        if (!alive) {
            if (log.isDebugEnabled()) {
                log.debug(logCacheName + "No longer alive so returning null for key = " + key);
            }
            return null;
        }

        if (log.isDebugEnabled()) {
            log.debug(logCacheName + "Trying to get from disk: " + key);
        }

        ICacheElement object = null;
        try {
            storageLock.readLock().acquire();
            try {
                int[] ded = this.keyStore.get(key);
                if (ded != null) {
                    object = (ICacheElement) this.dataFile.read(ded);
                }
            } finally {
                storageLock.readLock().release();
            }
        } catch (IOException ioe) {
            log.error(logCacheName + "Failure getting from disk--IOException, key = " + key, ioe);
            reset();
        } catch (Exception e) {
            log.error(logCacheName + "Failure getting from disk, key = " + key, e);
        }

        return object;
    }

    /**
     * Writes an element to disk. The program flow is as follows:
     * <ol>
     * <li>Aquire write lock.</li>
     * <li>See id an item exists for this key.</li>
     * <li>If an itme already exists, add its blocks to the remove list.</li>
     * <li>Have the Block disk write the item.</li>
     * <li>Create a descriptor and add it to the key map.</li>
     * <li>Release the write lock.</li>
     * </ol>
     * (non-Javadoc)
     * @see org.apache.jcs.auxiliary.disk.AbstractDiskCache#doUpdate(org.apache.jcs.engine.behavior.ICacheElement)
     */
    protected void doUpdate(ICacheElement element) {
        if (!alive) {
            if (log.isDebugEnabled()) {
                log.debug(logCacheName + "No longer alive; aborting put of key = " + element.getKey());
            }
            return;
        }

        int[] old = null;
        try {
            // make sure this only locks for one particular cache region
            storageLock.writeLock().acquire();
            try {
                old = this.keyStore.get(element.getKey());

                if (old != null) {
                    this.dataFile.freeBlocks(old);
                }

                int[] blocks = this.dataFile.write(element);

                this.keyStore.put(element.getKey(), blocks);
            } finally {
                storageLock.writeLock().release();
            }

            if (log.isDebugEnabled()) {
                log.debug(logCacheName + "Put to file [" + fileName + "] key [" + element.getKey() + "]");
            }
        } catch (Exception e) {
            log.error(logCacheName + "Failure updating element, key: " + element.getKey() + " old: " + old, e);
        }
        if (log.isDebugEnabled()) {
            log.debug(logCacheName + "Storing element on disk, key: " + element.getKey());
        }
    }

    /**
     * Returns true if the removal was succesful; or false if there is nothing to remove. Current
     * implementation always result in a disk orphan.
     * <p>
     * (non-Javadoc)
     * @see org.apache.jcs.auxiliary.disk.AbstractDiskCache#doRemove(java.io.Serializable)
     */
    protected boolean doRemove(Serializable key) {
        if (!alive) {
            if (log.isDebugEnabled()) {
                log.debug(logCacheName + "No longer alive so returning false for key = " + key);
            }
            return false;
        }

        boolean reset = false;
        boolean removed = false;
        try {
            storageLock.writeLock().acquire();

            if (key instanceof String && key.toString().endsWith(CacheConstants.NAME_COMPONENT_DELIMITER)) {
                // remove all keys of the same name group.

                Iterator iter = this.keyStore.entrySet().iterator();

                while (iter.hasNext()) {
                    Map.Entry entry = (Map.Entry) iter.next();

                    Object k = entry.getKey();

                    if (k instanceof String && k.toString().startsWith(key.toString())) {
                        int[] ded = this.keyStore.get(key);
                        this.dataFile.freeBlocks(ded);
                        iter.remove();
                        removed = true;
                        // TODO this needs to update the rmove count separately
                    }
                }
            } else if (key instanceof GroupId) {
                // remove all keys of the same name hierarchy.
                Iterator iter = this.keyStore.entrySet().iterator();
                while (iter.hasNext()) {
                    Map.Entry entry = (Map.Entry) iter.next();
                    Object k = entry.getKey();

                    if (k instanceof GroupAttrName && ((GroupAttrName) k).groupId.equals(key)) {
                        int[] ded = this.keyStore.get(key);
                        this.dataFile.freeBlocks(ded);
                        iter.remove();
                        removed = true;
                    }
                }
            } else {
                // remove single item.
                int[] ded = this.keyStore.remove(key);
                removed = (ded != null);
                if (ded != null) {
                    this.dataFile.freeBlocks(ded);
                }

                if (log.isDebugEnabled()) {
                    log.debug(logCacheName + "Disk removal: Removed from key hash, key [" + key + "] removed = "
                            + removed);
                }
            }
        } catch (Exception e) {
            log.error(logCacheName + "Problem removing element.", e);
            reset = true;
        } finally {
            storageLock.writeLock().release();
        }

        if (reset) {
            reset();
        }

        return removed;
    }

    /**
     * Resets the keyfile, the disk file, and the memory key map.
     * <p>
     * (non-Javadoc)
     * @see org.apache.jcs.auxiliary.disk.AbstractDiskCache#doRemoveAll()
     */
    protected void doRemoveAll() {
        try {
            reset();
        } catch (Exception e) {
            log.error(logCacheName + "Problem removing all.", e);
            reset();
        }
    }

    /**
     * Dispose of the disk cache in a background thread. Joins against this thread to put a cap on
     * the disposal time.
     * <p>
     * @todo make dispose window configurable.
     */
    public void doDispose() {
        Runnable disR = new Runnable() {
            public void run() {
                try {
                    disposeInternal();
                } catch (InterruptedException e) {
                    log.warn("Interrupted while diposing.");
                }
            }
        };
        Thread t = new Thread(disR, "BlockDiskCache-DisposalThread");
        t.start();
        // wait up to 60 seconds for dispose and then quit if not done.
        try {
            t.join(60 * 1000);
        } catch (InterruptedException ex) {
            log.error(logCacheName + "Interrupted while waiting for disposal thread to finish.", ex);
        }
    }

    /**
     * Internal method that handles the disposal.
     * @throws InterruptedException
     */
    private void disposeInternal() throws InterruptedException {
        if (!alive) {
            log.error(logCacheName + "Not alive and dispose was called, filename: " + fileName);
            return;
        }
        storageLock.writeLock().acquire();
        try {
            // Prevents any interaction with the cache while we're shutting down.
            alive = false;

            this.keyStore.saveKeys();

            try {
                if (log.isDebugEnabled()) {
                    log.debug(logCacheName + "Closing files, base filename: " + fileName);
                }
                dataFile.close();
                // dataFile = null;

                // TOD make a close
                // keyFile.close();
                // keyFile = null;
            } catch (IOException e) {
                log.error(logCacheName + "Failure closing files in dispose, filename: " + fileName, e);
            }
        } finally {
            storageLock.writeLock().release();
        }

        if (log.isInfoEnabled()) {
            log.info(logCacheName + "Shutdown complete.");
        }
    }

    /**
     * Returns the attributes.
     * <p>
     * (non-Javadoc)
     * @see org.apache.jcs.auxiliary.AuxiliaryCache#getAuxiliaryCacheAttributes()
     */
    public AuxiliaryCacheAttributes getAuxiliaryCacheAttributes() {
        return this.blockDiskCacheAttributes;
    }

    /**
     * Reset effectively clears the disk cache, creating new files, recyclebins, and keymaps.
     * <p>
     * It can be used to handle errors by last resort, force content update, or removeall.
     */
    private void reset() {
        if (log.isWarnEnabled()) {
            log.warn(logCacheName + "Reseting cache");
        }

        try {
            storageLock.writeLock().acquire();

            if (dataFile != null) {
                dataFile.close();
            }
            // TODO have the BlockDisk do this itself
            File dataFileTemp = new File(this.rootDirectory, fileName + ".data");
            dataFileTemp.delete();

            if (this.blockDiskCacheAttributes.getBlockSizeBytes() > 0) {
                this.dataFile = new BlockDisk(new File(rootDirectory, fileName + ".data"),
                        this.blockDiskCacheAttributes.getBlockSizeBytes());
            } else {
                this.dataFile = new BlockDisk(new File(rootDirectory, fileName + ".data"));
            }

            this.keyStore.reset();
        } catch (Exception e) {
            log.error(logCacheName + "Failure reseting state", e);
        } finally {
            storageLock.writeLock().release();
        }
    }

    /**
     * Add these blocks to the emptyBlock list.
     * <p>
     * @param blocksToFree
     */
    protected void freeBlocks(int[] blocksToFree) {
        this.dataFile.freeBlocks(blocksToFree);
    }

    /**
     * Called on shutdown. This gives use a chance to store the keys even if the cache manager's
     * shutdown method was not called.
     */
    class ShutdownHook extends Thread {
        /** Disposes of the cache. This will result force the keys to be persisted. */
        public void run() {
            if (alive) {
                log.warn(logCacheName + "Disk cache not shutdown properly, shutting down now.");
                doDispose();
            }
        }
    }

    /**
     * Gets basic stats for the disk cache.
     * <p>
     * @return String
     */
    public String getStats() {
        return getStatistics().toString();
    }

    /**
     * Returns info about the disk cache.
     * <p>
     * (non-Javadoc)
     * @see org.apache.jcs.auxiliary.AuxiliaryCache#getStatistics()
     */
    public IStats getStatistics() {
        IStats stats = new Stats();
        stats.setTypeName("Block Disk Cache");

        ArrayList elems = new ArrayList();

        IStatElement se = null;

        se = new StatElement();
        se.setName("Is Alive");
        se.setData("" + alive);
        elems.add(se);

        se = new StatElement();
        se.setName("Key Map Size");
        se.setData("" + this.keyStore.size());
        elems.add(se);

        try {
            se = new StatElement();
            se.setName("Data File Length");
            if (this.dataFile != null) {
                se.setData("" + this.dataFile.length());
            } else {
                se.setData("-1");
            }
            elems.add(se);
        } catch (Exception e) {
            log.error(e);
        }

        se = new StatElement();
        se.setName("Block Size Bytes");
        se.setData("" + this.dataFile.getBlockSizeBytes());
        elems.add(se);

        se = new StatElement();
        se.setName("Number Of Blocks");
        se.setData("" + this.dataFile.getNumberOfBlocks());
        elems.add(se);

        se = new StatElement();
        se.setName("Average Put Size Bytes");
        se.setData("" + this.dataFile.getAveragePutSizeBytes());
        elems.add(se);

        se = new StatElement();
        se.setName("Empty Blocks");
        se.setData("" + this.dataFile.getEmptyBlocks());
        elems.add(se);

        // get the stats from the super too
        // get as array, convert to list, add list to our outer list
        IStats sStats = super.getStatistics();
        IStatElement[] sSEs = sStats.getStatElements();
        List sL = Arrays.asList(sSEs);
        elems.addAll(sL);

        // get an array and put them in the Stats object
        IStatElement[] ses = (IStatElement[]) elems.toArray(new StatElement[0]);
        stats.setStatElements(ses);

        return stats;
    }
}