All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.techempower.data.jdbc.JdbcConnectionProfile Maven / Gradle / Ivy

There is a newer version: 3.3.14
Show newest version
/*******************************************************************************
 * Copyright (c) 2018, TechEmpower, Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name TechEmpower, Inc. nor the names of its
 *       contributors may be used to endorse or promote products derived from
 *       this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL TECHEMPOWER, INC. BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *******************************************************************************/

package com.techempower.data.jdbc;

import java.io.*;
import java.sql.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;

import com.techempower.data.*;
import com.techempower.helper.*;
import com.techempower.util.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Encapsulates each instance of a connection to a database.
 *   

* Applications do not typically interact directly with this class, but * rather interact with BasicConnectorFactory to get connectors and * JdbcConnector to execute queries. * * @see JdbcConnector * @see BasicConnectorFactory */ public class JdbcConnectionProfile { // // Constants. // public static final long CLOSE_DELAY = 10 * UtilityConstants.SECOND; private static final long UNUSED = -1; // // Member variables. // /** * The identifier number for this ConnectionProfile. */ private int id = 0; /** * The total number of uses for this profile. */ private long useCount = 1; /** * Number of connections closed (historical). */ private int closeCount = 0; /** * Number of connections established (historical). */ private int connectCount = 0; /** * Last used time stamp. */ private volatile long lastUsed = System.currentTimeMillis(); /** * A reference to the connection profile manager. */ private final JdbcConnectionManager manager; /** * A ComponentLog for debugging. */ private final Logger log = LoggerFactory.getLogger(getClass()); /** * A reference to the connection. */ private volatile ConnectionWrapper connection; /** * Should the Connection be closed immediately once the client releases this * Profile? */ private boolean closeOnRelease; /** * The ID of the thread that currently has this profile reserved. */ private final AtomicLong reservedForThread = new AtomicLong(UNUSED); /** * A ConnectionMonitor available for quick/lightweight access to the raw * JDBC Connection. */ private final Monitor connectionMonitor = new Monitor(); /** * Constructor. */ protected JdbcConnectionProfile(int id, JdbcConnectionManager manager) { this.manager = manager; this.id = id; } /** * Gets the Profile's ID number. */ public int getId() { return id; } /** * Sets the close-on-release flag. */ protected void setCloseOnRelease(boolean closeOnRelease) { this.closeOnRelease = closeOnRelease; } /** * Establishes a new connection. */ protected synchronized void establishDatabaseConnection() { // Close any existing connection. close(false); if (this.connection == null) { // Increment the historical connections counter. connectCount++; final JdbcConnectionAttributes attributes = manager.getAttributes(); // Just some sanity checking (this isn't really necessary, but it // doesn't hurt). if ( (attributes != null) && (StringHelper.isNonEmpty(attributes.getJdbcURLPrefix())) && (StringHelper.isNonEmpty(attributes.getConnectString())) ) { final String connectionUrl = attributes.getJdbcURLPrefix() + attributes.getConnectString(); // Connect to the database. try { log.trace("{}Establishing database connection: [{}, {}]", logPrefix(), connectionUrl, attributes.getUsername()); connection = new ConnectionWrapper(this, DriverManager.getConnection(connectionUrl, attributes.getUsername(), attributes.getPassword())); } catch (SQLException sqlexc) { log.warn("{}SQL Exception while connecting.", logPrefix(), sqlexc); connection = null; } } else { log.warn("{}JDBC URL prefix or connect string is empty; cannot connect to database.", logPrefix()); } } } /** * Gets the SQL Connection object. Records the last used time to be now. */ protected Connection getConnection() { // If the Connection has not been established or was closed, try to // establish a new Connection. if (!isConnectionAvailable()) { establishDatabaseConnection(); } return connection; } /** * Executes a keep-alive query on the Connection without affecting the * "last used" time. */ protected synchronized void keepAlive() { final Connection connect = connection; if (connect != null) { // Claim the Profile for ourselves right now. if (claim(false)) { final JdbcConnectionManager mgr = manager; Runnable keepAlive = new Runnable() { @Override public void run() { try { final String query = mgr.getAttributes().getTestQuery(); final String expectedResult = mgr.getAttributes().getTestValue(); try (Statement statement = connect.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)) { try (ResultSet resultSet = statement.executeQuery(query)) { if (resultSet.next()) { final String actualResult = resultSet.getString(1); // We don't actually have any defined behavior if the expected and // actual results don't match. We just ran the query to keep the // connection alive. But let's log something if they differ. if (!expectedResult.equals(actualResult)) { log.warn("{}Expected \"{}\" but received \"{}\".", logPrefix(), expectedResult, actualResult); } } else { log.warn("{}No results from keep-alive query.", logPrefix()); } } } } catch (SQLException sqlexc) { log.warn("{}SQLException during keep-alive: ", logPrefix(), sqlexc); // Close this connection (it will be reconnected on next use). close(true); } finally { // Release the profile for use by clients. close(); } } }; try { ThreadHelper.submit(keepAlive); } catch (RejectedExecutionException rje) { log.info("{}Cannot keep alive connection: ", logPrefix(), rje); close(); } } else { log.debug("{}Unable to run keep-alive. This is normal if a query is running.", logPrefix()); // If we were unable to claim this profile, let's see how long it has // been executing a query. If it's longer than the query timeout, we // will close the connection. final long time = System.currentTimeMillis() - lastUsed; if (time > manager.getAttributes().getAbortTimeout()) { log.info("{}Query timeout. Stopping query running for {}ms.", logPrefix(), time); close(true); } } } } /** * Returns whether or not this profile is in use. */ public boolean isInUse() { return reservedForThread.get() != UNUSED; } /** * Notifies listeners of a query starting. */ protected void notifyListenerOnClaim() { if (manager.getListener() != null) { manager.getListener().queryStarting(); } } /** * Notifies listeners of a query completing. */ protected void notifyListenerOnRelease() { if (manager.getListener() != null) { manager.getListener().queryCompleting(); } } /** * Claims this connection for use. */ public boolean claim() { boolean claimed = false; try { claimed = claim(true); return claimed; } finally { if (claimed) { notifyListenerOnClaim(); } } } /** * Claims this connection for use, optionally without tracking this as a * real use. The keep-alive code call claim(false) so that keep alive * requests do not reset the last-use timestamp. */ public boolean claim(boolean trackUsage) { final long threadId = Thread.currentThread().getId(); if (reservedForThread.compareAndSet(UNUSED, threadId)) { // Record the last used time. if (trackUsage) { lastUsed = System.currentTimeMillis(); useCount++; } return true; } return false; } /** * Releases this Connection Profile for use by other clients, but only if * we are called by the proper client. This allows for "failsafe" * extraneous calls to release. */ public void close() { final boolean close = closeOnRelease; reservedForThread.set(UNUSED); try { if (close) { close(false); } } finally { notifyListenerOnRelease(); } } /** * Is a Connection reference available? */ public boolean isConnectionAvailable() { return (connection != null); } /** * Gets the time of the last use. The last use is determined to be the * last time getConnection was called. */ public long getLastUse() { return lastUsed; } /** * Gets the manager reference. */ protected JdbcConnectionManager getManager() { return manager; } /** * Gets the connection profile's closed state. If the connection is null, * returns true. Otherwise, returns false. */ public boolean isClosed() { return (connection == null); } /** * Gets the connection's closed flag. If the connection is null, * returns true. If the parameter checkConnection is true, then this * method will also call the connection's isClosed method which * may result in a database hit of some kind. */ public boolean isClosed(boolean checkConnection) { try { if (connection != null) { if (checkConnection) { return connection.isClosedUnderlyingConnection(); } else { // Connection is non-null, but we're not going to call con.isClosed, // so we assume that it's not closed. return false; } } } catch (SQLException sqlexc) { log.warn("{}SQLException while determining connection's closed status.", logPrefix(), sqlexc); } // Connection is null, or we got an exception when asking if the // Connection is closed. So we'll assume it's closed. return true; } /** * Closes the connection. * * @param onNewThread true if the Connection should be closed on a new * thread. */ protected synchronized void close(boolean onNewThread) { if (connection != null) { // Increment the historical closes counter. closeCount++; final Runnable closer = new Closer(connection); if (onNewThread) { // Queue the closer to run in 10 seconds. ThreadHelper.schedule(closer, CLOSE_DELAY, TimeUnit.MILLISECONDS); } else { // Run the closer inline and immediately. closer.run(); } connection = null; } } private String logPrefix() { return "[c" + getId() + ";t" + reservedForThread.get() + "] "; } /** * Used by the close() method above. */ private class Closer implements Runnable { private final ConnectionWrapper localConnection; public Closer(ConnectionWrapper connection) { localConnection = connection; } @Override public void run() { log.debug("{}Closing connection profile {}.", logPrefix(), JdbcConnectionProfile.this.id); try { localConnection.closeUnderlyingConnection(); } catch (SQLException sqlexc) { log.debug("{}SQLException while closing connection: ", logPrefix(), sqlexc); } } } /** * Standard toString. */ @Override public String toString() { final StringList attributeList = new StringList("; "); synchronized (this) { if (isInUse()) { attributeList.add("in-use (thread " + reservedForThread.get() + ")"); } else { attributeList.add("idle"); } } if (isClosed()) { attributeList.add("CLOSED"); } return "JdbcCP " + "[id: " + this.id + "; " + attributeList + "; uses: " + this.useCount + "; connections: " + this.connectCount + "; closes: " + this.closeCount + (this.lastUsed > 0 ? "; last used " + DateHelper.getHumanDifference(this.lastUsed, 2) + " ago" : "") + "]"; } /** * Returns the DatabaseMetaData for the current connection or * null if there is no connection or there was an error retrieving the meta data */ protected DatabaseMetaData getConnectionMetaData() { if (this.connection != null) { try { return this.connection.getMetaData(); } catch (SQLException exc) { log.warn("{}Exception while fetching meta data.", logPrefix(), exc); } } return null; } /** * Gets the PassthroughConnector for this connection profile. */ public ConnectionMonitor getMonitor() { return this.connectionMonitor; } /** * A special implementation of DatabaseConnector that allows pass-through * access to the Connection. */ private class Monitor implements ConnectionMonitor { @Override public Connection getConnection() { return JdbcConnectionProfile.this.getConnection(); } @Override public void close() throws SQLException { JdbcConnectionProfile.this.close(); } @Override public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException { throw new SQLFeatureNotSupportedException(); } @Override public T unwrap(Class iface) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public boolean isWrapperFor(Class iface) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public Connection getConnection(String username, String password) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public PrintWriter getLogWriter() throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public void setLogWriter(PrintWriter out) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public void setLoginTimeout(int seconds) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public int getLoginTimeout() throws SQLException { throw new SQLFeatureNotSupportedException(); } } } // End JdbcConnectionProfile.





© 2015 - 2024 Weber Informatics LLC | Privacy Policy