
io.ebean.datasource.pool.PooledConnection Maven / Gradle / Ivy
package io.ebean.datasource.pool;
import io.ebean.datasource.delegate.ConnectionDelegator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Savepoint;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
/**
* Is a connection that belongs to a DataSourcePool.
*
*
* It is designed to be part of DataSourcePool. Closing the connection puts it
* back into the pool.
*
*
*
* It defaults autoCommit and Transaction Isolation to the defaults of the
* DataSourcePool.
*
*
*
* It has caching of Statements and PreparedStatements. Remembers the last
* statement that was executed. Keeps statistics on how long it is in use.
*
*/
public class PooledConnection extends ConnectionDelegator {
private static final Logger logger = LoggerFactory.getLogger(PooledConnection.class);
private static final String IDLE_CONNECTION_ACCESSED_ERROR = "Pooled Connection has been accessed whilst idle in the pool, via method: ";
/**
* Marker for when connection is closed due to exceeding the max allowed age.
*/
private static final String REASON_MAXAGE = "maxAge";
/**
* Marker for when connection is closed due to exceeding the max inactive time.
*/
private static final String REASON_IDLE = "idleTime";
/**
* Marker for when the connection is closed due to a reset.
*/
private static final String REASON_RESET = "reset";
/**
* Set when connection is idle in the pool. In general when in the pool the
* connection should not be modified.
*/
private static final int STATUS_IDLE = 88;
/**
* Set when connection given to client.
*/
private static final int STATUS_ACTIVE = 89;
/**
* Set when commit() or rollback() called.
*/
private static final int STATUS_ENDED = 87;
/**
* Name used to identify the PooledConnection for logging.
*/
private final String name;
/**
* The pool this connection belongs to.
*/
private final ConnectionPool pool;
/**
* The underlying connection.
*/
private final Connection connection;
/**
* The time this connection was created.
*/
private final long creationTime;
/**
* Cache of the PreparedStatements
*/
private final PstmtCache pstmtCache;
private final Object pstmtMonitor = new Object();
/**
* Helper for statistics collection.
*/
private final PooledConnectionStatistics stats = new PooledConnectionStatistics();
/**
* The status of the connection. IDLE, ACTIVE or ENDED.
*/
private int status = STATUS_IDLE;
/**
* The reason for a connection closing.
*/
private String closeReason;
/**
* Set this to true if the connection will be busy for a long time.
*
* This means it should skip the suspected connection pool leak checking.
*
*/
private boolean longRunning;
/**
* Flag to indicate that this connection had errors and should be checked to
* make sure it is okay.
*/
private boolean hadErrors;
private boolean resetAutoCommit;
/**
* The last start time. When the connection was given to a thread.
*/
private long startUseTime;
/**
* The last end time of this connection. This is to calculate the usage
* time.
*/
private long lastUseTime;
private long exeStartNanos;
/**
* The last statement executed by this connection.
*/
private String lastStatement;
/**
* The non avaje method that created the connection.
*/
private String createdByMethod;
/**
* Used to find connection pool leaks.
*/
private StackTraceElement[] stackTrace;
private final int maxStackTrace;
/**
* Slot position in the BusyConnectionBuffer.
*/
private int slotId;
private boolean resetIsolationReadOnlyRequired;
/**
* Construct the connection that can refer back to the pool it belongs to.
*
* close() will return the connection back to the pool , while
* closeDestroy() will close() the underlining connection properly.
*
*/
public PooledConnection(ConnectionPool pool, int uniqueId, Connection connection) {
super(connection);
this.pool = pool;
this.connection = connection;
this.name = pool.getName() + "" + uniqueId;
this.pstmtCache = new PstmtCache(pool.getPstmtCacheSize());
this.maxStackTrace = pool.getMaxStackTraceSize();
this.creationTime = System.currentTimeMillis();
this.lastUseTime = creationTime;
}
/**
* For testing the pool without real connections.
*/
protected PooledConnection(String name) {
super(null);
this.name = name;
this.pool = null;
this.connection = null;
this.pstmtCache = null;
this.maxStackTrace = 0;
this.creationTime = System.currentTimeMillis();
this.lastUseTime = creationTime;
}
/**
* Return the slot position in the busy buffer.
*/
int getSlotId() {
return slotId;
}
/**
* Set the slot position in the busy buffer.
*/
void setSlotId(int slotId) {
this.slotId = slotId;
}
/**
* Return a string to identify the connection.
*/
String getName() {
return name;
}
private String getNameSlot() {
return name + ":" + slotId;
}
public String toString() {
return getDescription();
}
private long getBusySeconds() {
return (System.currentTimeMillis() - startUseTime) / 1000;
}
String getDescription() {
return "name[" + name + "] slot[" + slotId + "] startTime[" + getStartUseTime() + "] busySeconds[" + getBusySeconds() + "] createdBy[" + getCreatedByMethod() + "] stmt[" + getLastStatement() + "]";
}
String getFullDescription() {
return "name[" + name + "] slot[" + slotId + "] startTime[" + getStartUseTime() + "] busySeconds[" + getBusySeconds() + "] stackTrace[" + getStackTraceAsString() + "] stmt[" + getLastStatement() + "]";
}
PooledConnectionStatistics getStatistics() {
return stats;
}
/**
* Return true if the connection should be treated as long running (skip connection pool leak check).
*/
boolean isLongRunning() {
return longRunning;
}
/**
* Set this to true if the connection is a long running connection and should skip the
* 'suspected connection pool leak' checking.
*/
public void setLongRunning(boolean longRunning) {
this.longRunning = longRunning;
}
/**
* Close the connection fully NOT putting in back into the pool.
*
* The logErrors parameter exists so that expected errors are not logged
* such as when the database is known to be down.
*
*
* @param logErrors if false then don't log errors when closing
*/
void closeConnectionFully(boolean logErrors) {
if (pool != null) {
// allow collection of load statistics
pool.reportClosingConnection(this);
}
if (logger.isDebugEnabled()) {
logger.debug("Closing Connection[{}] slot[{}] reason[{}] stats: {} , pstmtStats: {} ", name, slotId, closeReason, stats.getValues(false), pstmtCache.getDescription());
}
try {
if (connection.isClosed()) {
// Typically the JDBC Driver has its own JVM shutdown hook and already
// closed the connections in our DataSource pool so making this DEBUG level
logger.debug("Closing Connection[{}] that is already closed?", name);
return;
}
} catch (SQLException ex) {
if (logErrors) {
logger.error("Error checking if connection [" + getNameSlot() + "] is closed", ex);
}
}
try {
for (ExtendedPreparedStatement ps : pstmtCache.values()) {
ps.closeDestroy();
}
} catch (SQLException ex) {
if (logErrors) {
logger.warn("Error when closing connection Statements", ex);
}
}
try {
connection.close();
} catch (SQLException ex) {
if (logErrors || logger.isDebugEnabled()) {
logger.error("Error when fully closing connection [" + getFullDescription() + "]", ex);
}
}
}
/**
* Creates a wrapper ExtendedStatement so that I can get the executed sql. I
* want to do this so that I can get the slowest query statments etc, and
* log that information.
*/
public Statement createStatement() throws SQLException {
if (status == STATUS_IDLE) {
throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "createStatement()");
}
try {
return connection.createStatement();
} catch (SQLException ex) {
markWithError();
throw ex;
}
}
public Statement createStatement(int resultSetType, int resultSetConcurreny) throws SQLException {
if (status == STATUS_IDLE) {
throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "createStatement()");
}
try {
return connection.createStatement(resultSetType, resultSetConcurreny);
} catch (SQLException ex) {
markWithError();
throw ex;
}
}
/**
* Return a PreparedStatement back into the cache.
*/
void returnPreparedStatement(ExtendedPreparedStatement pstmt) {
synchronized (pstmtMonitor) {
if (!pstmtCache.returnStatement(pstmt)) {
try {
// Already an entry in the cache with the exact same SQL...
pstmt.closeDestroy();
} catch (SQLException e) {
logger.error("Error closing Pstmt", e);
}
}
}
}
/**
* This will try to use a cache of PreparedStatements.
*/
public PreparedStatement prepareStatement(String sql, int returnKeysFlag) throws SQLException {
StringBuilder cacheKey = new StringBuilder(sql.length() + 50);
cacheKey.append(sql);
cacheKey.append(':').append(currentSchema);
cacheKey.append(':').append(returnKeysFlag);
return prepareStatement(sql, true, returnKeysFlag, cacheKey.toString());
}
/**
* This will try to use a cache of PreparedStatements.
*/
public PreparedStatement prepareStatement(String sql) throws SQLException {
StringBuilder cacheKey = new StringBuilder(sql.length() + 50);
cacheKey.append(sql);
cacheKey.append(':').append(currentSchema);
return prepareStatement(sql, false, 0, cacheKey.toString());
}
/**
* This will try to use a cache of PreparedStatements.
*/
private PreparedStatement prepareStatement(String sql, boolean useFlag, int flag, String cacheKey) throws SQLException {
if (status == STATUS_IDLE) {
throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "prepareStatement()");
}
try {
synchronized (pstmtMonitor) {
lastStatement = sql;
// try to get a matching cached PStmt from the cache.
ExtendedPreparedStatement pstmt = pstmtCache.remove(cacheKey);
if (pstmt != null) {
return pstmt.reset();
}
// create a new PreparedStatement
PreparedStatement actualPstmt;
if (useFlag) {
actualPstmt = connection.prepareStatement(sql, flag);
} else {
actualPstmt = connection.prepareStatement(sql);
}
return new ExtendedPreparedStatement(this, actualPstmt, sql, cacheKey);
}
} catch (SQLException ex) {
markWithError();
throw ex;
}
}
public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurreny) throws SQLException {
if (status == STATUS_IDLE) {
throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "prepareStatement()");
}
try {
// no caching when creating PreparedStatements this way
lastStatement = sql;
return connection.prepareStatement(sql, resultSetType, resultSetConcurreny);
} catch (SQLException ex) {
markWithError();
throw ex;
}
}
/**
* Reset the connection for returning to the client. Resets the status,
* startUseTime and hadErrors.
*/
void resetForUse() {
this.status = STATUS_ACTIVE;
this.startUseTime = System.currentTimeMillis();
this.exeStartNanos = System.nanoTime();
this.createdByMethod = null;
this.lastStatement = null;
this.hadErrors = false;
this.longRunning = false;
}
/**
* When an error occurs during use add it the connection.
*
* Any PooledConnection that has an error is checked to make sure it works
* before it is placed back into the connection pool.
*
*/
void markWithError() {
hadErrors = true;
}
/**
* close the connection putting it back into the connection pool.
*
* Note that to ensure that the next transaction starts at the correct time
* a commit() or rollback() should be called. If neither has occured at this
* time then a rollback() is used (to end the transaction).
*
*
* To close the connection fully use closeConnectionFully().
*
*/
public void close() throws SQLException {
if (status == STATUS_IDLE) {
throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "close()");
}
long durationNanos = System.nanoTime() - exeStartNanos;
stats.add(durationNanos, hadErrors);
if (hadErrors) {
if (!pool.validateConnection(this)) {
// the connection is BAD, remove it, close it and test the pool
pool.returnConnectionForceClose(this);
return;
}
}
try {
// reset the autoCommit back if client code changed it
if (resetAutoCommit) {
connection.setAutoCommit(pool.isAutoCommit());
resetAutoCommit = false;
}
// Generally resetting Isolation level seems expensive.
// Hence using resetIsolationReadOnlyRequired flag
// performance reasons.
if (resetIsolationReadOnlyRequired) {
resetIsolationReadOnly();
resetIsolationReadOnlyRequired = false;
}
// the connection is assumed GOOD so put it back in the pool
lastUseTime = System.currentTimeMillis();
connection.clearWarnings();
status = STATUS_IDLE;
pool.returnConnection(this);
} catch (Exception ex) {
// the connection is BAD, remove it, close it and test the pool
logger.warn("Error when trying to return connection to pool, closing fully.", ex);
pool.returnConnectionForceClose(this);
}
}
private void resetIsolationReadOnly() throws SQLException {
// reset the transaction isolation if the client code changed it
//noinspection MagicConstant
if (connection.getTransactionIsolation() != pool.getTransactionIsolation()) {
//noinspection MagicConstant
connection.setTransactionIsolation(pool.getTransactionIsolation());
}
// reset readonly to false
if (connection.isReadOnly()) {
connection.setReadOnly(false);
}
}
protected void finalize() throws Throwable {
try {
if (connection != null && !connection.isClosed()) {
// connect leak?
logger.warn("Closing Connection on finalize() - {}", getFullDescription());
closeConnectionFully(false);
}
} catch (Exception e) {
logger.error("Error when finalize is closing a connection? (unexpected)", e);
}
super.finalize();
}
/**
* Return true if the connection is too old.
*/
private boolean exceedsMaxAge(long maxAgeMillis) {
if (maxAgeMillis > 0 && (creationTime < (System.currentTimeMillis() - maxAgeMillis))) {
this.closeReason = REASON_MAXAGE;
return true;
}
return false;
}
boolean shouldTrimOnReturn(long lastResetTime, long maxAgeMillis) {
if (creationTime <= lastResetTime) {
this.closeReason = REASON_RESET;
return true;
}
return exceedsMaxAge(maxAgeMillis);
}
/**
* Return true if the connection has been idle for too long or is too old.
*/
boolean shouldTrim(long usedSince, long createdSince) {
if (lastUseTime < usedSince) {
// been idle for too long so trim it
this.closeReason = REASON_IDLE;
return true;
}
if (createdSince > 0 && createdSince > creationTime) {
// exceeds max age so trim it
this.closeReason = REASON_MAXAGE;
return true;
}
return false;
}
/**
* Return the time the connection was passed to the client code.
*
* Used to detect busy connections that could be leaks.
*
*/
private long getStartUseTime() {
return startUseTime;
}
/**
* Returns the time the connection was last used.
*
* Used to close connections that have been idle for some time. Typically 5
* minutes.
*
*/
long getLastUsedTime() {
return lastUseTime;
}
/**
* Returns the last sql statement executed.
*/
private String getLastStatement() {
return lastStatement;
}
/**
* Called by ExtendedStatement to trace the sql being executed.
*
* Note with addBatch() this will not really work.
*
*/
void setLastStatement(String lastStatement) {
this.lastStatement = lastStatement;
if (logger.isTraceEnabled()) {
logger.trace(".setLastStatement[" + lastStatement + "]");
}
}
/**
* Also note the read only status needs to be reset when put back into the
* pool.
*/
public void setReadOnly(boolean readOnly) throws SQLException {
// A bit loose not checking for STATUS_IDLE
// if (status == STATUS_IDLE) {
// throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR +
// "setReadOnly()");
// }
resetIsolationReadOnlyRequired = true;
connection.setReadOnly(readOnly);
}
/**
* Also note the Isolation level needs to be reset when put back into the
* pool.
*/
public void setTransactionIsolation(int level) throws SQLException {
if (status == STATUS_IDLE) {
throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "setTransactionIsolation()");
}
try {
resetIsolationReadOnlyRequired = true;
connection.setTransactionIsolation(level);
} catch (SQLException ex) {
markWithError();
throw ex;
}
}
//
//
// Simple wrapper methods which pass a method call onto the acutal
// connection object. These methods are safe-guarded to prevent use of
// the methods whilst the connection is in the connection pool.
//
//
public void clearWarnings() throws SQLException {
if (status == STATUS_IDLE) {
throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "clearWarnings()");
}
connection.clearWarnings();
}
public void commit() throws SQLException {
if (status == STATUS_IDLE) {
throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "commit()");
}
try {
status = STATUS_ENDED;
connection.commit();
} catch (SQLException ex) {
markWithError();
throw ex;
}
}
public boolean getAutoCommit() throws SQLException {
if (status == STATUS_IDLE) {
throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "getAutoCommit()");
}
return connection.getAutoCommit();
}
public String getCatalog() throws SQLException {
if (status == STATUS_IDLE) {
throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "getCatalog()");
}
return connection.getCatalog();
}
public DatabaseMetaData getMetaData() throws SQLException {
if (status == STATUS_IDLE) {
throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "getMetaData()");
}
return connection.getMetaData();
}
public int getTransactionIsolation() throws SQLException {
if (status == STATUS_IDLE) {
throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "getTransactionIsolation()");
}
return connection.getTransactionIsolation();
}
public Map> getTypeMap() throws SQLException {
if (status == STATUS_IDLE) {
throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "getTypeMap()");
}
return connection.getTypeMap();
}
public SQLWarning getWarnings() throws SQLException {
if (status == STATUS_IDLE) {
throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "getWarnings()");
}
return connection.getWarnings();
}
public boolean isClosed() throws SQLException {
if (status == STATUS_IDLE) {
throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "isClosed()");
}
return connection.isClosed();
}
public boolean isReadOnly() throws SQLException {
if (status == STATUS_IDLE) {
throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "isReadOnly()");
}
return connection.isReadOnly();
}
public String nativeSQL(String sql) throws SQLException {
if (status == STATUS_IDLE) {
throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "nativeSQL()");
}
lastStatement = sql;
return connection.nativeSQL(sql);
}
public CallableStatement prepareCall(String sql) throws SQLException {
if (status == STATUS_IDLE) {
throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "prepareCall()");
}
lastStatement = sql;
return connection.prepareCall(sql);
}
public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurreny) throws SQLException {
if (status == STATUS_IDLE) {
throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "prepareCall()");
}
lastStatement = sql;
return connection.prepareCall(sql, resultSetType, resultSetConcurreny);
}
public void rollback() throws SQLException {
if (status == STATUS_IDLE) {
throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "rollback()");
}
try {
status = STATUS_ENDED;
connection.rollback();
} catch (SQLException ex) {
markWithError();
throw ex;
}
}
public void setAutoCommit(boolean autoCommit) throws SQLException {
if (status == STATUS_IDLE) {
throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "setAutoCommit()");
}
try {
connection.setAutoCommit(autoCommit);
resetAutoCommit = true;
} catch (SQLException ex) {
markWithError();
throw ex;
}
}
public void setCatalog(String catalog) throws SQLException {
if (status == STATUS_IDLE) {
throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "setCatalog()");
}
connection.setCatalog(catalog);
}
public void setTypeMap(Map> map) throws SQLException {
if (status == STATUS_IDLE) {
throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "setTypeMap()");
}
connection.setTypeMap(map);
}
public Savepoint setSavepoint() throws SQLException {
try {
return connection.setSavepoint();
} catch (SQLException ex) {
markWithError();
throw ex;
}
}
public Savepoint setSavepoint(String savepointName) throws SQLException {
try {
return connection.setSavepoint(savepointName);
} catch (SQLException ex) {
markWithError();
throw ex;
}
}
public void rollback(Savepoint sp) throws SQLException {
try {
connection.rollback(sp);
} catch (SQLException ex) {
markWithError();
throw ex;
}
}
public void releaseSavepoint(Savepoint sp) throws SQLException {
try {
connection.releaseSavepoint(sp);
} catch (SQLException ex) {
markWithError();
throw ex;
}
}
public void setHoldability(int i) throws SQLException {
try {
connection.setHoldability(i);
} catch (SQLException ex) {
markWithError();
throw ex;
}
}
public int getHoldability() throws SQLException {
try {
return connection.getHoldability();
} catch (SQLException ex) {
markWithError();
throw ex;
}
}
public Statement createStatement(int i, int x, int y) throws SQLException {
try {
return connection.createStatement(i, x, y);
} catch (SQLException ex) {
markWithError();
throw ex;
}
}
public PreparedStatement prepareStatement(String s, int i, int x, int y) throws SQLException {
try {
return connection.prepareStatement(s, i, x, y);
} catch (SQLException ex) {
markWithError();
throw ex;
}
}
public PreparedStatement prepareStatement(String s, int[] i) throws SQLException {
try {
return connection.prepareStatement(s, i);
} catch (SQLException ex) {
markWithError();
throw ex;
}
}
public PreparedStatement prepareStatement(String s, String[] s2) throws SQLException {
try {
return connection.prepareStatement(s, s2);
} catch (SQLException ex) {
markWithError();
throw ex;
}
}
public CallableStatement prepareCall(String s, int i, int x, int y) throws SQLException {
try {
return connection.prepareCall(s, i, x, y);
} catch (SQLException ex) {
markWithError();
throw ex;
}
}
/**
* Returns the method that created the connection.
*
* Used to help finding connection pool leaks.
*
*/
private String getCreatedByMethod() {
if (createdByMethod != null) {
return createdByMethod;
}
if (stackTrace == null) {
return null;
}
for (int j = 0; j < stackTrace.length; j++) {
String methodLine = stackTrace[j].toString();
if (!skipElement(methodLine)) {
createdByMethod = methodLine;
return createdByMethod;
}
}
return null;
}
private boolean skipElement(String methodLine) {
if (methodLine.startsWith("java.lang.")) {
return true;
} else if (methodLine.startsWith("java.util.")) {
return true;
} else if (methodLine.startsWith("org.avaje.datasource.")) {
return true;
}
return methodLine.startsWith("com.avaje.ebean");
}
/**
* Set the stack trace to help find connection pool leaks.
*/
void setStackTrace(StackTraceElement[] stackTrace) {
this.stackTrace = stackTrace;
}
/**
* Return the stackTrace as a String for logging purposes.
*/
private String getStackTraceAsString() {
StackTraceElement[] stackTrace = getStackTrace();
if (stackTrace == null) {
return "";
}
return Arrays.toString(stackTrace);
}
/**
* Return the full stack trace that got the connection from the pool. You
* could use this if getCreatedByMethod() doesn't work for you.
*/
private StackTraceElement[] getStackTrace() {
if (stackTrace == null) {
return null;
}
// filter off the top of the stack that we are not interested in
ArrayList filteredList = new ArrayList();
boolean include = false;
for (int i = 0; i < stackTrace.length; i++) {
if (!include && !skipElement(stackTrace[i].toString())) {
include = true;
}
if (include && filteredList.size() < maxStackTrace) {
filteredList.add(stackTrace[i]);
}
}
return filteredList.toArray(new StackTraceElement[filteredList.size()]);
}
}