/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2009-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2009-2012, Geomatys * * 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.geotoolkit.internal.sql; import java.util.Map; import java.util.Iterator; import java.util.Collection; import java.util.LinkedHashMap; import java.sql.Connection; import java.sql.SQLException; import javax.sql.DataSource; import org.geotoolkit.internal.Threads; import org.apache.sis.util.logging.Logging; import static org.apache.sis.util.collection.Containers.hashMapCapacity; /** * A pool of prepared statements with a maximal capacity. Oldest statements are automatically * closed and removed from the map when the number of statements exceed the maximal capacity. * Inactive statements are also closed after some timeout. * * {@note This class duplicates the work done by statement pools in modern JDBC drivers. * Nevertheless it still useful when we retain some additional JDBC resources together with * the <code>PreparedStatement</code>, for example the <code>ResultSet</code> created from * that statement. This is what the <code>geotk-metadata-sql</code> module does.} * * {@section Synchronization} * Every access to this pool <strong>must</strong> be synchronized on {@code this}. * Synchronization is user-responsibility; this class is not thread safe alone. It * will be verified if assertions are enabled. * <p> * Synchronization must be provided by the user because we typically need synchronized block * wider than the {@code get} and {@code put} scope. Execution of a prepared statement may * also need to be done inside the synchronized block, because a single JDBC connection can * not be assumed thread-safe. * <p> * Example of usage: * * {@preformat java * StatementPool<String,StatementEntry> pool = ...; * String key = ...; * synchronized (pool) { * // Get an entry, or create a new one if no entry is available. * // Note that we remove the entry from the map for preventing * // 'removeEldestEntry' to close it before we are done. * StatementEntry entry = pool.remove(key); * if (entry == null) { * entry = new StatementEntry(someStatement); * } * // Use the statement and give it back to the pool once we are * // done. We do not put it back in case of SQLException. * entry.statement.doSomeStuff(); * if (pool.put(key, entry) != null) { * throw new AssertionError(key); * } * } * } * * @param <K> The type of keys. * @param <V> The type of values. * * @author Martin Desruisseaux (Geomatys) * @version 3.19 * * @since 3.03 * @module */ @SuppressWarnings("serial") public class StatementPool<K,V extends StatementEntry> extends LinkedHashMap<K,V> implements Runnable { /** * The timeout before to close a prepared statement. This is set to 2 seconds, which is * a bit short but should be okay if the {@link DataSource} creates pooled connections. * In case there is no connection pool, then the mechanism defined in this package will * hopefully keeps the performance at a reasonable level. */ private static final int TIMEOUT = 2000; /** * An extra delay to add to the {@link #TIMEOUT} in order to increase the chances to * close many statement at once. */ private static final int EXTRA_DELAY = 500; /** * The maximum number of prepared statements to be kept in the pool. */ private final int capacity; /** * The data source object using for fetching the connection to the database. */ private final DataSource dataSource; /** * The connection to the database. This is automatically closed and set to * {@code null} when every statements have been closed. */ private Connection connection; /** * Creates a new pool of the given capacity. * * @param capacity The maximum number of entries to be kept in the pool. * @param dataSource The datasource to the database. */ public StatementPool(final int capacity, final DataSource dataSource) { super(hashMapCapacity(capacity)); this.capacity = capacity; this.dataSource = dataSource; } /** * Creates a new pool with the same configuration than the given pool. * The new pool will use its own connection - it will not be shared. * * @param source The pool from which to copy the configuration. */ public StatementPool(final StatementPool<K,V> source) { super(hashMapCapacity(source.capacity)); capacity = source.capacity; dataSource = source.dataSource; } /** * Returns the connection to the database, creating a new one if needed. This method shall * be invoked inside a synchronized block wider than just the scope of this method in order * to ensure that the connection is used by only one thread at time. This is also necessary * for preventing the background thread to close the connection too early. * * @return The connection to the database. * @throws SQLException If an error occurred while fetching the connection. */ public final Connection connection() throws SQLException { assert Thread.holdsLock(this); Connection c = connection; if (c == null) { assert isEmpty(); connection = c = dataSource.getConnection(); DefaultDataSource.log(c, StatementPool.class); Threads.executeDisposal(this, TIMEOUT + EXTRA_DELAY); } return c; } /** * Overridden in order to check for synchronization if assertions are enabled. * See class-javadoc for why the synchronization must be performed by the caller. */ @Override public final V get(final Object key) { assert Thread.holdsLock(this); return super.get(key); } /** * Overridden in order to check for synchronization if assertions are enabled. * See class-javadoc for why the synchronization must be performed by the caller. * <p> * <b>NOTE:</b> If this method returns a non-null value, then the caller * should consider {@linkplain StatementEntry#close() closing} it. */ @Override public final V put(final K key, final V value) { assert Thread.holdsLock(this); value.expireTime = System.currentTimeMillis() + TIMEOUT; return super.put(key, value); } /** * Overridden in order to check for synchronization if assertions are enabled. * See class-javadoc for why the synchronization must be performed by the caller. * <p> * <b>NOTE:</b> If this method returns a non-null value, then the caller should * either {@linkplain #put put} it back to this pool when finished using it, or * {@linkplain StatementEntry#close() close} it. */ @Override public final V remove(final Object key) { assert Thread.holdsLock(this); return super.remove(key); } /** * If the map has reached its maximal capacity, removes the eldest entries. * This method is invoked indirectly by {@code put} and {@code putAll} operations, * <strong>which must be invoked in a synchronized block</strong>. */ @Override protected final boolean removeEldestEntry(final Map.Entry<K,V> eldest) { assert Thread.holdsLock(this); if (size() > capacity) { eldest.getValue().closeQuietly(); return true; } return false; } /** * Closes all statements and remove them from the map. The connection is not closed - * it will be closed after the timeout if no new statement have been added in this pool. */ @Override public final void clear() { assert Thread.holdsLock(this); for (final StatementEntry entry : values()) { entry.closeQuietly(); } super.clear(); } /** * Intentionally disallows overriding. */ @Override public final Collection<V> values() { assert Thread.holdsLock(this); return super.values(); } /** * Executed in a background thread for closing statements after their expiration time. * This task will be given to the executor every time the first statement is put in an * empty map. * <p> * This method is public as an implementation side-effect but should never been invoked * directly. */ @Override public final synchronized void run() { final long currentTime = System.currentTimeMillis(); final Iterator<V> it = values().iterator(); while (it.hasNext()) { final V entry = it.next(); final long waitTime = entry.expireTime - currentTime; if (waitTime > 0) { // Some statements can not be disposed yet. Threads.executeDisposal(this, waitTime + EXTRA_DELAY); monitorExit(false); return; } it.remove(); entry.closeQuietly(); } /* * No more prepared statements. Close the connection. */ final Connection c = this.connection; connection = null; if (c != null) try { c.close(); } catch (Exception e) { Logging.recoverableException(DefaultDataSource.LOGGER, Connection.class, "close", e); } monitorExit(true); } /** * Invoked in a background thread when the user thread exits its {@code synchronized} statement. * If there is many nested {@code synchronized} statements, then this method is invoked only * after the outer one. * <p> * Assuming that this {@code StatementPool} is used in the way documented in the class javadoc: * * {@preformat java * synchronized (pool) { * StatementEntry entry = pool.remove(key); * // etc... * } * } * * Then subclasses can override this method in order to be informed when the above work is * finished. Note that this method may not be invoked immediately after the work completion. * It may be invoked with a delay up to {@value #TIMEOUT} milliseconds (or a bit more). * * @param closed {@code true} if this method is invoked after the connection has been closed. * In such case, this {@code HashMap} is guaranteed to be empty. * * @since 3.12 */ protected void monitorExit(final boolean closed) { assert Thread.holdsLock(this); assert !closed || isEmpty(); } /** * Closes all statements and removes them from the map. * * @throws SQLException If an error occurred while closing the statements. */ public final synchronized void close() throws SQLException { final Iterator<V> it = values().iterator(); while (it.hasNext()) { it.next().close(); it.remove(); } final Connection c = this.connection; connection = null; if (c != null) { c.close(); } // monitorExit(boolean) will be invoked later by the run() method. } }