Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

Failed BEGIN leaves Connection._top_xact set; every later transaction becomes a SAVEPOINT and fails with NoActiveSQLTransactionError

Đang mở
#1,386 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

@devtechedge đang làm issue này rồi.

Từ ngày 7/10/2026.

  • #1387 của @devtechedge — đang mở

Đánh giá

Độ khó
2/5
Thời gian dự kiến
1-3 giờ
Mức phù hợp với người mới
35/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Đình trệ
Công nghệ
postgresql, python
Lĩnh vực
backend, databases

Hướng nghiên cứu

Read Transaction.start() and the transaction tests in tests/test_transaction.py, focusing on the path where BEGIN raises. Check the existing cleanup in __commit() and __rollback() and add coverage for a failed BEGIN, including the nested-savepoint case. Done means the connection can start a later transaction after a failed BEGIN without leaving an outer transaction’s state corrupted; linked pull request #1387 is already open.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

Summary

Transaction.start() assigns con._top_xact = self before it sends BEGIN. If BEGIN raises, the transaction is marked FAILED, but _top_xact is never reset. From then on, every conn.transaction() on that connection is treated as nested and sends SAVEPOINT ..., which the server rejects:

asyncpg.exceptions.NoActiveSQLTransactionError: SAVEPOINT can only be used in transaction blocks

The connection is still open and idle (is_closed() == False, is_in_transaction() == False), but it cannot run a transaction again until it is closed or reset().

Reproduction

The concurrent query below is just a convenient way to make BEGIN raise. Any exception from BEGIN has the same effect (see "How we hit it").

import asyncio
import asyncpg


async def main():
    conn = await asyncpg.connect("postgresql://postgres:postgres@localhost:5432/postgres")

    # Make BEGIN fail: the connection is busy, so execute("BEGIN") raises before sending anything.
    busy = asyncio.create_task(conn.execute("SELECT pg_sleep(0.5)"))
    await asyncio.sleep(0.1)
    try:
        await conn.transaction().start()
    except asyncpg.InterfaceError as e:
        print("BEGIN failed:", e)
    await busy

    print("is_in_transaction:", conn.is_in_transaction(), "| _top_xact:", conn._top_xact)

    async with conn.transaction():  # sent as SAVEPOINT -> NoActiveSQLTransactionError
        await conn.execute("SELECT 1")


asyncio.run(main())
BEGIN failed: cannot perform operation: another operation is in progress
is_in_transaction: False | _top_xact: <asyncpg.Transaction state:failed 0x...>
asyncpg.exceptions.NoActiveSQLTransactionError: SAVEPOINT can only be used in transaction blocks
How we hit it

We run Aurora PostgreSQL behind Amazon RDS Proxy, with SQLAlchemy 2.0's asyncpg dialect. RDS Proxy could not borrow a backend connection in time, answered BEGIN with an error (SQLSTATE 08000) and kept the client socket open:

03:00:03  PostgresConnectionError: Timed-out waiting to acquire database connection.
03:00:05  NoActiveSQLTransactionError: SAVEPOINT can only be used in transaction blocks
03:00:07  NoActiveSQLTransactionError: SAVEPOINT can only be used in transaction blocks
...       (every write until the process was restarted)

asyncpg.Pool clears it on release(), which calls Connection._reset() and resets _top_xact. Until then, retries within the same acquire() fail the same way. Connections reused without reset(), such as SQLAlchemy's pool or a long-lived connection, stay broken until they are closed.

Is this intentional? We couldn't tell whether leaving _top_xact set after a failed BEGIN is by design. The tests in tests/test_transaction.py assert _top_xact is None after every error case they cover, but none covers BEGIN itself failing.

Proposed fix

In Transaction.start():

         try:
             await self._connection.execute(query)
         except BaseException:
             self._state = TransactionState.FAILED
+            if con._top_xact is self:
+                con._top_xact = None
             raise

This mirrors what __commit() and __rollback() already do. The is self guard leaves the outer transaction in place when a nested SAVEPOINT fails.

Why this is safe for any exception

Only __commit() and __rollback() clear _top_xact, and both raise for a FAILED transaction before reaching that line. So after a failed BEGIN, nothing can ever release it, whatever the exception was. Whether the server is actually in a transaction is tracked separately, through is_in_transaction(), and start() already checks that when _top_xact is None:

How BEGIN failed Server afterwards Today With the fix
Error response (e.g. the proxy's 08000) not in a transaction every later transaction fails with 25P01 next BEGIN works
Raised before sending (e.g. the repro above) not in a transaction same next BEGIN works
Cancelled after the server ran BEGIN in a transaction next transaction becomes a SAVEPOINT in that open transaction, reports success, but nothing is committed if the reply to the cancelled BEGIN has been read: InterfaceError: cannot use Connection.transaction() in a manually started transaction; if not yet: BEGIN is sent again (the server only warns) and the transaction commits normally
Environment
  • asyncpg 0.32.0 (also 0.31.0)
  • PostgreSQL 17 (repro); Aurora PostgreSQL behind RDS Proxy (production)
  • Python 3.14
Ngôn ngữ chính
Python
Star
8.1k
Fork
474
Merge trung bình
1 ngày 17 giờ
Pull request đã merge (30 ngày)
24

Chuẩn bị môi trường

Dự án này không cung cấp dev container, Dockerfile hay hướng dẫn đóng góp, nên bạn cần tự thiết lập môi trường: hãy bắt đầu từ README và xem hướng dẫn đóng góp lần đầu của chúng tôi để biết các bước chung.

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của MagicStack/asyncpg

Tất cả issue của MagicStack/asyncpg

Issue tương tự

Thêm issue về Python

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.