Skip to content

Commit 1fa4082

Browse files
committed
Added warm-up connection tracking and fixed race condition in the ConnectionPool class
1 parent cb25478 commit 1fa4082

File tree

1 file changed

+75
-32
lines changed

1 file changed

+75
-32
lines changed

src/javaxt/sql/ConnectionPool.java

Lines changed: 75 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -457,16 +457,13 @@ private Connection getRecycledConnection() throws SQLException {
457457

458458
PooledConnection pconn = wrapper.pooledConnection;
459459

460-
// Check if this is a warm-up connection that was never opened
461-
boolean isWarmupConnection =
462-
(wrapper.lastUsedTime == wrapper.createdTime && wrapper.connection.getConnection() == null);
463-
464460
// Check if the connection was recently recycled (< 5 seconds)
465461
boolean connectionIsStale = (System.currentTimeMillis() - wrapper.lastUsedTime) > 5000;
466462

467463
// Skip validation for very recently recycled connections (< 5 seconds)
468464
// This dramatically improves performance for high-frequency connection reuse scenarios
469-
if (!isWarmupConnection && connectionIsStale) {
465+
// Also skip validation for warm-up connections (they're brand new)
466+
if (!wrapper.isWarmup && connectionIsStale) {
470467

471468
// Standard validation path for older connections
472469
boolean needsValidation = wrapper.isExpired(connectionMaxAgeMs) ||
@@ -490,10 +487,6 @@ private Connection getRecycledConnection() throws SQLException {
490487
}
491488
}
492489

493-
// Connection is valid, update its usage time
494-
wrapper = wrapper.markUsed();
495-
connectionWrappers.put(pconn, wrapper);
496-
497490
try {
498491
connectionInTransition = pconn;
499492
activeConnections.incrementAndGet(); // Increment before getting connection
@@ -505,15 +498,31 @@ private Connection getRecycledConnection() throws SQLException {
505498
// This updates the underlying connection reference without creating a new wrapper object
506499
wrapper.connection.open(rawConn, database);
507500

508-
return wrapper.connection; // Return reused wrapper with fresh connection
501+
// Connection successfully acquired! Now update the wrapper state
502+
// IMPORTANT: Only update wrapper and map AFTER successfully acquiring the connection
503+
// This prevents race conditions where the wrapper state changes before the connection is ready
504+
PooledConnectionWrapper updatedWrapper = wrapper.markUsed();
505+
connectionWrappers.put(pconn, updatedWrapper);
506+
507+
return updatedWrapper.connection; // Return reused wrapper with fresh connection
509508
} catch (SQLException e) {
510509
connectionInTransition = null;
511-
// Failed, decrement the activeConnections counter we just incremented
510+
// Failed to acquire connection
511+
512+
// Decrement the activeConnections counter we just incremented
512513
activeConnections.decrementAndGet();
513-
// Dispose the wrapper
514-
connectionWrappers.remove(pconn);
514+
515+
// IMPORTANT: Set doPurgeConnection BEFORE closing to prevent double-decrement
516+
// This ensures that if pconn.close() triggers connectionClosed event,
517+
// it will call disposeConnection() instead of recycleConnection()
515518
doPurgeConnection.set(true);
519+
516520
try {
521+
// Don't remove the wrapper if it's a warmup connection - it needs to stay in the map
522+
// Only remove if it was updated to non-warmup
523+
// Actually, remove it to be safe - disposeConnection will handle it
524+
connectionWrappers.remove(pconn);
525+
517526
pconn.removeConnectionEventListener(poolConnectionEventListener);
518527
pconn.close();
519528
} catch (SQLException ex) {
@@ -549,8 +558,7 @@ private Connection createNewConnection() throws SQLException {
549558
connection.open(rawConn, database);
550559

551560
// Store the pre-wrapped connection for reuse
552-
PooledConnectionWrapper wrapper = new PooledConnectionWrapper(connection, pconn);
553-
connectionWrappers.put(pconn, wrapper);
561+
connectionWrappers.put(pconn, new PooledConnectionWrapper(connection, pconn, false));
554562

555563
// totalConnections was already incremented in acquireConnection
556564
return connection;
@@ -703,7 +711,7 @@ private void performHealthCheck() {
703711
// Create unopened wrapper for warm-up
704712
// The wrapper will be opened when first acquired from the pool
705713
Connection connection = new Connection();
706-
PooledConnectionWrapper w = new PooledConnectionWrapper(connection, pconn);
714+
PooledConnectionWrapper w = new PooledConnectionWrapper(connection, pconn, true); // Mark as warmup
707715
connectionWrappers.put(pconn, w);
708716

709717
// Add directly to recycled queue (skip the active state for warm-up)
@@ -814,14 +822,48 @@ private void recycleConnection (PooledConnection pconn) {
814822
return;
815823
}
816824

825+
// Get the existing wrapper
826+
PooledConnectionWrapper wrapper = connectionWrappers.get(pconn);
827+
828+
// Check if this connection is already in the recycled queue (double-close protection)
829+
if (wrapper != null) {
830+
for (PooledConnectionWrapper w : recycledConnections) {
831+
if (w.pooledConnection == pconn) {
832+
log("Warning: Connection already recycled, ignoring duplicate close");
833+
return;
834+
}
835+
}
836+
837+
// Additional safety check: verify the wrapper's connection is actually closed
838+
// If the javaxt.sql.Connection wrapper is still open, this indicates a problem
839+
if (wrapper.connection != null && wrapper.connection.isOpen()) {
840+
log("Warning: Recycle called but javaxt.sql.Connection wrapper is still open - disposing instead");
841+
disposeConnection(pconn);
842+
return;
843+
}
844+
}
845+
817846
// Use atomic decrement to avoid TOCTOU race condition
818847
int prev = activeConnections.decrementAndGet();
848+
819849
if (prev < 0) {
820-
throw new AssertionError("Active connections count went negative");
850+
// This is a double-close - the connection was already recycled
851+
// Don't restore the counter; instead increment it back and ignore this duplicate close
852+
activeConnections.incrementAndGet();
853+
854+
String wrapperInfo = wrapper != null ?
855+
"(isWarmup=" + wrapper.isWarmup + ", created=" + wrapper.createdTime + ", lastUsed=" + wrapper.lastUsedTime + ")" :
856+
"(wrapper=null)";
857+
858+
log("WARNING: Detected double-close (activeConnections went negative). " +
859+
"Ignoring duplicate recycle. This indicates the same javaxt.sql.Connection object was closed twice. " +
860+
"wrapper=" + wrapperInfo);
861+
862+
// Don't process this recycle - the connection was already recycled on the first close
863+
return;
821864
}
822865

823866
// Get the existing wrapper and update its usage time
824-
PooledConnectionWrapper wrapper = connectionWrappers.get(pconn);
825867
if (wrapper != null) {
826868
// Update the wrapper with current usage time and add to recycled connections
827869
// The javaxt.sql.Connection wrapper is reused - no new object creation!
@@ -897,10 +939,13 @@ private void disposeConnection (PooledConnection pconn) {
897939
//** log
898940
//**************************************************************************
899941
private void log(String msg) {
942+
if (true) return;
900943
String s = "ConnectionPool: "+msg;
901944
try {
902945
if (logWriter == null) {
903-
//System.err.println(s);
946+
if (msg.startsWith("WARNING") || msg.startsWith("Error")) {
947+
System.err.println(s);
948+
}
904949
}
905950
else {
906951
logWriter.println(s);
@@ -955,23 +1000,18 @@ public void connectionErrorOccurred (ConnectionEvent event) {
9551000
/** Wrapper class to track connection metadata
9561001
*/
9571002
private static class PooledConnectionWrapper {
958-
final Connection connection;
959-
final PooledConnection pooledConnection;
960-
final long createdTime;
961-
final long lastUsedTime;
1003+
private final Connection connection;
1004+
private final PooledConnection pooledConnection;
1005+
private long createdTime;
1006+
private long lastUsedTime;
1007+
private final boolean isWarmup;
9621008

963-
PooledConnectionWrapper(Connection connection, PooledConnection pooledConnection) {
1009+
PooledConnectionWrapper(Connection connection, PooledConnection pooledConnection, boolean isWarmup) {
9641010
this.connection = connection;
9651011
this.pooledConnection = pooledConnection;
9661012
this.createdTime = System.currentTimeMillis();
9671013
this.lastUsedTime = System.currentTimeMillis();
968-
}
969-
970-
PooledConnectionWrapper(Connection connection, PooledConnection pooledConnection, long createdTime, long lastUsedTime) {
971-
this.connection = connection;
972-
this.pooledConnection = pooledConnection;
973-
this.createdTime = createdTime;
974-
this.lastUsedTime = lastUsedTime;
1014+
this.isWarmup = isWarmup;
9751015
}
9761016

9771017
boolean isIdle(long idleTimeoutMs) {
@@ -983,7 +1023,10 @@ boolean isExpired(long maxAgeMs) {
9831023
}
9841024

9851025
PooledConnectionWrapper markUsed() {
986-
return new PooledConnectionWrapper(connection, pooledConnection, createdTime, System.currentTimeMillis());
1026+
PooledConnectionWrapper wrapper = new PooledConnectionWrapper(connection, pooledConnection, false);
1027+
wrapper.createdTime = createdTime;
1028+
wrapper.lastUsedTime = System.currentTimeMillis();
1029+
return wrapper;
9871030
}
9881031
}
9891032

0 commit comments

Comments
 (0)