Skip to content

Commit 3e3843c

Browse files
fix: handle task cancellation in pool acquire() to prevent leaks
Changed except Exception to except BaseException so that asyncio.CancelledError (a BaseException since Python 3.9) properly closes the connection and decrements the pool size counter. Previously, cancelled tasks leaked their connections permanently. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3e96097 commit 3e3843c

2 files changed

Lines changed: 37 additions & 3 deletions

File tree

src/dqliteclient/pool.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,11 @@ async def acquire(self) -> AsyncIterator[DqliteConnection]:
8282
await conn.connect()
8383

8484
yield conn
85-
except Exception:
86-
# On error, close connection and create new one
85+
except BaseException:
86+
# On error (including cancellation), close connection
8787
import contextlib
8888

89-
with contextlib.suppress(Exception):
89+
with contextlib.suppress(BaseException):
9090
await conn.close()
9191
self._size -= 1
9292
raise

tests/test_pool.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,40 @@ async def test_acquire_when_closed(self) -> None:
3333
pass
3434

3535

36+
async def test_cancellation_does_not_leak_connection(self) -> None:
37+
"""Cancelling a task that holds a connection should clean it up."""
38+
import asyncio
39+
40+
pool = ConnectionPool(["localhost:9001"], max_size=1)
41+
42+
mock_conn = MagicMock()
43+
mock_conn.is_connected = True
44+
mock_conn.connect = AsyncMock()
45+
mock_conn.close = AsyncMock()
46+
47+
with patch.object(pool._cluster, "connect", return_value=mock_conn):
48+
await pool.initialize()
49+
50+
initial_size = pool._size
51+
52+
async def hold_connection():
53+
async with pool.acquire() as conn:
54+
await asyncio.sleep(10) # Hold forever
55+
56+
task = asyncio.create_task(hold_connection())
57+
await asyncio.sleep(0.01) # Let task acquire
58+
59+
task.cancel()
60+
try:
61+
await task
62+
except asyncio.CancelledError:
63+
pass
64+
65+
# Connection should have been closed, size decremented
66+
mock_conn.close.assert_called()
67+
assert pool._size < initial_size
68+
69+
3670
class TestConnectionPoolIntegration:
3771
"""Integration tests requiring mocked connections."""
3872

0 commit comments

Comments
 (0)