Async Thin: call_timeout cannot interrupt server-side waits, causing DPY-4011 and lingering transaction locks
Personne n'a encore pris cette issue.
Évaluation
- Difficulté
- 4/5
- Temps estimé
- 3-5 jours
- Accessibilité débutants
- 48/100
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
- 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).
- 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.
- 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.
- Does your application call init_oracle_client()?
No. Both the synchronous control and asynchronous reproduction use Thin mode.
- 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
- Aucun Dockerfile ni fichier Docker Compose
- Propose un modèle de pull request
- Lire le guide de contribution
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Autres issues de oracle/python-oracledb
-
bug
Difficulté 4/5 1-2 jours Accessibilité débutants 45/100
oracle/python-oracledb#624 ·
-
Difficulté 4/5 3-5 jours Accessibilité débutants 48/100
oracle/python-oracledb#623 ·
-
Difficulté 4/5 3-5 jours Accessibilité débutants 55/100
oracle/python-oracledb#622 ·
-
bug
Difficulté 4/5 3-5 jours Accessibilité débutants 32/100
oracle/python-oracledb#621 · 3 commentaires ·
-
Client Library or Database
Difficulté 4/5 3-5 jours Accessibilité débutants 25/100
oracle/python-oracledb#620 · 4 commentaires ·
Toutes les issues de oracle/python-oracledb
Issues similaires
-
Project submission:Ouverte
Difficulté 1/5 Moins d'une heure Accessibilité débutants 72/100
slavakurilyak/awesome-ai-agents#845 ·
Les mainteneurs répondent en général sous 1 jour
-
tests: pin that the atomic writers in utils/paths.py stage the temporary file in the target's directoryPeut-être pris Une pull request liée à cette issue est ouverte ou déjà fusionnée. Ouvertedifficulty:2 enhancement good first issue
Difficulté 1/5 1-3 heures Accessibilité débutants 92/100
MakazhanAlpamys/Soup#2178 ·
Les mainteneurs répondent en général sous 1 jour
-
[BUG] dcg.waggr permutation p-values are folded twice, so near-zero scores get the smallest p-valuesOuverte
Difficulté 2/5 1-3 heures Accessibilité débutants 75/100
scverse/rapids-singlecell#854 ·
Les mainteneurs répondent en général sous 1 jour
-
Slang input: int8_t4_packed and uint8_t4_packed are copied into CrossGLPeut-être pris @ninglinLiu l’a pris aujourd’hui. Ouvertearea:frontend backend:slang bug direction:input
Difficulté 2/5 1-3 heures Accessibilité débutants 70/100
Les mainteneurs répondent en général sous 1 jour
-
random_elements(unique=True) on an OrderedDict: ZeroDivisionError once only zero weights remain, AssertionError with use_weighting=FalsePeut-être pris Une pull request liée à cette issue est ouverte ou déjà fusionnée. Ouverte
Difficulté 2/5 1-3 heures Accessibilité débutants 85/100
Les mainteneurs répondent en général sous 13 jours