util.ExpiringObjectPool.java Source code

Java tutorial

Introduction

Here is the source code for util.ExpiringObjectPool.java

Source

/**
* Copyright (c) 2001-2012 "Redbasin Networks, INC" [http://redbasin.org]
*
* This file is part of Redbasin OpenDocShare community project.
*
* Redbasin OpenDocShare is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package util;

import java.util.TreeMap;
import java.util.Map;
import java.util.Iterator;

import model.Stringifier;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Used mainly in MVC frameworks, where the DAO's query allocates the
 * object (Stringifier bean) and the object is not required after the
 * request is complete. Instead of using the heap to manage the lifecycle
 * of this object, we checkout this object from this pool, and set a
 * fixed expiry on this. Assumption is made that no request (that is
 * not gone awry) will take longer than expiryInSecs.<p>
 *
 * All configuration for this object can be and is set using spring.
 * 
 *
 * @author Smitha Gudur (smitha@redbasin.com)
 * @version $Revision: 1.1 $
 */
public class ExpiringObjectPool implements Runnable {

    /** Logger for this class and subclasses */
    protected final Log logger = LogFactory.getLog(getClass());

    // reasonable defaults overridden by spring
    private volatile int expiryInSecs = 60;
    private volatile int maxObjects = 5000;
    private volatile int sleepTime = 60;
    private volatile float effSweepThreshold = 80;
    private volatile int logFreq = 200;
    private volatile boolean disablePool = false;

    // stats that are constantly updated by updateStats
    private volatile float avgCumEff = 100;
    private volatile float avgMaxCumEff = 100;
    private volatile float avgMinCumEff = 100;
    private volatile float avgCumQSize = 1;
    private volatile float avgMaxCumQSize = 1;
    private volatile float avgMinCumQSize = 1;
    private volatile float avgCumCost = 1;
    private volatile float avgMaxCumCost = 1;
    private volatile float avgMinCumCost = 1;

    private volatile TreeMap objectMap = null;
    private volatile TreeMap queueMap = null;
    private volatile TreeMap statsMap = null;

    private volatile Stringifier stats = null;

    // some constants
    private final int MSEC = 1000;
    private final String POOL = "pool";
    private final String AGE = "age";

    /**
     * This method called by spring. How long (in secs) an item should
     * live in the queue before being expired. Once the object is expired
     * it is reset() or it's data is initialized.
     *
     * @param expiryInSecs how many secs before expiry of object in queue
     */
    public void setExpiry(int expiryInSecs) {
        this.expiryInSecs = expiryInSecs;
    }

    /**
     * This method called by spring. Max number of objects managed
     * per queue. If this number for a queue is exceeded, newObject()
     * does not throw an exception, but logs a warning, and allocates
     * an object on the heap that is not managed by eop. So the GC is
     * likely to garbage collect it when this object is no longer used.
     *
     * @param maxObjects max objects per queue
     */
    public void setMaxObjects(int maxObjects) {
        this.maxObjects = maxObjects;
    }

    /**
     * This method called by spring. Sleep time intervals between 
     * sweeps.
     *
     * @param sleepTime sleep for how many secs
     */
    public void setSleepTime(int sleepTime) {
        this.sleepTime = sleepTime;
    }

    /**
     * This method can be called by spring. If the total cost as a percentage
     * of maxObjects goes below this value, a "manual" sweep is performed.
     * This is to prevent the sweep thread from being sidelined by high
     * user activity in terms of calling newObject().
     *
     * @param effSweepThreshold
     */
    public void setEffSweepThreshold(int effSweepThreshold) {
        this.effSweepThreshold = effSweepThreshold;
    }

    /**
     * This method can be called by spring. Use it to set the
     * reference to the HeapStrategy bean.
     * 
     * @param heapStrategy
     */
    public void setHeapStrategy(HeapStrategy heapStrategy) {
        disablePool = (!heapStrategy.getEopEnabled());
    }

    /**
     * This method is called by spring. This initializes all the data
     * and also starts the thread. This method will likely trigger 
     * GC especially in hot war deployments.
     */
    public synchronized void init() {
        objectMap = new TreeMap();
        queueMap = new TreeMap();
        statsMap = new TreeMap();
        stats = new Stringifier();
        new Thread(this).start();
    }

