Hacktoberfest 2026 : les issues que les mainteneurs ont marquées pour octobre, ouvertes et accessibles aux débutants. Parcourir les issues Hacktoberfest

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

Ouverte
#625 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Personne n'a encore pris cette issue.

Évaluation

Difficulté
4/5
Temps estimé
3-5 jours
Accessibilité débutants
48/100
Type d'issue
Bug
Clarté
Plutôt claire
Activité
Active
Stack technique
python
Domaine
backend, database

Piste de recherche

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.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Description

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())
Langage dominant
Python
Étoiles
455
Forks
119
Métriques de merge des PR
Aucune PR mergée en 30 j

Préparer son environnement

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de oracle/python-oracledb

Toutes les issues de oracle/python-oracledb

Issues similaires

Plus d'issues Python

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.