@@ -132,8 +132,8 @@ async def fail_then_succeed(**kwargs):
132132
133133 await pool .close ()
134134
135- async def test_cancellation_does_not_leak_connection (self ) -> None :
136- """Cancelling a task that holds a connection should clean it up ."""
135+ async def test_cancellation_returns_healthy_connection_to_pool (self ) -> None :
136+ """Cancelling a task holding a healthy connection should return it to the pool ."""
137137 import asyncio
138138
139139 pool = ConnectionPool (["localhost:9001" ], max_size = 1 )
@@ -152,18 +152,21 @@ async def test_cancellation_does_not_leak_connection(self) -> None:
152152 async def hold_connection ():
153153 async with pool .acquire () as _ :
154154 acquired .set ()
155- await asyncio .sleep (10 ) # Hold forever
155+ await asyncio .sleep (10 ) # Hold forever — will be cancelled
156156
157157 task = asyncio .create_task (hold_connection ())
158- await acquired .wait () # Deterministic: wait until connection is acquired
158+ await acquired .wait ()
159159
160160 task .cancel ()
161161 with contextlib .suppress (asyncio .CancelledError ):
162162 await task
163163
164- # Connection should have been closed, size decremented
165- mock_conn .close .assert_called ()
166- assert pool ._size < initial_size
164+ # Connection is still healthy (CancelledError was in user code, not in
165+ # a protocol operation), so it should be returned to the pool
166+ assert pool ._size == initial_size
167+ assert pool ._pool .qsize () == 1
168+
169+ await pool .close ()
167170
168171 async def test_acquire_timeout_when_pool_exhausted (self ) -> None :
169172 """acquire() should timeout, not block forever, when pool is exhausted."""
@@ -252,7 +255,9 @@ async def holder():
252255 assert conn1 is mock_conn1
253256 holder_acquired .set ()
254257 await holder_release .wait ()
255- raise RuntimeError ("simulated failure" )
258+ # Simulate a real connection failure (broken connection)
259+ conn1 .is_connected = False
260+ raise DqliteConnectionError ("connection lost" )
256261
257262 holder_task = asyncio .create_task (holder ())
258263 await holder_acquired .wait ()
@@ -273,9 +278,9 @@ async def waiter():
273278 # check (_size == max_size) and enter the queue.get() wait
274279 await asyncio .sleep (0.1 )
275280
276- # Now release the holder — it fails, closing conn and decrementing _size
281+ # Now release the holder — connection breaks, _size decrements
277282 holder_release .set ()
278- with contextlib .suppress (RuntimeError ):
283+ with contextlib .suppress (DqliteConnectionError ):
279284 await holder_task
280285 # At this point _size == 0. The waiter is blocked on queue.get().
281286 # With the fix, the waiter should wake up and create a new connection.
@@ -384,6 +389,59 @@ async def test_close_then_return_closes_connection(self) -> None:
384389 mock_conn .close .assert_called ()
385390
386391
392+ async def test_user_exception_preserves_healthy_connection (self ) -> None :
393+ """A user-code exception should not destroy a healthy connection."""
394+ pool = ConnectionPool (["localhost:9001" ], max_size = 2 )
395+
396+ mock_conn = MagicMock ()
397+ mock_conn .is_connected = True
398+ mock_conn .connect = AsyncMock ()
399+ mock_conn .close = AsyncMock ()
400+
401+ with patch .object (pool ._cluster , "connect" , return_value = mock_conn ):
402+ await pool .initialize ()
403+
404+ assert pool ._size == 1
405+
406+ # User code raises a non-connection error
407+ with pytest .raises (ValueError , match = "application error" ):
408+ async with pool .acquire ():
409+ raise ValueError ("application error" )
410+
411+ # Connection should NOT have been closed — it's healthy
412+ mock_conn .close .assert_not_called ()
413+ # Pool size should be unchanged
414+ assert pool ._size == 1
415+ # Connection should be back in the pool queue
416+ assert pool ._pool .qsize () == 1
417+
418+ await pool .close ()
419+
420+ async def test_broken_connection_discarded_on_exception (self ) -> None :
421+ """A broken connection should be discarded even if user code raised."""
422+ pool = ConnectionPool (["localhost:9001" ], max_size = 2 )
423+
424+ mock_conn = MagicMock ()
425+ mock_conn .is_connected = True
426+ mock_conn .connect = AsyncMock ()
427+ mock_conn .close = AsyncMock ()
428+
429+ with patch .object (pool ._cluster , "connect" , return_value = mock_conn ):
430+ await pool .initialize ()
431+
432+ # Simulate a connection that becomes broken during use
433+ with pytest .raises (ValueError , match = "user error" ):
434+ async with pool .acquire () as conn :
435+ conn .is_connected = False # Mark as broken (simulates invalidation)
436+ raise ValueError ("user error" )
437+
438+ # Broken connection SHOULD have been closed and discarded
439+ mock_conn .close .assert_called ()
440+ assert pool ._size == 0
441+
442+ await pool .close ()
443+
444+
387445class TestConnectionPoolIntegration :
388446 """Integration tests requiring mocked connections."""
389447
0 commit comments