Async Thin: call_timeout cannot interrupt server-side waits, causing DPY-4011 and lingering transaction locks
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 48/100
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
- 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())
- Lingua principale
- Python
- Stelle
- 455
- Fork
- 119
- Metriche di merge delle PR
- Nessuna PR unita negli ultimi 30g
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di oracle/python-oracledb
-
bug
Difficoltà 4/5 1-2 giorni Idoneità per principianti 45/100
oracle/python-oracledb#624 ·
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 48/100
oracle/python-oracledb#623 ·
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 55/100
oracle/python-oracledb#622 ·
-
bug
Difficoltà 4/5 3-5 giorni Idoneità per principianti 32/100
oracle/python-oracledb#621 · 3 commenti ·
-
Client Library or Database
Difficoltà 4/5 3-5 giorni Idoneità per principianti 25/100
oracle/python-oracledb#620 · 4 commenti ·
Tutte le issue di oracle/python-oracledb
Issue simili
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 84/100
GoogleCloudPlatform/gce-rescue#195 ·
I maintainer di solito rispondono entro 2 giorni
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 83/100
I maintainer di solito rispondono entro 1 giorno
-
intake mcp-intake needs-ac needs-human-review priority:medium
Difficoltà 2/5 1-3 ore Idoneità per principianti 67/100
Ikalus1988/MisakaNet#3848 · 2 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 75/100
521xueweihan/HelloGitHub#4105 ·
-
bug size:XS
Difficoltà 2/5 1-3 ore Idoneità per principianti 83/100
I maintainer di solito rispondono entro 1 giorno