diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index bc82e55..1f5c33e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -24,7 +24,7 @@ jobs:
java-version: ${{ matrix.java_version }}
distribution: 'zulu'
- name: Maven cache
- uses: actions/cache@v2
+ uses: actions/cache@v4
env:
cache-name: maven-cache
with:
diff --git a/ebean-datasource-api/pom.xml b/ebean-datasource-api/pom.xml
index 4c3217f..cea0c8d 100644
--- a/ebean-datasource-api/pom.xml
+++ b/ebean-datasource-api/pom.xml
@@ -4,7 +4,7 @@
ebean-datasource-parent
io.ebean
- 9.3-SNAPSHOT
+ 9.3-FOC4-SNAPSHOT
ebean-datasource-api
diff --git a/ebean-datasource-api/src/main/java/io/ebean/datasource/DataSourceBuilder.java b/ebean-datasource-api/src/main/java/io/ebean/datasource/DataSourceBuilder.java
index 89b574f..501f2f2 100644
--- a/ebean-datasource-api/src/main/java/io/ebean/datasource/DataSourceBuilder.java
+++ b/ebean-datasource-api/src/main/java/io/ebean/datasource/DataSourceBuilder.java
@@ -749,6 +749,27 @@ default DataSourceBuilder initDatabaseForPlatform(String platform) {
*/
DataSourceBuilder loadSettings(Properties properties, String poolName);
+
+ /**
+ * When enabled, the datasource enforces a clean close. This means, if you close a possible dirty
+ * connection, that was not committed or rolled back, an exception is thrown.
+ *
+ * When disabled, the situation is logged as warning.
+ *
+ * This option has no effect on readonly or autocommit connections.
+ *
+ * Note: It is recommended to enable this option in tests/test systems to find possible
+ * programming errors. See https://github.com/ebean-orm/ebean-datasource/issues/116 for details.
+ */
+ DataSourceBuilder enforceCleanClose(boolean enforceCleanClose);
+
+ /**
+ * When true, an exception is thrown when a dirty connection is closed.
+ *
+ * See {@link #enforceCleanClose(boolean)}.
+ */
+ boolean enforceCleanClose();
+
/**
* The settings of the DataSourceBuilder. Provides getters/accessors
* to read the configured properties of this DataSourceBuilder.
diff --git a/ebean-datasource-api/src/main/java/io/ebean/datasource/DataSourceConfig.java b/ebean-datasource-api/src/main/java/io/ebean/datasource/DataSourceConfig.java
index 21d24fc..0320e6f 100644
--- a/ebean-datasource-api/src/main/java/io/ebean/datasource/DataSourceConfig.java
+++ b/ebean-datasource-api/src/main/java/io/ebean/datasource/DataSourceConfig.java
@@ -83,6 +83,7 @@ public class DataSourceConfig implements DataSourceBuilder.Settings {
private String applicationName;
private boolean shutdownOnJvmExit;
private boolean validateOnHeartbeat = !System.getenv().containsKey("LAMBDA_TASK_ROOT");
+ private boolean enforceCleanClose;
@Override
public Settings settings() {
@@ -144,6 +145,7 @@ public DataSourceConfig copy() {
copy.initSql = initSql;
copy.alert = alert;
copy.listener = listener;
+ copy.enforceCleanClose = enforceCleanClose;
return copy;
}
@@ -798,6 +800,8 @@ private void loadSettings(ConfigPropertiesHelper properties) {
offline = properties.getBoolean("offline", offline);
shutdownOnJvmExit = properties.getBoolean("shutdownOnJvmExit", shutdownOnJvmExit);
validateOnHeartbeat = properties.getBoolean("validateOnHeartbeat", validateOnHeartbeat);
+ enforceCleanClose = properties.getBoolean("enforceCleanClose", enforceCleanClose);
+
String isoLevel = properties.get("isolationLevel", _isolationLevel(isolationLevel));
this.isolationLevel = _isolationLevel(isoLevel);
@@ -846,6 +850,17 @@ Map parseCustom(String customProperties) {
return propertyMap;
}
+ @Override
+ public DataSourceBuilder enforceCleanClose(boolean enforceCleanClose) {
+ this.enforceCleanClose = enforceCleanClose;
+ return this;
+ }
+
+ @Override
+ public boolean enforceCleanClose() {
+ return enforceCleanClose;
+ }
+
/**
* Return the isolation level description from the associated Connection int value.
*/
diff --git a/ebean-datasource-api/src/main/java/io/ebean/datasource/DataSourcePoolListener.java b/ebean-datasource-api/src/main/java/io/ebean/datasource/DataSourcePoolListener.java
index 555628f..5471c51 100644
--- a/ebean-datasource-api/src/main/java/io/ebean/datasource/DataSourcePoolListener.java
+++ b/ebean-datasource-api/src/main/java/io/ebean/datasource/DataSourcePoolListener.java
@@ -17,11 +17,12 @@ public interface DataSourcePoolListener {
/**
* Called after a connection has been retrieved from the connection pool
*/
- void onAfterBorrowConnection(Connection connection);
+ default void onAfterBorrowConnection(Connection connection) {}
/**
* Called before a connection will be put back to the connection pool
*/
- void onBeforeReturnConnection(Connection connection);
+ default void onBeforeReturnConnection(Connection connection) {}
+
}
diff --git a/ebean-datasource/pom.xml b/ebean-datasource/pom.xml
index 04a2cbc..afcd6ef 100644
--- a/ebean-datasource/pom.xml
+++ b/ebean-datasource/pom.xml
@@ -4,7 +4,7 @@
io.ebean
ebean-datasource-parent
- 9.3-SNAPSHOT
+ 9.3-FOC4-SNAPSHOT
ebean-datasource
@@ -16,7 +16,7 @@
io.ebean
ebean-datasource-api
- 9.3-SNAPSHOT
+ 9.3-FOC4-SNAPSHOT
@@ -76,6 +76,13 @@
test
+
+ com.ibm.db2
+ jcc
+ 11.5.9.0
+ test
+
+
diff --git a/ebean-datasource/src/main/java/io/ebean/datasource/pool/BusyConnectionBuffer.java b/ebean-datasource/src/main/java/io/ebean/datasource/pool/BusyConnectionBuffer.java
index 2fe702d..2f67baa 100644
--- a/ebean-datasource/src/main/java/io/ebean/datasource/pool/BusyConnectionBuffer.java
+++ b/ebean-datasource/src/main/java/io/ebean/datasource/pool/BusyConnectionBuffer.java
@@ -1,6 +1,8 @@
package io.ebean.datasource.pool;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
/**
* A buffer especially designed for Busy PooledConnections.
@@ -82,9 +84,10 @@ boolean remove(PooledConnection pc) {
}
/**
- * Close connections that should be considered leaked.
+ * Remove connections that should be considered leaked.
*/
- void closeBusyConnections(long leakTimeMinutes) {
+ List removeBusyConnections(long leakTimeMinutes) {
+ List busyConnections = null;
long olderThanTime = System.currentTimeMillis() - (leakTimeMinutes * 60000);
Log.debug("Closing busy connections using leakTimeMinutes {0}", leakTimeMinutes);
for (int i = 0; i < slots.length; i++) {
@@ -98,20 +101,14 @@ void closeBusyConnections(long leakTimeMinutes) {
} else {
slots[i] = null;
--size;
- closeBusyConnection(pc);
+ if (busyConnections == null) {
+ busyConnections = new ArrayList<>();
+ }
+ busyConnections.add(pc);
}
}
}
- }
-
- private void closeBusyConnection(PooledConnection pc) {
- try {
- Log.warn("DataSource closing busy connection? {0}", pc.fullDescription());
- System.out.println("CLOSING busy connection: " + pc.fullDescription());
- pc.closeConnectionFully(false);
- } catch (Exception ex) {
- Log.error("Error when closing potentially leaked connection " + pc.description(), ex);
- }
+ return busyConnections;
}
/**
diff --git a/ebean-datasource/src/main/java/io/ebean/datasource/pool/ConnectionPool.java b/ebean-datasource/src/main/java/io/ebean/datasource/pool/ConnectionPool.java
index c58651f..7662b58 100644
--- a/ebean-datasource/src/main/java/io/ebean/datasource/pool/ConnectionPool.java
+++ b/ebean-datasource/src/main/java/io/ebean/datasource/pool/ConnectionPool.java
@@ -55,6 +55,7 @@ final class ConnectionPool implements DataSourcePool {
private final boolean failOnStart;
private final int maxInactiveMillis;
private final long validateStaleMillis;
+ private final boolean enforceCleanClose;
/**
* Max age a connection is allowed in millis.
* A value of 0 means no limit (no trimming based on max age).
@@ -128,6 +129,7 @@ final class ConnectionPool implements DataSourcePool {
this.user = params.getUsername();
this.shutdownOnJvmExit = params.isShutdownOnJvmExit();
this.source = DriverDataSource.of(name, params);
+ this.enforceCleanClose = params.enforceCleanClose();
if (!params.isOffline()) {
init();
}
@@ -290,7 +292,7 @@ public SQLException dataSourceDownReason() {
private void notifyDataSourceIsDown(SQLException reason) {
if (dataSourceUp.get()) {
- reset();
+ reset(false);
notifyDown(reason);
}
}
@@ -314,7 +316,7 @@ private void notifyDown(SQLException reason) {
private void notifyDataSourceIsUp() {
if (!dataSourceUp.get()) {
- reset();
+ reset(true);
notifyUp();
}
}
@@ -447,6 +449,10 @@ private Connection createConnection() throws SQLException {
return initConnection(source.getConnection());
}
+ public int forceTrim(int trimCount) {
+ return queue.forceTrim(trimCount);
+ }
+
@Override
public void setMaxSize(int max) {
queue.setMaxSize(max);
@@ -516,6 +522,13 @@ boolean invalidConnection(PooledConnection conn) {
}
}
+ /**
+ * Fail hard in close() when there is uncommitted work. This is for debugging to find wrong code.
+ */
+ boolean enforceCleanClose() {
+ return enforceCleanClose;
+ }
+
/**
* Called by the PooledConnection themselves, returning themselves to the
* pool when they have been finished with.
@@ -537,7 +550,7 @@ void returnConnectionForceClose(PooledConnection pooledConnection) {
}
void removeClosedConnection(PooledConnection pooledConnection) {
- queue.returnPooledConnection(pooledConnection, true);
+ queue.returnPooledConnection(pooledConnection, true, false);
}
/**
@@ -548,7 +561,7 @@ private void returnTheConnection(PooledConnection pooledConnection, boolean forc
if (poolListener != null && !forceClose) {
poolListener.onBeforeReturnConnection(pooledConnection);
}
- queue.returnPooledConnection(pooledConnection, forceClose);
+ queue.returnPooledConnection(pooledConnection, forceClose, true);
if (forceClose) {
// Got a bad connection so check the pool
testConnection();
@@ -556,9 +569,9 @@ private void returnTheConnection(PooledConnection pooledConnection, boolean forc
}
void returnConnectionReset(PooledConnection pooledConnection) {
- queue.returnPooledConnection(pooledConnection, true);
+ queue.returnPooledConnection(pooledConnection, true, false);
Log.warn("Resetting DataSource on read-only failure [{0}]", name);
- reset();
+ reset(false);
}
/**
@@ -587,9 +600,9 @@ PooledConnection createConnectionForQueue(int connId) throws SQLException {
* Busy connections are closed when they are returned to the pool.
*
*/
- private void reset() {
+ private void reset(boolean logErrors) {
heartbeatPoolExhaustedCount = 0;
- queue.reset(leakTimeMinutes);
+ queue.reset(leakTimeMinutes, logErrors);
}
/**
diff --git a/ebean-datasource/src/main/java/io/ebean/datasource/pool/FreeConnectionBuffer.java b/ebean-datasource/src/main/java/io/ebean/datasource/pool/FreeConnectionBuffer.java
index a78ee39..8bbc37e 100644
--- a/ebean-datasource/src/main/java/io/ebean/datasource/pool/FreeConnectionBuffer.java
+++ b/ebean-datasource/src/main/java/io/ebean/datasource/pool/FreeConnectionBuffer.java
@@ -62,18 +62,22 @@ void closeAll(boolean logErrors) {
/**
* Trim any inactive connections that have not been used since usedSince.
+ *
+ * The connections are returned to close.
*/
- int trim(int minSize, long usedSince, long createdSince) {
- int trimCount = 0;
+ List trim(int minSize, long usedSince, long createdSince, boolean forced) {
+ List trimmedConnections = null;
ListIterator iterator = freeBuffer.listIterator(minSize);
while (iterator.hasNext()) {
PooledConnection pooledConnection = iterator.next();
- if (pooledConnection.shouldTrim(usedSince, createdSince)) {
+ if (pooledConnection.shouldTrim(usedSince, createdSince, forced)) {
iterator.remove();
- pooledConnection.closeConnectionFully(true);
- trimCount++;
+ if (trimmedConnections == null) {
+ trimmedConnections = new ArrayList<>();
+ }
+ trimmedConnections.add(pooledConnection);
}
}
- return trimCount;
+ return trimmedConnections;
}
}
diff --git a/ebean-datasource/src/main/java/io/ebean/datasource/pool/PooledConnection.java b/ebean-datasource/src/main/java/io/ebean/datasource/pool/PooledConnection.java
index 2e8185a..228399a 100644
--- a/ebean-datasource/src/main/java/io/ebean/datasource/pool/PooledConnection.java
+++ b/ebean-datasource/src/main/java/io/ebean/datasource/pool/PooledConnection.java
@@ -37,6 +37,11 @@ final class PooledConnection extends ConnectionDelegator {
*/
private static final String REASON_RESET = "reset";
+ /**
+ * Marker for when connection is closed due a forced trim.
+ */
+ private static final String REASON_FORCED = "forced";
+
/**
* Set when connection is idle in the pool. In general when in the pool the
* connection should not be modified.
@@ -219,7 +224,7 @@ String description() {
}
String fullDescription() {
- return "name[" + name + "] startTime[" + startUseTime() + "] busySeconds[" + busySeconds() + "] stackTrace[" + stackTraceAsString() + "] stmt[" + lastStatement() + "]";
+ return "name[" + name + "] startTime[" + startUseTime() + "] busySeconds[" + busySeconds() + "] stackTrace[" + stackTraceAsString(stackTrace) + "] stmt[" + lastStatement() + "]";
}
/**
@@ -363,7 +368,7 @@ private PreparedStatement prepareStatement(String sql, boolean useFlag, int flag
lastStatement = sql;
// try to get a matching cached PStmt from the cache.
ExtendedPreparedStatement pstmt = pstmtCache.remove(cacheKey);
- if (pstmt != null) {
+ if (pstmt != null && !pstmt.isClosed()) {
return pstmt.reset();
}
@@ -448,6 +453,11 @@ public void close() throws SQLException {
if (status == STATUS_IDLE) {
throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "close()");
}
+ boolean mayHaveUncommittedChanges = !autoCommit && !readOnly && status == STATUS_ACTIVE;
+ if (mayHaveUncommittedChanges && pool.enforceCleanClose()) {
+ pool.returnConnectionForceClose(this);
+ throw new AssertionError("Tried to close a dirty connection. See https://github.com/ebean-orm/ebean-datasource/issues/116 for details.");
+ }
if (hadErrors) {
if (failoverToReadOnly) {
pool.returnConnectionReset(this);
@@ -464,6 +474,11 @@ public void close() throws SQLException {
pool.removeClosedConnection(this);
return;
}
+ if (mayHaveUncommittedChanges) {
+ Log.warn("Tried to close a dirty connection at {0}. See https://github.com/ebean-orm/ebean-datasource/issues/116 for details.",
+ stackTraceAsString(Thread.currentThread().getStackTrace()));
+ connection.rollback();
+ }
// reset the autoCommit back if client code changed it
if (autoCommit != pool.isAutoCommit()) {
connection.setAutoCommit(pool.isAutoCommit());
@@ -529,7 +544,7 @@ boolean shouldTrimOnReturn(long lastResetTime, long maxAgeMillis) {
/**
* Return true if the connection has been idle for too long or is too old.
*/
- boolean shouldTrim(long usedSince, long createdSince) {
+ boolean shouldTrim(long usedSince, long createdSince, boolean forced) {
if (lastUseTime < usedSince) {
// been idle for too long so trim it
this.closeReason = REASON_IDLE;
@@ -540,6 +555,10 @@ boolean shouldTrim(long usedSince, long createdSince) {
this.closeReason = REASON_MAXAGE;
return true;
}
+ if (forced) {
+ this.closeReason = REASON_FORCED;
+ return true;
+ }
return false;
}
@@ -942,35 +961,23 @@ void setStackTrace(StackTraceElement[] stackTrace) {
/**
* Return the stackTrace as a String for logging purposes.
*/
- private String stackTraceAsString() {
- StackTraceElement[] stackTrace = stackTrace();
+ private String stackTraceAsString(StackTraceElement[] stackTrace) {
if (stackTrace == null) {
return "";
}
- return Arrays.toString(stackTrace);
- }
-
- /**
- * Return the full stack trace that got the connection from the pool. You
- * could use this if getCreatedByMethod() doesn't work for you.
- */
- private StackTraceElement[] stackTrace() {
- 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 (StackTraceElement stackTraceElement : stackTrace) {
- if (!include && includeMethodLine(stackTraceElement.toString())) {
+ if (!include && includeMethodLine(stackTraceElement.getClassName())) {
include = true;
}
if (include && filteredList.size() < maxStackTrace) {
filteredList.add(stackTraceElement);
}
}
- return filteredList.toArray(new StackTraceElement[0]);
+ return filteredList.toString();
}
}
diff --git a/ebean-datasource/src/main/java/io/ebean/datasource/pool/PooledConnectionQueue.java b/ebean-datasource/src/main/java/io/ebean/datasource/pool/PooledConnectionQueue.java
index 7a66c98..2f6dbd1 100644
--- a/ebean-datasource/src/main/java/io/ebean/datasource/pool/PooledConnectionQueue.java
+++ b/ebean-datasource/src/main/java/io/ebean/datasource/pool/PooledConnectionQueue.java
@@ -5,6 +5,7 @@
import io.ebean.datasource.pool.ConnectionPool.Status;
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;
@@ -87,12 +88,7 @@ private PoolStatus createStatus() {
@Override
public String toString() {
- lock.lock();
- try {
- return createStatus().toString();
- } finally {
- lock.unlock();
- }
+ return status(false).toString();
}
PoolStatus status(boolean reset) {
@@ -147,31 +143,40 @@ void ensureMinimumConnections() throws SQLException {
/**
* Return a PooledConnection.
*/
- void returnPooledConnection(PooledConnection c, boolean forceClose) {
+ void returnPooledConnection(PooledConnection c, boolean forceClose, boolean logErrors) {
+ boolean closeConnection = false;
lock.lock();
try {
if (!busyList.remove(c)) {
Log.error("Connection [{0}] not found in BusyList?", c);
}
if (forceClose || c.shouldTrimOnReturn(lastResetTime, maxAgeMillis)) {
- c.closeConnectionFully(false);
+ closeConnection = true; // close outside lock
} else {
freeList.add(c);
- notEmpty.signal();
+ notEmpty.signal(); // notify _obtainConnectionWaitLoop, that there are new connections in the freelist
}
} finally {
lock.unlock();
}
+ if (closeConnection) {
+ c.closeConnectionFully(logErrors);
+ }
}
- private PooledConnection extractFromFreeList() {
+ /**
+ * Returns one connection from the free list and put it into the busy list or null if no connections are free.
+ *
+ * @throws StaleConnectionException there was a connection in the freeList, but it is stale.
+ * The connection has to be closed outside the lock.
+ */
+ private PooledConnection extractFromFreeList() throws StaleConnectionException {
if (freeList.isEmpty()) {
return null;
}
final PooledConnection c = freeList.remove();
if (validateStaleMillis > 0 && staleEviction(c)) {
- c.closeConnectionFully(false);
- return null;
+ throw new StaleConnectionException(c);
}
registerBusyConnection(c);
return c;
@@ -192,15 +197,24 @@ private boolean stale(PooledConnection c) {
}
PooledConnection obtainConnection() throws SQLException {
- try {
- PooledConnection pc = _obtainConnection();
- pc.resetForUse();
- return pc;
-
- } catch (InterruptedException e) {
- // restore the interrupted status as we throw SQLException
- Thread.currentThread().interrupt();
- throw new SQLException("Interrupted getting connection from pool", e);
+ var start = System.nanoTime();
+ while (true) {
+ try {
+ PooledConnection pc = _obtainConnection();
+ pc.resetForUse();
+ final var elapsed = System.nanoTime() - start;
+ totalAcquireNanos += elapsed;
+ maxAcquireNanos = Math.max(maxAcquireNanos, elapsed);
+ return pc;
+
+ } catch (InterruptedException e) {
+ // restore the interrupted status as we throw SQLException
+ Thread.currentThread().interrupt();
+ throw new SQLException("Interrupted getting connection from pool", e);
+ } catch (StaleConnectionException e) {
+ e.getConnection().closeConnectionFully(true);
+ // try again...
+ }
}
}
@@ -215,8 +229,8 @@ private int registerBusyConnection(PooledConnection connection) {
return busySize;
}
- private PooledConnection _obtainConnection() throws InterruptedException, SQLException {
- var start = System.nanoTime();
+ private PooledConnection _obtainConnection() throws InterruptedException, SQLException, StaleConnectionException {
+
lock.lockInterruptibly();
try {
if (doingShutdown) {
@@ -246,9 +260,6 @@ private PooledConnection _obtainConnection() throws InterruptedException, SQLExc
waitingThreads--;
}
} finally {
- final var elapsed = System.nanoTime() - start;
- totalAcquireNanos += elapsed;
- maxAcquireNanos = Math.max(maxAcquireNanos, elapsed);
lock.unlock();
}
}
@@ -270,7 +281,7 @@ private PooledConnection createConnection() throws SQLException {
/**
* Got into a loop waiting for connections to be returned to the pool.
*/
- private PooledConnection _obtainConnectionWaitLoop() throws SQLException, InterruptedException {
+ private PooledConnection _obtainConnectionWaitLoop() throws SQLException, InterruptedException, StaleConnectionException {
long nanos = MILLIS_TIME_UNIT.toNanos(waitTimeoutMillis);
for (; ; ) {
if (nanos <= 0) {
@@ -307,7 +318,7 @@ PoolStatus shutdown(boolean closeBusyConnections) {
try {
doingShutdown = true;
PoolStatus status = createStatus();
- closeFreeConnections(true);
+ freeList.closeAll(true);
if (!closeBusyConnections) {
// connections close on return to pool
@@ -332,14 +343,14 @@ PoolStatus shutdown(boolean closeBusyConnections) {
*
* This is typically done when a database down event occurs.
*/
- void reset(long leakTimeMinutes) {
+ void reset(long leakTimeMinutes, boolean logErrors) {
lock.lock();
try {
PoolStatus status = createStatus();
Log.info("Resetting DataSource [{0}] {1}", name, status);
lastResetTime = System.currentTimeMillis();
- closeFreeConnections(false);
+ freeList.closeAll(logErrors);
closeBusyConnections(leakTimeMinutes);
String busyInfo = getBusyConnectionInformation();
@@ -353,17 +364,12 @@ void reset(long leakTimeMinutes) {
}
void trim(long maxInactiveMillis, long maxAgeMillis) {
- lock.lock();
- try {
- if (trimInactiveConnections(maxInactiveMillis, maxAgeMillis)) {
- try {
- ensureMinimumConnections();
- } catch (SQLException e) {
- Log.error("Error trying to ensure minimum connections", e);
- }
+ if (trimInactiveConnections(maxInactiveMillis, maxAgeMillis)) {
+ try {
+ ensureMinimumConnections();
+ } catch (SQLException e) {
+ Log.error("Error trying to ensure minimum connections", e);
}
- } finally {
- lock.unlock();
}
}
@@ -372,33 +378,57 @@ void trim(long maxInactiveMillis, long maxAgeMillis) {
*/
private boolean trimInactiveConnections(long maxInactiveMillis, long maxAgeMillis) {
final long createdSince = (maxAgeMillis == 0) ? 0 : System.currentTimeMillis() - maxAgeMillis;
- final int trimmedCount;
- if (freeList.size() > minSize) {
- // trim on maxInactive and maxAge
- long usedSince = System.currentTimeMillis() - maxInactiveMillis;
- trimmedCount = freeList.trim(minSize, usedSince, createdSince);
- } else if (createdSince > 0) {
- // trim only on maxAge
- trimmedCount = freeList.trim(0, createdSince, createdSince);
- } else {
- trimmedCount = 0;
+ final List trimmed;
+ lock.lock();
+ try {
+ if (freeList.size() > minSize) {
+ // trim on maxInactive and maxAge
+ long usedSince = System.currentTimeMillis() - maxInactiveMillis;
+ trimmed = freeList.trim(minSize, usedSince, createdSince, false);
+ } else if (createdSince > 0) {
+ // trim only on maxAge
+ trimmed = freeList.trim(0, createdSince, createdSince, false);
+ } else {
+ trimmed = null;
+ }
+ } finally {
+ lock.unlock();
}
- if (trimmedCount > 0 && Log.isLoggable(DEBUG)) {
- Log.debug("DataSource [{0}] trimmed [{1}] inactive connections. New size[{2}]", name, trimmedCount, totalConnections());
+ if (trimmed != null) {
+ if (Log.isLoggable(DEBUG)) {
+ Log.debug("DataSource [{0}] trimmed [{1}] inactive connections. New size[{2}]", name, trimmed.size(), totalConnections());
+ }
+ for (PooledConnection pc : trimmed) {
+ pc.closeConnectionFully(true);
+ }
+ return freeList.size() < minSize;
}
- return trimmedCount > 0 && freeList.size() < minSize;
+ return false;
}
- /**
- * Close all the connections that are in the free list.
- */
- private void closeFreeConnections(boolean logErrors) {
+ int forceTrim(int trimCount) {
+ List trimmed = null;
lock.lock();
try {
- freeList.closeAll(logErrors);
+ int trimStart = freeList.size() - trimCount;
+ trimStart = Math.max(trimStart, 0);
+
+ if (freeList.size() > trimStart) {
+ trimmed = freeList.trim(trimStart, 0, 0, true);
+ }
} finally {
lock.unlock();
}
+ if (trimmed != null) {
+ if (Log.isLoggable(DEBUG)) {
+ Log.debug("DataSource [{0}] forced trimmed [{1}] connections. New size[{2}]", name, trimmed.size(), totalConnections());
+ }
+ for (PooledConnection pc : trimmed) {
+ pc.closeConnectionFully(true);
+ }
+ return trimmed.size();
+ }
+ return 0;
}
/**
@@ -412,12 +442,23 @@ private void closeFreeConnections(boolean logErrors) {
* closed and put back into the pool.
*/
void closeBusyConnections(long leakTimeMinutes) {
+ List busyConnections;
lock.lock();
try {
- busyList.closeBusyConnections(leakTimeMinutes);
+ busyConnections = busyList.removeBusyConnections(leakTimeMinutes);
} finally {
lock.unlock();
}
+ if (busyConnections != null) {
+ for (PooledConnection pc : busyConnections) {
+ try {
+ Log.warn("DataSource closing busy connection? {0}", pc.fullDescription());
+ pc.closeConnectionFully(true);
+ } catch (Exception ex) {
+ Log.error("Error when closing potentially leaked connection " + pc.description(), ex);
+ }
+ }
+ }
}
String getBusyConnectionInformation() {
diff --git a/ebean-datasource/src/main/java/io/ebean/datasource/pool/StaleConnectionException.java b/ebean-datasource/src/main/java/io/ebean/datasource/pool/StaleConnectionException.java
new file mode 100644
index 0000000..4502179
--- /dev/null
+++ b/ebean-datasource/src/main/java/io/ebean/datasource/pool/StaleConnectionException.java
@@ -0,0 +1,18 @@
+package io.ebean.datasource.pool;
+
+/**
+ * Used to return a stale connection, so that it can be closed outside a lock.
+ *
+ * @author Roland Praml, Foconis Analytics GmbH
+ */
+class StaleConnectionException extends Exception {
+ private final PooledConnection connection;
+
+ StaleConnectionException(PooledConnection connection) {
+ this.connection = connection;
+ }
+
+ public PooledConnection getConnection() {
+ return connection;
+ }
+}
diff --git a/ebean-datasource/src/test/java/io/ebean/datasource/pool/ConnectionPoolCloseTest.java b/ebean-datasource/src/test/java/io/ebean/datasource/pool/ConnectionPoolCloseTest.java
new file mode 100644
index 0000000..7b37969
--- /dev/null
+++ b/ebean-datasource/src/test/java/io/ebean/datasource/pool/ConnectionPoolCloseTest.java
@@ -0,0 +1,175 @@
+package io.ebean.datasource.pool;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.read.ListAppender;
+import io.ebean.datasource.DataSourceBuilder;
+import io.ebean.datasource.DataSourcePool;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.LoggerFactory;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.SQLSyntaxErrorException;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests, if connections are clean when closing them.
+ *
+ * Test must run with "do not use module path" option in IntelliJ.
+ *
+ * See also https://github.com/ebean-orm/ebean-datasource/issues/116 for more details
+ */
+class ConnectionPoolCloseTest {
+
+ private static ListAppender logCapture = new ListAppender<>();
+
+ private static DataSourcePool pool = createPool(false, false);
+ private static DataSourcePool poolRo = createPool(false, true);
+ private static DataSourcePool poolEnforce = createPool(true, false);
+ private static DataSourcePool poolEnforceRo = createPool(true, true);
+
+
+ @BeforeAll
+ static void before() {
+ // attach logback appender to capture log messages
+ ((ch.qos.logback.classic.Logger) LoggerFactory.getLogger("io.ebean.datasource")).addAppender(logCapture);
+ logCapture.start();
+ }
+
+ @BeforeEach
+ void beforeEach() {
+ logCapture.list.clear();
+ }
+
+ @AfterAll
+ static void after() {
+ logCapture.stop();
+ pool.shutdown();
+ poolRo.shutdown();
+ poolEnforce.shutdown();
+ poolEnforceRo.shutdown();
+ }
+
+ @Test
+ void testNoCommitOrRollback() throws SQLException {
+
+ doNoCommitOrRollback(pool);
+ assertThat(getAndResetLogWarinings())
+ .hasSize(1)
+ .first().asString().startsWith("[WARN] Tried to close a dirty connection at");
+
+ assertThatThrownBy(() -> doNoCommitOrRollback(poolEnforce)).isInstanceOf(AssertionError.class);
+
+ doNoCommitOrRollback(poolRo);
+ assertThat(getAndResetLogWarinings()).isEmpty();
+
+ doNoCommitOrRollback(poolEnforceRo);
+ assertThat(getAndResetLogWarinings()).isEmpty();
+
+ }
+
+ @Test
+ void testCommit() throws SQLException {
+ doCommit(pool);
+ doCommit(poolRo);
+ doCommit(poolEnforce);
+ doCommit(poolEnforceRo);
+ assertThat(getAndResetLogWarinings()).isEmpty();
+ }
+
+ @Test
+ void testRollback() throws SQLException {
+ doRollback(pool);
+ doRollback(poolRo);
+ doRollback(poolEnforce);
+ doRollback(poolEnforceRo);
+ assertThat(getAndResetLogWarinings()).isEmpty();
+ }
+
+ @Test
+ void testExecuteValidSql() throws SQLException {
+ doExecute(pool, "SELECT 1");
+ doExecute(poolRo, "SELECT 1");
+ doExecute(poolEnforce, "SELECT 1");
+ doExecute(poolEnforceRo, "SELECT 1");
+ assertThat(getAndResetLogWarinings()).isEmpty();
+ }
+
+ @Test
+ void testExecuteInvalidSql() throws SQLException {
+ assertThatThrownBy(() -> doExecute(pool, "invalid query"))
+ .isInstanceOf(SQLSyntaxErrorException.class)
+ .hasNoSuppressedExceptions();
+ // (un)fortunately, we will log a missing rollback, when exception in try-with-resourches is thrown.
+ assertThat(getAndResetLogWarinings())
+ .hasSize(1)
+ .first().asString().startsWith("[WARN] Tried to close a dirty connection at");
+
+ assertThatThrownBy(() -> doExecute(poolEnforce, "invalid query"))
+ .isInstanceOf(SQLSyntaxErrorException.class)
+ .hasSuppressedException(new AssertionError("Tried to close a dirty connection. See https://github.com/ebean-orm/ebean-datasource/issues/116 for details."));
+
+ assertThatThrownBy(() -> doExecute(poolRo, "invalid query"))
+ .isInstanceOf(SQLSyntaxErrorException.class)
+ .hasNoSuppressedExceptions();
+
+ assertThatThrownBy(() -> doExecute(poolEnforceRo, "invalid query"))
+ .isInstanceOf(SQLSyntaxErrorException.class)
+ .hasNoSuppressedExceptions();
+ assertThat(getAndResetLogWarinings()).isEmpty();
+ }
+
+ private static void doNoCommitOrRollback(DataSourcePool pool) throws SQLException {
+ try (Connection connection = pool.getConnection()) {
+ // we do nothing here.
+ }
+ }
+
+ private static void doCommit(DataSourcePool pool) throws SQLException {
+ try (Connection connection = pool.getConnection()) {
+ connection.commit();
+ }
+ }
+
+ private static void doRollback(DataSourcePool pool) throws SQLException {
+ try (Connection connection = pool.getConnection()) {
+ connection.rollback();
+ }
+ }
+
+ private static void doExecute(DataSourcePool pool, String query) throws SQLException {
+ try (Connection connection = pool.getConnection()) {
+ connection.createStatement().execute(query);
+ connection.commit();
+ }
+ }
+
+ private static List getAndResetLogWarinings() {
+ List warnings = logCapture.list.stream().filter(e -> e.getLevel().levelInt >= Level.WARN_INT).collect(Collectors.toList());
+ logCapture.list.clear();
+ return warnings;
+ }
+
+ private static DataSourcePool createPool(boolean enforceCleanClose, boolean readOnly) {
+ DataSourcePool ret = DataSourceBuilder.create()
+ .url("jdbc:h2:mem:tests")
+ .username("sa")
+ .password("")
+ .minConnections(2)
+ .maxConnections(4)
+ .enforceCleanClose(enforceCleanClose)
+ .readOnly(readOnly)
+ .build();
+ // clear capturer after create
+ logCapture.list.clear();
+ return ret;
+ }
+}
diff --git a/ebean-datasource/src/test/java/io/ebean/datasource/test/Db2Test.java b/ebean-datasource/src/test/java/io/ebean/datasource/test/Db2Test.java
new file mode 100644
index 0000000..4fb68d5
--- /dev/null
+++ b/ebean-datasource/src/test/java/io/ebean/datasource/test/Db2Test.java
@@ -0,0 +1,163 @@
+package io.ebean.datasource.test;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.read.ListAppender;
+import com.ibm.db2.jcc.DB2Connection;
+import io.avaje.applog.AppLog;
+import io.ebean.datasource.DataSourceBuilder;
+import io.ebean.datasource.DataSourcePool;
+import io.ebean.datasource.DataSourcePoolListener;
+import io.ebean.test.containers.Db2Container;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.slf4j.LoggerFactory;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * DB2 has a strange, but API-compliant behaviour, when a connection is in a dirty state and neither committed nor rolled back.
+ *
+ * By default, a DB2-connection cannot be closed if it is in a unit of work (=transaction) and an exception is thrown.
+ *
+ * This can be controlled with the "connectionCloseWithInFlightTransaction" parameter
+ * https://www.ibm.com/docs/en/db2/11.5?topic=pdsdjs-common-data-server-driver-jdbc-sqlj-properties-all-database-products
+ *
+ * There are several cases, when there is an open unit of work:
+ *
+ * - forget commit/rollback before closing the connection, because an exception occurs
+ * - calling connection.getSchema() starts a new UOW (because it internally executes a query)
+ *
+ *
+ * See also https://github.com/ebean-orm/ebean-datasource/issues/116 for more details
+ */
+@Disabled("DB2 container start is slow - run manually")
+class Db2Test {
+
+ private static Db2Container container;
+
+ private static ListAppender logCapture = new ListAppender<>();
+
+ @BeforeAll
+ static void before() {
+ container = Db2Container.builder("11.5.6.0a")
+ .dbName("unit")
+ .user("unit")
+ .password("unit")
+ // to change collation, charset and other parameters like pagesize:
+ .configOptions("USING CODESET UTF-8 TERRITORY DE COLLATE USING IDENTITY PAGESIZE 32768")
+ .configOptions("USING STRING_UNITS CODEUNITS32")
+ .build();
+
+ container.start();
+
+ // attach logback appender to capture log messages
+ ((ch.qos.logback.classic.Logger) LoggerFactory.getLogger("io.ebean.datasource")).addAppender(logCapture);
+ logCapture.start();
+ }
+
+ @AfterAll
+ static void after() {
+ logCapture.stop();
+ }
+
+ @Test
+ void testFalseFriendRollback() throws SQLException {
+
+ DataSourcePool pool = getPool();
+
+ logCapture.list.clear();
+ try {
+ try (Connection connection = pool.getConnection()) {
+ // we do a rollback here
+ connection.rollback();
+ connection.getSchema(); // will re-open a new UOW
+ }
+ } finally {
+ pool.shutdown();
+ }
+
+ assertThat(logCapture.list.stream().filter(e -> e.getLevel().levelInt >= Level.WARN_INT))
+ .extracting(ILoggingEvent::toString).containsExactly("[WARN] There is a DB2 UOW open!");
+ }
+
+ @Test
+ void testErrorOccured() throws SQLException {
+
+ DataSourcePool pool = getPool();
+
+ logCapture.list.clear();
+ try {
+ try (Connection connection = pool.getConnection()) {
+ try (PreparedStatement statement = connection.prepareStatement("i am invalid")) {
+ statement.execute();
+ }
+ connection.commit(); // we will not get here
+ } catch (SQLException e) {
+ // expected
+ }
+ } finally {
+ pool.shutdown();
+ }
+
+ assertThat(logCapture.list.stream().filter(e -> e.getLevel().levelInt >= Level.WARN_INT))
+ .hasSize(1)
+ .first().asString().startsWith("[WARN] Tried to close a dirty connection at");
+ }
+
+
+ @Test
+ void testProperShutdownWithMissingListener() throws SQLException {
+
+ DataSourcePool pool = DataSourceBuilder.create()
+ .url(container.jdbcUrl())
+ .username("unit")
+ .password("unit")
+ .ownerUsername("unit")
+ .ownerPassword("unit")
+ .build();
+
+ logCapture.list.clear();
+ try {
+ try (Connection connection = pool.getConnection()) {
+ // we do a rollback here
+ connection.rollback();
+ connection.getSchema();
+ }
+ } finally {
+ pool.shutdown();
+ }
+ // when shuttind down the pool, a rollback will be performed on all connections.
+ assertThat(logCapture.list.stream().filter(e -> e.getLevel().levelInt >= Level.WARN_INT)).isEmpty();
+ }
+
+ private static DataSourcePool getPool() {
+ return DataSourceBuilder.create()
+ .url(container.jdbcUrl())
+ .username("unit")
+ .password("unit")
+ .ownerUsername("unit")
+ .ownerPassword("unit")
+ .listener(new DataSourcePoolListener() {
+ @Override
+ public void onBeforeReturnConnection(Connection connection) {
+ try {
+ DB2Connection db2conn = connection.unwrap(DB2Connection.class);
+ if (db2conn.isInDB2UnitOfWork()) {
+ AppLog.getLogger("io.ebean.datasource").log(System.Logger.Level.WARNING, "There is a DB2 UOW open!");
+ db2conn.rollback();
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ })
+ .build();
+ }
+}
diff --git a/pom.xml b/pom.xml
index 46744bf..5df0152 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,7 +9,7 @@
io.ebean
ebean-datasource-parent
- 9.3-SNAPSHOT
+ 9.3-FOC4-SNAPSHOT
pom