Merge branch 'dbpool-logging'

This commit is contained in:
Rob Bygrave
2014-01-22 01:09:19 +13:00
18 changed files with 1401 additions and 807 deletions
@@ -36,25 +36,31 @@ public class DataSourceConfig {
private int heartbeatFreqSecs = 30;
private int heartbeatTimeoutSeconds = 3;
private boolean captureStackTrace;
private int maxStackTraceSize = 5;
private int leakTimeMinutes = 30;
private int maxInactiveTimeSecs = 900;
private int maxInactiveTimeSecs = 720;
private int maxAgeMinutes = 0;
private int trimPoolFreqSecs = 59;
private int pstmtCacheSize = 20;
private int cstmtCacheSize = 20;
private int waitTimeoutMillis = 1000;
private String poolListener;
private boolean offline;
Map<String, String> customProperties;
protected Map<String, String> customProperties;
/**
* Return the connection URL.
@@ -194,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
@@ -309,6 +329,23 @@ public class DataSourceConfig {
return maxInactiveTimeSecs;
}
/**
* Return the maximum age a connection is allowed to be before it is closed.
* <p>
* This can be used to close really old connections.
* </p>
*/
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.
@@ -321,6 +358,25 @@ public class DataSourceConfig {
this.maxInactiveTimeSecs = maxInactiveTimeSecs;
}
/**
* Return the minimum time gap between pool trim checks.
* <p>
* This defaults to 59 seconds meaning that the pool trim check will run every
* minute assuming the heart beat check runs every 30 seconds.
* </p>
*/
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.
*/
@@ -359,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.
*/
@@ -388,18 +444,18 @@ 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.maxAgeMinutes = properties.getInt(prefix + "maxAgeMinutes", 0);
this.minConnections = properties.getInt(prefix + "minConnections", 0);
this.maxConnections = properties.getInt(prefix + "maxConnections", 20);
@@ -409,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);
@@ -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);
}
}
}
@@ -410,32 +415,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.
* <p>
@@ -1,9 +1,11 @@
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;
import com.avaje.ebeaninternal.server.lib.sql.PooledConnectionStatistics.LoadValues;
/**
* A buffer especially designed for Busy PooledConnections.
@@ -21,6 +23,8 @@ import java.util.List;
*/
class BusyConnectionBuffer {
private static final Logger logger = LoggerFactory.getLogger(BusyConnectionBuffer.class);
private PooledConnection[] slots;
private int growBy;
@@ -46,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];
@@ -66,8 +70,8 @@ class BusyConnectionBuffer {
return size;
}
protected boolean isEmpty(){
return size == 0;
protected boolean isEmpty() {
return size == 0;
}
protected int add(PooledConnection pc){
@@ -75,41 +79,103 @@ 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.
* <p>
* Note that the {@link #remove(PooledConnection)} MUST be used to remove PooledConnections.
* </p>
* @return
* Close connections that should be considered leaked.
*/
protected List<PooledConnection> getShallowCopy() {
ArrayList<PooledConnection> tmp = new ArrayList<PooledConnection>();
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.
*/
@@ -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.
* <p>
@@ -73,8 +74,13 @@ public class DataSourcePool implements DataSource {
* The sql used to test a connection.
*/
private final String heartbeatsql;
private final int heartbeatFreqSecs;
private final int heartbeatTimeoutSeconds;
private final long trimPoolFreqMillis;
/**
* The transaction isolation level as per java.sql.Connection.
@@ -86,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).
*/
@@ -135,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;
@@ -166,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();
@@ -180,6 +193,8 @@ public class DataSourcePool implements DataSource {
this.waitTimeoutMillis = params.getWaitTimeoutMillis();
this.heartbeatsql = params.getHeartbeatSql();
this.heartbeatFreqSecs = params.getHeartbeatFreqSecs();
this.heartbeatTimeoutSeconds = params.getHeartbeatTimeoutSeconds();
this.trimPoolFreqMillis = 1000 * params.getTrimPoolFreqSecs();
queue = new PooledConnectionQueue(this);
@@ -317,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();
@@ -366,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.
* <p>
@@ -374,17 +402,19 @@ public class DataSourcePool implements DataSource {
* </p>
*/
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 + (maxInactiveTimeSecs * 1000))) {
queue.trim(maxInactiveTimeSecs);
lastTrimTime = System.currentTimeMillis();
if (testConnection(conn)) {
notifyDataSourceIsUp();
} else {
notifyDataSourceIsDown(null);
}
} catch (SQLException ex) {
@@ -487,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 {
@@ -539,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() + "]");
@@ -573,7 +602,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.
*/
@@ -635,7 +672,6 @@ public class DataSourcePool implements DataSource {
notifyDataSourceIsDown(ex);
throw ex;
}
}
/**
@@ -834,6 +870,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 +917,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+"]";
}
/**
@@ -0,0 +1,97 @@
package com.avaje.ebeaninternal.server.lib.sql;
/**
* Represents aggregated statistics collected from the DataSourcePool.
* <p>
* 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.
* </p>
* <p>
* 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.
* </p>
*/
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;
}
}
@@ -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;
@@ -1,138 +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.
* <p>
* It is circular in nature.
* </p>
* <p>
* All thread safety controlled externally (by PooledConnectionQueue).
* </p>
*
* @author rbygrave
*
*/
class FreeConnectionBuffer {
private PooledConnection[] conns;
private static final Logger logger = LoggerFactory.getLogger(FreeConnectionBuffer.class);
/**
* Buffer oriented for add and remove.
*/
private final LinkedList<PooledConnection> freeBuffer = new LinkedList<PooledConnection>();
private int removeIndex;
protected FreeConnectionBuffer() {
}
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<PooledConnection> tempList = new ArrayList<PooledConnection>(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;
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);
}
protected boolean isEmpty() {
return size == 0;
}
/**
* Add at connection.
*/
protected void add(PooledConnection pc) {
conns[addIndex] = pc;
addIndex = inc(addIndex);
++size;
}
/**
* Trim any inactive connections that have not been used since usedSince.
*/
protected int trim(long usedSince, long createdSince) {
int trimCount = 0;
Iterator<PooledConnection> 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;
}
/**
* Return a shallow copy of the free connections.
*/
protected List<PooledConnection> getShallowCopy() {
List<PooledConnection> copy = new ArrayList<PooledConnection>(conns.length);
for (int i = 0; i < conns.length; i++) {
if (conns[i] != null){
copy.add(conns[i]);
}
}
return copy;
}
/**
* Set the free list to be the connections in this copy. This is done after
* unused connections have been trimmed.
* <p>
* Not a particularly performant approach but this should not be called very
* often
* </p>
*/
protected void setShallowCopy(List<PooledConnection> copy) {
// reset to empty state
this.removeIndex = 0;
this.addIndex = 0;
this.size = 0;
return trimCount;
}
// null all the current connections
for (int i = 0; i < conns.length; i++) {
conns[i] = null;
}
/**
* Collect the load statistics from all the free connections.
*/
protected void collectStatistics(LoadValues values, boolean reset) {
// add connections 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));
}
/**
* 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<PooledConnection> copy = getShallowCopy();
// reset to empty state
this.removeIndex = 0;
this.addIndex = 0;
this.size = 0;
this.conns = new PooledConnection[newCapacity];
// add the connections back from the copy
for (int i = 0; i < copy.size(); i++) {
add(copy.get(i));
}
}
}
/**
* Circularly increment i.
*/
private final int inc(int i) {
return (++i == conns.length)? 0 : i;
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,18 +1,16 @@
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;
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 +32,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
*/
@@ -46,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;
@@ -94,11 +104,12 @@ public class PooledConnectionQueue {
this.warningSize = pool.getWarningSize();
this.waitTimeoutMillis = pool.getWaitTimeoutMillis();
this.leakTimeMinutes = pool.getLeakTimeMinutes();
this.busyList = new BusyConnectionBuffer(50,20);
this.freeList = new FreeConnectionBuffer(maxSize);
this.lock = new ReentrantLock(true);
this.maxAgeMillis = pool.getMaxAgeMillis();
this.busyList = new BusyConnectionBuffer(maxSize, 20);
this.freeList = new FreeConnectionBuffer();
this.lock = new ReentrantLock(false);
this.notEmpty = lock.newCondition();
}
@@ -116,6 +127,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();
@@ -152,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();
@@ -203,10 +244,11 @@ 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) {
if (c.shouldTrimOnReturn(lastResetTime, maxAgeMillis)) {
c.closeConnectionFully(false);
} else {
freeList.add(c);
notEmpty.signal();
@@ -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();
}
@@ -272,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;
}
@@ -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 [{}] shutdown {} - Statistics {}", name, 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);
}
@@ -360,27 +401,30 @@ 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);
closeBusyConnections(leakTimeMinutes);
String busyMsg = "Busy Connections:\r\n" + getBusyConnectionInformation();
logger.info(busyMsg);
logger.info("Busy Connections:\n" + getBusyConnectionInformation());
} finally {
lock.unlock();
}
}
public void trim(int maxInactiveTimeSecs) throws SQLException {
public void trim(long maxInactiveMillis, long maxAgeMillis) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
trimInactiveConnections(maxInactiveTimeSecs);
ensureMinimumConnections();
if (trimInactiveConnections(maxInactiveMillis, maxAgeMillis) > 0) {
try {
ensureMinimumConnections();
} catch (SQLException e) {
logger.error("Error trying to ensure minimum connections", e);
}
}
} finally {
lock.unlock();
}
@@ -389,40 +433,14 @@ 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<PooledConnection> freeListCopy = freeList.getShallowCopy();
Iterator<PooledConnection> 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;
}
}
}
if (trimedCount > 0) {
private int trimInactiveConnections(long maxInactiveMillis, long maxAgeMillis) {
// 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);
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());
}
return trimedCount;
}
@@ -434,11 +452,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();
}
@@ -461,62 +475,12 @@ public class PooledConnectionQueue {
final ReentrantLock lock = this.lock;
lock.lock();
try {
long olderThanTime = System.currentTimeMillis() - (leakTimeMinutes*60000);
List<PooledConnection> copy = busyList.getShallowCopy();
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);
}
}
busyList.closeBusyConnections(leakTimeMinutes);
} finally {
lock.unlock();
}
}
private void closeBusyConnection(PooledConnection pc) {
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
+ "] lastStmt[" + pc.getLastStatement() + "]";
logger.warn(msg);
logStackElement(pc, "Possible Leaked Connection: ");
System.out.println("CLOSING BUSY CONNECTION ??? "+pc);
pc.close();
} catch (SQLException ex) {
// this should never actually happen
logger.error(null, ex);
}
}
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
@@ -557,26 +521,8 @@ 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<PooledConnection> copy = busyList.getShallowCopy();
for (int i = 0; i < copy.size(); i++) {
PooledConnection pc = copy.get(i);
if (toLogger) {
logger.info(pc.getDescription());
logStackElement(pc, "Busy Connection: ");
} else {
sb.append(pc.getDescription()).append("\r\n");
}
}
return sb.toString();
return busyList.getBusyConnectionInformation(toLogger);
} finally {
lock.unlock();
}
@@ -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.
* <p>
* These are aggregated up to get a total for the DataSourcePool.
* </p>
*/
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;
}
}
}
@@ -14,37 +14,37 @@ public class PstmtCache extends LinkedHashMap<String, ExtendedPreparedStatement>
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<String, ExtendedPreparedStatement>
* 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<String, ExtendedPreparedStatement>
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.
*/