Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

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

Abierto
#1,386 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

@devtechedge ya está trabajando en esto.

Desde el 7/10/2026.

  • #1387 de @devtechedge — abierto

Evaluación

Dificultad
2/5
Tiempo estimado
1-3 horas
Aptitud para principiantes
35/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Estancado
Stack tecnológico
postgresql, python

Línea de trabajo

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.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

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
Lenguaje dominante
Python
Estrellas
8.1k
Forks
474
Merge medio
1 d 17 h
PR fusionados (30 d)
24

Preparar el entorno

Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de MagicStack/asyncpg

Todos los issues de MagicStack/asyncpg

Issues similares

Más issues de Python

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.