@@ -2134,3 +2134,90 @@ func getSessionsPerChannel(sp *sessionPool) map[string]int {
21342134 }
21352135 return sessionsPerChannel
21362136}
2137+
2138+ func TestSessionRecycleAfterPoolClose_NoDoubleDecrement (t * testing.T ) {
2139+ t .Parallel ()
2140+ ctx := t .Context ()
2141+ var buf bytes.Buffer
2142+ logger := log .New (& buf , "" , 0 )
2143+
2144+ _ , client , teardown := setupMockedTestServerWithConfig (t , ClientConfig {
2145+ Logger : logger ,
2146+ SessionPoolConfig : SessionPoolConfig {
2147+ MinOpened : 1 ,
2148+ MaxOpened : 1 ,
2149+ },
2150+ DisableNativeMetrics : true ,
2151+ })
2152+ defer teardown ()
2153+
2154+ sp := client .idleSessions
2155+
2156+ // Take a session
2157+ sh , err := sp .take (ctx )
2158+ if err != nil {
2159+ t .Fatalf ("failed to take session: %v" , err )
2160+ }
2161+
2162+ // Simulate the race condition where the pool is closed but the session is still valid.
2163+ // We cannot use client.Close() because it invalidates all sessions in the health check queue.
2164+ // Instead, we manually set the pool to invalid.
2165+ sp .mu .Lock ()
2166+ sp .valid = false
2167+ sp .mu .Unlock ()
2168+
2169+ sh .recycle ()
2170+
2171+ // Check logs for the error message
2172+ logOutput := buf .String ()
2173+ if strings .Contains (logOutput , "Number of sessions in use is negative" ) {
2174+ t .Errorf ("Unexpected error \" Number of sessions in use is negative\" logged: %s" , logOutput )
2175+ }
2176+ }
2177+
2178+ func TestSessionRecycle_AlreadyInvalidSession (t * testing.T ) {
2179+ t .Parallel ()
2180+ ctx := t .Context ()
2181+ var buf bytes.Buffer
2182+ logger := log .New (& buf , "" , 0 )
2183+
2184+ _ , client , teardown := setupMockedTestServerWithConfig (t , ClientConfig {
2185+ DisableNativeMetrics : true ,
2186+ Logger : logger ,
2187+ SessionPoolConfig : SessionPoolConfig {
2188+ MinOpened : 1 ,
2189+ MaxOpened : 10 ,
2190+ },
2191+ })
2192+ defer teardown ()
2193+
2194+ sp := client .idleSessions
2195+ sh , err := sp .take (ctx )
2196+ if err != nil {
2197+ t .Fatalf ("failed to take session: %v" , err )
2198+ }
2199+
2200+ // Manually invalidate the session to simulate it being closed/expired by another process.
2201+ // We need to access the underlying session.
2202+ s := sh .session
2203+
2204+ // Calling destroy() on the session will invalidate it and decrement numInUse.
2205+ // We want to simulate the case where it IS already invalid when recycle is called.
2206+ // So we call destroy first.
2207+ s .destroy (false , true )
2208+
2209+ // Now s is invalid.
2210+ // And numInUse has been decremented once.
2211+
2212+ sh .recycle ()
2213+
2214+ // If the bug exists (double decrement), numInUse will be decremented AGAIN.
2215+ // Since we started with 1 session in use, destroy made it 0.
2216+ // Recycle would make it -1 (which triggers the log).
2217+
2218+ // Check logs for the error message
2219+ logOutput := buf .String ()
2220+ if strings .Contains (logOutput , "Number of sessions in use is negative" ) {
2221+ t .Errorf ("Unexpected error \" Number of sessions in use is negative\" logged: %s" , logOutput )
2222+ }
2223+ }
0 commit comments