@@ -278,81 +278,95 @@ def try_overflow():
278278 c .close ()
279279
280280
281- @pytest .mark .skip ("Flaky test - idle timeout behavior needs investigation" )
282281def test_pool_idle_timeout_removes_connections (conn_str ):
283282 """Test that idle_timeout removes connections from the pool after the timeout."""
284283 pooling (max_size = 2 , idle_timeout = 1 )
285284 conn1 = connect (conn_str )
286- spid_list = []
287285 cursor1 = conn1 .cursor ()
288- cursor1 .execute ("SELECT @@SPID" )
289- spid1 = cursor1 .fetchone ()[0 ]
290- spid_list .append (spid1 )
286+ # Use connection_id (a GUID unique per physical connection) instead of @@SPID,
287+ # because SQL Server can reassign the same SPID to a new connection.
288+ cursor1 .execute ("SELECT connection_id FROM sys.dm_exec_connections WHERE session_id = @@SPID" )
289+ conn_id1 = cursor1 .fetchone ()[0 ]
291290 conn1 .close ()
292291
293- # Wait for longer than idle_timeout
294- time .sleep (3 )
292+ # Wait well beyond the idle_timeout to account for slow CI and integer-second granularity
293+ time .sleep (5 )
295294
296- # Get a new connection, which should not reuse the previous SPID
295+ # Get a new connection — the idle one should have been evicted during acquire()
297296 conn2 = connect (conn_str )
298297 cursor2 = conn2 .cursor ()
299- cursor2 .execute ("SELECT @@SPID" )
300- spid2 = cursor2 .fetchone ()[0 ]
301- spid_list .append (spid2 )
298+ cursor2 .execute ("SELECT connection_id FROM sys.dm_exec_connections WHERE session_id = @@SPID" )
299+ conn_id2 = cursor2 .fetchone ()[0 ]
302300 conn2 .close ()
303301
304- assert spid1 != spid2 , "Idle timeout did not remove connection from pool"
302+ assert (
303+ conn_id1 != conn_id2
304+ ), "Idle timeout did not remove connection from pool — same connection_id reused"
305305
306306
307307# =============================================================================
308308# Error Handling and Recovery Tests
309309# =============================================================================
310310
311311
312- @pytest .mark .skip (
313- "Test causes fatal crash - forcibly closing underlying connection leads to undefined behavior"
314- )
315312def test_pool_removes_invalid_connections (conn_str ):
316- """Test that the pool removes connections that become invalid (simulate by closing underlying connection)."""
313+ """Test that the pool removes connections that become invalid and recovers gracefully.
314+
315+ This test simulates a connection being returned to the pool in a dirty state
316+ (with an open transaction) by calling _conn.close() directly, bypassing the
317+ normal Python close() which does a rollback. The pool's acquire() should detect
318+ the bad connection during reset(), discard it, and create a fresh one.
319+ """
317320 pooling (max_size = 1 , idle_timeout = 30 )
318321 conn = connect (conn_str )
319322 cursor = conn .cursor ()
320323 cursor .execute ("SELECT 1" )
321- # Simulate invalidation by forcibly closing the connection at the driver level
322- try :
323- # Try to access a private attribute or method to forcibly close the underlying connection
324- # This is implementation-specific; if not possible, skip
325- if hasattr (conn , "_conn" ) and hasattr (conn ._conn , "close" ):
326- conn ._conn .close ()
327- else :
328- pytest .skip ("Cannot forcibly close underlying connection for this driver" )
329- except Exception :
330- pass
331- # Safely close the connection, ignoring errors due to forced invalidation
324+ cursor .fetchone ()
325+
326+ # Record the connection_id of the original connection
327+ cursor .execute ("SELECT connection_id FROM sys.dm_exec_connections WHERE session_id = @@SPID" )
328+ original_conn_id = cursor .fetchone ()[0 ]
329+
330+ # Force-return the connection to the pool WITHOUT rollback.
331+ # This leaves the pooled connection in a dirty state (open implicit transaction)
332+ # which will cause reset() to fail on next acquire().
333+ conn ._conn .close ()
334+
335+ # Python close() will fail since the underlying handle is already gone
332336 try :
333337 conn .close ()
334- except RuntimeError as e :
335- if "not initialized" not in str ( e ):
336- raise
337- # Now, get a new connection from the pool and ensure it works
338+ except RuntimeError :
339+ pass
340+
341+ # Now get a new connection — the pool should discard the dirty one and create fresh
338342 new_conn = connect (conn_str )
339343 new_cursor = new_conn .cursor ()
340- try :
341- new_cursor .execute ("SELECT 1" )
342- result = new_cursor .fetchone ()
343- assert result is not None and result [0 ] == 1 , "Pool did not remove invalid connection"
344- finally :
345- new_conn .close ()
344+ new_cursor .execute ("SELECT 1" )
345+ result = new_cursor .fetchone ()
346+ assert result is not None and result [0 ] == 1 , "Pool did not recover from invalid connection"
347+
348+ # Verify it's a different physical connection
349+ new_cursor .execute (
350+ "SELECT connection_id FROM sys.dm_exec_connections WHERE session_id = @@SPID"
351+ )
352+ new_conn_id = new_cursor .fetchone ()[0 ]
353+ assert (
354+ original_conn_id != new_conn_id
355+ ), "Expected a new physical connection after pool discarded the dirty one"
356+
357+ new_conn .close ()
346358
347359
348360def test_pool_recovery_after_failed_connection (conn_str ):
349361 """Test that the pool recovers after a failed connection attempt."""
350362 pooling (max_size = 1 , idle_timeout = 30 )
351363 # First, try to connect with a bad password (should fail)
352- if "Pwd=" in conn_str :
353- bad_conn_str = conn_str .replace ("Pwd=" , "Pwd=wrongpassword" )
354- elif "Password=" in conn_str :
355- bad_conn_str = conn_str .replace ("Password=" , "Password=wrongpassword" )
364+ import re
365+
366+ pwd_match = re .search (r"(Pwd|Password)=" , conn_str , re .IGNORECASE )
367+ if pwd_match :
368+ key = pwd_match .group (0 ) # e.g. "PWD=" or "Pwd=" or "Password="
369+ bad_conn_str = conn_str .replace (key , key + "wrongpassword" )
356370 else :
357371 pytest .skip ("No password found in connection string to modify" )
358372 with pytest .raises (Exception ):
0 commit comments