From f895d1c28596dda63390ffb66dbf7cc60d7110f3 Mon Sep 17 00:00:00 2001 From: devtechedge Date: Wed, 7 Oct 2026 21:39:59 +0530 Subject: [PATCH 1/3] tests: Cover a failed BEGIN in Transaction.start() --- tests/test_transaction.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_transaction.py b/tests/test_transaction.py index f84cf7c0..900ef119 100644 --- a/tests/test_transaction.py +++ b/tests/test_transaction.py @@ -5,6 +5,8 @@ # the Apache 2.0 License: http://www.apache.org/licenses/LICENSE-2.0 +import asyncio + import asyncpg from asyncpg import _testbase as tb @@ -180,6 +182,29 @@ async def test_transaction_within_manual_transaction(self): self.assertIsNone(self.con._top_xact) self.assertFalse(self.con.is_in_transaction()) + async def test_transaction_failed_begin(self): + self.assertIsNone(self.con._top_xact) + + # Make BEGIN fail by running it while another + # operation is in progress on the connection. + busy = asyncio.ensure_future(self.con.execute('SELECT pg_sleep(0.5)')) + await asyncio.sleep(0.1) + with self.assertRaisesRegex(asyncpg.InterfaceError, + 'another operation is in progress'): + await self.con.transaction().start() + await busy + + self.assertIsNone(self.con._top_xact) + self.assertFalse(self.con.is_in_transaction()) + + # The next transaction must be a real top-level one. + async with self.con.transaction(): + self.assertTrue(self.con.is_in_transaction()) + await self.con.execute('SELECT 1') + + self.assertIsNone(self.con._top_xact) + self.assertFalse(self.con.is_in_transaction()) + async def test_isolation_level(self): await self.con.reset() default_isolation = await self.con.fetchval( From a7100facc4394389d2b72cb591b6a71ea7616a97 Mon Sep 17 00:00:00 2001 From: devtechedge Date: Wed, 7 Oct 2026 21:40:14 +0530 Subject: [PATCH 2/3] Reset Connection._top_xact when BEGIN fails in Transaction.start() --- asyncpg/transaction.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/asyncpg/transaction.py b/asyncpg/transaction.py index 562811e6..280a323c 100644 --- a/asyncpg/transaction.py +++ b/asyncpg/transaction.py @@ -146,6 +146,8 @@ async def start(self): await self._connection.execute(query) except BaseException: self._state = TransactionState.FAILED + if con._top_xact is self: + con._top_xact = None raise else: self._state = TransactionState.STARTED From af00698210f11146e1b6010da0b87210d1e0f885 Mon Sep 17 00:00:00 2001 From: devtechedge Date: Thu, 8 Oct 2026 03:00:34 +0530 Subject: [PATCH 3/3] tests: Stub execute so a failed BEGIN does not race the loop --- tests/test_transaction.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/tests/test_transaction.py b/tests/test_transaction.py index 900ef119..c14a13e2 100644 --- a/tests/test_transaction.py +++ b/tests/test_transaction.py @@ -5,7 +5,7 @@ # the Apache 2.0 License: http://www.apache.org/licenses/LICENSE-2.0 -import asyncio +from unittest import mock import asyncpg @@ -185,14 +185,19 @@ async def test_transaction_within_manual_transaction(self): async def test_transaction_failed_begin(self): self.assertIsNone(self.con._top_xact) - # Make BEGIN fail by running it while another - # operation is in progress on the connection. - busy = asyncio.ensure_future(self.con.execute('SELECT pg_sleep(0.5)')) - await asyncio.sleep(0.1) - with self.assertRaisesRegex(asyncpg.InterfaceError, - 'another operation is in progress'): - await self.con.transaction().start() - await busy + tr = self.con.transaction() + error = asyncpg.PostgresConnectionError( + 'Timed-out waiting to acquire database connection.' + ) + + with mock.patch.object( + type(self.con), 'execute', side_effect=error + ) as execute: + with self.assertRaises(asyncpg.PostgresConnectionError) as caught: + await tr.start() + + self.assertIs(caught.exception, error) + execute.assert_awaited_once_with('BEGIN;') self.assertIsNone(self.con._top_xact) self.assertFalse(self.con.is_in_transaction())