    /**
     * This method is called by spring framework. It calls init() if
     * init() has not already been called.
     *
     * @param mappings the map of bean names to fully qualified class path
     */
    public synchronized void setMappings(Map mappings) {
        if (objectMap == null) {
            init();
        }

        Iterator iter = mappings.keySet().iterator();
        while (iter.hasNext()) {
            String key = (String) iter.next();
            try {
                addQueue(key, (String) mappings.get(key));
            } catch (Exception e) {
                logger.error("Could not add key " + key + " and value " + mappings.get(key) + " to queue!", e);
            }
        }
    }

    /**
     * Sweep through each queue looking and marking expired objects. 
     * Since we are using TreeMap, if we don't find expired objects in
     * a queue, we break out of the loop, as the objects are in sorted
     * ascending order of entry date.
     */
    private synchronized void sweep() {
        if (disablePool)
            return;
        if (objectMap == null)
            return;
        Iterator iter = objectMap.values().iterator();
        while (iter.hasNext()) {
            long ctime = System.currentTimeMillis();
            TreeMap beanMap = (TreeMap) iter.next();
            Iterator iter1 = beanMap.values().iterator();
            while (iter1.hasNext()) {
                Stringifier bean = (Stringifier) iter1.next();
                if (((Boolean) bean.getObject(POOL)).booleanValue()) {
                    long age = ((Long) bean.getObject(AGE)).longValue();
                    if (((ctime - age) / MSEC) > expiryInSecs) {
                        bean.setObject(POOL, new Boolean(false));
                        StringBuffer sb = new StringBuffer("Expiring bean ");
                        sb.append(bean.getClass().getName());
                        sb.append(" (");
                        sb.append(bean.hashCode());
                        sb.append(")");
                        sb.append(bean.toString());
                        logger.info(sb.toString());
                    } else
                        break;
                }
            }
        }
    }

    /**
     * Iterate through each item in each queue and expire beans that have
     * expired. Sleep for a configured time after each iteration. This loop
     * should run forever.
     */
    public void run() {
        while (true) {
            if (!disablePool)
                sweep();
            try {
                Thread.sleep(sleepTime * MSEC);
            } catch (InterruptedException e) {
                logger.warn("thread interrupted while sleeping ", e);
            }
        }
    }

    /**
     * Supports bean constructors with no arguments only. If you call
     * this method with the same beanName, it will replace the existing
     * entry in the queue and also destroy the current cached bean pool
     * if the entry existed.
     *
     * @param beanName a unique name for the bean
     * @param className the fully qualified class name
     * @exception ObjException throw an exception if hell breaks loose
     */
    public synchronized void addQueue(String beanName, String className) throws ObjException {
        try {
            Class myclass = Class.forName(className);
            queueMap.put((Object) beanName, (Object) myclass);
            objectMap.put((Object) beanName, (Object) new TreeMap());
            Stringifier stats = new Stringifier();
            stats.setObject("useCount", new Long(1));
            stats.setObject("avgQSize", new Long(1));
            stats.setObject("avgCost", new Long(1));
            stats.setObject("avgEff", new Float(100));
            statsMap.put((Object) beanName, (Object) stats);
        } catch (ClassNotFoundException e) {
            throw new ObjException("Could not create a class from " + className, e);
        }
    }

    /**
     * Remove a specific queue by the bean name.
     *
     * @param beanName the name of the bean to index into the queue
     */
    public synchronized void removeQueue(String beanName) {
        queueMap.remove((Object) beanName);
        objectMap.remove((Object) beanName);
        statsMap.remove((Object) beanName);
    }

