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

Async Thin: call_timeout cannot interrupt server-side waits, causing DPY-4011 and lingering transaction locks

Aperta
#625 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
48/100
Tipo di issue
Bug
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
python
Ambito
backend, database

Direzione di ricerca

The report points to BaseAsyncProtocol.__init__ in protocol.pyx and the supports_oob setting; start by reading that code and comparing it with the synchronous protocol's interrupt handling. Run the supplied reproduction against a server with OOB enabled and check the reported timeout codes and row lock release time. Done means async Thin interrupts the server wait at call_timeout and releases the earlier transaction lock promptly, with regression coverage for both waits.

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

Descrizione

bug
  1. What versions are you using?
  • Oracle Database Free 23.5.0.24.7 (local testing); Observed in production against Oracle Database 19c.
  • python-oracledb 4.0.2 and 26.0.1, Thin mode. The same relevant code is present on main (c92c8420).
  • Python 3.13, asyncio, on macOS arm64 and Linux arm64.
  • Production also uses uvloop and SQLAlchemy 2.0 (oracle+oracledb_async), but the reproduction below uses native python-oracledb only.
  • Oracle Client: not applicable (Thin mode).
  1. Is it an error or a hang or a crash?

A timeout error with connection loss. Server-side waits and previously acquired transaction locks can continue after the client receives the error.

  1. What error(s) or behavior you are seeing?

With call_timeout = 500, async Thin does not interrupt a row-lock wait or a long DBMS_SESSION.SLEEP. The call fails after approximately 1 second (2× the timeout) with DPY-4011, rather than recovering with DPY-4024.

Under the same conditions, sync Thin with working out-of-band (OOB) support interrupts the row-lock wait after approximately 500 ms and raises DPY-4024.

After the async error, the client closes its socket, but the server session can remain in its wait. A row lock acquired earlier in the transaction remains held until that wait ends. With a 5-second sleep, the original reproduction observed the lock remaining held for approximately 4 seconds after the client received DPY-4011.

Observed output from the original reproduction (26.0.1, Linux arm64, OOB enabled):

Issue 2
  sync:  DPY-4024 after 0.51s
  async: DPY-4011 after 1.00s
  victim: DPY-4011 after 1.00s
  row lock released after 5.09s

The final measurement is elapsed time from the start of the victim's sleep, not from the error. The same behavior was reproduced on 4.0.2.

Source inspection shows BaseAsyncProtocol.__init__ setting self._caps.supports_oob = False (protocol.pyx:329 in 26.0.1, 619 in 4.0.2). Async Thin therefore sends an in-band interrupt, which the server does not read while blocked in these waits.

The sync comparison requires OOB enabled on the server and a network path that delivers TCP urgent data. In the tested Oracle Database Free container, DISABLE_OOB=ON was removed from sqlnet.ora, and the client ran in a container on the same Docker network. With OOB disabled or blocked, both sync and async failed after approximately 2× the timeout.

Is this an intentional async Thin limitation? Is OOB break support planned for plain TCP, or what supported approach should an async application use to interrupt server-side waits and avoid retaining transaction locks after a timeout? I am not assuming that closing the client socket guarantees immediate server-side rollback.

  1. Does your application call init_oracle_client()?

No. Both the synchronous control and asynchronous reproduction use Thin mode.

  1. Include a runnable Python script that shows the problem.
import asyncio
import os
import platform
import sys
import time
import uuid

import oracledb

PARAMS = dict(
    user=os.environ["ORACLE_USER"],
    password=os.environ["ORACLE_PASSWORD"],
    dsn=os.environ["ORACLE_DSN"],
)
TABLE = "t_wait_" + uuid.uuid4().hex[:12]


def sync_lock_wait():
    with oracledb.connect(**PARAMS) as conn:
        conn.call_timeout = 500
        start = time.monotonic()
        with conn.cursor() as cur:
            try:
                cur.execute(f"update {TABLE} set val = 0 where id = 1")
                print("  sync: unexpectedly completed")
            except oracledb.DatabaseError as exc:
                print(f"  sync:  {exc.args[0].full_code} "
                      f"after {time.monotonic() - start:.2f}s")


async def async_lock_wait():
    async with oracledb.connect_async(**PARAMS) as conn:
        conn.call_timeout = 500
        start = time.monotonic()
        with conn.cursor() as cur:
            try:
                await cur.execute(f"update {TABLE} set val = 0 where id = 1")
                print("  async: unexpectedly completed")
            except oracledb.DatabaseError as exc:
                print(f"  async: {exc.args[0].full_code} "
                      f"after {time.monotonic() - start:.2f}s")


async def main():

    async with oracledb.connect_async(**PARAMS) as holder:
        print("connection.version:", holder.version)
        holder.call_timeout = 3000
        with holder.cursor() as cur:
            await cur.execute(
                f"create table {TABLE} (id number primary key, val number)"
            )
            try:
                await cur.execute(f"insert into {TABLE} values (1, 0)")
                await cur.execute(f"insert into {TABLE} values (2, 0)")
                await holder.commit()

                # Compare sync and async while another session holds row 1.
                await cur.execute(f"update {TABLE} set val = 1 where id = 1")
                await asyncio.to_thread(sync_lock_wait)
                await async_lock_wait()
                await holder.rollback()

                # Acquire row 2, then time out during a long server-side sleep.
                async with oracledb.connect_async(**PARAMS) as victim:
                    with victim.cursor() as vcur:
                        await vcur.execute(
                            f"update {TABLE} set val = 2 where id = 2"
                        )
                        victim.call_timeout = 500
                        start = time.monotonic()
                        try:
                            await vcur.execute("begin dbms_session.sleep(5); end;")
                            print("  victim: sleep completed without an error")
                        except oracledb.DatabaseError as exc:
                            print(f"  victim: {exc.args[0].full_code} "
                                  f"after {time.monotonic() - start:.2f}s")

                        # ORA-00054 means the earlier transaction lock remains.
                        deadline = time.monotonic() + 15
                        while True:
                            try:
                                await cur.execute(
                                    f"select 1 from {TABLE} "
                                    "where id = 2 for update nowait"
                                )
                                print("  row lock released after "
                                      f"{time.monotonic() - start:.2f}s")
                                break
                            except oracledb.DatabaseError as exc:
                                if exc.args[0].code != 54:
                                    raise
                                if time.monotonic() >= deadline:
                                    raise RuntimeError(
                                        "Row lock still held after observation window"
                                    ) from exc
                                await asyncio.sleep(0.25)
            finally:
                await holder.rollback()
                await cur.execute(f"drop table {TABLE} purge")


asyncio.run(main())
Lingua principale
Python
Stelle
455
Fork
119
Metriche di merge delle PR
Nessuna PR unita negli ultimi 30g

Preparare l'ambiente

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 oracle/python-oracledb

Tutte le issue di oracle/python-oracledb

Issue simili

Altre issue su Python

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.