Skip to content

Commit 7da5f4b

Browse files
committed
Fix leak detection wrongly closing actively-in-use connections during pool reset
Problem In production we saw this warning on a read-only pool: Connection [name[...] startTime[...] busySeconds[0] createdBy[null] stmt[select ...]] not found in BusyList? busySeconds[0] shows the connection had just been checked out — it was not a leak, yet it had already been removed from the busy list. Root cause BusyConnectionBuffer.closeBusyConnections(leakTimeMinutes) decided whether a busy connection was a leak using lastUsedTime() — the time the connection was last returned to the pool (intended for trimming idle/free connections). The correct field is startUseTime() (the time the connection was checked out for its current use), which is even documented as "Used to detect busy connections that could be leaks" but was unused. As a result, when reset() runs (read-only failover or a DB up/down heartbeat event), a connection that had sat idle in the free list longer than leakTimeMinutes and was then freshly checked out would be misidentified as a leak and force-closed mid-query. When the owning thread finished and called close(), its busy slot was already gone → "not found in BusyList?". Fix - closeBusyConnections now uses startUseTime() (continuous checkout duration) instead of lastUsedTime(). Connections in genuine active use are left to be closed normally when returned, as the reset() Javadoc already intends. - Made PooledConnection.startUseTime() package-private. - Removed a stray System.out.println left in closeBusyConnection. - Moved the test-only pool == null guard above a TRACE log that dereferenced a null pstmtCache. Tests Added BusyConnectionBufferTest.closeBusyConnections_onlyClosesLeaks_notActiveConnections, which fails on the old behaviour and passes with the fix. Full module suite green (62 passed).
1 parent 5e3ced8 commit 7da5f4b

3 files changed

Lines changed: 34 additions & 9 deletions

File tree

ebean-datasource/src/main/java/io/ebean/datasource/pool/BusyConnectionBuffer.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,11 @@ void closeBusyConnections(long leakTimeMinutes) {
8989
Log.debug("Closing busy connections using leakTimeMinutes {0}", leakTimeMinutes);
9090
for (int i = 0; i < slots.length; i++) {
9191
if (slots[i] != null) {
92-
//tmp.add(slots[i]);
9392
PooledConnection pc = slots[i];
9493
//noinspection StatementWithEmptyBody
95-
if (pc.lastUsedTime() > olderThanTime) {
96-
// PooledConnection has been used recently or
97-
// expected to be longRunning so not closing...
94+
if (pc.startUseTime() > olderThanTime) {
95+
// PooledConnection was checked out recently so it is in active
96+
// use (not a leak) - leave it to be closed when returned to the pool
9897
} else {
9998
slots[i] = null;
10099
--size;
@@ -107,7 +106,6 @@ void closeBusyConnections(long leakTimeMinutes) {
107106
private void closeBusyConnection(PooledConnection pc) {
108107
try {
109108
Log.warn("DataSource closing busy connection? {0}", pc.fullDescription());
110-
System.out.println("CLOSING busy connection: " + pc.fullDescription());
111109
pc.closeConnectionFully(false);
112110
} catch (Exception ex) {
113111
Log.error("Error when closing potentially leaked connection " + pc.description(), ex);

ebean-datasource/src/main/java/io/ebean/datasource/pool/PooledConnection.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -232,12 +232,12 @@ String fullDescription() {
232232
* @param logErrors if false then don't log errors when closing
233233
*/
234234
void closeConnectionFully(boolean logErrors) {
235-
if (Log.isLoggable(System.Logger.Level.TRACE)) {
236-
Log.trace("Closing Connection[{0}] reason[{1}], pstmtStats: {2}", name, closeReason, pstmtCache.description());
237-
}
238235
if (pool == null) {
239236
return; // this can happen in tests only.
240237
}
238+
if (Log.isLoggable(System.Logger.Level.TRACE)) {
239+
Log.trace("Closing Connection[{0}] reason[{1}], pstmtStats: {2}", name, closeReason, pstmtCache.description());
240+
}
241241
pool.pstmtCacheMetrics(pstmtCache);
242242
pool.closeConnectionFullyAsync(this, logErrors);
243243
}
@@ -585,7 +585,7 @@ boolean shouldTrim(long usedSince, long createdSince) {
585585
* <p>
586586
* Used to detect busy connections that could be leaks.
587587
*/
588-
private long startUseTime() {
588+
long startUseTime() {
589589
return startUseTime;
590590
}
591591

ebean-datasource/src/test/java/io/ebean/datasource/pool/BusyConnectionBufferTest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import org.junit.jupiter.api.Test;
44

55
import static org.junit.jupiter.api.Assertions.assertEquals;
6+
import static org.junit.jupiter.api.Assertions.assertFalse;
7+
import static org.junit.jupiter.api.Assertions.assertTrue;
68

79
public class BusyConnectionBufferTest {
810

@@ -96,4 +98,29 @@ public void test_rotate() {
9698

9799
}
98100

101+
@Test
102+
public void closeBusyConnections_onlyClosesLeaks_notActiveConnections() {
103+
BusyConnectionBuffer b = new BusyConnectionBuffer(4, 4);
104+
105+
// a connection that was checked out long ago and never returned (a leak)
106+
PooledConnection leaked = new PooledConnection("leaked");
107+
// a connection that sat idle in the free list for a while then was just
108+
// checked out - it is in active use, not a leak (startUseTime is recent)
109+
PooledConnection active = new PooledConnection("active");
110+
active.resetForUse();
111+
112+
b.add(leaked);
113+
b.add(active);
114+
assertEquals(2, b.size());
115+
116+
// close connections considered leaked using a 1 minute leak time
117+
b.closeBusyConnections(1);
118+
119+
// the leaked connection is removed, the active connection is retained
120+
assertEquals(1, b.size());
121+
// active is still held in its slot, leaked is gone
122+
assertTrue(b.remove(active));
123+
assertFalse(b.remove(leaked));
124+
}
125+
99126
}

0 commit comments

Comments
 (0)