/* * 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.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.WeakHashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; import org.geotools.arcsde.ArcSdeException; import org.geotools.arcsde.logging.Loggers; import org.geotools.arcsde.versioning.ArcSdeVersionHandler; import com.esri.sde.sdk.client.SeColumnDefinition; import com.esri.sde.sdk.client.SeConnection; import com.esri.sde.sdk.client.SeCoordinateReference; import com.esri.sde.sdk.client.SeDBMSInfo; import com.esri.sde.sdk.client.SeDelete; import com.esri.sde.sdk.client.SeException; import com.esri.sde.sdk.client.SeFilter; import com.esri.sde.sdk.client.SeInsert; import com.esri.sde.sdk.client.SeLayer; import com.esri.sde.sdk.client.SeObjectId; import com.esri.sde.sdk.client.SeQuery; import com.esri.sde.sdk.client.SeQueryInfo; import com.esri.sde.sdk.client.SeRasterColumn; import com.esri.sde.sdk.client.SeRegistration; import com.esri.sde.sdk.client.SeRelease; import com.esri.sde.sdk.client.SeSqlConstruct; import com.esri.sde.sdk.client.SeState; import com.esri.sde.sdk.client.SeStreamOp; import com.esri.sde.sdk.client.SeTable; import com.esri.sde.sdk.client.SeUpdate; import com.esri.sde.sdk.geom.GeometryFactory; /** * Default implementation of an {@link ISession} * <p> * As for the ESRI ArcSDE Java API v9.3.0, the {@link SeQuery#prepareQuery} and * {@link SeQuery#prepareQueryInfo} methods lead to a memory leak, with {@code SgCoordRef} and * {@link SeCoordinateReference} instances somehow tied to the {@link SeConnection}. To avoid Heap * Memory starvation, this {@link Session} will auto-close upon a fixed number of calls to those * {@code SeQuery} methods, so that the memory can be reclaimed by the garbage collector before it * becomes a real problem. When that happens, this Session will be marked closed and discarded from * the {@link SessionPool}, leaving room in the pool to create a new Session as needed. * </p> * <p> * Both the {@link #createAndExecuteQuery} and {@link #prepareQuery} methods will increment the * auto-close counter. * <p> * The default value for the auto-close threshold is {@code 500}. A different value can be specified * through the {@code "org.geotools.arcsde.session.AutoCloseThreshold"} System property. For * example, by running your application like * {@code java -Dorg.geotools.arcsde.session.AutoCloseThreshold=100 -cp... MyApp} * </p> * * @author Gabriel Roldan * @author Jody Garnett * @version $Id$ * @since 2.3.x */ class Session implements ISession { private static final Logger LOGGER = Loggers.getLogger("org.geotools.arcsde.session"); /** * Threshold to be reached by {@link #autoCloseCounter} to automatically recycle (close) the * Session and its {@link SeConnection} */ private static final int AUTO_CLOSE_COUNTER_THRESHOLD; static { Integer systemPropValue = Integer .getInteger("org.geotools.arcsde.session.AutoCloseThreshold"); AUTO_CLOSE_COUNTER_THRESHOLD = systemPropValue == null ? 500 : systemPropValue.intValue(); LOGGER.info("Session auto-close threshold set to " + AUTO_CLOSE_COUNTER_THRESHOLD); } /** * Counter incremented every time an operation that degrades the performance of the running * application is executed, in order to close the {@link SeConnection} when it reaches * {@link #AUTO_CLOSE_COUNTER_THRESHOLD}. See class' JavaDocs for more details * * @see <a href="http://jira.codehaus.org/browse/GEOT-3227">GEOT-3227</a> * @see #prepareQuery(SeQueryInfo, SeFilter[], ArcSdeVersionHandler) */ private int autoCloseCounter; /** * How many seconds must have elapsed since the last connection round trip to the server for * {@link #testServer()} to actually check the connection's validity */ protected static final long TEST_SERVER_ROUNDTRIP_INTERVAL_SECONDS = 5; /** Actual SeConnection being protected */ private final SeConnection connection; /** * SessionPool used to manage open connections (shared). */ private final SessionPool pool; private final ArcSDEConnectionConfig config; /** * Used to assign unique ids to each new session */ private static final AtomicInteger sessionCounter = new AtomicInteger(); /** * Global unique id for this session */ private final int sessionId; private boolean transactionInProgress; private boolean isPassivated; private Map<String, SeTable> cachedTables = new WeakHashMap<String, SeTable>(); private Map<String, SeLayer> cachedLayers = new WeakHashMap<String, SeLayer>(); /** * Keeps track of the number of references to this session (ie, how many times it has been * {@link #markActive() activated} so it's only actually {@link #dispose() disposed} when the * reference count gets down to zero. */ private final AtomicInteger referenceCounter = new AtomicInteger(); /** * Provides safe access to an SeConnection. * * @param pool * SessionPool used to manage SeConnection * @param config * Used to set up a SeConnection * @throws SeException * If we cannot connect */ Session(final SessionPool pool, final ArcSDEConnectionConfig config) throws IOException { this.sessionId = sessionCounter.incrementAndGet(); this.config = config; this.pool = pool; final CreateSeConnectionCommand connectionCommand; connectionCommand = new CreateSeConnectionCommand(config, sessionId); try { this.connection = issue(connectionCommand); } catch (IOException e) { throw e; } catch (RuntimeException shouldntHappen) { throw shouldntHappen; } } /** * @see ISession#issue(org.geotools.arcsde.session.Command) */ public synchronized <T> T issue(final Command<T> command) throws IOException { try { if (connection == null) { return command.execute(this, null); } else { return command.execute(this, connection); } } catch (SeException e) { throw new ArcSdeException(e); } } /** * @see ISession#testServer() */ public final void testServer() throws IOException { /* * This method is called often (every time a session is to be returned from the pool) to * check if it's still valid. We can call getTimeSinceLastRT safely since it does not * require a server roundtrip and hence there's no risk of violating thread safety. So we do * it before issuing the command to avoid the perf penalty imposed by running the command if * not needed. */ final long secondsSinceLastServerRoundTrip = this.connection.getTimeSinceLastRT(); if (TEST_SERVER_ROUNDTRIP_INTERVAL_SECONDS < secondsSinceLastServerRoundTrip) { issue(Commands.TEST_SERVER); } } /** * @see ISession#isClosed() */ public final boolean isClosed() { return this.connection.isClosed(); } /** * Marks the connection as being active (i.e. its out of the pool and ready to be used). * <p> * Shall be called just before being returned from the connection pool * </p> * * @see #isPassivated * @see #checkActive() */ void markActive() { referenceCounter.incrementAndGet(); this.isPassivated = false; } /** * Marks the connection as being inactive (i.e. laying on the connection pool) * <p> * Shall be callled just before sending it back to the pool * </p> * * @see #markActive() * @see #isPassivated * @see #checkActive() */ void markInactive() { if (referenceCounter.get() != 0) { throw new IllegalStateException("referenceCount = " + referenceCounter); } this.isPassivated = true; } /** * @see ISession#isPassivated() */ public boolean isDisposed() { return isPassivated; } /** * Sanity check method called before every public operation delegates to the superclass. * * @throws IllegalStateException * if {@link #isDisposed() isPassivated() == true} as this is a serious workflow * breackage. */ private void checkActive() { if (isDisposed()) { throw new IllegalStateException("Unrecoverable error: " + toString() + " is passivated, shall not be used!"); } } /** * @see ISession#getLayer(java.lang.String) */ public SeLayer getLayer(final String layerName) throws IOException { checkActive(); if (!cachedLayers.containsKey(layerName)) { synchronized (cachedLayers) { if (!cachedLayers.containsKey(layerName)) { SeTable table = getTable(layerName); SeLayer layer = issue(new Commands.GetLayerCommand(table)); if (layer != null) { cachedLayers.put(layerName, layer); } } } } SeLayer seLayer = cachedLayers.get(layerName); if (seLayer == null) { throw new NoSuchElementException("Layer '" + layerName + "' not found"); } return seLayer; } /** * @see ISession#getRasterColumn(java.lang.String) */ public synchronized SeRasterColumn getRasterColumn(final String rasterName) throws IOException { throw new UnsupportedOperationException("Waiting for a proper implementation"); } /** * @see org.geotools.arcsde.session.ISession#getRasterColumns() */ public List<String> getRasterColumns() throws IOException { checkActive(); List<String> rasterNames = issue(Commands.GET_RASTER_COLUMN_NAMES); return rasterNames; } /** * @see ISession#getTable(java.lang.String) */ public SeTable getTable(final String tableName) throws IOException { checkActive(); if (!cachedTables.containsKey(tableName)) { synchronized (cachedTables) { if (!cachedTables.containsKey(tableName)) { SeTable table = issue(new Commands.GetTableCommand(tableName)); cachedTables.put(tableName, table); } } } SeTable seTable = (SeTable) cachedTables.get(tableName); return seTable; } /** * @see ISession#startTransaction() */ public void startTransaction() throws IOException { checkActive(); issue(Commands.START_TRANSACTION); transactionInProgress = true; } /** * @see ISession#commitTransaction() */ public void commitTransaction() throws IOException { checkActive(); issue(Commands.COMMIT_TRANSACTION); transactionInProgress = false; } /** * @see ISession#isTransactionActive() */ public boolean isTransactionActive() { checkActive(); return transactionInProgress; } /** * @see ISession#rollbackTransaction() */ public void rollbackTransaction() throws IOException { checkActive(); try { issue(Commands.ROLLBACK_TRANSACTION); } finally { transactionInProgress = false; } } /** * @see ISession#dispose() */ public void dispose() throws IllegalStateException { checkActive(); final int refCount = referenceCounter.decrementAndGet(); if (refCount > 0) { // ignore if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest("---------> Ignoring disposal, ref count is still " + refCount + " for " + this); } // System.err.println("---------> Ignoring disposal, ref count is still " + refCount // + " for " + this); return; } if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest(" -> RefCount is " + refCount + ". Disposing " + this); } if (transactionInProgress) { throw new IllegalStateException( "Transaction is in progress, should commit or rollback before closing"); } if (autoCloseCounter >= AUTO_CLOSE_COUNTER_THRESHOLD) { LOGGER.warning("Auto-closing " + this + " to avoid memory leak in ESRI Java API (see GEOT-3227)"); this.destroy(); } try { // System.err.println("---------> Disposing " + this + " on thread " + // Thread.currentThread().getName()); this.pool.returnObject(this); } catch (Exception e) { LOGGER.log(Level.SEVERE, e.getMessage(), e); } } @Override public String toString() { return "Session[" + sessionId + "]"; } /** * Actually closes the connection, called when the session is discarded from the pool */ void destroy() { LOGGER.fine("Destroying connection " + toString()); try { issue(Commands.CLOSE_CONNECTION); } catch (Exception e) { LOGGER.log(Level.FINE, "closing connection " + toString(), e); } finally { // taskExecutor.shutdown(); } } /** * @see ISession#equals(java.lang.Object) */ @Override public boolean equals(Object other) { return other == this; } /** * @see ISession#hashCode() */ @Override public int hashCode() { return 17 ^ this.config.hashCode(); } /** * @see ISession#getLayers() */ public List<SeLayer> getLayers() throws IOException { return issue(Commands.GET_LAYERS); } /** * @see ISession#getUser() */ public String getUser() throws IOException { return issue(Commands.GET_USER); } /** * @see ISession#getRelease() */ public SeRelease getRelease() throws IOException { return issue(Commands.GET_RELEASE); } /** * @see ISession#getDatabaseName() */ public String getDatabaseName() throws IOException { return issue(Commands.GET_DATABASENAME); } /** * @see ISession#getDBMSInfo() */ public SeDBMSInfo getDBMSInfo() throws IOException { return issue(Commands.GET_DBMS_INFO); } /** * @see ISession#createSeRegistration(java.lang.String) */ public SeRegistration createSeRegistration(final String typeName) throws IOException { return issue(new Commands.CreateSeRegistrationCommand(typeName)); } /** * @see ISession#createSeTable(java.lang.String) */ public SeTable createSeTable(final String qualifiedName) throws IOException { return issue(new Commands.CreateSeTableCommand(qualifiedName)); } /** * @see ISession#createSeInsert() */ public SeInsert createSeInsert() throws IOException { return issue(Commands.CREATE_SEINSERT); } /** * @see ISession#createSeUpdate() */ public SeUpdate createSeUpdate() throws IOException { return issue(Commands.CREATE_SEUPDATE); } /** * @see ISession#createSeDelete() */ public SeDelete createSeDelete() throws IOException { return issue(Commands.CREATE_SEDELETE); } /** * @see ISession#describe(java.lang.String) */ public SeColumnDefinition[] describe(final String tableName) throws IOException { final SeTable table = getTable(tableName); return describe(table); } /** * @see ISession#describe(com.esri.sde.sdk.client.SeTable) */ public SeColumnDefinition[] describe(final SeTable table) throws IOException { return issue(new Commands.DescribeTableCommand(table)); } /** * @see ISession#fetch(com.esri.sde.sdk.client.SeQuery) */ public SdeRow fetch(final SeQuery query) throws IOException { return fetch(query, new SdeRow((GeometryFactory) null)); } public SdeRow fetch(final SeQuery query, final SdeRow currentRow) throws IOException { return issue(new Commands.FetchRowCommand(query, currentRow)); } /** * @see ISession#close(com.esri.sde.sdk.client.SeState) */ public void close(final SeState state) throws IOException { issue(new Commands.CloseStateCommand(state)); } /** * @see ISession#close(com.esri.sde.sdk.client.SeStreamOp) */ public void close(final SeStreamOp stream) throws IOException { issue(new Commands.CloseStreamCommand(stream)); } /** * @see ISession#createState(com.esri.sde.sdk.client.SeObjectId) */ public SeState createState(final SeObjectId stateId) throws IOException { return issue(new Commands.CreateSeStateCommand(stateId)); } /** * @see ISession#createAndExecuteQuery(java.lang.String[], * com.esri.sde.sdk.client.SeSqlConstruct) */ public SeQuery createAndExecuteQuery(final String[] propertyNames, final SeSqlConstruct sql) throws IOException { this.autoCloseCounter++; return issue(new Commands.CreateAndExecuteQueryCommand(propertyNames, sql)); } /** * Creates either a direct child state of parentStateId, or a sibling being an exact copy of * parentStatId if either the state can't be closed because its in use or parentStateId does not * belong to the current user. */ public SeState createChildState(final long parentStateId) throws IOException { return issue(new Commands.CreateVersionStateCommand(parentStateId)); } private static final class CreateSeConnectionCommand extends Command<SeConnection> { private final ArcSDEConnectionConfig config; private final int sessionId; /** * * @param config * @param sessionId * the session id the connection is to be created for. For exception reporting * purposes only */ private CreateSeConnectionCommand(final ArcSDEConnectionConfig config, final int sessionId) { this.config = config; this.sessionId = sessionId; } @Override public SeConnection execute(final ISession session, final SeConnection connection) throws SeException, IOException { final String serverName = config.getServerName(); final String portNumber = config.getPortNumber(); final String databaseName = config.getDatabaseName(); final String userName = config.getUserName(); final String userPassword = config.getPassword(); NegativeArraySizeException cause = null; SeConnection conn = null; try { for (int i = 0; i < 3; i++) { try { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Creating connection for session #" + sessionId + "(try " + (i + 1) + " of 3)"); } conn = new SeConnection(serverName, portNumber, databaseName, userName, userPassword); break; } catch (NegativeArraySizeException nase) { LOGGER.warning("Strange failed ArcSDE connection error. " + "Trying again (try " + (i + 1) + " of 3). SessionId: " + sessionId); cause = nase; } } } catch (SeException e) { throw new ArcSdeException("Can't create connection to " + serverName + " for Session #" + sessionId, e); } catch (RuntimeException e) { throw (IOException) new IOException("Can't create connection to " + serverName + " for Session #" + sessionId).initCause(e); } if (cause != null) { throw (IOException) new IOException("Couldn't create ArcSDE connection to " + serverName + " for Session #" + sessionId + " because of strange SDE internal exception. " + " Tried 3 times, giving up.").initCause(cause); } return conn; } } /** * @see org.geotools.arcsde.session.ISession#prepareQuery(com.esri.sde.sdk.client.SeQueryInfo, * com.esri.sde.sdk.client.SeFilter[], org.geotools.arcsde.versioning.ArcSdeVersionHandler) */ public SeQuery prepareQuery(final SeQueryInfo qInfo, final SeFilter[] spatialConstraints, final ArcSdeVersionHandler version) throws IOException { this.autoCloseCounter++; return issue(new Commands.PrepareQueryCommand(qInfo, spatialConstraints, version)); } }