Skip to content

Commit 650b457

Browse files
fix: discard dead pool connections instead of reconnecting to stale leader
When a pooled connection was dead (is_connected=False), the pool called conn.connect() which reconnected to the original address. After a leader election, this reconnects to a follower. Now discards the dead connection and creates a fresh one via leader discovery. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4df05c7 commit 650b457

2 files changed

Lines changed: 32 additions & 3 deletions

File tree

src/dqliteclient/pool.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,13 @@ async def acquire(self) -> AsyncIterator[DqliteConnection]:
9090
f"(max_size={self._max_size}, timeout={self._timeout}s)"
9191
) from None
9292

93+
# If connection is dead, discard and create a fresh one with leader discovery
94+
if not conn.is_connected:
95+
self._size -= 1
96+
conn = await self._create_connection()
97+
9398
self._in_use.add(conn)
9499
try:
95-
# Verify connection is still good
96-
if not conn.is_connected:
97-
await conn.connect()
98100

99101
yield conn
100102
except BaseException:

tests/test_pool.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,33 @@ async def test_acquire_timeout_when_pool_exhausted(self) -> None:
109109
pass
110110

111111

112+
async def test_dead_connection_triggers_leader_rediscovery(self) -> None:
113+
"""A dead connection should be replaced via leader discovery, not reconnected."""
114+
pool = ConnectionPool(["localhost:9001"], max_size=1)
115+
116+
dead_conn = MagicMock()
117+
dead_conn.is_connected = False # Connection is dead
118+
dead_conn.connect = AsyncMock()
119+
dead_conn.close = AsyncMock()
120+
121+
new_conn = MagicMock()
122+
new_conn.is_connected = True
123+
new_conn.connect = AsyncMock()
124+
new_conn.close = AsyncMock()
125+
new_conn.execute = AsyncMock(return_value=(1, 1))
126+
127+
# Initialize with the connection that will go dead
128+
with patch.object(pool._cluster, "connect", return_value=dead_conn):
129+
await pool.initialize()
130+
131+
# When acquiring, the dead conn should be discarded and a new one created
132+
with patch.object(pool._cluster, "connect", return_value=new_conn):
133+
async with pool.acquire() as conn:
134+
assert conn is new_conn # Got a fresh connection, not the dead one
135+
136+
# Dead connection should NOT have had connect() called (no stale reconnect)
137+
dead_conn.connect.assert_not_called()
138+
112139
async def test_close_handles_checked_out_connections(self) -> None:
113140
"""close() should close in-flight connections, not just idle ones."""
114141
pool = ConnectionPool(["localhost:9001"], max_size=2)

0 commit comments

Comments
 (0)