From d16f33d26bb31ed82bac90e1461e4295c29fe876 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Thu, 12 Dec 2013 20:57:50 +1300 Subject: [PATCH 1/4] Modified DataSource pool - cleanup of reset and statistics collection --- .../avaje/ebean/config/DataSourceConfig.java | 4 +- .../server/lib/sql/BusyConnectionBuffer.java | 36 +- .../server/lib/sql/DataSourcePool.java | 29 +- .../lib/sql/DataSourcePoolStatistics.java | 97 +++ .../lib/sql/ExtendedPreparedStatement.java | 7 +- .../server/lib/sql/FreeConnectionBuffer.java | 34 + .../server/lib/sql/PooledConnection.java | 791 +++++++++--------- .../server/lib/sql/PooledConnectionQueue.java | 98 ++- .../lib/sql/PooledConnectionStatistics.java | 162 ++++ .../server/lib/sql/PstmtCache.java | 34 +- .../server/lib/sql/TestBusyBuffer.java | 12 + 11 files changed, 852 insertions(+), 452 deletions(-) create mode 100644 src/main/java/com/avaje/ebeaninternal/server/lib/sql/DataSourcePoolStatistics.java create mode 100644 src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnectionStatistics.java diff --git a/src/main/java/com/avaje/ebean/config/DataSourceConfig.java b/src/main/java/com/avaje/ebean/config/DataSourceConfig.java index d6481e28c..9ed13c363 100644 --- a/src/main/java/com/avaje/ebean/config/DataSourceConfig.java +++ b/src/main/java/com/avaje/ebean/config/DataSourceConfig.java @@ -42,14 +42,14 @@ public class DataSourceConfig { private int leakTimeMinutes = 30; - private int maxInactiveTimeSecs = 900; + private int maxInactiveTimeSecs = 720; private int pstmtCacheSize = 20; private int cstmtCacheSize = 20; private int waitTimeoutMillis = 1000; - + private String poolListener; private boolean offline; diff --git a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/BusyConnectionBuffer.java b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/BusyConnectionBuffer.java index dc5a314c2..98b55d682 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/BusyConnectionBuffer.java +++ b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/BusyConnectionBuffer.java @@ -5,6 +5,11 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.avaje.ebeaninternal.server.lib.sql.PooledConnectionStatistics.LoadValues; + /** * A buffer especially designed for Busy PooledConnections. *

@@ -21,6 +26,8 @@ import java.util.List; */ class BusyConnectionBuffer { + private static final Logger logger = LoggerFactory.getLogger(BusyConnectionBuffer.class); + private PooledConnection[] slots; private int growBy; @@ -66,8 +73,13 @@ class BusyConnectionBuffer { return size; } - protected boolean isEmpty(){ - return size == 0; + protected boolean isEmpty() { + for (int i = 0; i < slots.length; i++) { + if (slots[i] != null) { + return false; + } + } + return true; } protected int add(PooledConnection pc){ @@ -75,23 +87,37 @@ class BusyConnectionBuffer { // grow the capacity setCapacity(slots.length + growBy); } - ++size; int slot = nextEmptySlot(); pc.setSlotId(slot); slots[slot] = pc; - return size; + return ++size; } protected boolean remove(PooledConnection pc) { - --size; + int slotId = pc.getSlotId(); if (slots[slotId] != pc){ + PooledConnection heldBy = slots[slotId]; + logger.warn("Failed to remove from slot[{}] PooledConnection[{}] - HeldBy[{}]", pc.getSlotId(), pc, heldBy); return false; } slots[slotId] = null; + --size; return true; } + /** + * Collect the load statistics from all the busy connections. + * @param reset + */ + protected void collectStatistics(LoadValues values, boolean reset) { + + for (int i = 0; i < slots.length; i++) { + if (slots[i] != null){ + values.plus(slots[i].getStatistics().getValues(reset)); + } + } + } /** * Get a shallow read only List of the busy connections. diff --git a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/DataSourcePool.java b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/DataSourcePool.java index c54d5582e..3cb861427 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/DataSourcePool.java +++ b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/DataSourcePool.java @@ -15,11 +15,12 @@ import java.util.Set; import javax.persistence.PersistenceException; import javax.sql.DataSource; -import com.avaje.ebean.config.DataSourceConfig; -import com.avaje.ebeaninternal.api.ClassUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.avaje.ebean.config.DataSourceConfig; +import com.avaje.ebeaninternal.api.ClassUtil; + /** * A robust DataSource. *

@@ -73,7 +74,7 @@ public class DataSourcePool implements DataSource { * The sql used to test a connection. */ private final String heartbeatsql; - + private final int heartbeatFreqSecs; /** @@ -573,7 +574,15 @@ public class DataSourcePool implements DataSource { } queue.returnPooledConnection(pooledConnection); } - + + /** + * Collect statistics of a connection that is fully closing + */ + protected void reportClosingConnection(PooledConnection pooledConnection) { + + queue.reportClosingConnection(pooledConnection); + } + /** * Returns information describing connections that are currently being used. */ @@ -834,6 +843,14 @@ public class DataSourcePool implements DataSource { public Status getStatus(boolean reset) { return queue.getStatus(reset); } + + /** + * Return the aggregated load statistics collected on all the connections in the pool. + */ + public DataSourcePoolStatistics getStatistics(boolean reset) { + + return queue.getStatistics(reset); + } /** * Deregister the JDBC driver. @@ -873,8 +890,8 @@ public class DataSourcePool implements DataSource { } public String toString() { - return "min:" + minSize + " max:" + maxSize + " free:" + free + " busy:" + busy + " waiting:" + waiting - + " highWaterMark:" + highWaterMark + " waitCount:" + waitCount + " hitCount:" + hitCount; + return "min[" + minSize + "] max[" + maxSize + "] free[" + free + "] busy[" + busy + "] waiting[" + waiting + + "] highWaterMark[" + highWaterMark + "] waitCount[" + waitCount + "] hitCount[" + hitCount+"]"; } /** diff --git a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/DataSourcePoolStatistics.java b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/DataSourcePoolStatistics.java new file mode 100644 index 000000000..5bdc1c9ad --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/DataSourcePoolStatistics.java @@ -0,0 +1,97 @@ +package com.avaje.ebeaninternal.server.lib.sql; + +/** + * Represents aggregated statistics collected from the DataSourcePool. + *

+ * The goal is to present insight into the overload load of the DataSourcePool. + * These statistics can be collected and reported regularly to show load over + * time. + *

+ *

+ * Each pooled connection collects statistics. When a pooled connection is fully + * closed it can report it's statistics to the pool to be included as part of + * the collected statistics. + *

+ */ +public class DataSourcePoolStatistics { + + private final long collectionStart; + + private final long count; + + private final long errorCount; + + private final long hwmMicros; + + private final long totalMicros; + + /** + * No statistics collected. + */ + public DataSourcePoolStatistics() { + this.collectionStart = 0; + this.count = 0; + this.errorCount = 0; + this.hwmMicros = 0; + this.totalMicros = 0; + } + + /** + * Construct with statistics collected. + */ + public DataSourcePoolStatistics(long collectionStart, long count, long errorCount, long hwmMicros, long totalMicros) { + this.collectionStart = collectionStart; + this.count = count; + this.errorCount = errorCount; + this.hwmMicros = hwmMicros; + this.totalMicros = totalMicros; + } + + public String toString() { + return "count[" + count + "] errors[" + errorCount + "] totalMicros[" + totalMicros + "] hwmMicros[" + hwmMicros + + "] avgMicros[" + getAvgMicros() + "]"; + } + + /** + * Return the start time this set of statistics was collected from. + */ + public long getCollectionStart() { + return collectionStart; + } + + /** + * Return the total number of 'get connection' requests. + */ + public long getCount() { + return count; + } + + /** + * Return the number of SQLExceptions reported. + */ + public long getErrorCount() { + return errorCount; + } + + /** + * Return the high water mark for the duration a connection was busy/used. + */ + public long getHwmMicros() { + return hwmMicros; + } + + /** + * Return the aggregate time connections were busy/used. + */ + public long getTotalMicros() { + return totalMicros; + } + + /** + * Return the average time connections were busy/used. + */ + public long getAvgMicros() { + return (totalMicros == 0) ? 0 : totalMicros / count; + } + +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/ExtendedPreparedStatement.java b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/ExtendedPreparedStatement.java index 3b3b40344..439b0731f 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/ExtendedPreparedStatement.java +++ b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/ExtendedPreparedStatement.java @@ -31,18 +31,17 @@ public class ExtendedPreparedStatement extends ExtendedStatement implements Prep /** * The SQL used to create the underlying PreparedStatement. */ - final String sql; + private final String sql; /** * The key used to cache this in the connection. */ - final String cacheKey; + private final String cacheKey; /** * Create a wrapped PreparedStatement that can be cached. */ - public ExtendedPreparedStatement(PooledConnection pooledConnection, PreparedStatement pstmt, - String sql, String cacheKey) { + public ExtendedPreparedStatement(PooledConnection pooledConnection, PreparedStatement pstmt, String sql, String cacheKey) { super(pooledConnection, pstmt); this.sql = sql; this.cacheKey = cacheKey; diff --git a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/FreeConnectionBuffer.java b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/FreeConnectionBuffer.java index 23f160091..813740633 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/FreeConnectionBuffer.java +++ b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/FreeConnectionBuffer.java @@ -3,6 +3,8 @@ package com.avaje.ebeaninternal.server.lib.sql; import java.util.ArrayList; import java.util.List; +import com.avaje.ebeaninternal.server.lib.sql.PooledConnectionStatistics.LoadValues; + /** * A buffer designed especially to hold free pooled connections. *

@@ -48,11 +50,31 @@ class FreeConnectionBuffer { * Add at connection. */ protected void add(PooledConnection pc) { + if (conns[addIndex] != null) { + throw new RuntimeException("Buffer slot ["+addIndex+"] already full?"); + } conns[addIndex] = pc; addIndex = inc(addIndex); ++size; } + protected void closeAll(boolean logErrors) { + + final PooledConnection[] items = this.conns; + + this.conns = new PooledConnection[items.length]; + this.size = 0; + this.removeIndex = 0; + this.addIndex = 0; + + for (int i = 0; i < items.length; i++) { + PooledConnection c = items[i]; + if (c != null) { + c.closeConnectionFully(logErrors); + } + } + } + /** * Remove a connection at current remove position. */ @@ -79,6 +101,18 @@ class FreeConnectionBuffer { return copy; } + /** + * Collect the load statistics from all the free connections. + */ + protected void collectStatistics(LoadValues values, boolean reset) { + + for (int i = 0; i < conns.length; i++) { + if (conns[i] != null){ + values.plus(conns[i].getStatistics().getValues(reset)); + } + } + } + /** * Set the free list to be the connections in this copy. This is done after * unused connections have been trimmed. diff --git a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnection.java b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnection.java index 5b29b7fb5..d6c3293bc 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnection.java +++ b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnection.java @@ -34,8 +34,7 @@ import org.slf4j.LoggerFactory; * statement that was executed. Keeps statistics on how long it is in use. *

*/ -public class PooledConnection extends ConnectionDelegator -{ +public class PooledConnection extends ConnectionDelegator { private static final Logger logger = LoggerFactory.getLogger(PooledConnection.class); @@ -45,49 +44,49 @@ public class PooledConnection extends ConnectionDelegator * Set when connection is idle in the pool. In general when in the pool the * connection should not be modified. */ - static final int STATUS_IDLE = 88; + private static final int STATUS_IDLE = 88; /** * Set when connection given to client. */ - static final int STATUS_ACTIVE = 89; + private static final int STATUS_ACTIVE = 89; /** * Set when commit() or rollback() called. */ - static final int STATUS_ENDED = 87; + private static final int STATUS_ENDED = 87; /** * Name used to identify the PooledConnection for logging. */ - final String name; + private final String name; /** * The pool this connection belongs to. */ - final DataSourcePool pool; + private final DataSourcePool pool; /** * The underlying connection. */ - final Connection connection; + private final Connection connection; /** * The time this connection was created. */ - final long creationTime; + private final long creationTime; /** * Cache of the PreparedStatements */ - final PstmtCache pstmtCache; + private final PstmtCache pstmtCache; - final Object pstmtMonitor = new Object(); + private final Object pstmtMonitor = new Object(); /** * The status of the connection. IDLE, ACTIVE or ENDED. */ - int status = STATUS_IDLE; + private int status = STATUS_IDLE; /** * Set this to true if the connection will be busy for a long time. @@ -95,56 +94,53 @@ public class PooledConnection extends ConnectionDelegator * This means it should skip the suspected connection pool leak checking. *

*/ - boolean longRunning; + private boolean longRunning; /** * Flag to indicate that this connection had errors and should be checked to * make sure it is okay. */ - boolean hadErrors; + private boolean hadErrors; /** * The last start time. When the connection was given to a thread. */ - long startUseTime; + private long startUseTime; /** * The last end time of this connection. This is to calculate the usage * time. */ - long lastUseTime; + private long lastUseTime; + + private long exeStartNanos; + + private final PooledConnectionStatistics stats = new PooledConnectionStatistics(); /** * The last statement executed by this connection. */ - String lastStatement; - - /** - * The number of hits against the preparedStatement cache. - */ - int pstmtHitCounter; - - /** - * The number of misses against the preparedStatement cache. - */ - int pstmtMissCounter; + private String lastStatement; /** * The non avaje method that created the connection. */ - String createdByMethod; + private String createdByMethod; /** * Used to find connection pool leaks. */ - StackTraceElement[] stackTrace; + private StackTraceElement[] stackTrace; - int maxStackTrace; + private int maxStackTrace; /** * Slot position in the BusyConnectionBuffer. */ - int slotId; + private int slotId; + + private boolean resetIsolationReadOnlyRequired; + /** * Construct the connection that can refer back to the pool it belongs to. @@ -165,47 +161,47 @@ public class PooledConnection extends ConnectionDelegator 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. - */ - public int getSlotId() { - return slotId; - } + /** + * 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; + } - /** - * Set the slot position in the busy buffer. - */ - public void setSlotId(int slotId) { - this.slotId = slotId; - } + /** + * Return the slot position in the busy buffer. + */ + public int getSlotId() { + return slotId; + } - /** - * Return the DataSourcePool that this connection belongs to. - */ - public DataSourcePool getDataSourcePool() { - return pool; - } + /** + * Set the slot position in the busy buffer. + */ + public void setSlotId(int slotId) { + this.slotId = slotId; + } - /** - * Return the time the connection was created. - */ - public long getCreationTime() { - return creationTime; - } + /** + * Return the DataSourcePool that this connection belongs to. + */ + public DataSourcePool getDataSourcePool() { + return pool; + } + + /** + * Return the time the connection was created. + */ + public long getCreationTime() { + return creationTime; + } /** * Return a string to identify the connection. @@ -214,16 +210,24 @@ public class PooledConnection extends ConnectionDelegator return name; } + public String getNameSlot() { + return name+":"+slotId; + } + public String toString() { - return name; + return getDescription(); } public String getDescription() { - return "name["+name+"] startTime["+getStartUseTime()+"] stmt["+getLastStatement()+"] createdBy["+getCreatedByMethod()+"]"; + return "name["+name+"] slot["+slotId+"] startTime["+getStartUseTime()+"] stmt["+getLastStatement()+"] createdBy["+getCreatedByMethod()+"]"; } - public String getStatistics() { - return "name["+name+"] startTime["+getStartUseTime()+"] pstmtHits["+pstmtHitCounter+"] pstmtMiss["+pstmtMissCounter+"] "+pstmtCache.getDescription(); + public String getPstmtStatistics() { + return "name["+name+"] startTime["+getStartUseTime()+"] "+pstmtCache.getDescription(); + } + + public PooledConnectionStatistics getStatistics() { + return stats; } /** @@ -253,21 +257,25 @@ public class PooledConnection extends ConnectionDelegator */ public void closeConnectionFully(boolean logErrors) { - String msg = "Closing Connection[" + getName() + "]" + " psReuse[" + pstmtHitCounter - + "] psCreate[" + pstmtMissCounter + "] psSize[" + pstmtCache.size() + "]"; - - logger.debug(msg); + if (pool != null) { + // allow collection of load statistics + pool.reportClosingConnection(this); + } + + if (logger.isDebugEnabled()) { + logger.debug("Closing Connection[{}] slot[{}] Stats: {} , PstmtStats: {} ", name, slotId, 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[" + getName() + "] that is already closed?"); + logger.debug("Closing Connection[{}] that is already closed?", name); return; } } catch (SQLException ex) { if (logErrors) { - logger.error("Error when fully closing connection [" + getName() + "]", ex); + logger.error("Error checking if connection [" + getNameSlot() + "] is closed", ex); } } @@ -287,8 +295,8 @@ public class PooledConnection extends ConnectionDelegator try { connection.close(); } catch (SQLException ex) { - if (logErrors) { - logger.error("Error when fully closing connection [" + getName() + "]", ex); + if (logErrors || logger.isDebugEnabled()) { + logger.error("Error when fully closing connection [" + getNameSlot() + "]", ex); } } } @@ -337,26 +345,15 @@ public class PooledConnection extends ConnectionDelegator protected void returnPreparedStatement(ExtendedPreparedStatement pstmt) { synchronized (pstmtMonitor) { - ExtendedPreparedStatement alreadyInCache = pstmtCache.get(pstmt.getCacheKey()); + if (!pstmtCache.returnStatement(pstmt)) { + try { + // Already an entry in the cache with the exact same SQL... + pstmt.closeDestroy(); - if (alreadyInCache == null) { - // add the returning prepared statement to the cache. - // Note that the LRUCache will automatically close fully old unused - // PStmts when the cache has hit its maximum size. - pstmtCache.put(pstmt.getCacheKey(), pstmt); - - } else { - try { - // if a entry in the cache exists for the exact same SQL... - // then remove it from the cache and close it fully. - // Only having one PreparedStatement per unique SQL - // statement - pstmt.closeDestroy(); - - } catch (SQLException e) { - logger.error("Error closing Pstmt", e); - } - } + } catch (SQLException e) { + logger.error("Error closing Pstmt", e); + } + } } } @@ -392,12 +389,10 @@ public class PooledConnection extends ConnectionDelegator ExtendedPreparedStatement pstmt = pstmtCache.remove(cacheKey); if (pstmt != null) { - pstmtHitCounter++; return pstmt; } // create a new PreparedStatement - pstmtMissCounter++; PreparedStatement actualPstmt; if (useFlag) { actualPstmt = connection.prepareStatement(sql, flag); @@ -420,7 +415,6 @@ public class PooledConnection extends ConnectionDelegator } try { // no caching when creating PreparedStatements this way - pstmtMissCounter++; lastStatement = sql; return connection.prepareStatement(sql, resultSetType, resultSetConcurreny); } catch (SQLException ex) { @@ -436,6 +430,7 @@ public class PooledConnection extends ConnectionDelegator protected void resetForUse() { this.status = STATUS_ACTIVE; this.startUseTime = System.currentTimeMillis(); + this.exeStartNanos = System.nanoTime(); this.createdByMethod = null; this.lastStatement = null; this.hadErrors = false; @@ -480,6 +475,9 @@ public class PooledConnection extends ConnectionDelegator 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, close it and test the pool @@ -530,12 +528,11 @@ public class PooledConnection extends ConnectionDelegator try { if (connection != null && !connection.isClosed()) { // connect leak? - String msg = "Closing Connection[" + getName() + "] on finalize()."; - logger.warn(msg); + logger.warn("Closing Connection[" + getName() + "] on finalize()."); closeConnectionFully(false); } } catch (Exception e) { - logger.error(null, e); + logger.error("Error when finalize is closing a connection? (unexpected)", e); } super.finalize(); } @@ -581,7 +578,6 @@ public class PooledConnection extends ConnectionDelegator } } - boolean resetIsolationReadOnlyRequired = false; /** * Also note the read only status needs to be reset when put back into the @@ -614,327 +610,326 @@ public class PooledConnection extends ConnectionDelegator } } - // - // - // 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(); - } + // + // + // 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) { - addError(ex); - throw ex; - } - } - - public boolean getAutoCommit() throws SQLException { - if (status == STATUS_IDLE) { - throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "getAutoCommit()"); - } - return connection.getAutoCommit(); - } + 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) { + addError(ex); + throw ex; + } + } - public String getCatalog() throws SQLException { - if (status == STATUS_IDLE) { - throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "getCatalog()"); - } - return connection.getCatalog(); - } + public boolean getAutoCommit() throws SQLException { + if (status == STATUS_IDLE) { + throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "getAutoCommit()"); + } + return connection.getAutoCommit(); + } - public DatabaseMetaData getMetaData() throws SQLException { - if (status == STATUS_IDLE) { - throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "getMetaData()"); - } - return connection.getMetaData(); - } + public String getCatalog() throws SQLException { + if (status == STATUS_IDLE) { + throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "getCatalog()"); + } + return connection.getCatalog(); + } - public int getTransactionIsolation() throws SQLException { - if (status == STATUS_IDLE) { - throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "getTransactionIsolation()"); - } - return connection.getTransactionIsolation(); - } + public DatabaseMetaData getMetaData() throws SQLException { + if (status == STATUS_IDLE) { + throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "getMetaData()"); + } + return connection.getMetaData(); + } - public Map> getTypeMap() throws SQLException { - if (status == STATUS_IDLE) { - throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "getTypeMap()"); - } - return connection.getTypeMap(); - } + public int getTransactionIsolation() throws SQLException { + if (status == STATUS_IDLE) { + throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "getTransactionIsolation()"); + } + return connection.getTransactionIsolation(); + } - public SQLWarning getWarnings() throws SQLException { - if (status == STATUS_IDLE) { - throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "getWarnings()"); - } - return connection.getWarnings(); - } + public Map> getTypeMap() throws SQLException { + if (status == STATUS_IDLE) { + throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "getTypeMap()"); + } + return connection.getTypeMap(); + } - public boolean isClosed() throws SQLException { - if (status == STATUS_IDLE) { - throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "isClosed()"); - } - return connection.isClosed(); - } + public SQLWarning getWarnings() throws SQLException { + if (status == STATUS_IDLE) { + throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "getWarnings()"); + } + return connection.getWarnings(); + } - public boolean isReadOnly() throws SQLException { - if (status == STATUS_IDLE) { - throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "isReadOnly()"); - } - return connection.isReadOnly(); - } + public boolean isClosed() throws SQLException { + if (status == STATUS_IDLE) { + throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "isClosed()"); + } + return connection.isClosed(); + } - 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 boolean isReadOnly() throws SQLException { + if (status == STATUS_IDLE) { + throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "isReadOnly()"); + } + return connection.isReadOnly(); + } - 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 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, 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 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 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) { - addError(ex); - throw ex; - } - } + 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 setAutoCommit(boolean autoCommit) throws SQLException { - if (status == STATUS_IDLE) { - throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "setAutoCommit()"); - } - try { - connection.setAutoCommit(autoCommit); - } catch (SQLException ex) { - addError(ex); - throw ex; - } - } + 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) { + addError(ex); + 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 setAutoCommit(boolean autoCommit) throws SQLException { + if (status == STATUS_IDLE) { + throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "setAutoCommit()"); + } + try { + connection.setAutoCommit(autoCommit); + } catch (SQLException ex) { + addError(ex); + throw ex; + } + } - public void setTypeMap(Map> map) throws SQLException { - if (status == STATUS_IDLE) { - throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "setTypeMap()"); - } - connection.setTypeMap(map); - } + public void setCatalog(String catalog) throws SQLException { + if (status == STATUS_IDLE) { + throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "setCatalog()"); + } + connection.setCatalog(catalog); + } - public Savepoint setSavepoint() throws SQLException { - try { - return connection.setSavepoint(); - } catch (SQLException ex) { - addError(ex); - throw ex; - } - } + 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(String savepointName) throws SQLException { - try { - return connection.setSavepoint(savepointName); - } catch (SQLException ex) { - addError(ex); - throw ex; - } - } + public Savepoint setSavepoint() throws SQLException { + try { + return connection.setSavepoint(); + } catch (SQLException ex) { + addError(ex); + throw ex; + } + } - public void rollback(Savepoint sp) throws SQLException { - try { - connection.rollback(sp); - } catch (SQLException ex) { - addError(ex); - throw ex; - } - } + public Savepoint setSavepoint(String savepointName) throws SQLException { + try { + return connection.setSavepoint(savepointName); + } catch (SQLException ex) { + addError(ex); + throw ex; + } + } - public void releaseSavepoint(Savepoint sp) throws SQLException { - try { - connection.releaseSavepoint(sp); - } catch (SQLException ex) { - addError(ex); - throw ex; - } - } + public void rollback(Savepoint sp) throws SQLException { + try { + connection.rollback(sp); + } catch (SQLException ex) { + addError(ex); + throw ex; + } + } - public void setHoldability(int i) throws SQLException { - try { - connection.setHoldability(i); - } catch (SQLException ex) { - addError(ex); - throw ex; - } - } + public void releaseSavepoint(Savepoint sp) throws SQLException { + try { + connection.releaseSavepoint(sp); + } catch (SQLException ex) { + addError(ex); + throw ex; + } + } - public int getHoldability() throws SQLException { - try { - return connection.getHoldability(); - } catch (SQLException ex) { - addError(ex); - throw ex; - } - } + public void setHoldability(int i) throws SQLException { + try { + connection.setHoldability(i); + } catch (SQLException ex) { + addError(ex); + throw ex; + } + } - public Statement createStatement(int i, int x, int y) throws SQLException { - try { - return connection.createStatement(i, x, y); - } catch (SQLException ex) { - addError(ex); - throw ex; - } - } + public int getHoldability() throws SQLException { + try { + return connection.getHoldability(); + } catch (SQLException ex) { + addError(ex); + 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) { - addError(ex); - throw ex; - } - } + public Statement createStatement(int i, int x, int y) throws SQLException { + try { + return connection.createStatement(i, x, y); + } catch (SQLException ex) { + addError(ex); + throw ex; + } + } - public PreparedStatement prepareStatement(String s, int[] i) throws SQLException { - try { - return connection.prepareStatement(s, i); - } catch (SQLException ex) { - addError(ex); - 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) { + addError(ex); + throw ex; + } + } - public PreparedStatement prepareStatement(String s, String[] s2) throws SQLException { - try { - return connection.prepareStatement(s, s2); - } catch (SQLException ex) { - addError(ex); - throw ex; - } - } + public PreparedStatement prepareStatement(String s, int[] i) throws SQLException { + try { + return connection.prepareStatement(s, i); + } catch (SQLException ex) { + addError(ex); + 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) { - addError(ex); - throw ex; - } - } + public PreparedStatement prepareStatement(String s, String[] s2) throws SQLException { + try { + return connection.prepareStatement(s, s2); + } catch (SQLException ex) { + addError(ex); + throw ex; + } + } - /** - * Returns the method that created the connection. - *

- * Used to help finding connection pool leaks. - *

- */ - public String getCreatedByMethod() { - if (createdByMethod != null) { - return createdByMethod; - } - if (stackTrace == null) { - return null; - } + public CallableStatement prepareCall(String s, int i, int x, int y) throws SQLException { + try { + return connection.prepareCall(s, i, x, y); + } catch (SQLException ex) { + addError(ex); + throw ex; + } + } - for (int j = 0; j < stackTrace.length; j++) { - String methodLine = stackTrace[j].toString(); - if (skipElement(methodLine)) { - // ignore these methods... - } else { - createdByMethod = methodLine; - return createdByMethod; - } - } + /** + * Returns the method that created the connection. + *

+ * Used to help finding connection pool leaks. + *

+ */ + public String getCreatedByMethod() { + if (createdByMethod != null) { + return createdByMethod; + } + if (stackTrace == null) { + return null; + } - return null; - } + for (int j = 0; j < stackTrace.length; j++) { + String methodLine = stackTrace[j].toString(); + if (skipElement(methodLine)) { + // ignore these methods... + } else { + createdByMethod = methodLine; + return createdByMethod; + } + } - private boolean skipElement(String methodLine) { - if (methodLine.startsWith("java.lang.")) { - return true; - } else if (methodLine.startsWith("java.util.")) { - return true; - } else if (methodLine.startsWith("com.avaje.ebeaninternal.server.query.CallableQuery.")) { - // creating connection on future... - return true; - } else if (methodLine.startsWith("com.avaje.ebeaninternal.server.query.Callable")) { - // it is a future task being executed... - return false; - } else if (methodLine.startsWith("com.avaje.ebeaninternal")) { - return true; - } else { - return false; - } - } - - /** - * Set the stack trace to help find connection pool leaks. - */ - protected void setStackTrace(StackTraceElement[] stackTrace) { - this.stackTrace = stackTrace; - } + return null; + } - /** - * Return the full stack trace that got the connection from the pool. You - * could use this if getCreatedByMethod() doesn't work for you. - */ - public 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()]); - - } + private boolean skipElement(String methodLine) { + if (methodLine.startsWith("java.lang.")) { + return true; + } else if (methodLine.startsWith("java.util.")) { + return true; + } else if (methodLine.startsWith("com.avaje.ebeaninternal.server.query.CallableQuery.")) { + // creating connection on future... + return true; + } else if (methodLine.startsWith("com.avaje.ebeaninternal.server.query.Callable")) { + // it is a future task being executed... + return false; + } else if (methodLine.startsWith("com.avaje.ebeaninternal")) { + return true; + } else { + return false; + } + } + + /** + * Set the stack trace to help find connection pool leaks. + */ + protected void setStackTrace(StackTraceElement[] stackTrace) { + this.stackTrace = stackTrace; + } + + /** + * Return the full stack trace that got the connection from the pool. You + * could use this if getCreatedByMethod() doesn't work for you. + */ + public 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()]); + + } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnectionQueue.java b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnectionQueue.java index b7daa3942..fe5d45c04 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnectionQueue.java +++ b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnectionQueue.java @@ -9,10 +9,12 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; -import com.avaje.ebeaninternal.server.lib.sql.DataSourcePool.Status; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.avaje.ebeaninternal.server.lib.sql.DataSourcePool.Status; +import com.avaje.ebeaninternal.server.lib.sql.PooledConnectionStatistics.LoadValues; + public class PooledConnectionQueue { private static final Logger logger = LoggerFactory.getLogger(PooledConnectionQueue.class); @@ -34,6 +36,16 @@ public class PooledConnectionQueue { */ private final BusyConnectionBuffer busyList; + /** + * Load statistics collected off connections that have closed fully (left the pool). + */ + private final PooledConnectionStatistics collectedStats = new PooledConnectionStatistics(); + + /** + * Currently accumulated load statistics. + */ + private LoadValues accumulatedValues = new LoadValues(); + /** * Main lock guarding all access */ @@ -95,10 +107,10 @@ public class PooledConnectionQueue { this.waitTimeoutMillis = pool.getWaitTimeoutMillis(); this.leakTimeMinutes = pool.getLeakTimeMinutes(); - this.busyList = new BusyConnectionBuffer(50,20); + this.busyList = new BusyConnectionBuffer(maxSize, 20); this.freeList = new FreeConnectionBuffer(maxSize); - - this.lock = new ReentrantLock(true); + + this.lock = new ReentrantLock(false); this.notEmpty = lock.newCondition(); } @@ -116,6 +128,36 @@ public class PooledConnectionQueue { } } + /** + * Collect statistics of a connection that is fully closing + */ + protected void reportClosingConnection(PooledConnection pooledConnection) { + + collectedStats.add(pooledConnection.getStatistics()); + } + + public DataSourcePoolStatistics getStatistics(boolean reset) { + + final ReentrantLock lock = this.lock; + lock.lock(); + try { + + LoadValues aggregate = collectedStats.getValues(reset); + + freeList.collectStatistics(aggregate, reset); + busyList.collectStatistics(aggregate, reset); + + aggregate.plus(accumulatedValues); + + this.accumulatedValues = (reset) ? new LoadValues() : aggregate; + + return new DataSourcePoolStatistics(aggregate.getCollectionStart(), aggregate.getCount(), aggregate.getErrorCount(), aggregate.getHwmMicros(), aggregate.getTotalMicros()); + + } finally { + lock.unlock(); + } + } + public Status getStatus(boolean reset) { final ReentrantLock lock = this.lock; lock.lock(); @@ -203,7 +245,7 @@ public class PooledConnectionQueue { lock.lock(); try { if (!busyList.remove(c)) { - logger.error("Connection [" + c + "] not found in BusyList? "); + logger.error("Connection [{}] not found in BusyList? ", c); } if (c.getCreationTime() <= lastResetTime) { c.closeConnectionFully(false); @@ -261,8 +303,7 @@ public class PooledConnectionQueue { // are other threads already waiting? (they get priority) if (waitingThreads == 0){ - int freeSize = freeList.size(); - if (freeSize > 0){ + if (!freeList.isEmpty()){ // we have a free connection to return return extractFromFreeList(); } @@ -333,13 +374,13 @@ public class PooledConnectionQueue { try { doingShutdown = true; Status status = createStatus(); - logger.debug("DataSourcePool [" + name + "] shutdown: "+status); + DataSourcePoolStatistics statistics = pool.getStatistics(false); + logger.debug("DataSourcePool [" + name + "] shutdown {} - Statistics {}", status, statistics); closeFreeConnections(true); if (!busyList.isEmpty()) { - logger.warn("A potential connection leak was detected. Busy connections: "+ busyList.size()); - + logger.warn("Closing busy connections on shutdown size: "+ busyList.size()); dumpBusyConnectionInformation(); closeBusyConnections(0); } @@ -366,8 +407,7 @@ public class PooledConnectionQueue { closeFreeConnections(false); closeBusyConnections(leakTimeMinutes); - String busyMsg = "Busy Connections:\r\n" + getBusyConnectionInformation(); - logger.info(busyMsg); + logger.info("Busy Connections:\n" + getBusyConnectionInformation()); } finally { lock.unlock(); @@ -434,11 +474,7 @@ public class PooledConnectionQueue { final ReentrantLock lock = this.lock; lock.lock(); try { - while (!freeList.isEmpty()) { - PooledConnection c = freeList.remove(); - logger.debug("PSTMT Statistics: "+c.getStatistics()); - c.closeConnectionFully(logErrors); - } + freeList.closeAll(logErrors); } finally { lock.unlock(); } @@ -465,6 +501,9 @@ public class PooledConnectionQueue { long olderThanTime = System.currentTimeMillis() - (leakTimeMinutes*60000); List copy = busyList.getShallowCopy(); + + logger.debug("Closing busy connections using leakTimeMinutes {}", leakTimeMinutes); + for (int i = 0; i < copy.size(); i++) { PooledConnection pc = copy.get(i); if (pc.isLongRunning() || pc.getLastUsedTime() > olderThanTime) { @@ -472,7 +511,7 @@ public class PooledConnectionQueue { // expected to be longRunning so not closing... } else { busyList.remove(pc); - closeBusyConnection(pc); + closeBusyConnection(pc, leakTimeMinutes); } } @@ -481,26 +520,26 @@ public class PooledConnectionQueue { } } - private void closeBusyConnection(PooledConnection pc) { + private void closeBusyConnection(PooledConnection pc, long leakMinutes) { try { String methodLine = pc.getCreatedByMethod(); Date luDate = new Date(); luDate.setTime(pc.getLastUsedTime()); - String msg = "DataSourcePool closing leaked connection? " + " name[" - + pc.getName() + "] lastUsed[" + luDate + "] createdBy[" + methodLine + String msg = "DataSourcePool closing leaked connection? name[" + + pc.getName() + "] leakMinutes["+leakMinutes+"] lastUsed[" + luDate + "] createdBy[" + methodLine + "] lastStmt[" + pc.getLastStatement() + "]"; logger.warn(msg); logStackElement(pc, "Possible Leaked Connection: "); + System.out.println("CLOSING Possibly leaked connection: "+pc); - System.out.println("CLOSING BUSY CONNECTION ??? "+pc); - pc.close(); + pc.closeConnectionFully(false); - } catch (SQLException ex) { + } catch (Exception ex) { // this should never actually happen - logger.error(null, ex); + logger.error("Error when closing potentially leaked connection "+pc.getDescription(), ex); } } @@ -557,13 +596,14 @@ public class PooledConnectionQueue { lock.lock(); try { - if (toLogger) { - logger.info("Dumping busy connections: (Use datasource.xxx.capturestacktrace=true ... to get stackTraces)"); - } - StringBuilder sb = new StringBuilder(); List copy = busyList.getShallowCopy(); + + if (toLogger) { + logger.info("Dumping [{}] busy connections: (Use datasource.xxx.capturestacktrace=true ... to get stackTraces)", copy.size()); + } + for (int i = 0; i < copy.size(); i++) { PooledConnection pc = copy.get(i); if (toLogger) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnectionStatistics.java b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnectionStatistics.java new file mode 100644 index 000000000..6f9eeac53 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnectionStatistics.java @@ -0,0 +1,162 @@ +package com.avaje.ebeaninternal.server.lib.sql; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Collects load statistics for a PooledConnection. + */ +public class PooledConnectionStatistics { + + private final AtomicLong count = new AtomicLong(); + + private final AtomicLong errorCount = new AtomicLong(); + + private final AtomicLong hwmNanos = new AtomicLong(); + + private final AtomicLong totalNanos = new AtomicLong(); + + private final AtomicLong collectionStart; + + public PooledConnectionStatistics() { + this.collectionStart = new AtomicLong(System.currentTimeMillis()); + } + + /** + * Add statistics from another collector. + */ + public void add(PooledConnectionStatistics other) { + + errorCount.addAndGet(other.getErrorCount()); + totalNanos.addAndGet(other.totalNanos.get()); + count.addAndGet(other.getCount()); + + final long otherHwm = other.hwmNanos.get(); + if (otherHwm > hwmNanos.get()) { + hwmNanos.set(otherHwm); + } + } + + /** + * Add some time duration to the statistics. + */ + public void add(long durationNanos, boolean hasError) { + + // This will be done in pretty much single threaded fashion + // as the Connections generally are not shared across threads + + if (hasError) { + errorCount.incrementAndGet(); + } + count.incrementAndGet(); + totalNanos.addAndGet(durationNanos); + if (durationNanos > hwmNanos.get()) { + hwmNanos.set(durationNanos); + } + } + + public String toString() { + return "count["+count+"] errors["+errorCount+"] totalMicros["+getTotalMicros()+"] hwmMicros["+getHwmMicros()+"]"; + } + + public long getCollectionStart() { + return collectionStart.get(); + } + + public long getCount() { + return count.get(); + } + + public long getErrorCount() { + return errorCount.get(); + } + + public long getTotalMicros() { + return TimeUnit.MICROSECONDS.convert(totalNanos.get(), TimeUnit.NANOSECONDS); + } + + public long getHwmMicros() { + return TimeUnit.MICROSECONDS.convert(hwmNanos.get(), TimeUnit.NANOSECONDS); + } + + /** + * Get the current values and reset the statistics if necessary. + */ + public LoadValues getValues(boolean reset) { + LoadValues value = new LoadValues(collectionStart.get(), count.get(), errorCount.get(), getHwmMicros(), getTotalMicros()); + if (reset) { + count.set(0); + errorCount.set(0); + hwmNanos.set(0); + totalNanos.set(0); + collectionStart.set(System.currentTimeMillis()); + } + return value; + } + + /** + * Values representing the load or activity of a PooledConnection. + *

+ * These are aggregated up to get a total for the DataSourcePool. + *

+ */ + public static class LoadValues { + + private long collectionStart; + private long count; + private long errorCount; + private long hwmMicros; + private long totalMicros; + + public LoadValues() { + } + + public LoadValues(long collectionStart, long count, long errorCount, long hwmMicros, long totalMicros) { + this.collectionStart = collectionStart; + this.count = count; + this.errorCount = errorCount; + this.hwmMicros = hwmMicros; + this.totalMicros = totalMicros; + } + + public void plus(LoadValues additional) { + collectionStart = (collectionStart == 0) ? additional.collectionStart : Math.min(collectionStart, additional.collectionStart); + count += additional.count; + errorCount += additional.errorCount; + hwmMicros = Math.max(hwmMicros, additional.hwmMicros); + totalMicros += additional.totalMicros; + } + + public String toString() { + return "count["+count+"] errors["+errorCount+"] totalMicros["+totalMicros+"] hwmMicros["+hwmMicros+"] avgMicros["+getAvgMicros()+"]"; + } + + public long getCollectionStart() { + return collectionStart; + } + + public long getCount() { + return count; + } + + public long getErrorCount() { + return errorCount; + } + + public long getHwmMicros() { + return hwmMicros; + } + + public long getTotalMicros() { + return totalMicros; + } + + public long getAvgMicros() { + return (count == 0) ? 0 : totalMicros/count; + } + } + + + + +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PstmtCache.java b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PstmtCache.java index 065d3c4ce..f28bfe46d 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PstmtCache.java +++ b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PstmtCache.java @@ -14,37 +14,37 @@ public class PstmtCache extends LinkedHashMap private static final Logger logger = LoggerFactory.getLogger(PstmtCache.class); - static final long serialVersionUID = -3096406924865550697L; + static final long serialVersionUID = -3096406924865550697L; /** * The name of the cache, for tracing purposes. */ - final String cacheName; + protected final String cacheName; /** * The maximum size of the cache. When this is exceeded the oldest entry is removed. */ - final int maxSize; + private final int maxSize; /** * The total number of entries removed from this cache. */ - int removeCounter; + private int removeCounter; /** * The number of get hits. */ - int hitCounter; + private int hitCounter; /** * The number of get() misses. */ - int missCounter; + private int missCounter; /** * The number of puts into this cache. */ - int putCounter; + private int putCounter; public PstmtCache(String cacheName, int maxCacheSize) { @@ -58,7 +58,7 @@ public class PstmtCache extends LinkedHashMap * Return a summary description of this cache. */ public String getDescription() { - return cacheName+" size:"+size()+" max:"+maxSize+" totalHits:"+hitCounter+" hitRatio:"+getHitRatio()+" removes:"+removeCounter; + return "size["+size()+"] max["+maxSize+"] hits["+hitCounter+"] miss["+missCounter+"] hitRatio["+getHitRatio()+"] removes["+removeCounter+"]"; } /** @@ -101,6 +101,24 @@ public class PstmtCache extends LinkedHashMap return putCounter; } + /** + * Try to add the returning statement to the cache. If there is already a + * matching ExtendedPreparedStatement in the cache return false else add + * the statement to the cache and return true. + */ + public boolean returnStatement(ExtendedPreparedStatement pstmt) { + + ExtendedPreparedStatement alreadyInCache = super.get(pstmt.getCacheKey()); + if (alreadyInCache != null) { + return false; + } + // add the returning prepared statement to the cache. + // Note that the LRUCache will automatically close fully old unused + // PStmts when the cache has hit its maximum size. + put(pstmt.getCacheKey(), pstmt); + return true; + } + /** * additionally maintains hit and miss statistics. */ diff --git a/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestBusyBuffer.java b/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestBusyBuffer.java index d1bd7e870..24fc550d4 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestBusyBuffer.java +++ b/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestBusyBuffer.java @@ -44,6 +44,7 @@ public class TestBusyBuffer extends BaseTestCase { } + @Test public void test_rotate() { BusyConnectionBuffer b = new BusyConnectionBuffer(2, 2); @@ -54,13 +55,17 @@ public class TestBusyBuffer extends BaseTestCase { PooledConnection p3 = new PooledConnection("3"); Assert.assertEquals(2, b.getCapacity()); + Assert.assertEquals(0, b.size()); b.add(p0); b.add(p1); + Assert.assertEquals(2, b.size()); Assert.assertEquals(2, b.getCapacity()); b.add(p2); + Assert.assertEquals(3, b.size()); Assert.assertEquals(4, b.getCapacity()); b.add(p3); + Assert.assertEquals(4, b.size()); Assert.assertEquals(4, b.getCapacity()); Assert.assertEquals(0, p0.getSlotId()); @@ -69,13 +74,20 @@ public class TestBusyBuffer extends BaseTestCase { Assert.assertEquals(3, p3.getSlotId()); b.remove(p2); + Assert.assertEquals(3, b.size()); b.remove(p0); + Assert.assertEquals(2, b.size()); b.remove(p3); + Assert.assertEquals(1, b.size()); b.add(p2); + Assert.assertEquals(2, b.size()); Assert.assertEquals(0, p2.getSlotId()); b.remove(p0); + Assert.assertEquals(2, b.size()); b.add(p0); + Assert.assertEquals(3, b.size()); + // p1 is still in it's slot Assert.assertEquals(2, p0.getSlotId()); From 4900a460d1224a7642b1b5f7e879d12b6860b9dc Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Sat, 14 Dec 2013 21:24:26 +1300 Subject: [PATCH 2/4] Fix for DataSourcePool trim idle connections not firing frequently enough --- pom.xml | 7 ++ .../avaje/ebean/config/DataSourceConfig.java | 34 +++++++-- .../server/lib/sql/DataSourcePool.java | 5 +- .../server/lib/sql/FreeConnectionBuffer.java | 75 +++++++++--------- .../server/lib/sql/PooledConnection.java | 27 ++++++- .../server/lib/sql/PooledConnectionQueue.java | 76 +++---------------- .../server/lib/sql/TestDataSourceMax.java | 61 +++++++++++---- .../server/lib/sql/TestFreeBufferTrim.java | 63 +++++++++++++++ 8 files changed, 225 insertions(+), 123 deletions(-) create mode 100644 src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestFreeBufferTrim.java diff --git a/pom.xml b/pom.xml index 03033ccac..7358781ca 100644 --- a/pom.xml +++ b/pom.xml @@ -140,6 +140,13 @@ test + + org.mockito + mockito-core + 1.9.5 + test + + ch.qos.logback logback-classic diff --git a/src/main/java/com/avaje/ebean/config/DataSourceConfig.java b/src/main/java/com/avaje/ebean/config/DataSourceConfig.java index 9ed13c363..e2bde6dbe 100644 --- a/src/main/java/com/avaje/ebean/config/DataSourceConfig.java +++ b/src/main/java/com/avaje/ebean/config/DataSourceConfig.java @@ -43,6 +43,8 @@ public class DataSourceConfig { private int leakTimeMinutes = 30; private int maxInactiveTimeSecs = 720; + + private int trimPoolFreqSecs = 59; private int pstmtCacheSize = 20; @@ -321,6 +323,25 @@ public class DataSourceConfig { this.maxInactiveTimeSecs = maxInactiveTimeSecs; } + + /** + * Return the minimum time gap between pool trim checks. + *

+ * This defaults to 59 seconds meaning that the pool trim check will run every + * minute assuming the heart beat check runs every 30 seconds. + *

+ */ + public int getTrimPoolFreqSecs() { + return trimPoolFreqSecs; + } + + /** + * Set the minimum trim gap between pool trim checks. + */ + public void setTrimPoolFreqSecs(int trimPoolFreqSecs) { + this.trimPoolFreqSecs = trimPoolFreqSecs; + } + /** * Return the pool listener. */ @@ -388,18 +409,17 @@ public class DataSourceConfig { this.username = properties.get(prefix + "username", null); this.password = properties.get(prefix + "password", null); - String v; + String dbDriver = properties.get(prefix + "databaseDriver", null); + this.driver = properties.get(prefix + "driver", dbDriver); - v = properties.get(prefix + "databaseDriver", null); - this.driver = properties.get(prefix + "driver", v); - - v = properties.get(prefix + "databaseUrl", null); - this.url = properties.get(prefix + "url", v); + String dbUrl = properties.get(prefix + "databaseUrl", null); + this.url = properties.get(prefix + "url", dbUrl); this.captureStackTrace = properties.getBoolean(prefix + "captureStackTrace", false); this.maxStackTraceSize = properties.getInt(prefix + "maxStackTraceSize", 5); this.leakTimeMinutes = properties.getInt(prefix + "leakTimeMinutes", 30); - this.maxInactiveTimeSecs = properties.getInt(prefix + "maxInactiveTimeSecs", 900); + this.maxInactiveTimeSecs = properties.getInt(prefix + "maxInactiveTimeSecs", 720); + this.trimPoolFreqSecs = properties.getInt(prefix + "trimPoolFreqSecs", 59); this.minConnections = properties.getInt(prefix + "minConnections", 0); this.maxConnections = properties.getInt(prefix + "maxConnections", 20); diff --git a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/DataSourcePool.java b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/DataSourcePool.java index 3cb861427..0dff23448 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/DataSourcePool.java +++ b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/DataSourcePool.java @@ -77,6 +77,8 @@ public class DataSourcePool implements DataSource { private final int heartbeatFreqSecs; + private final int trimPoolFreqSecs; + /** * The transaction isolation level as per java.sql.Connection. */ @@ -181,6 +183,7 @@ public class DataSourcePool implements DataSource { this.waitTimeoutMillis = params.getWaitTimeoutMillis(); this.heartbeatsql = params.getHeartbeatSql(); this.heartbeatFreqSecs = params.getHeartbeatFreqSecs(); + this.trimPoolFreqSecs = params.getTrimPoolFreqSecs(); queue = new PooledConnectionQueue(this); @@ -383,7 +386,7 @@ public class DataSourcePool implements DataSource { notifyDataSourceIsUp(); - if (System.currentTimeMillis() > (lastTrimTime + (maxInactiveTimeSecs * 1000))) { + if (System.currentTimeMillis() > (lastTrimTime + (trimPoolFreqSecs * 1000))) { queue.trim(maxInactiveTimeSecs); lastTrimTime = System.currentTimeMillis(); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/FreeConnectionBuffer.java b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/FreeConnectionBuffer.java index 813740633..6186aebd8 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/FreeConnectionBuffer.java +++ b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/FreeConnectionBuffer.java @@ -19,10 +19,19 @@ import com.avaje.ebeaninternal.server.lib.sql.PooledConnectionStatistics.LoadVal */ class FreeConnectionBuffer { + /** + * The buffer itself. + */ private PooledConnection[] conns; + /** + * The position in the buffer where the next connection is removed from. + */ private int removeIndex; + /** + * Position in the buffer where the next connection is added to. + */ private int addIndex; /** @@ -51,13 +60,16 @@ class FreeConnectionBuffer { */ protected void add(PooledConnection pc) { if (conns[addIndex] != null) { - throw new RuntimeException("Buffer slot ["+addIndex+"] already full?"); + throw new IllegalStateException("Buffer slot ["+addIndex+"] already full?"); } conns[addIndex] = pc; addIndex = inc(addIndex); ++size; } + /** + * Close all connections in this buffer. + */ protected void closeAll(boolean logErrors) { final PooledConnection[] items = this.conns; @@ -88,19 +100,25 @@ class FreeConnectionBuffer { } /** - * Return a shallow copy of the free connections. + * Trim any inactive connections that have not been used since usedSince. */ - protected List getShallowCopy() { - - List copy = new ArrayList(conns.length); - for (int i = 0; i < conns.length; i++) { - if (conns[i] != null){ - copy.add(conns[i]); - } + protected int trim(long usedSince) { + + int trimCount = 0; + for (int i = 0; i < conns.length; i++) { + if (conns[i] != null){ + if (conns[i].getLastUsedTime() < usedSince) { + trimCount++; + conns[i].closeConnectionFully(true); + conns[i] = null; + --size; + } } - return copy; + } + + return trimCount; } - + /** * Collect the load statistics from all the free connections. */ @@ -114,36 +132,25 @@ class FreeConnectionBuffer { } /** - * Set the free list to be the connections in this copy. This is done after - * unused connections have been trimmed. - *

- * Not a particularly performant approach but this should not be called very - * often - *

+ * Return a shallow copy of the free connections. */ - protected void setShallowCopy(List copy) { - - // reset to empty state - this.removeIndex = 0; - this.addIndex = 0; - this.size = 0; - - // null all the current connections - for (int i = 0; i < conns.length; i++) { - conns[i] = null; - } - - // add connections from the copy - for (int i = 0; i < copy.size(); i++) { - add(copy.get(i)); - } - } + private List getShallowCopy() { + List copy = new ArrayList(conns.length); + for (int i = 0; i < conns.length; i++) { + if (conns[i] != null){ + copy.add(conns[i]); + } + } + return copy; + } + /** * Increase the capacity of the buffer. This is a relatively expensive * operation but should occur very infrequently. */ protected void setCapacity(int newCapacity) { + if (newCapacity > conns.length){ List copy = getShallowCopy(); diff --git a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnection.java b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnection.java index d6c3293bc..9712d5482 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnection.java +++ b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnection.java @@ -9,10 +9,12 @@ import java.sql.SQLWarning; import java.sql.Savepoint; import java.sql.Statement; import java.util.ArrayList; +import java.util.Arrays; import java.util.Iterator; import java.util.Map; import com.avaje.ebeaninternal.jdbc.ConnectionDelegator; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -218,10 +220,18 @@ public class PooledConnection extends ConnectionDelegator { return getDescription(); } - public String getDescription() { - return "name["+name+"] slot["+slotId+"] startTime["+getStartUseTime()+"] stmt["+getLastStatement()+"] createdBy["+getCreatedByMethod()+"]"; + public long getBusySeconds() { + return (System.currentTimeMillis() - startUseTime)/1000; } - + + public String getDescription() { + return "name["+name+"] slot["+slotId+"] startTime["+getStartUseTime()+"] busySeconds["+getBusySeconds()+"] createdBy["+getCreatedByMethod()+"] stmt["+getLastStatement()+"]"; + } + + public String getFullDescription() { + return "name["+name+"] slot["+slotId+"] startTime["+getStartUseTime()+"] busySeconds["+getBusySeconds()+"] stackTrace["+getStackTraceAsString()+"] stmt["+getLastStatement()+"]"; + } + public String getPstmtStatistics() { return "name["+name+"] startTime["+getStartUseTime()+"] "+pstmtCache.getDescription(); } @@ -907,6 +917,17 @@ public class PooledConnection extends ConnectionDelegator { this.stackTrace = stackTrace; } + /** + * Return the stackTrace as a String for logging purposes. + */ + public 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. diff --git a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnectionQueue.java b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnectionQueue.java index fe5d45c04..1076f9ad0 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnectionQueue.java +++ b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnectionQueue.java @@ -1,9 +1,6 @@ package com.avaje.ebeaninternal.server.lib.sql; import java.sql.SQLException; -import java.util.Arrays; -import java.util.Date; -import java.util.Iterator; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; @@ -418,9 +415,9 @@ public class PooledConnectionQueue { final ReentrantLock lock = this.lock; lock.lock(); try { - trimInactiveConnections(maxInactiveTimeSecs); - ensureMinimumConnections(); - + if (trimInactiveConnections(maxInactiveTimeSecs) > 0) { + ensureMinimumConnections(); + } } finally { lock.unlock(); } @@ -430,39 +427,12 @@ public class PooledConnectionQueue { * Trim connections that have been not used for some time. */ private int trimInactiveConnections(int maxInactiveTimeSecs) { - - int maxTrim = freeList.size() - minSize; - if (maxTrim <= 0) { - return 0; - } - - int trimedCount = 0; + long usedSince = System.currentTimeMillis() - (maxInactiveTimeSecs * 1000); - // get a shallow copy to manipulate - List freeListCopy = freeList.getShallowCopy(); - - Iterator it = freeListCopy.iterator(); - while (it.hasNext()) { - PooledConnection pc = it.next(); - if (pc.getLastUsedTime() < usedSince) { - // trim this connection as it hasn't been used in a while - trimedCount++; - it.remove(); - pc.closeConnectionFully(true); - if (trimedCount >= maxTrim) { - break; - } - } - } - + int trimedCount = freeList.trim(usedSince); if (trimedCount > 0) { - - // rebuild the free list from the trimmed copy - freeList.setShallowCopy(freeListCopy); - - String msg = "DataSourcePool [" + name + "] trimmed [" + trimedCount + "] inactive connections. New size[" + totalConnections() + "]"; - logger.debug(msg); + logger.debug("DataSourcePool [{}] trimmed [{}] inactive connections. New size[{}]", name, trimedCount, totalConnections()); } return trimedCount; } @@ -522,18 +492,9 @@ public class PooledConnectionQueue { private void closeBusyConnection(PooledConnection pc, long leakMinutes) { try { - String methodLine = pc.getCreatedByMethod(); - - Date luDate = new Date(); - luDate.setTime(pc.getLastUsedTime()); - - String msg = "DataSourcePool closing leaked connection? name[" - + pc.getName() + "] leakMinutes["+leakMinutes+"] lastUsed[" + luDate + "] createdBy[" + methodLine - + "] lastStmt[" + pc.getLastStatement() + "]"; - - logger.warn(msg); - logStackElement(pc, "Possible Leaked Connection: "); - System.out.println("CLOSING Possibly leaked connection: "+pc); + + logger.warn("DataSourcePool closing busy connection? "+pc.getFullDescription()); + System.out.println("CLOSING busy connection: "+pc.getFullDescription()); pc.closeConnectionFully(false); @@ -543,19 +504,6 @@ public class PooledConnectionQueue { } } - private void logStackElement(PooledConnection pc, String prefix) { - StackTraceElement[] stackTrace = pc.getStackTrace(); - if (stackTrace != null){ - String s = Arrays.toString(stackTrace); - String msg = prefix+" name["+pc.getName()+"] stackTrace: "+s; - logger.warn(msg); - // also send to syserr ... as the loggers get turned - // off early in JVM shutdown - System.err.println(msg); - } - } - - /** * As the pool grows it gets closer to the maxConnections limit. We can send * an Alert (or warning) as we get close to this limit and hence an @@ -607,11 +555,9 @@ public class PooledConnectionQueue { for (int i = 0; i < copy.size(); i++) { PooledConnection pc = copy.get(i); if (toLogger) { - logger.info(pc.getDescription()); - logStackElement(pc, "Busy Connection: "); - + logger.info("Busy Connection - {}", pc.getFullDescription()); } else { - sb.append(pc.getDescription()).append("\r\n"); + sb.append(pc.getFullDescription()).append("\r\n"); } } diff --git a/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestDataSourceMax.java b/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestDataSourceMax.java index cd3ee4b7f..26fe97c91 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestDataSourceMax.java +++ b/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestDataSourceMax.java @@ -1,11 +1,13 @@ package com.avaje.ebeaninternal.server.lib.sql; import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; -import org.junit.Assert; import org.junit.Test; import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; import com.avaje.ebean.config.DataSourceConfig; import com.avaje.ebeaninternal.server.core.DefaultBackgroundExecutor; import com.avaje.ebeaninternal.server.lib.sql.DataSourcePool.Status; @@ -15,38 +17,42 @@ public class TestDataSourceMax extends BaseTestCase { @Test public void test() { - boolean runThisManuallyNow = true; + boolean skipThisTest = true; - if (!runThisManuallyNow) { + if (skipThisTest) { return; } + Ebean.getServer(null); + String name = "h2"; DataSourceConfig dsConfig = new DataSourceConfig(); dsConfig.loadSettings(name); - dsConfig.setMinConnections(3); - dsConfig.setMaxConnections(3); + dsConfig.setMinConnections(2); + dsConfig.setMaxConnections(8); dsConfig.setWaitTimeoutMillis(30000); + dsConfig.setCaptureStackTrace(true); DataSourcePool pool = new DataSourcePool(null, name, dsConfig); - Assert.assertEquals(3, pool.getMaxSize()); + // Assert.assertEquals(3, pool.getMaxSize()); DefaultBackgroundExecutor bg = new DefaultBackgroundExecutor(10, 2, 180, 30, "testDs"); try { for (int i = 0; i < 12; i++) { // Thread.sleep(10*i); - bg.execute(new ConnRunner(pool, 100)); + bg.execute(new ConnRunner(pool, 4000)); } System.out.println("main thread sleep ... " + pool.getStatus(false)); - Thread.sleep(1000); + Thread.sleep(10000); Status status = pool.getStatus(false); System.out.println(status); + pool.shutdown(false); // this dumpOrder was for 3 vectors used in PooledConnectionQueue // that logged the order of wait, notify and obtain events // I have remove that code. @@ -71,14 +77,43 @@ public class TestDataSourceMax extends BaseTestCase { } public void run() { + Connection connection = null; + PreparedStatement pstmt = null; + ResultSet rset = null; try { - Connection connection = pool.getConnection(); + connection = pool.getConnection(); + pstmt = connection.prepareStatement("select count(*) from o_customer"); + rset = pstmt.executeQuery(); + + System.out.println("sleep " + sleepMillis); Thread.sleep(sleepMillis); - connection.close(); - } catch (Exception e) { - e.printStackTrace(); + System.out.println("sleep done"); + + } catch (Exception ex) { + ex.printStackTrace(); + } finally { + if (rset != null) { + try { + rset.close(); + } catch (Exception e) { + e.printStackTrace(); + } + if (pstmt != null) { + try { + pstmt.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + if (connection != null) { + try { + connection.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } } } - } } diff --git a/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestFreeBufferTrim.java b/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestFreeBufferTrim.java new file mode 100644 index 000000000..9a3c8b293 --- /dev/null +++ b/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestFreeBufferTrim.java @@ -0,0 +1,63 @@ +package com.avaje.ebeaninternal.server.lib.sql; + +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import com.avaje.ebean.BaseTestCase; + +public class TestFreeBufferTrim extends BaseTestCase { + + @Test + public void test() { + + FreeConnectionBuffer b = new FreeConnectionBuffer(3); + Assert.assertEquals(0, b.size()); + + PooledConnection p0 = Mockito.mock(PooledConnection.class); + PooledConnection p1 = Mockito.mock(PooledConnection.class); + PooledConnection p2 = Mockito.mock(PooledConnection.class); + b.add(p0); + b.add(p1); + b.add(p2); + + Assert.assertEquals(3, b.size()); + + // add 1 second as this is going to fast for System.currentTimeMillis() + long now = System.currentTimeMillis()+1000; + int trimCount = b.trim(now); + + Assert.assertEquals(0, b.size()); + Assert.assertEquals(3, trimCount); + } + + @Test + public void testWithTime() { + + FreeConnectionBuffer b = new FreeConnectionBuffer(3); + Assert.assertEquals(0, b.size()); + + PooledConnection p0 = Mockito.mock(PooledConnection.class); + Mockito.when(p0.getLastUsedTime()).thenReturn(1000l); + + PooledConnection p1 = Mockito.mock(PooledConnection.class); + Mockito.when(p1.getLastUsedTime()).thenReturn(2000l); + + PooledConnection p2 = Mockito.mock(PooledConnection.class); + Mockito.when(p2.getLastUsedTime()).thenReturn(1100l); + + b.add(p0); + b.add(p1); + b.add(p2); + + Assert.assertEquals(3, b.size()); + + int trimCount = b.trim(1500); + + Assert.assertEquals(1, b.size()); + Assert.assertEquals(2, trimCount); + } + + + +} From 5f4789f9ba83995d95a286d4bff6ef0b32a23ca0 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Thu, 16 Jan 2014 00:28:00 +1300 Subject: [PATCH 3/4] Support heart beat timeout, tidy up free buffer --- pom.xml | 2 +- .../avaje/ebean/config/DataSourceConfig.java | 43 +++- .../server/core/DefaultServerFactory.java | 22 -- .../server/lib/sql/BusyConnectionBuffer.java | 82 +++++-- .../server/lib/sql/DataSourcePool.java | 104 +++++---- .../server/lib/sql/FreeConnectionBuffer.java | 221 ++++++------------ .../server/lib/sql/PooledConnection.java | 96 ++++++-- .../server/lib/sql/PooledConnectionQueue.java | 102 +++----- .../server/lib/sql/TestDataSourceMax.java | 56 +++-- .../lib/sql/TestDataSourceMaxWithEntity.java | 70 ++++++ .../server/lib/sql/TestFreeBuffer.java | 3 +- .../server/lib/sql/TestFreeBufferTrim.java | 35 +-- src/test/resources/ebean.properties | 6 +- 13 files changed, 464 insertions(+), 378 deletions(-) create mode 100644 src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestDataSourceMaxWithEntity.java diff --git a/pom.xml b/pom.xml index 7358781ca..5ad4142d6 100644 --- a/pom.xml +++ b/pom.xml @@ -129,7 +129,7 @@ mysql mysql-connector-java - 5.1.15 + 5.1.27 test diff --git a/src/main/java/com/avaje/ebean/config/DataSourceConfig.java b/src/main/java/com/avaje/ebean/config/DataSourceConfig.java index e2bde6dbe..5e55e4065 100644 --- a/src/main/java/com/avaje/ebean/config/DataSourceConfig.java +++ b/src/main/java/com/avaje/ebean/config/DataSourceConfig.java @@ -36,6 +36,8 @@ public class DataSourceConfig { private int heartbeatFreqSecs = 30; + private int heartbeatTimeoutSeconds = 3; + private boolean captureStackTrace; private int maxStackTraceSize = 5; @@ -44,6 +46,8 @@ public class DataSourceConfig { private int maxInactiveTimeSecs = 720; + private int maxAgeMinutes = 0; + private int trimPoolFreqSecs = 59; private int pstmtCacheSize = 20; @@ -55,8 +59,8 @@ public class DataSourceConfig { private String poolListener; private boolean offline; - - Map customProperties; + + protected Map customProperties; /** * Return the connection URL. @@ -196,6 +200,20 @@ public class DataSourceConfig { public void setHeartbeatFreqSecs(int heartbeatFreqSecs) { this.heartbeatFreqSecs = heartbeatFreqSecs; } + + /** + * Return the heart beat timeout in seconds. + */ + public int getHeartbeatTimeoutSeconds() { + return heartbeatTimeoutSeconds; + } + + /** + * Set the heart beat timeout in seconds. + */ + public void setHeartbeatTimeoutSeconds(int heartbeatTimeoutSeconds) { + this.heartbeatTimeoutSeconds = heartbeatTimeoutSeconds; + } /** * Return true if a stack trace should be captured when obtaining a connection @@ -311,6 +329,23 @@ public class DataSourceConfig { return maxInactiveTimeSecs; } + /** + * Return the maximum age a connection is allowed to be before it is closed. + *

+ * This can be used to close really old connections. + *

+ */ + public int getMaxAgeMinutes() { + return maxAgeMinutes; + } + + /** + * Set the maximum age a connection can be in minutes. + */ + public void setMaxAgeMinutes(int maxAgeMinutes) { + this.maxAgeMinutes = maxAgeMinutes; + } + /** * Set the time in seconds a connection can be idle after which it can be * trimmed from the pool. @@ -380,7 +415,7 @@ public class DataSourceConfig { public void setOffline(boolean offline) { this.offline = offline; } - + /** * Return a map of custom properties for the jdbc driver connection. */ @@ -420,6 +455,7 @@ public class DataSourceConfig { this.leakTimeMinutes = properties.getInt(prefix + "leakTimeMinutes", 30); this.maxInactiveTimeSecs = properties.getInt(prefix + "maxInactiveTimeSecs", 720); this.trimPoolFreqSecs = properties.getInt(prefix + "trimPoolFreqSecs", 59); + this.maxAgeMinutes = properties.getInt(prefix + "maxAgeMinutes", 0); this.minConnections = properties.getInt(prefix + "minConnections", 0); this.maxConnections = properties.getInt(prefix + "maxConnections", 20); @@ -429,6 +465,7 @@ public class DataSourceConfig { this.waitTimeoutMillis = properties.getInt(prefix + "waitTimeout", 1000); this.heartbeatSql = properties.get(prefix + "heartbeatSql", null); + this.heartbeatTimeoutSeconds = properties.getInt(prefix + "heartbeatTimeoutSeconds", 3); this.poolListener = properties.get(prefix + "poolListener", null); this.offline = properties.getBoolean(prefix + "offline", false); diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServerFactory.java b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServerFactory.java index 2bf4dc6fe..6aa762474 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServerFactory.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServerFactory.java @@ -410,32 +410,10 @@ public class DefaultServerFactory implements BootupEbeanManager { return null; } - if (dsConfig.getHeartbeatSql() == null) { - // use default heartbeatSql from the DatabasePlatform - String heartbeatSql = getHeartbeatSql(dsConfig.getDriver()); - dsConfig.setHeartbeatSql(heartbeatSql); - } - DataSourceAlert notify = new SimpleDataSourceAlert(); return new DataSourcePool(notify, config.getName(), dsConfig); } - /** - * Return a heartbeatSql depending on the jdbc driver name. - */ - private String getHeartbeatSql(String driver) { - if (driver != null) { - String d = driver.toLowerCase(); - if (d.contains("oracle")) { - return "select 'x' from dual"; - } - if (d.contains(".h2.") || d.contains(".mysql.") || d.contains("postgre")) { - return "select 1"; - } - } - return null; - } - /** * Check the autoCommit and Transaction Isolation levels of the DataSource. *

diff --git a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/BusyConnectionBuffer.java b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/BusyConnectionBuffer.java index 98b55d682..98b50346b 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/BusyConnectionBuffer.java +++ b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/BusyConnectionBuffer.java @@ -1,9 +1,6 @@ package com.avaje.ebeaninternal.server.lib.sql; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; -import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -53,7 +50,7 @@ class BusyConnectionBuffer { /** * We can only grow (not shrink) the capacity. */ - private void setCapacity(int newCapacity) { + protected void setCapacity(int newCapacity) { if (newCapacity > slots.length){ PooledConnection[] current = this.slots; this.slots = new PooledConnection[newCapacity]; @@ -74,12 +71,7 @@ class BusyConnectionBuffer { } protected boolean isEmpty() { - for (int i = 0; i < slots.length; i++) { - if (slots[i] != null) { - return false; - } - } - return true; + return size == 0; } protected int add(PooledConnection pc){ @@ -120,22 +112,70 @@ class BusyConnectionBuffer { } /** - * Get a shallow read only List of the busy connections. - *

- * Note that the {@link #remove(PooledConnection)} MUST be used to remove PooledConnections. - *

- * @return + * Close connections that should be considered leaked. */ - protected List getShallowCopy() { - ArrayList tmp = new ArrayList(); - for (int i = 0; i < slots.length; i++) { - if (slots[i] != null){ - tmp.add(slots[i]); + protected void closeBusyConnections(long leakTimeMinutes) { + + long olderThanTime = System.currentTimeMillis() - (leakTimeMinutes*60000); + + logger.debug("Closing busy connections using leakTimeMinutes {}", leakTimeMinutes); + + for (int i = 0; i < slots.length; i++) { + if (slots[i] != null){ + //tmp.add(slots[i]); + PooledConnection pc = slots[i]; + if (pc.isLongRunning() || pc.getLastUsedTime() > olderThanTime) { + // PooledConnection has been used recently or + // expected to be longRunning so not closing... + } else { + slots[i] = null; + --size; + closeBusyConnection(pc); } } - return Collections.unmodifiableList(tmp); + } } + private void closeBusyConnection(PooledConnection pc) { + try { + + logger.warn("DataSourcePool closing busy connection? "+pc.getFullDescription()); + System.out.println("CLOSING busy connection: "+pc.getFullDescription()); + + pc.closeConnectionFully(false); + + } catch (Exception ex) { + // this should never actually happen + logger.error("Error when closing potentially leaked connection "+pc.getDescription(), ex); + } + } + + /** + * Returns information describing connections that are currently being used. + */ + protected String getBusyConnectionInformation(boolean toLogger) { + + if (toLogger) { + logger.info("Dumping [{}] busy connections: (Use datasource.xxx.capturestacktrace=true ... to get stackTraces)", size()); + } + + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < slots.length; i++) { + if (slots[i] != null){ + PooledConnection pc = slots[i]; + if (toLogger) { + logger.info("Busy Connection - {}", pc.getFullDescription()); + } else { + sb.append(pc.getFullDescription()).append("\r\n"); + } + } + } + + return sb.toString(); + } + + /** * Return the position of the next empty slot. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/DataSourcePool.java b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/DataSourcePool.java index 0dff23448..de80b9c7b 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/DataSourcePool.java +++ b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/DataSourcePool.java @@ -76,8 +76,11 @@ public class DataSourcePool implements DataSource { private final String heartbeatsql; private final int heartbeatFreqSecs; + + private final int heartbeatTimeoutSeconds; + - private final int trimPoolFreqSecs; + private final long trimPoolFreqMillis; /** * The transaction isolation level as per java.sql.Connection. @@ -89,6 +92,17 @@ public class DataSourcePool implements DataSource { */ private final boolean autoCommit; + /** + * Max idle time in millis. + */ + private final int maxInactiveMillis; + + /** + * Max age a connection is allowed in millis. + * A value of 0 means no limit (no trimming based on max age). + */ + private final long maxAgeMillis; + /** * Flag set to true to capture stackTraces (can be expensive). */ @@ -138,17 +152,12 @@ public class DataSourcePool implements DataSource { /** * The time a thread will wait for a connection to become available. */ - private int waitTimeoutMillis; + private final int waitTimeoutMillis; /** * The size of the preparedStatement cache; */ private int pstmtCacheSize; - - /** - * By default trim connections that are inactive for longer than this time. - */ - private int maxInactiveTimeSecs; private final PooledConnectionQueue queue; @@ -169,8 +178,9 @@ public class DataSourcePool implements DataSource { this.autoCommit = false; this.transactionIsolation = params.getIsolationLevel(); - - this.maxInactiveTimeSecs = params.getMaxInactiveTimeSecs(); + + this.maxInactiveMillis = 1000 * params.getMaxInactiveTimeSecs(); + this.maxAgeMillis = 60000 * params.getMaxAgeMinutes(); this.leakTimeMinutes = params.getLeakTimeMinutes(); this.captureStackTrace = params.isCaptureStackTrace(); this.maxStackTraceSize = params.getMaxStackTraceSize(); @@ -183,7 +193,8 @@ public class DataSourcePool implements DataSource { this.waitTimeoutMillis = params.getWaitTimeoutMillis(); this.heartbeatsql = params.getHeartbeatSql(); this.heartbeatFreqSecs = params.getHeartbeatFreqSecs(); - this.trimPoolFreqSecs = params.getTrimPoolFreqSecs(); + this.heartbeatTimeoutSeconds = params.getHeartbeatTimeoutSeconds(); + this.trimPoolFreqMillis = 1000 * params.getTrimPoolFreqSecs(); queue = new PooledConnectionQueue(this); @@ -321,12 +332,11 @@ public class DataSourcePool implements DataSource { private void notifyDataSourceIsDown(SQLException ex) { if (!dataSourceDownAlertSent) { - logger.error("FATAL: DataSourcePool [" + name + "] is down!!!", ex); + logger.error("FATAL: DataSourcePool [" + name + "] is down or has network error!!!", ex); if (notify != null) { notify.dataSourceDown(name); } dataSourceDownAlertSent = true; - } if (dataSourceUp) { reset(); @@ -370,6 +380,20 @@ public class DataSourcePool implements DataSource { return heartbeatRunnable; } + /** + * Trim connections (in the free list) based on idle time and maximum age. + */ + private void trimIdleConnections() { + if (System.currentTimeMillis() > (lastTrimTime + trimPoolFreqMillis)) { + try { + queue.trim(maxInactiveMillis, maxAgeMillis); + lastTrimTime = System.currentTimeMillis(); + } catch (Exception e) { + logger.error("Error trying to trim idle connections", e); + } + } + } + /** * Check the dataSource is up. Trim connections. *

@@ -378,17 +402,19 @@ public class DataSourcePool implements DataSource { *

*/ public void checkDataSource() { + + // first trim idle connections + trimIdleConnections(); + Connection conn = null; try { // Get a connection from the pool and test it conn = getConnection(); - testConnection(conn); - - notifyDataSourceIsUp(); - - if (System.currentTimeMillis() > (lastTrimTime + (trimPoolFreqSecs * 1000))) { - queue.trim(maxInactiveTimeSecs); - lastTrimTime = System.currentTimeMillis(); + if (testConnection(conn)) { + notifyDataSourceIsUp(); + + } else { + notifyDataSourceIsDown(null); } } catch (SQLException ex) { @@ -491,33 +517,38 @@ public class DataSourcePool implements DataSource { return waitTimeoutMillis; } - /** - * Set the time after which inactive connections are trimmed. - */ - public void setMaxInactiveTimeSecs(int maxInactiveTimeSecs) { - this.maxInactiveTimeSecs = maxInactiveTimeSecs; - } - /** * Return the time after which inactive connections are trimmed. */ - public int getMaxInactiveTimeSecs() { - return maxInactiveTimeSecs; + public int getMaxInactiveMillis() { + return maxInactiveMillis; + } + + /** + * Return the maximum age a connection is allowed to be before it is trimmed + * out of the pool. This value can be 0 which means there is no maximum age. + */ + public long getMaxAgeMillis() { + return maxAgeMillis; } - private void testConnection(Connection conn) throws SQLException { + private boolean testConnection(Connection conn) throws SQLException { if (heartbeatsql == null) { - return; + return conn.isValid(heartbeatTimeoutSeconds); } Statement stmt = null; ResultSet rset = null; try { - // It should only error IF the DataSource is down ? (or a network - // issue?) + // It should only error IF the DataSource is down or a network issue stmt = conn.createStatement(); + if (heartbeatTimeoutSeconds > 0) { + stmt.setQueryTimeout(heartbeatTimeoutSeconds); + } rset = stmt.executeQuery(heartbeatsql); conn.commit(); + + return true; } finally { try { @@ -543,13 +574,7 @@ public class DataSourcePool implements DataSource { */ protected boolean validateConnection(PooledConnection conn) { try { - if (heartbeatsql == null) { - logger.debug("Can not test connection as heartbeatsql is not set"); - return false; - } - - testConnection(conn); - return true; + return testConnection(conn); } catch (Exception e) { logger.warn("heartbeatsql test failed on connection[" + conn.getName() + "]"); @@ -647,7 +672,6 @@ public class DataSourcePool implements DataSource { notifyDataSourceIsDown(ex); throw ex; } - } /** diff --git a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/FreeConnectionBuffer.java b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/FreeConnectionBuffer.java index 6186aebd8..8fede2fc2 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/FreeConnectionBuffer.java +++ b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/FreeConnectionBuffer.java @@ -1,179 +1,106 @@ package com.avaje.ebeaninternal.server.lib.sql; import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.avaje.ebeaninternal.server.lib.sql.PooledConnectionStatistics.LoadValues; /** * A buffer designed especially to hold free pooled connections. *

- * It is circular in nature. - *

- *

* All thread safety controlled externally (by PooledConnectionQueue). *

- * - * @author rbygrave - * */ class FreeConnectionBuffer { - /** - * The buffer itself. - */ - private PooledConnection[] conns; + private static final Logger logger = LoggerFactory.getLogger(FreeConnectionBuffer.class); + + /** + * Buffer oriented for add and remove. + */ + private final LinkedList freeBuffer = new LinkedList(); - /** - * The position in the buffer where the next connection is removed from. - */ - private int removeIndex; + protected FreeConnectionBuffer() { + } - /** - * Position in the buffer where the next connection is added to. - */ - private int addIndex; + protected int size() { + return freeBuffer.size(); + } - /** - * The current number of connections in the buffer - */ - private int size; + protected boolean isEmpty() { + return freeBuffer.isEmpty(); + } + + /** + * Add connection to the free list. + */ + protected void add(PooledConnection pc) { + freeBuffer.addLast(pc); + } + + /** + * Remove a connection from the free list. + */ + protected PooledConnection remove() { + return freeBuffer.removeFirst(); + } + + /** + * Close all connections in this buffer. + */ + protected void closeAll(boolean logErrors) { + + // create a temporary list + List tempList = new ArrayList(freeBuffer.size()); - protected FreeConnectionBuffer(int capacity) { - this.conns = new PooledConnection[capacity]; + // add all the connections into it + for (PooledConnection c : freeBuffer) { + tempList.add(c); } - protected int getCapacity() { - return conns.length; - } + // clear the buffer (in case it takes some time to close these connections). + freeBuffer.clear(); - protected int size() { - return size; - } - - protected boolean isEmpty() { - return size == 0; - } - - /** - * Add at connection. - */ - protected void add(PooledConnection pc) { - if (conns[addIndex] != null) { - throw new IllegalStateException("Buffer slot ["+addIndex+"] already full?"); - } - conns[addIndex] = pc; - addIndex = inc(addIndex); - ++size; + logger.debug("... closing all {} connections from the free list with logErrors: {}", tempList.size(), logErrors); + for (int i = 0; i < tempList.size(); i++) { + PooledConnection pooledConnection = tempList.get(i); + logger.debug("... closing {} of {} connections from the free list", i, tempList.size()); + pooledConnection.closeConnectionFully(logErrors); } + } + + /** + * Trim any inactive connections that have not been used since usedSince. + */ + protected int trim(long usedSince, long createdSince) { - /** - * Close all connections in this buffer. - */ - protected void closeAll(boolean logErrors) { - - final PooledConnection[] items = this.conns; - - this.conns = new PooledConnection[items.length]; - this.size = 0; - this.removeIndex = 0; - this.addIndex = 0; + int trimCount = 0; - for (int i = 0; i < items.length; i++) { - PooledConnection c = items[i]; - if (c != null) { - c.closeConnectionFully(logErrors); - } + Iterator iterator = freeBuffer.iterator(); + while (iterator.hasNext()) { + PooledConnection pooledConnection = iterator.next(); + if (pooledConnection.shouldTrim(usedSince, createdSince)) { + iterator.remove(); + pooledConnection.closeConnectionFully(true); + trimCount++; } } - - /** - * Remove a connection at current remove position. - */ - protected PooledConnection remove() { - final PooledConnection[] items = this.conns; - PooledConnection pc = items[removeIndex]; - items[removeIndex] = null; - removeIndex = inc(removeIndex); - --size; - return pc; - } - - /** - * Trim any inactive connections that have not been used since usedSince. - */ - protected int trim(long usedSince) { - - int trimCount = 0; - for (int i = 0; i < conns.length; i++) { - if (conns[i] != null){ - if (conns[i].getLastUsedTime() < usedSince) { - trimCount++; - conns[i].closeConnectionFully(true); - conns[i] = null; - --size; - } - } - } - - return trimCount; - } - - /** - * Collect the load statistics from all the free connections. - */ - protected void collectStatistics(LoadValues values, boolean reset) { - - for (int i = 0; i < conns.length; i++) { - if (conns[i] != null){ - values.plus(conns[i].getStatistics().getValues(reset)); - } - } - } - - /** - * Return a shallow copy of the free connections. - */ - private List getShallowCopy() { - - List copy = new ArrayList(conns.length); - for (int i = 0; i < conns.length; i++) { - if (conns[i] != null){ - copy.add(conns[i]); - } - } - return copy; - } - /** - * Increase the capacity of the buffer. This is a relatively expensive - * operation but should occur very infrequently. - */ - protected void setCapacity(int newCapacity) { - - if (newCapacity > conns.length){ - - List copy = getShallowCopy(); - - // reset to empty state - this.removeIndex = 0; - this.addIndex = 0; - this.size = 0; + return trimCount; + } - this.conns = new PooledConnection[newCapacity]; + /** + * Collect the load statistics from all the free connections. + */ + protected void collectStatistics(LoadValues values, boolean reset) { - // add the connections back from the copy - for (int i = 0; i < copy.size(); i++) { - add(copy.get(i)); - } - } + for (PooledConnection c : freeBuffer) { + values.plus(c.getStatistics().getValues(reset)); } - - /** - * Circularly increment i. - */ - private final int inc(int i) { - return (++i == conns.length)? 0 : i; - } - + } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnection.java b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnection.java index 9712d5482..cd6ade3e0 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnection.java +++ b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnection.java @@ -10,14 +10,13 @@ import java.sql.Savepoint; import java.sql.Statement; import java.util.ArrayList; import java.util.Arrays; -import java.util.Iterator; import java.util.Map; -import com.avaje.ebeaninternal.jdbc.ConnectionDelegator; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.avaje.ebeaninternal.jdbc.ConnectionDelegator; + /** * Is a connection that belongs to a DataSourcePool. * @@ -40,8 +39,23 @@ public class PooledConnection extends ConnectionDelegator { private static final Logger logger = LoggerFactory.getLogger(PooledConnection.class); - private static String IDLE_CONNECTION_ACCESSED_ERROR = "Pooled Connection has been accessed whilst idle in the pool, via method: "; + 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. @@ -85,11 +99,21 @@ public class PooledConnection extends ConnectionDelegator { 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. *

@@ -117,8 +141,6 @@ public class PooledConnection extends ConnectionDelegator { private long exeStartNanos; - private final PooledConnectionStatistics stats = new PooledConnectionStatistics(); - /** * The last statement executed by this connection. */ @@ -273,7 +295,7 @@ public class PooledConnection extends ConnectionDelegator { } if (logger.isDebugEnabled()) { - logger.debug("Closing Connection[{}] slot[{}] Stats: {} , PstmtStats: {} ", name, slotId, stats.getValues(false), pstmtCache.getDescription()); + logger.debug("Closing Connection[{}] slot[{}] reason[{}] stats: {} , pstmtStats: {} ", name, slotId, closeReason, stats.getValues(false), pstmtCache.getDescription()); } try { @@ -290,11 +312,9 @@ public class PooledConnection extends ConnectionDelegator { } try { - Iterator psi = pstmtCache.values().iterator(); - while (psi.hasNext()) { - ExtendedPreparedStatement ps = (ExtendedPreparedStatement) psi.next(); - ps.closeDestroy(); - } + for (ExtendedPreparedStatement ps : pstmtCache.values()) { + ps.closeDestroy(); + } } catch (SQLException ex) { if (logErrors) { @@ -335,8 +355,7 @@ public class PooledConnection extends ConnectionDelegator { } } - public Statement createStatement(int resultSetType, int resultSetConcurreny) - throws SQLException { + public Statement createStatement(int resultSetType, int resultSetConcurreny) throws SQLException { if (status == STATUS_IDLE) { throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "createStatement()"); } @@ -388,8 +407,7 @@ public class PooledConnection extends ConnectionDelegator { private PreparedStatement prepareStatement(String sql, boolean useFlag, int flag, String cacheKey) throws SQLException { if (status == STATUS_IDLE) { - String m = IDLE_CONNECTION_ACCESSED_ERROR + "prepareStatement()"; - throw new SQLException(m); + throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "prepareStatement()"); } try { synchronized (pstmtMonitor) { @@ -418,8 +436,8 @@ public class PooledConnection extends ConnectionDelegator { } } - public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurreny) - throws SQLException { + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurreny) throws SQLException { + if (status == STATUS_IDLE) { throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "prepareStatement()"); } @@ -518,6 +536,7 @@ public class PooledConnection extends ConnectionDelegator { } catch (Exception ex) { // the connection is BAD, close it and test the pool + logger.warn("Error when trying to return connection to pool, closing fully.", ex); closeConnectionFully(false); pool.checkDataSource(); } @@ -538,7 +557,7 @@ public class PooledConnection extends ConnectionDelegator { try { if (connection != null && !connection.isClosed()) { // connect leak? - logger.warn("Closing Connection[" + getName() + "] on finalize()."); + logger.warn("Closing Connection on finalize() - {}", getFullDescription()); closeConnectionFully(false); } } catch (Exception e) { @@ -547,6 +566,45 @@ public class PooledConnection extends ConnectionDelegator { super.finalize(); } + /** + * Return true if the connection is too old. + */ + public boolean exceedsMaxAge(long maxAgeMillis) { + if (maxAgeMillis > 0 && (creationTime < (System.currentTimeMillis() - maxAgeMillis))){ + this.closeReason = REASON_MAXAGE; + return true; + } + return false; + } + + public boolean shouldTrimOnReturn(long lastResetTime, long maxAgeMillis) { + if (creationTime <= lastResetTime) { + this.closeReason = REASON_RESET; + return true; + } + if (exceedsMaxAge(maxAgeMillis)) { + return true; + } + return false; + } + + /** + * Return true if the connection has been idle for too long or is too old. + */ + public 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. *

diff --git a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnectionQueue.java b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnectionQueue.java index 1076f9ad0..e355394d1 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnectionQueue.java +++ b/src/main/java/com/avaje/ebeaninternal/server/lib/sql/PooledConnectionQueue.java @@ -1,7 +1,6 @@ package com.avaje.ebeaninternal.server.lib.sql; import java.sql.SQLException; -import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; @@ -55,10 +54,12 @@ public class PooledConnectionQueue { private int connectionId; - private long waitTimeoutMillis; - - private long leakTimeMinutes; + private final long waitTimeoutMillis; + private final long leakTimeMinutes; + + private final long maxAgeMillis; + private int warningSize; private int maxSize; @@ -103,9 +104,10 @@ public class PooledConnectionQueue { this.warningSize = pool.getWarningSize(); this.waitTimeoutMillis = pool.getWaitTimeoutMillis(); this.leakTimeMinutes = pool.getLeakTimeMinutes(); - + this.maxAgeMillis = pool.getMaxAgeMillis(); + this.busyList = new BusyConnectionBuffer(maxSize, 20); - this.freeList = new FreeConnectionBuffer(maxSize); + this.freeList = new FreeConnectionBuffer(); this.lock = new ReentrantLock(false); this.notEmpty = lock.newCondition(); @@ -191,7 +193,7 @@ public class PooledConnectionQueue { if (maxSize < this.minSize){ throw new IllegalArgumentException("maxSize "+maxSize+" < minSize "+this.minSize); } - freeList.setCapacity(maxSize); + this.busyList.setCapacity(maxSize); this.maxSize = maxSize; } finally { lock.unlock(); @@ -244,8 +246,9 @@ public class PooledConnectionQueue { if (!busyList.remove(c)) { logger.error("Connection [{}] not found in BusyList? ", c); } - if (c.getCreationTime() <= lastResetTime) { + if (c.shouldTrimOnReturn(lastResetTime, maxAgeMillis)) { c.closeConnectionFully(false); + } else { freeList.add(c); notEmpty.signal(); @@ -310,9 +313,9 @@ public class PooledConnectionQueue { PooledConnection c = pool.createConnectionForQueue(connectionId++); int busySize = registerBusyConnection(c); - String msg = "DataSourcePool [" + name + "] grow; id["+c.getName()+"] busy["+busySize+"] max["+maxSize+"]"; - logger.debug(msg); - + if (logger.isDebugEnabled()) { + logger.debug("DataSourcePool [{}] grow; id[{}] busy[{}] max[{}]", name, c.getName(), busySize, maxSize); + } checkForWarningSize(); return c; } @@ -372,7 +375,7 @@ public class PooledConnectionQueue { doingShutdown = true; Status status = createStatus(); DataSourcePoolStatistics statistics = pool.getStatistics(false); - logger.debug("DataSourcePool [" + name + "] shutdown {} - Statistics {}", status, statistics); + logger.debug("DataSourcePool [{}] shutdown {} - Statistics {}", name, status, statistics); closeFreeConnections(true); @@ -398,7 +401,7 @@ public class PooledConnectionQueue { lock.lock(); try { Status status = createStatus(); - logger.info("Reseting DataSourcePool [" + name + "] "+status); + logger.info("Reseting DataSourcePool [{}] {}", name, status); lastResetTime = System.currentTimeMillis(); closeFreeConnections(false); @@ -411,12 +414,16 @@ public class PooledConnectionQueue { } } - public void trim(int maxInactiveTimeSecs) throws SQLException { + public void trim(long maxInactiveMillis, long maxAgeMillis) { final ReentrantLock lock = this.lock; lock.lock(); try { - if (trimInactiveConnections(maxInactiveTimeSecs) > 0) { - ensureMinimumConnections(); + if (trimInactiveConnections(maxInactiveMillis, maxAgeMillis) > 0) { + try { + ensureMinimumConnections(); + } catch (SQLException e) { + logger.error("Error trying to ensure minimum connections", e); + } } } finally { lock.unlock(); @@ -426,11 +433,12 @@ public class PooledConnectionQueue { /** * Trim connections that have been not used for some time. */ - private int trimInactiveConnections(int maxInactiveTimeSecs) { + private int trimInactiveConnections(long maxInactiveMillis, long maxAgeMillis) { - long usedSince = System.currentTimeMillis() - (maxInactiveTimeSecs * 1000); - - int trimedCount = freeList.trim(usedSince); + long usedSince = System.currentTimeMillis() - maxInactiveMillis; + long createdSince = (maxAgeMillis == 0) ? 0 : System.currentTimeMillis() - maxAgeMillis; + + int trimedCount = freeList.trim(usedSince, createdSince); if (trimedCount > 0) { logger.debug("DataSourcePool [{}] trimmed [{}] inactive connections. New size[{}]", name, trimedCount, totalConnections()); } @@ -467,42 +475,11 @@ public class PooledConnectionQueue { final ReentrantLock lock = this.lock; lock.lock(); try { - - long olderThanTime = System.currentTimeMillis() - (leakTimeMinutes*60000); - - List copy = busyList.getShallowCopy(); - - logger.debug("Closing busy connections using leakTimeMinutes {}", leakTimeMinutes); - - for (int i = 0; i < copy.size(); i++) { - PooledConnection pc = copy.get(i); - if (pc.isLongRunning() || pc.getLastUsedTime() > olderThanTime) { - // PooledConnection has been used recently or - // expected to be longRunning so not closing... - } else { - busyList.remove(pc); - closeBusyConnection(pc, leakTimeMinutes); - } - } - + busyList.closeBusyConnections(leakTimeMinutes); } finally { lock.unlock(); } } - - private void closeBusyConnection(PooledConnection pc, long leakMinutes) { - try { - - logger.warn("DataSourcePool closing busy connection? "+pc.getFullDescription()); - System.out.println("CLOSING busy connection: "+pc.getFullDescription()); - - pc.closeConnectionFully(false); - - } catch (Exception ex) { - // this should never actually happen - logger.error("Error when closing potentially leaked connection "+pc.getDescription(), ex); - } - } /** * As the pool grows it gets closer to the maxConnections limit. We can send @@ -544,25 +521,8 @@ public class PooledConnectionQueue { lock.lock(); try { - StringBuilder sb = new StringBuilder(); - - List copy = busyList.getShallowCopy(); - - if (toLogger) { - logger.info("Dumping [{}] busy connections: (Use datasource.xxx.capturestacktrace=true ... to get stackTraces)", copy.size()); - } - - for (int i = 0; i < copy.size(); i++) { - PooledConnection pc = copy.get(i); - if (toLogger) { - logger.info("Busy Connection - {}", pc.getFullDescription()); - } else { - sb.append(pc.getFullDescription()).append("\r\n"); - } - } - - return sb.toString(); - + return busyList.getBusyConnectionInformation(toLogger); + } finally { lock.unlock(); } diff --git a/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestDataSourceMax.java b/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestDataSourceMax.java index 26fe97c91..c392515a9 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestDataSourceMax.java +++ b/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestDataSourceMax.java @@ -7,7 +7,6 @@ import java.sql.ResultSet; import org.junit.Test; import com.avaje.ebean.BaseTestCase; -import com.avaje.ebean.Ebean; import com.avaje.ebean.config.DataSourceConfig; import com.avaje.ebeaninternal.server.core.DefaultBackgroundExecutor; import com.avaje.ebeaninternal.server.lib.sql.DataSourcePool.Status; @@ -22,43 +21,45 @@ public class TestDataSourceMax extends BaseTestCase { if (skipThisTest) { return; } - - Ebean.getServer(null); - String name = "h2"; + String name = "mysql"; DataSourceConfig dsConfig = new DataSourceConfig(); dsConfig.loadSettings(name); dsConfig.setMinConnections(2); - dsConfig.setMaxConnections(8); + dsConfig.setMaxConnections(25); dsConfig.setWaitTimeoutMillis(30000); dsConfig.setCaptureStackTrace(true); DataSourcePool pool = new DataSourcePool(null, name, dsConfig); - // Assert.assertEquals(3, pool.getMaxSize()); - - DefaultBackgroundExecutor bg = new DefaultBackgroundExecutor(10, 2, 180, 30, "testDs"); + + //pool.checkDataSource(); + +// if (true) { +// pool.shutdown(false); +// return; +// } + + DefaultBackgroundExecutor bg = new DefaultBackgroundExecutor(1, 2, 180, 30, "testDs"); try { for (int i = 0; i < 12; i++) { // Thread.sleep(10*i); - bg.execute(new ConnRunner(pool, 4000)); + bg.execute(new ConnRunner(pool, 4000, i)); } System.out.println("main thread sleep ... " + pool.getStatus(false)); Thread.sleep(10000); + pool.getStatistics(true); + + Thread.sleep(30000); + Status status = pool.getStatus(false); System.out.println(status); pool.shutdown(false); - // this dumpOrder was for 3 vectors used in PooledConnectionQueue - // that logged the order of wait, notify and obtain events - // I have remove that code. - - // String s = pool.dumpOrder(); - // System.err.println(s); } catch (Exception e) { e.printStackTrace(); @@ -70,24 +71,38 @@ public class TestDataSourceMax extends BaseTestCase { final DataSourcePool pool; final long sleepMillis; + final int position; - ConnRunner(DataSourcePool pool, long sleepMillis) { + ConnRunner(DataSourcePool pool, long sleepMillis, int position) { this.pool = pool; this.sleepMillis = sleepMillis; + this.position = position; } + private void waitSomeTime(long count) { + try { + System.out.println(position+" sleep " + sleepMillis+" count:"+count); + Thread.sleep(sleepMillis); + System.out.println(position+" sleep done"); + } catch (InterruptedException e){ + throw new RuntimeException(e); + } + } + public void run() { Connection connection = null; PreparedStatement pstmt = null; ResultSet rset = null; + long count = -1; try { connection = pool.getConnection(); pstmt = connection.prepareStatement("select count(*) from o_customer"); rset = pstmt.executeQuery(); - - System.out.println("sleep " + sleepMillis); - Thread.sleep(sleepMillis); - System.out.println("sleep done"); + + while (rset.next()) { + // do nothing actually + count = rset.getLong(1); + } } catch (Exception ex) { ex.printStackTrace(); @@ -113,6 +128,7 @@ public class TestDataSourceMax extends BaseTestCase { } } } + waitSomeTime(count); } } } diff --git a/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestDataSourceMaxWithEntity.java b/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestDataSourceMaxWithEntity.java new file mode 100644 index 000000000..b4d136a7c --- /dev/null +++ b/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestDataSourceMaxWithEntity.java @@ -0,0 +1,70 @@ +package com.avaje.ebeaninternal.server.lib.sql; + +import org.junit.Test; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.EbeanServer; +import com.avaje.ebeaninternal.server.core.DefaultBackgroundExecutor; +import com.avaje.tests.model.basic.Customer; + +public class TestDataSourceMaxWithEntity extends BaseTestCase { + + @Test + public void test() { + + boolean skipThisTest = true; + + if (skipThisTest) { + return; + } + + EbeanServer server = Ebean.getServer(null); + + + DefaultBackgroundExecutor bg = new DefaultBackgroundExecutor(1, 2, 180, 30, "testDs"); + + try { + for (int i = 0; i < 12; i++) { + // Thread.sleep(10*i); + bg.execute(new ConnRunner(server, 4000, i)); + } + + System.out.println("main thread sleep ... "); + + + Thread.sleep(30000); + + server.shutdown(true, false); + + } catch (Exception e) { + e.printStackTrace(); + } + + } + + private static class ConnRunner implements Runnable { + + final EbeanServer server; + final long sleepMillis; + final int position; + + ConnRunner(EbeanServer server, long sleepMillis, int position) { + this.server = server; + this.sleepMillis = sleepMillis; + this.position = position; + } + + public void run() { + + server.find(Customer.class).findRowCount(); + try { + System.out.println(position+" sleep " + sleepMillis); + Thread.sleep(sleepMillis); + System.out.println(position+" sleep done"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +} diff --git a/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestFreeBuffer.java b/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestFreeBuffer.java index de3a451b1..85e0f1a4e 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestFreeBuffer.java +++ b/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestFreeBuffer.java @@ -10,14 +10,13 @@ public class TestFreeBuffer extends BaseTestCase { @Test public void test() { - FreeConnectionBuffer b = new FreeConnectionBuffer(3); + FreeConnectionBuffer b = new FreeConnectionBuffer(); PooledConnection p0 = new PooledConnection("0"); PooledConnection p1 = new PooledConnection("1"); PooledConnection p2 = new PooledConnection("2"); // PooledConnection p3 = new PooledConnection("3"); - Assert.assertEquals(3, b.getCapacity()); Assert.assertEquals(0, b.size()); Assert.assertEquals(true, b.isEmpty()); diff --git a/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestFreeBufferTrim.java b/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestFreeBufferTrim.java index 9a3c8b293..a7a4052c2 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestFreeBufferTrim.java +++ b/src/test/java/com/avaje/ebeaninternal/server/lib/sql/TestFreeBufferTrim.java @@ -7,44 +7,21 @@ import org.mockito.Mockito; import com.avaje.ebean.BaseTestCase; public class TestFreeBufferTrim extends BaseTestCase { - - @Test - public void test() { - - FreeConnectionBuffer b = new FreeConnectionBuffer(3); - Assert.assertEquals(0, b.size()); - - PooledConnection p0 = Mockito.mock(PooledConnection.class); - PooledConnection p1 = Mockito.mock(PooledConnection.class); - PooledConnection p2 = Mockito.mock(PooledConnection.class); - b.add(p0); - b.add(p1); - b.add(p2); - - Assert.assertEquals(3, b.size()); - - // add 1 second as this is going to fast for System.currentTimeMillis() - long now = System.currentTimeMillis()+1000; - int trimCount = b.trim(now); - - Assert.assertEquals(0, b.size()); - Assert.assertEquals(3, trimCount); - } @Test public void testWithTime() { - FreeConnectionBuffer b = new FreeConnectionBuffer(3); + FreeConnectionBuffer b = new FreeConnectionBuffer(); Assert.assertEquals(0, b.size()); PooledConnection p0 = Mockito.mock(PooledConnection.class); - Mockito.when(p0.getLastUsedTime()).thenReturn(1000l); + Mockito.when(p0.shouldTrim(1500, 0)).thenReturn(true); PooledConnection p1 = Mockito.mock(PooledConnection.class); - Mockito.when(p1.getLastUsedTime()).thenReturn(2000l); - + Mockito.when(p1.shouldTrim(1500, 0)).thenReturn(true); + PooledConnection p2 = Mockito.mock(PooledConnection.class); - Mockito.when(p2.getLastUsedTime()).thenReturn(1100l); + Mockito.when(p2.shouldTrim(1500, 0)).thenReturn(false); b.add(p0); b.add(p1); @@ -52,7 +29,7 @@ public class TestFreeBufferTrim extends BaseTestCase { Assert.assertEquals(3, b.size()); - int trimCount = b.trim(1500); + int trimCount = b.trim(1500, 0); Assert.assertEquals(1, b.size()); Assert.assertEquals(2, trimCount); diff --git a/src/test/resources/ebean.properties b/src/test/resources/ebean.properties index b86889338..676df1c62 100644 --- a/src/test/resources/ebean.properties +++ b/src/test/resources/ebean.properties @@ -25,7 +25,7 @@ ebean.autofetch.traceUsageCollection=false ebean.ddl.generate=true -ebean.ddl.run=true +#ebean.ddl.run=true ebean.debug.sql=true @@ -80,7 +80,7 @@ ebean.cacheWarmingDelay=-1 ## DataSources (If using default Ebean DataSourceFactory) ## ------------------------------------------------------------- -datasource.default=h2 +datasource.default=mysql datasource.h2.username=sa datasource.h2.password= @@ -115,7 +115,7 @@ datasource.mysql.username=test datasource.mysql.password=test datasource.mysql.databaseUrl=jdbc:mysql://127.0.0.1:3306/test datasource.mysql.databaseDriver=com.mysql.jdbc.Driver -datasource.mysql.minConnections=1 +datasource.mysql.minConnections=2 datasource.mysql.maxConnections=25 #datasource.mysql.heartbeatsql=select count(*) from dual datasource.mysql.isolationlevel=read_committed From e97e77e2e0081212017f2f7b89ecb28156241a83 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Fri, 17 Jan 2014 00:31:03 +1300 Subject: [PATCH 4/4] Add reading/setting of namingconvention.schema from ebean.properties --- .../ebeaninternal/server/core/DefaultServerFactory.java | 5 +++++ src/test/resources/ebean.properties | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServerFactory.java b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServerFactory.java index 6aa762474..99dcb029f 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServerFactory.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServerFactory.java @@ -353,6 +353,11 @@ public class DefaultServerFactory implements BootupEbeanManager { if (sequenceFormat != null) { nc.setSequenceFormat(sequenceFormat); } + + String schema = config.getProperty("namingConvention.schema"); + if (schema != null) { + nc.setSchema(schema); + } } } diff --git a/src/test/resources/ebean.properties b/src/test/resources/ebean.properties index 676df1c62..0af68ce63 100644 --- a/src/test/resources/ebean.properties +++ b/src/test/resources/ebean.properties @@ -24,7 +24,7 @@ ebean.autofetch.profiling.base=10 ebean.autofetch.traceUsageCollection=false -ebean.ddl.generate=true +#ebean.ddl.generate=true #ebean.ddl.run=true @@ -74,6 +74,7 @@ ebean.cacheWarmingDelay=-1 #ebean.namingConvention=com.avaje.ebean.config.UnderscoreNamingConvention #ebean.namingConvention.sequenceFormat={table}_{column}_seq +#ebean.namingConvention.schema=banan #ebean.databaseSequenceBatchSize=1 ## -------------------------------------------------------------