/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2002-2008, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library 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 * Lesser General Public License for more details. * */ package org.geotools.arcsde.session; import java.io.IOException; import java.util.LinkedList; import java.util.NoSuchElementException; import java.util.Queue; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.pool.BasePoolableObjectFactory; import org.apache.commons.pool.impl.GenericObjectPool; import org.apache.commons.pool.impl.GenericObjectPool.Config; import org.geotools.arcsde.ArcSdeException; import org.geotools.arcsde.logging.Loggers; import com.esri.sde.sdk.client.SeException; import com.esri.sde.sdk.client.SeRelease; /** * Maintains <code>SeConnection</code>'s for a single set of connection properties (for instance: by * server, port, user and password) in a pool to recycle used connections. * <p> * We are making use of an Apache Commons ObjectPool to maintain connections. This connection pool * is configurable in the sense that some parameters can be passed to establish the pooling policy. * To pass parameters to the connection pool, you should set properties in the parameters Map passed * to SdeDataStoreFactory.createDataStore, which will invoke SdeConnectionPoolFactory to get the SDE * instance's pool singleton. That instance singleton will be created with the preferences passed * the first time createDataStore is called for a given SDE instance/user, if subsequent calls * change that preferences, they will be ignored. * </p> * <p> * The expected optional parameters that you can set up in the argument Map for createDataStore are: * <ul> * <li>pool.minConnections Integer, tells the minimum number of open connections the pool will * maintain opened</li> * <li>pool.maxConnections Integer, tells the maximum number of open connections the pool will * create and maintain opened</li> * <li>pool.timeOut Integer, tells how many milliseconds a calling thread is guaranteed to wait * before getConnection() throws an UnavailableArcSDEConnectionException</li> * </ul> * </p> * * @author Gabriel Roldan * @version $Id$ */ class SessionPool implements ISessionPool { private static final Logger LOGGER = Loggers.getLogger("org.geotools.arcsde.session"); protected static final Level INFO_LOG_LEVEL = Level.WARNING; private SeConnectionFactory seConnectionFactory; /** this connection pool connection's parameters */ protected ArcSDEConnectionConfig config; /** Apache commons-pool used to pool arcsde connections */ private GenericObjectPool pool; private final Queue<Session> openSessionsNonTransactional = new LinkedList<Session>();// new // ConcurrentLinkedQueue<Session>(); /** * Creates a new SessionPool object for the given config. * * @param config * holds connection options such as server, user and password, as well as tuning * options as maximum number of connections allowed * @throws IOException * If connection could not be established * @throws NullPointerException * If config is null */ protected SessionPool(ArcSDEConnectionConfig config) throws IOException { if (config == null) { throw new NullPointerException("parameter config can't be null"); } this.config = config; LOGGER.fine("populating ArcSDE connection pool"); this.seConnectionFactory = createConnectionFactory(); final int minConnections = config.getMinConnections().intValue(); final int maxConnections = config.getMaxConnections().intValue(); if (minConnections > maxConnections) { throw new IllegalArgumentException("pool.minConnections > pool.maxConnections"); } {// configure connection pool Config poolCfg = new Config(); // pool upper limit poolCfg.maxActive = config.getMaxConnections().intValue(); // minimum number of idle objects. MAKE SURE this is 0, otherwise the pool will start // trying to create connections permanently even if there's a connection failure, // ultimately leading to the exhaustion of resources poolCfg.minIdle = 0; // how many connections may be idle at any time? -1 = no limit. We're running an // eviction thread to take care of idle connections (see minEvictableIdleTimeMillis and // timeBetweenEvictionRunsMillis) poolCfg.maxIdle = -1; // When reached the pool upper limit, block and wait for an idle connection for maxWait // milliseconds before failing poolCfg.maxWait = config.getConnTimeOut().longValue(); if (poolCfg.maxWait > 0) { poolCfg.whenExhaustedAction = GenericObjectPool.WHEN_EXHAUSTED_BLOCK; } else { poolCfg.whenExhaustedAction = GenericObjectPool.WHEN_EXHAUSTED_FAIL; } // check connection health at borrowObject()? poolCfg.testOnBorrow = true; // check connection health at returnObject()? poolCfg.testOnReturn = false; // check periodically the health of idle connections and discard them if can't be // validated? poolCfg.testWhileIdle = false; // check health of idle connections every 30 seconds // /poolCfg.timeBetweenEvictionRunsMillis = 30000; // drop connections that have been idle for at least 5 minutes poolCfg.minEvictableIdleTimeMillis = 5 * 60 * 1000; pool = new GenericObjectPool(seConnectionFactory, poolCfg); LOGGER.fine("Created ArcSDE connection pool for " + config); } ISession[] preload = new ISession[minConnections]; try { for (int i = 0; i < minConnections; i++) { preload[i] = (ISession) pool.borrowObject(); if (i == 0) { SeRelease seRelease = preload[i].getRelease(); String sdeDesc = seRelease.getDesc(); int major = seRelease.getMajor(); int minor = seRelease.getMinor(); int bugFix = seRelease.getBugFix(); String desc = "ArcSDE " + major + "." + minor + "." + bugFix + " " + sdeDesc; LOGGER.fine("Connected to " + desc); } } for (int i = 0; i < minConnections; i++) { pool.returnObject(preload[i]); } } catch (Exception e) { close(); if (e instanceof IOException) { throw (IOException) e; } throw (IOException) new IOException().initCause(e); } } /** * SeConnectionFactory used to create {@link ISession} instances for the pool. * <p> * Subclass may overide to customize this behaviour. * </p> * * @return SeConnectionFactory. */ protected SeConnectionFactory createConnectionFactory() { return new SeConnectionFactory(this.config); } /* * (non-Javadoc) * * @see org.geotools.arcsde.session.ISessionPool#getPoolSize() */ public int getPoolSize() { checkOpen(); return pool.getMaxActive(); } /* * (non-Javadoc) * * @see org.geotools.arcsde.session.ISessionPool#close() */ public void close() { if (pool != null) { try { pool.close(); pool = null; LOGGER.fine("SDE connection pool closed. "); } catch (Exception e) { LOGGER.log(Level.WARNING, "Closing pool: " + e.getMessage(), e); } } } /* * (non-Javadoc) * * @see org.geotools.arcsde.session.ISessionPool#isClosed() */ public boolean isClosed() { return pool == null; } private void checkOpen() throws IllegalStateException { if (isClosed()) { throw new IllegalStateException("This session pool is closed"); } } /** * Ensures proper closure of connection pool at this object's finalization stage. */ @Override protected void finalize() { close(); } /** * @see org.geotools.arcsde.session.ISessionPool#getAvailableCount() */ public synchronized int getAvailableCount() { checkOpen(); return pool.getMaxActive() - pool.getNumActive(); } /** * @see org.geotools.arcsde.session.ISessionPool#getInUseCount() */ public int getInUseCount() { checkOpen(); return pool.getNumActive(); } /** * @see org.geotools.arcsde.session.ISessionPool#getSession() */ public ISession getSession() throws IOException, UnavailableConnectionException { return getSession(true); } public void returnObject(Session session) throws Exception { synchronized (openSessionsNonTransactional) { openSessionsNonTransactional.remove(session); } pool.returnObject(session); } /** * @see org.geotools.arcsde.session.ISessionPool#getSession(boolean) */ public ISession getSession(final boolean transactional) throws IOException, UnavailableConnectionException { checkOpen(); try { Session connection = null; if (transactional) { LOGGER.finest("Borrowing session from pool for transactional access"); connection = (Session) pool.borrowObject(); } else { synchronized (openSessionsNonTransactional) { try { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("Grabbing session from pool on " + Thread.currentThread().getName()); } connection = (Session) pool.borrowObject(); if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("Got session from the pool on " + Thread.currentThread().getName()); } } catch (NoSuchElementException e) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("No available sessions in the pool, falling back to queued session"); } connection = openSessionsNonTransactional.remove(); } openSessionsNonTransactional.add(connection); if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("Got session from the in use queue on " + Thread.currentThread().getName()); } } } connection.markActive(); return connection; } catch (NoSuchElementException e) { LOGGER.log(Level.WARNING, "Out of connections: " + e.getMessage() + ". Config: " + this.config); throw new UnavailableConnectionException(config.getMaxConnections(), this.config); } catch (SeException se) { ArcSdeException sdee = new ArcSdeException(se); LOGGER.log(Level.WARNING, "ArcSDE error getting connection for " + config, sdee); throw sdee; } catch (Exception e) { LOGGER.log(Level.WARNING, "Unknown problem getting connection: " + e.getMessage(), e); throw (IOException) new IOException( "Unknown problem fetching connection from connection pool").initCause(e); } } /* * (non-Javadoc) * * @see org.geotools.arcsde.session.ISessionPool#getConfig() */ public ArcSDEConnectionConfig getConfig() { return this.config; } /** * PoolableObjectFactory intended to be used by a Jakarta's commons-pool objects pool, that * provides ArcSDE's SeConnections. * * @author Gabriel Roldan, Axios Engineering * @version $Id$ */ private class SeConnectionFactory extends BasePoolableObjectFactory { private ArcSDEConnectionConfig config; /** * Creates a new SeConnectionFactory object. * * @param config */ public SeConnectionFactory(ArcSDEConnectionConfig config) { super(); this.config = config; } /** * Called whenever a new instance is needed. * <p> * The implementation for this method needs to be synchronized in order to make sure no two * {@code SeConnection} instances are created at the same time. Otherwise, when that happens * under load, SeConnection's constructor uses to throw a nasty * {@code NegativeArraySizeException}. * </p> * * @return a newly created <code>SeConnection</code> * @throws SeException * if the connection can't be created */ @Override public synchronized Object makeObject() throws IOException { ISession seConn; seConn = new Session(SessionPool.this, config); return seConn; } @Override public void passivateObject(Object obj) { LOGGER.finest(" passivating connection " + obj); final Session conn = (Session) obj; conn.markInactive(); } /** * is invoked in an implementation-specific fashion to determine if an instance is still * valid to be returned by the pool. It will only be invoked on an "activated" instance. * * @param an * instance of {@link Session} maintained by this pool. * @return <code>true</code> if the connection is still alive and operative (checked by * asking its user name), <code>false</code> otherwise. */ @Override public boolean validateObject(Object obj) { ISession session = (ISession) obj; boolean valid = !session.isClosed(); // MAKE PROPER VALIDITY CHECK HERE as for GEOT-1273 if (valid) { try { if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest(" Validating SDE Connection " + session); } /* * Validate the connection's health with testServer instead of getUser. The * former is lighter weight, getUser() forced a server round trip and under * heavy concurrency ate about 30% the time */ session.testServer(); } catch (IOException e) { LOGGER.info("Can't validate SeConnection, discarding it: " + session + ". Reason: " + e.getMessage()); valid = false; } } return valid; } /** * is invoked on every instance when it is being "dropped" from the pool (whether due to the * response from validateObject, or for reasons specific to the pool implementation.) * * @param obj * an instance of {@link Session} maintained by this pool. */ @Override public void destroyObject(Object obj) { Session conn = (Session) obj; conn.destroy(); } } @Override public String toString() { StringBuilder ret = new StringBuilder(getClass().getSimpleName()); ret.append("[config=").append(getConfig()); if (pool == null) { ret.append("[Session pool is disposed]"); } else { ret.append("[ACTIVE: "); ret.append(pool.getNumActive() + "/" + ((GenericObjectPool) pool).getMaxActive()); ret.append(" INACTIVE: "); ret.append(pool.getNumIdle() + "/" + ((GenericObjectPool) pool).getMaxIdle() + "]"); } ret.append("]"); return ret.toString(); } }