@@ -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