@@ -1319,3 +1319,88 @@ func TestCloseDuringWaitForConn(t *testing.T) {
13191319 require .EqualValues (t , 0 , state .open .Load ())
13201320 }
13211321}
1322+
1323+ // TestIdleTimeoutConnectionLeak checks for leaked connections after idle timeout
1324+ func TestIdleTimeoutConnectionLeak (t * testing.T ) {
1325+ var state TestState
1326+
1327+ // Slow connection creation to ensure idle timeout happens during reopening
1328+ state .chaos .delayConnect = 300 * time .Millisecond
1329+
1330+ p := NewPool (& Config [* TestConn ]{
1331+ Capacity : 2 ,
1332+ IdleTimeout : 50 * time .Millisecond ,
1333+ LogWait : state .LogWait ,
1334+ }).Open (newConnector (& state ), nil )
1335+
1336+ getCtx , cancel := context .WithTimeout (t .Context (), 500 * time .Millisecond )
1337+ defer cancel ()
1338+
1339+ // Get and return two connections
1340+ conn1 , err := p .Get (getCtx , nil )
1341+ require .NoError (t , err )
1342+
1343+ conn2 , err := p .Get (getCtx , nil )
1344+ require .NoError (t , err )
1345+
1346+ p .put (conn1 )
1347+ p .put (conn2 )
1348+
1349+ // At this point: Active=2, InUse=0, Available=2
1350+ require .EqualValues (t , 2 , p .Active ())
1351+ require .EqualValues (t , 0 , p .InUse ())
1352+ require .EqualValues (t , 2 , p .Available ())
1353+
1354+ // Wait for idle timeout to kick in and start expiring connections
1355+ require .EventuallyWithT (t , func (c * assert.CollectT ) {
1356+ // Check the actual number of currently open connections
1357+ assert .Equal (c , int64 (2 ), state .open .Load ())
1358+ // Check the total number of closed connections
1359+ assert .Equal (c , int64 (1 ), state .close .Load ())
1360+ }, 100 * time .Millisecond , 10 * time .Millisecond )
1361+
1362+ // At this point, the idle timeout worker has expired the connections
1363+ // and is trying to reopen them (which takes 300ms due to delayConnect)
1364+
1365+ // Try to get connections while they're being reopened
1366+ // This should trigger the bug where connections get discarded
1367+ for i := 0 ; i < 2 ; i ++ {
1368+ getCtx , cancel := context .WithTimeout (t .Context (), 50 * time .Millisecond )
1369+ defer cancel ()
1370+
1371+ conn , err := p .Get (getCtx , nil )
1372+ require .NoError (t , err )
1373+
1374+ p .put (conn )
1375+ }
1376+
1377+ // Wait a moment for all reopening to complete
1378+ require .EventuallyWithT (t , func (c * assert.CollectT ) {
1379+ // Check the actual number of currently open connections
1380+ require .Equal (c , int64 (2 ), state .open .Load ())
1381+ // Check the total number of closed connections
1382+ require .Equal (c , int64 (2 ), state .close .Load ())
1383+ }, 400 * time .Millisecond , 10 * time .Millisecond )
1384+
1385+ // Check the pool state
1386+ assert .Equal (t , int64 (2 ), p .Active ())
1387+ assert .Equal (t , int64 (0 ), p .InUse ())
1388+ assert .Equal (t , int64 (2 ), p .Available ())
1389+ assert .Equal (t , int64 (2 ), p .Metrics .IdleClosed ())
1390+
1391+ // Try to close the pool - if there are leaked connections, this will timeout
1392+ closeCtx , cancel := context .WithTimeout (t .Context (), 500 * time .Millisecond )
1393+ defer cancel ()
1394+
1395+ err = p .CloseWithContext (closeCtx )
1396+ require .NoError (t , err )
1397+
1398+ // Pool should be completely closed now
1399+ assert .Equal (t , int64 (0 ), p .Active ())
1400+ assert .Equal (t , int64 (0 ), p .InUse ())
1401+ assert .Equal (t , int64 (0 ), p .Available ())
1402+ assert .Equal (t , int64 (2 ), p .Metrics .IdleClosed ())
1403+
1404+ assert .Equal (t , int64 (0 ), state .open .Load ())
1405+ assert .Equal (t , int64 (4 ), state .close .Load ())
1406+ }
0 commit comments