Skip to content

Commit 4a83cc2

Browse files
pchasehelprans
andauthored
Fix connection pool deadlock (#1324)
Closes #1307 Co-authored-by: Elvis Pranskevichus <el@prans.net>
1 parent 1b11846 commit 4a83cc2

2 files changed

Lines changed: 38 additions & 2 deletions

File tree

‎asyncpg/pool.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,10 @@ async def release(self, timeout: Optional[float]) -> None:
199199
'a free connection holder')
200200

201201
if self._con.is_closed():
202-
# When closing, pool connections perform the necessary
203-
# cleanup, so we don't have to do anything else here.
202+
# A protocol abort may close the connection without running
203+
# Connection._cleanup(). Terminate it to finish cleanup and
204+
# return the holder to the pool via _release_on_close().
205+
self._con.terminate()
204206
return
205207

206208
self._timeout = None

‎tests/test_pool.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,6 +1015,40 @@ async def worker():
10151015
conn = await pool.acquire(timeout=POOL_NOMINAL_TIMEOUT)
10161016
await pool.release(conn)
10171017

1018+
async def test_pool_release_after_protocol_abort(self):
1019+
pool = await self.create_pool(min_size=1, max_size=1)
1020+
1021+
conn = await pool.acquire()
1022+
raw_conn = conn._con
1023+
holder = conn._holder
1024+
terminated = asyncio.Event()
1025+
conn.add_termination_listener(lambda _: terminated.set())
1026+
self.assertEqual(await conn.fetchval('SELECT 1'), 1)
1027+
stmt = next(iter(raw_conn._stmt_cache.iter_statements()))
1028+
1029+
raw_conn._protocol.abort()
1030+
1031+
await pool.release(conn)
1032+
1033+
# Releasing an aborted connection must finish connection cleanup.
1034+
await asyncio.wait_for(terminated.wait(), timeout=1.0)
1035+
self.assertTrue(stmt.closed)
1036+
self.assertEqual(len(raw_conn._stmt_cache), 0)
1037+
self.assertIsNone(holder._con)
1038+
self.assertIsNone(holder._in_use)
1039+
self.assertIsNone(conn._con)
1040+
1041+
# Repeated release must not return the holder to the queue twice.
1042+
await pool.release(conn)
1043+
self.assertEqual(pool._queue.qsize(), 1)
1044+
1045+
# The holder must reconnect and support queries after the abort.
1046+
conn2 = await pool.acquire(timeout=1.0)
1047+
self.assertIsNot(conn2._con, raw_conn)
1048+
self.assertEqual(await conn2.fetchval('SELECT 1'), 1)
1049+
await pool.release(conn2)
1050+
await pool.close()
1051+
10181052

10191053
@unittest.skipIf(os.environ.get('PGHOST'), 'unmanaged cluster')
10201054
class TestPoolReconnectWithTargetSessionAttrs(tb.ClusterTestCase):

0 commit comments

Comments
 (0)