Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

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

Aperta
#1,386 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

@devtechedge ci sta già lavorando.

Dal 7/10/2026.

  • #1387 di @devtechedge — aperta

Valutazione

Difficoltà
2/5
Tempo stimato
1-3 ore
Idoneità per principianti
35/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Ferma
Stack tecnologico
postgresql, python
Ambito
backend, databases

Direzione di ricerca

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.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

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
Lingua principale
Python
Stelle
8.1k
Fork
474
Merge medio
1g 17h
PR unite (30g)
24

Preparare l'ambiente

Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di MagicStack/asyncpg

Tutte le issue di MagicStack/asyncpg

Issue simili

Altre issue su Python

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.