    /**
     * Get the stats as a Stringifier bean. The field names set
     * are:<p>
     *
     * <DL>
     * <LI>avgCumEff</LI>
     * <LI>avgMaxCumEff</LI>
     * <LI>avgMinCumEff</LI>
     * <LI>avgCumQSize</LI>
     * <LI>avgMaxCumQSize</LI>
     * <LI>avgMinCumQSize</LI>
     * <LI>avgCumCost</LI>
     * <LI>avgMaxCumCost</LI>
     * <LI>avgMinCumCost</LI>
     * </DL>
     *
     * This method allocates a new Float instances on each call.
     * So use it judiciously.
     *
     * @return Stringifier a generic stats bean
     */
    public synchronized Stringifier reportStats() {
        stats.setObject("avgCumEff", new Float(avgCumEff));
        stats.setObject("avgMaxCumEff", new Float(avgMaxCumEff));
        stats.setObject("avgMinCumEff", new Float(avgMinCumEff));
        stats.setObject("avgCumQSize", new Float(avgCumQSize));
        stats.setObject("avgMaxCumQSize", new Float(avgMaxCumQSize));
        stats.setObject("avgMinCumQSize", new Float(avgMinCumQSize));
        stats.setObject("avgCumCost", new Float(avgCumCost));
        stats.setObject("avgMaxCumCost", new Float(avgMaxCumCost));
        stats.setObject("avgMinCumCost", new Float(avgMinCumCost));
        stats.setObject("expiryInSecs", new Integer(expiryInSecs));
        stats.setObject("maxObjects", new Integer(maxObjects));
        stats.setObject("sleepTime", new Integer(sleepTime));
        stats.setObject("effSweepThreshold", new Float(effSweepThreshold));
        return stats;
    }

    /**
     * Update statistics can be called with any frequency depending on
     * the performance requirements.
     *
     * @param beanName bean name for which stats are updated
     * @param cost the number of iterations in beanMap 
     * @param qsize the size of the beanMap
     */
    private synchronized void updateStats(String beanName, long cost, long qsize) {
        Stringifier stats = (Stringifier) statsMap.get((Object) beanName);
        long useCount = ((Long) stats.getObject("useCount")).longValue();
        long avgQSize = ((Long) stats.getObject("avgQSize")).longValue();
        long avgCost = ((Long) stats.getObject("avgCost")).longValue();
        float sweepEff = 100 - (((float) avgCost / (float) maxObjects) * (float) 100);
        float avgEff = ((Float) stats.getObject("avgEff")).floatValue();

        avgEff = (avgEff + sweepEff) / 2;
        avgCost = (avgCost + cost) / 2;
        avgQSize = (avgQSize + qsize) / 2;
        avgCumEff = (avgCumEff + avgEff) / (float) 2;
        avgMaxCumEff = Math.max(avgCumEff, avgMaxCumEff);
        avgMinCumEff = Math.min(avgCumEff, avgMinCumEff);
        avgCumCost = ((float) avgCumCost + (float) avgCost) / (float) 2;
        avgMaxCumCost = Math.max(avgCumCost, avgMaxCumCost);
        avgMinCumCost = Math.min(avgCumCost, avgMinCumCost);
        avgCumQSize = (avgCumQSize + avgQSize) / 2;
        avgMaxCumQSize = Math.max(avgCumQSize, avgMaxCumQSize);
        avgMinCumQSize = Math.min(avgCumQSize, avgMinCumQSize);

        stats.setObject("avgEff", new Float(avgEff));
        stats.setObject("useCount", new Long(++useCount));
        stats.setObject("avgCost", new Long(avgCost));
        stats.setObject("avgQSize", new Long(avgQSize));

        if (BasicMathUtil.randomInt(0, logFreq) == 1) {
            logger.info("Avg. Cum Eff : " + avgCumEff + "%" + ", Max Avg Cum Eff : " + avgMaxCumEff + "%"
                    + ", Min Avg Cum Eff : " + avgMinCumEff + "%");
            logger.info("Avg. Cum Cost : " + avgCumCost + ", Max Avg Cum Cost : " + avgMaxCumCost
                    + ", Min Avg Cum Cost : " + avgMinCumCost);
            logger.info("Avg. Cum QSize : " + avgCumQSize + ", Max Avg Cum QSize : " + avgMaxCumQSize
                    + ", Min Avg Cum QSize : " + avgMinCumQSize);
            logger.info("JVM Free Memory = " + Runtime.getRuntime().freeMemory() + ", JVM Total Memory = "
                    + Runtime.getRuntime().totalMemory());
            //logger.info("Avg. Cum Cost : " + avgCumCost);
            //logger.info("Avg. Cum QSize : " + avgCumQSize);
            //logger.info("Avg. Eff for " + beanName + ": " + avgEff + "%");
            //logger.info("Avg. Cost for " + beanName + ": " + avgCost);
            //logger.info("Avg. Q Size for " + beanName + ": " + avgQSize);
        }
    }

