package jeffaschenk.commons.frameworks.cnxidx.utility.pool; import jeffaschenk.commons.exceptions.NoFreeResourcesException; import jeffaschenk.commons.exceptions.PoolNotAvailableException; import jeffaschenk.commons.frameworks.cnxidx.utility.logging.FrameworkLogger; import jeffaschenk.commons.frameworks.cnxidx.utility.logging.FrameworkLoggerLevel; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.Constructor; import java.util.Date; import java.util.Enumeration; import java.util.Hashtable; import java.util.Properties; import java.util.Timer; import java.util.TimerTask; import javax.naming.Context; import javax.naming.NamingException; import javax.naming.directory.DirContext; /** * This class provides access to a pool of DirContext resources and is * implemented as a singleton / delegator idiom. * <p> The DirContextPool uses several different properties from the * properties that are passed in at initialization time. The following * properties are must be provided and be prefixed with * jeffaschenk.commons.framework.cnxidx.DirContextPool:<br> * <p/> * <li><b>min</b> - Minimum number of resources to create and keep open. * Defaults to <i>10</i> * <li><b>max</b> - Maximum number of resources that can be created. * Defaults to <i>20</i> * <li><b>timeout</b> - Default wait period for obtaining a resource * from the free pool. Defaults to <i>20</i> * <li><b>resourceProviderClassName</b> - The java class name of the pool's * resource provider * <li><b>trackUsage</b> - A boolean indicating if we should track * detailed usage information of resource allocation. * Defaults to <i>False</i> * <li><b>trackUsageInterval</b> - Number of seconds between reporting intervals * Defaults to <i>15 seconds</i> * <li><b>trackUsageResourceAge</b> - # seconds resource must be held * (not released) before being reported on. * Defaults to <i>15 seconds</i> */ public class DirContextPool { /** * DirContextPool provides access to a ResourcePool * containing DirContext objects. */ private static ResourcePool pool; private static Properties poolProperties; /** * Used to validate directory contexts on an asyncronous period cycle. * Somewhat robust, not perfect but also not a performance detriment */ protected static Timer validationTimer; protected static TimerTask validationTimerTask; /** * Strictly a developer mechanism. * Used primarily to determine if resources have not been released. */ private static boolean trackUsage; private static Timer usageTimer; private static TimerTask usageTimerTask; private static Hashtable<DirContext,DirContextUsage> poolUsage; /** * Properties from System properties and read in from the ICos * properties file (combined.) */ static private Properties resourcePoolProps = null; /** * My classname for use with CNXLogger. */ static private final String CLASS_NAME = DirContextPool.class.getName(); /** * The key value prefix for this object's properties in the ICos * properties file. For example, based on the value of this variable, * entries in the properties file would look like: * <br><i>cjeffaschenk.commons.framework.cnxidx.DirContextPool.min=10</i> * <p> Note that the terminating '.' is important. */ static private final String KEY_PREFIX = CLASS_NAME + "."; // terminating '.' important /** * Owner of the pool. Not really required by ICos, but, by the * ResourcePool. Any value is fine. */ static private final String OWNER = "Framework Directory Context Pool"; // Assume properties missing for ones without reasonable defaults // The values in <> designate values that aren't easily defaulted. /** * Preloaded with a default value and used to create a * DirContext object. The preloaded default value, maybe * invalid, may be overridden by values placed in the * ICos properties file. */ static private final int DEFAULT_MIN = 10; /** * Preloaded with a default value and used to create a * DirContext object. The preloaded default value, maybe * invalid, may be overridden by values placed in the * ICos properties file. */ static private final int DEFAULT_MAX = 20; /** * Preloaded with a default value and used to create a * DirContext object. The preloaded default value, maybe * invalid, may be overridden by values placed in the * ICos properties file. */ static private final int DEFAULT_TIMEOUT = 20; /** * Private constructor per the singleton idiom. */ private DirContextPool() { /* do nothing */ } /** * Utilizes the singleton context pool to acquire a DirContext. * * @return A reference to an DirContext object. * @throws jeffaschenk.commons.exceptions.NoFreeResourcesException Thrown when no free DirContexts * are available. */ public static DirContext acquireContext() throws NoFreeResourcesException { DirContext idc = (DirContext) getContextPool().acquireResource(); if (trackUsage) { poolUsage.put(idc, new DirContextUsage(idc)); } return idc; } /** * Utilizes the singleton context pool to release a DirContext. * * @param idc The DirContext to release. */ public static void releaseContext(DirContext idc) { final String METHOD = "releaseContext"; try { if (idc == null) { return; } Hashtable env = idc.getEnvironment(); if (env.containsKey(Context.OBJECT_FACTORIES)) { idc.removeFromEnvironment(Context.OBJECT_FACTORIES); } if (env.containsKey(Context.STATE_FACTORIES)) { idc.removeFromEnvironment(Context.STATE_FACTORIES); } } catch (NamingException ignore) { FrameworkLogger.log(CLASS_NAME, "releaseContext", FrameworkLoggerLevel.INFO, MessageConstants.ICOS_UTIL_DIRECTORY_CONTEXT_POOL_FACTORIES_REMOVAL_FAILED); } try { getContextPool().releaseResource(idc); if (trackUsage) poolUsage.remove(idc); } catch (PoolNotAvailableException pnae) { } } /** * Locks the pool. If false is returned, follow-up with * isPoolInUse() calls until isPoolInUse() returns false. * * @param why Reason for locking the pool. Unused if pool is * already locked. Reason shows up in exception messages. * @param timeoutSeconds wait in seconds to lock the pool and * ensure that no DirContext connections in the pool are in use. * @return Whether there are any pool connections in use. */ public static boolean lockPool(String why, int timeoutSeconds) throws PoolNotAvailableException { return getContextPool().lockPool(why, timeoutSeconds); } /** * Unlocks the pool - making the pool available for use. */ public static void unlockPool() throws PoolNotAvailableException { getContextPool().unlockPool(); } /** * Returns whether the pool has been locked. * * @return boolean indicating whether the pool was successfully locked. */ public static boolean isPoolLocked() throws PoolNotAvailableException { return getContextPool().isPoolLocked(); } /** * Convenience routine for checking if the pool is unlocked * * @return boolean indicating whether the pool is locked or not */ public static boolean isPoolLocked(int timeoutSeconds) throws PoolNotAvailableException { return getContextPool().isPoolLocked(timeoutSeconds); } /** * Convenience routine for checking if the pool is unlocked and * has no connections in use. * * @return boolean indicating whether the pool was successfully locked. */ public static boolean isPoolLockedAndUnused(int timeoutSeconds) throws PoolNotAvailableException { return getContextPool().isPoolLockedAndUnused(timeoutSeconds); } /** * Creates a resource pool of DirContexts if the pool does not * already exist; and always returns a reference to the pool. * * @return A reference to a resource pool of DirContext objects. */ private static synchronized ResourcePool getContextPool() throws PoolNotAvailableException { if (pool == null) { throw new PoolNotAvailableException( "Directory Context Pool has not been initialized yet."); } return pool; } /** * Logically resizes and redirects the dir context pool to the * specified parameters. * * @param typedProps A set of properties to reset the pool to. */ public static synchronized void resetPool(Properties typedProps) throws PoolNotAvailableException { ResourcePool oldPool = pool; Properties oldPoolProps = poolProperties; try { // default to old properties if new properties is missing stuff poolProperties = new Properties(typedProps);//, oldPoolProps); pool = createPool(poolProperties); oldPool.destroy(); } catch (PoolNotAvailableException pnae) { pool = oldPool; poolProperties = oldPoolProps; throw pnae; } } /** * Refresh the pool with a complete reset and recreate (this seems to be the only * way that it really gets reset. Attempting to just do individual resources * doesn't seem to work. */ public static synchronized void refreshPool() { if ((pool != null) && (!pool.isPoolLocked())) { // drop all the existing dircontexts and recreate as necessary try { resetPool(poolProperties); } catch (PoolNotAvailableException pnae) { // ignore this } } } public static synchronized void destroyPool() { try { //while (! isPoolLockedAndUnused(-1)) { //isPoolLockedAndUnused(5); //} getContextPool().destroy(); } catch (Exception ignore) { } } /** * Creates a resource pool of DirContexts if the pool does not * already exist; and always returns a reference to the pool. * * @param typedProps -- Pool Properties. */ public static synchronized void initializePool(Properties typedProps) throws PoolNotAvailableException { if (pool == null) { poolProperties = typedProps; pool = createPool(typedProps); } } /** * Method to validate all the free resources in the pool. Validation * determines if the resources are still active. */ public static void validatePool() throws PoolNotAvailableException { FrameworkLogger.log(CLASS_NAME, "validatePool", FrameworkLoggerLevel.INFO, MessageConstants.ICOS_UTIL_DIRECTORY_CONTEXT_POOL_VALIDATE); getContextPool().validate(); } /** * Creates a resource pool of DirContexts if the pool does not * already exist; and always returns a reference to the pool. * * @return A reference to a resource pool of DirContext objects. */ @SuppressWarnings("unchecked") private static ResourcePool createPool(Properties typedProps) throws PoolNotAvailableException { ResourcePool resPool = null; int min = Integer.parseInt((String)typedProps.get(KEY_PREFIX + "min"));//, DEFAULT_MIN); int max = Integer.parseInt((String)typedProps.get(KEY_PREFIX + "max")); // , DEFAULT_MAX); int timeout = Integer.parseInt((String)typedProps.get(KEY_PREFIX + "timeout")); // , DEFAULT_TIMEOUT); int validationInterval = Integer.parseInt((String)typedProps.get(KEY_PREFIX + "validateIntervalSeconds")); // , 30); boolean fullValidation = false; if (validationInterval == 0) { fullValidation = true; } // Obtain the name of the resource pools provider class String resourceProviderClassName = (String) typedProps.get( KEY_PREFIX + "resourceProviderClassName"); if (resourceProviderClassName == null || resourceProviderClassName.length() == 0) { String[] arguments = new String[]{KEY_PREFIX}; FrameworkLogger.log(CLASS_NAME, "createPool", FrameworkLoggerLevel.SEVERE, MessageConstants.ICOS_UTIL_DIRECTORY_CONTEXT_POOL_INVALID_PROPERTY, arguments); throw new PoolNotAvailableException(MessageConstants.ICOS_UTIL_DIRECTORY_CONTEXT_POOL_INVALID_PROPERTY, arguments); } try { Class resProviderClass = Class.forName(resourceProviderClassName); Constructor resProvCtor = resProviderClass.getConstructor( new Class[]{Properties.class}); ResourceProvider resProv = (ResourceProvider) resProvCtor.newInstance(new Object[]{typedProps}); resPool = new ResourcePool(OWNER, min, max, timeout, resProv, fullValidation); } catch (Exception e) { String[] arguments = new String[]{resourceProviderClassName}; FrameworkLogger.log(CLASS_NAME, "createPool", FrameworkLoggerLevel.SEVERE, MessageConstants.ICOS_UTIL_DIRECTORY_CONTEXT_POOL_INVALID_RESOURCEPROVIDER, arguments, e); throw new PoolNotAvailableException(MessageConstants.ICOS_UTIL_DIRECTORY_CONTEXT_POOL_INVALID_RESOURCEPROVIDER, arguments, e); } // set up the DirContextPool validation setupValidation(validationInterval * 1000); // Set up DirContextPool usage reporting trackUsage = false; long trackUsageInterval = 1000 * 15;//typedProps.getInt( //KEY_PREFIX + "trackUsageInterval", 15); long trackUsageResourceAge = 1000 * 15;//typedProps.getInt( //KEY_PREFIX + "trackUsageResourceAge", 15); setupUsageTracking(trackUsage, trackUsageInterval, trackUsageResourceAge); return resPool; } private static void setupValidation(long validationInterval) { if (validationInterval > 0) { if (validationTimer == null) { validationTimer = new Timer(true); validationTimerTask = new ValidationTimerTask(); validationTimer.schedule(validationTimerTask, validationInterval, validationInterval); } } else { if ((validationTimer != null) && (validationTimerTask != null)) { validationTimerTask.cancel(); } } } private static void setupUsageTracking(boolean trackUsage, long usageInterval, long resUsedTimeMillis) { if (trackUsage) { if (poolUsage == null) { poolUsage = new Hashtable<>(); } if (usageTimer == null) { usageTimer = new Timer(true); } usageTimerTask = new UsageTimerTask(resUsedTimeMillis); if (usageInterval > 0) { usageTimer.schedule(usageTimerTask, usageInterval, usageInterval); } } else { if (usageTimer != null && usageTimerTask != null) { usageTimerTask.cancel(); } } } //public static PropertiesChangeListener getPropertiesChangeListener() { // return new DirContextPoolListener(); //} public static class DirContextUsage { private Object resource; private String callStack; private Date acquisitionTime; public DirContextUsage(Object resource) { this.resource = resource; this.acquisitionTime = new Date(); try { Exception callStackException = new Exception(); StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); callStackException.printStackTrace(pw); callStack = sw.toString(); pw.close(); sw.close(); // strip two lines (separated by \n) of text off of the stack for (int ii = 0; ii < 2; ii++) { int idx = callStack.indexOf('\n'); if (idx > -1) { callStack = callStack.substring(idx + 1); } } } catch (Exception ignore) { } } public boolean isOlderThan(long milliseconds) { return getSecondsUsed() > milliseconds; } public String getCallStack() { return callStack; } public Date getAcquisitionTime() { return acquisitionTime; } public long getSecondsUsed() { long current = System.currentTimeMillis(); long created = getAcquisitionTime().getTime(); long difference = current - created; return difference; } } public static class ValidationTimerTask extends TimerTask { public ValidationTimerTask() { } public void run() { try { DirContextPool.validatePool(); } catch (Exception e) { // don't want this to end } } } public static class UsageTimerTask extends TimerTask { private long resUsedTimeMillis; public UsageTimerTask(long resUsedTimeMillis) { this.resUsedTimeMillis = resUsedTimeMillis; } public void run() { String txt = "\n\t#######################################"; txt += "\n\tResources currently in use: " + poolUsage.size(); if (poolUsage.size() > 0) { Enumeration iter = poolUsage.elements(); while (iter.hasMoreElements()) { DirContextUsage dcu = (DirContextUsage) iter.nextElement(); if (dcu.isOlderThan(resUsedTimeMillis)) { txt += "\n\t======= Resource held too long ========"; txt += "\n\tTime Acquired: " + dcu.getAcquisitionTime(); txt += "\n\tSeconds In Use: " + dcu.getSecondsUsed(); txt += "\n\tAcquisition Stack:\n" + dcu.getCallStack(); txt += "\n\t======================================="; } } } txt += "\n\t#######################################"; String[] arguments = new String[]{txt}; FrameworkLogger.log(CLASS_NAME, "resourceUsageTracker", FrameworkLoggerLevel.DEBUG, MessageConstants.ICOS_UTIL_DIRECTORY_CONTEXT_POOL_USAGETRACKER, arguments); } } } class DirContextPoolListener { //implements PropertiesChangeListener { /** * My classname for use with CNXLogger. */ static private final String CLASS_NAME = DirContextPoolListener.class.getName(); /** * This will be called when a change is made. To recieve the message * you must register with the AdministrationSB. * * @param propertySetName The name of the propertie set in question. * @param changedProperties The set of properties that have changed. */ public void propertiesChanged(String propertySetName, Properties changedProperties) { if (changedProperties != null) { try { DirContextPool.resetPool( new Properties(changedProperties)); } catch (PoolNotAvailableException pnae) { FrameworkLogger.log(CLASS_NAME, "propertiesChanged", FrameworkLoggerLevel.SEVERE, MessageConstants.ICOS_UTIL_DIRECTORY_CONTEXT_POOL_UNABLE_TO_RESET, pnae); } } } }