Java tutorial
/** * 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; } }