    /**
     * Perform a sweep only if the eff is less than effSweepThreshold.
     * Check efficiency for only current bean, but perform a sweep for
     * all the beans! This is kind of counter-intuitive but done on purpose.
     *
     * @param beanName the bean for which the sweep is desired.
     */
    private synchronized void sweepIfRequired(String beanName) {
        if (disablePool)
            return;
        Stringifier stats = (Stringifier) statsMap.get((Object) beanName);
        long avgQSize = ((Long) stats.getObject("avgQSize")).longValue();
        long avgCost = ((Long) stats.getObject("avgCost")).longValue();
        float sweepEff = 100 - (((float) avgCost / (float) maxObjects) * (float) 100);
        //logger.info("Current Efficiency (beanName) = " + sweepEff + "%");
        if ((((float) avgCost / (float) maxObjects) * (float) 100) < effSweepThreshold) {
            sweep();
        }
    }

    /**
     * Use this method to get objects from the pool. You must pass the
     * name of the bean, like "directory", "userpage" etc. It will look
     * for an expired bean in the queue, and return a reference to this
     * bean if it is found. If it does not find an expired bean, it will
     * create a new bean on the heap and add it to the queue if the 
     * max size of the queue has not been exceeded. If the max size of the
     * queue has been exceeded, it will not add it to the queue, and this
     * means the bean on the heap will be garbage collected. Over time
     * the max size of the pool can be calibrated. The method will log
     * a warning whenever, the max size of the queue is exceeded for a 
     * specific bean name.
     *
     * @param beanName the name of the bean which corresponds to a queue
     * @return Object return the bean as an Object
     * @exception ObjException if anything goes wrong
     */
    public synchronized Object newObject(String beanName) throws ObjException {
        if (disablePool) {
            Stringifier bean = null;
            try {
                Class myclass = (Class) queueMap.get(beanName);
                bean = (Stringifier) myclass.newInstance();
            } catch (Exception e) {
                throw new ObjException("Could not instantiate object of type " + beanName, e);
            }
            return bean;
        }

        sweepIfRequired(beanName);

        TreeMap beanMap = (TreeMap) objectMap.get((Object) beanName);
        if (beanMap == null) {
            throw new ObjException("No queue exists for " + beanName
                    + ", update the spring config to add a new mapping for " + beanName);
        }
        Iterator iter = beanMap.values().iterator();
        Long age = new Long(System.currentTimeMillis());
        long counter = 1;

        while (iter.hasNext()) {
            counter++;
            Stringifier bean = (Stringifier) iter.next();
            if (!((Boolean) bean.getObject(POOL)).booleanValue()) {
                bean.reset();
                bean.setObject(AGE, age);
                bean.setObject(POOL, new Boolean(true));
                updateStats(beanName, counter, beanMap.size());
                return (Object) bean;
            }
        }

        updateStats(beanName, counter, beanMap.size());

        // didn't find an expired object in pool, allocating on heap now
        Stringifier bean = null;
        try {
            Class myclass = (Class) queueMap.get(beanName);
            bean = (Stringifier) myclass.newInstance();
        } catch (Exception e) {
            throw new ObjException("Could not instantiate object of type " + beanName, e);
        }

        // check if exceeded max objects on the pool
        if (beanMap.size() > maxObjects) {
            logger.warn("Object pool size " + maxObjects + ", exceeded for beanName " + beanName);
        } else {
            bean.setObject(POOL, new Boolean(true));
            bean.setObject(AGE, age);
            beanMap.put(new Long(System.currentTimeMillis()), bean);
        }
        return (Object) bean;
    }

}