26.0.x: pool stops handing out connections after the database closes one (background thread dies with StopIteration; failed release() leaks its slot)
还没有人认领这个 Issue。
评估
调研方向
Start with BaseConnImpl.process_sync_operation and process_async_operation in src/oracledb/impl/base/connection.pyx, then compare the pool paths in src/oracledb/impl/thin/pool.pyx, especially _close_connection, return_connection, and _bg_task_func. Use the supplied TCP-forwarder script to reproduce both failures. Done means the background thread survives a dropped connection, failed release does not retain a busy slot, and subsequent acquire() calls succeed.
由索引模型根据 Issue 内容生成。
描述
- What versions are you using?
- python-oracledb 26.0.1 (reproduced below) and 26.0.0 (where we first hit it, in production). 4.0.2 is not affected.
- Thin mode.
- Oracle Autonomous Database,
connection.version23.26.3.3.0, over TCPS. - Reproduced on macOS arm64 with Python 3.12.8; first seen on Linux aarch64 (glibc 2.39) with Python 3.12.3.
platform.platform: macOS-27.2-arm64-arm-64bit
sys.maxsize > 2**32: True
platform.python_version: 3.12.8
oracledb.__version__: 26.0.1
- Is it an error or a hang or a crash?
A hang. Once the database or network has closed a pooled connection, the pool can stop handing out connections, and with the default POOL_GETMODE_WAIT every later acquire() blocks forever. In production (26.0.0) this hung our API for 27 hours, until a restart; no exception ever reached the application.
- What error(s) or behavior you are seeing?
Two separate problems. Both are deterministic with the script below, and neither happens with 4.0.2.
A. The pool's background thread dies. When the pool closes a dropped connection whose network is gone, the thread exits with StopIteration, so the pool never opens another connection:
Exception in thread Thread-2 (_bg_task_func):
Traceback (most recent call last):
File "src/oracledb/impl/thin/connection.pyx", line 140, in _close
File "src/oracledb/impl/base/connection.pyx", line 443, in oracledb.base_impl.BaseConnImpl.process_sync_operation
File "src/oracledb/impl/thin/connection.pyx", line 655, in oracledb.thin_impl.ThinConnImpl._process_sync_operation_sub_op
File "src/oracledb/impl/thin/protocol.pyx", line 259, in oracledb.thin_impl.Protocol._process_single_message
File "src/oracledb/impl/thin/protocol.pyx", line 260, in oracledb.thin_impl.Protocol._process_single_message
File "src/oracledb/impl/thin/protocol.pyx", line 250, in oracledb.thin_impl.Protocol._process_round_trip
File "src/oracledb/impl/thin/protocol.pyx", line 246, in oracledb.thin_impl.Protocol._process_round_trip
File "src/oracledb/impl/thin/protocol.pyx", line 208, in oracledb.thin_impl.Protocol._process_message
File "src/oracledb/impl/thin/protocol.pyx", line 183, in oracledb.thin_impl.Protocol._process_message
File "src/oracledb/impl/thin/protocol.pyx", line 276, in oracledb.thin_impl.Protocol._receive_packet
File "src/oracledb/impl/thin/packet.pyx", line 728, in oracledb.thin_impl.ReadBuffer.wait_for_packets_sync
File "src/oracledb/impl/thin/transport.pyx", line 361, in oracledb.thin_impl.Transport.read_packet
File ".../site-packages/oracledb/errors.py", line 215, in _raise_err
raise _create_exception(error_num, context_error_message, cause, **args)
oracledb.exceptions.DatabaseError: DPY-4011: the database or network closed the connection
Help: https://python-oracledb.readthedocs.io/en/latest/user_guide/troubleshooting.html#dpy-4011
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File ".../threading.py", line 1075, in _bootstrap_inner
self.run()
File ".../threading.py", line 1012, in run
self._target(*self._args, **self._kwargs)
File "src/oracledb/impl/thin/pool.pyx", line 614, in oracledb.thin_impl.ThinPoolImpl._bg_task_func
File "src/oracledb/impl/thin/pool.pyx", line 617, in oracledb.thin_impl.ThinPoolImpl._bg_task_func
File "src/oracledb/impl/base/connection.pyx", line 452, in oracledb.base_impl.BaseConnImpl.process_sync_operation
File "src/oracledb/impl/base/connection.pyx", line 448, in oracledb.base_impl.BaseConnImpl.process_sync_operation
StopIteration
B. A failed release() leaks the slot for good. release() raises DPY-4011 (4.0.2 raises too), but the connection is never returned to the pool: pool.busy stays at 1 even after gc.collect(). With 4.0.2 the slot comes back when the connection is garbage collected.
Script output with 26.0.1 (the traceback above printed where marked):
oracledb 26.0.1 | Python 3.12.8 | macOS-27.2-arm64-arm-64bit
1. pool.drop() on a connection whose network is gone
database 23.26.3.3.0
[Exception in thread Thread-2 (_bg_task_func): ... StopIteration]
pool background thread alive: False
acquire(): DPY-4005: timed out waiting for the connection pool to return a connection (after 10s)
2. pool.release() on a connection whose network is gone
release(): DPY-4011: the database or network closed the connection
pool.busy after gc.collect(): 1
acquire(): DPY-4005: timed out waiting for the connection pool to return a connection (after 10s)
With 4.0.2:
oracledb 4.0.2 | Python 3.12.8 | macOS-27.2-arm64-arm-64bit
1. pool.drop() on a connection whose network is gone
database 23.26.3.3.0
pool background thread alive: True
acquire(): ok
2. pool.release() on a connection whose network is gone
release(): DPY-4011: the database or network closed the connection
pool.busy after gc.collect(): 0
acquire(): ok
Likely cause. BaseConnImpl.process_sync_operation (base/connection.pyx L438-L448) calls generator.throw(e) inside its except BaseException clause and ignores what throw() returns:
- A:
ThinPoolImpl._close_connection(thin/pool.pyx L167-L176) swallows the error, so the generator finishes andthrow()raisesStopIteration. Since that is raised inside theexcept BaseExceptionhandler, the siblingexcept StopIterationdoes not catch it, and it escapesprocess_sync_operation. The drop branch of_bg_task_func(L614-L620) has no handler, unlike 4.0.2'stry: conn_impl._close() except: pass(4.0.2 L554-L562). - B:
ThinPoolImpl.return_connection(L478-L489) yields aReturnToPoolSubOpfrom itsfinally. When the error is thrown in, that sub-op is whatthrow()returns, and it is discarded, so the connection is never returned.Connection._close(connection.py L105-L117) has already setself._impl = None, so__del__cannot return it either.
Treating the value throw() returns as the next sub-op, and its StopIteration as completion, would fix both. A sketch:
exc = None
while True:
try:
sub_op = next(generator) if exc is None else generator.throw(exc)
except StopIteration as e:
result = e.value
break
exc = None
if sub_op is not None:
try:
self._process_sync_operation_sub_op(sub_op)
except BaseException as e:
exc = e
process_async_operation (L403-L413) has the same pattern, so async pools are probably affected too. I have not tested them.
- Does your application call init_oracle_client()?
No, Thin mode.
- Include a runnable Python script that shows the problem.
The pool connects through a local TCP forwarder, which the script then cuts, as if the database or network had closed the connection. No schema is needed.
"""A connection pool stops working once the database or network closes a
pooled connection (python-oracledb 26.0.x, Thin mode).
The pool connects through a local TCP forwarder, which is then cut as if the
database or the network had closed the connection. Environment variables:
PYO_USER, PYO_PASSWORD, PYO_HOST, PYO_PORT (1521), PYO_SERVICE_NAME,
PYO_PROTOCOL (tcp or tcps).
"""
import gc
import os
import platform
import socket
import sys
import threading
import time
import oracledb
class Tunnel:
"""Forwards 127.0.0.1:<port> to the database; cut() closes every
connection made through it so far."""
def __init__(self):
self.target = (os.environ["PYO_HOST"], int(os.environ.get("PYO_PORT", "1521")))
self.pairs = []
self.lock = threading.Lock()
self.listener = socket.create_server(("127.0.0.1", 0))
self.port = self.listener.getsockname()[1]
threading.Thread(target=self._serve, daemon=True).start()
def _serve(self):
while True:
client, _ = self.listener.accept()
upstream = socket.create_connection(self.target)
with self.lock:
self.pairs.append((client, upstream))
for src, dst in ((client, upstream), (upstream, client)):
threading.Thread(target=self._pipe, args=(src, dst), daemon=True).start()
@staticmethod
def _close(*socks):
for s in socks:
try:
s.shutdown(socket.SHUT_RDWR)
except OSError:
pass
s.close()
def _pipe(self, src, dst):
try:
while data := src.recv(65536):
dst.sendall(data)
except OSError:
pass
finally:
self._close(src, dst)
def cut(self):
with self.lock:
for pair in self.pairs:
self._close(*pair)
self.pairs.clear()
def make_pool(tunnel):
return oracledb.create_pool(
user=os.environ["PYO_USER"],
password=os.environ["PYO_PASSWORD"],
host="127.0.0.1",
port=tunnel.port,
service_name=os.environ["PYO_SERVICE_NAME"],
protocol=os.environ.get("PYO_PROTOCOL", "tcp"),
ssl_server_dn_match=False, # tcps only: the certificate is not for 127.0.0.1
min=0,
max=1,
increment=1,
# The default, POOL_GETMODE_WAIT, makes the final acquire() hang forever.
getmode=oracledb.POOL_GETMODE_TIMEDWAIT,
wait_timeout=10_000,
)
def first_line(exc):
return str(exc).splitlines()[0]
def background_thread_alive():
return any(t.name.endswith("(_bg_task_func)") for t in threading.enumerate())
def try_acquire(pool):
start = time.monotonic()
try:
conn = pool.acquire()
conn.ping()
print(" acquire(): ok")
pool.release(conn)
except oracledb.Error as exc:
print(f" acquire(): {first_line(exc)} (after {time.monotonic() - start:.0f}s)")
print("oracledb", oracledb.__version__, "| Python", platform.python_version(), "|", platform.platform())
print("\n1. pool.drop() on a connection whose network is gone")
tunnel = Tunnel()
pool = make_pool(tunnel)
conn = pool.acquire()
print(" database", conn.version)
tunnel.cut()
pool.drop(conn)
time.sleep(2)
print(f" pool background thread alive: {background_thread_alive()}")
try_acquire(pool)
print("\n2. pool.release() on a connection whose network is gone")
tunnel = Tunnel()
pool = make_pool(tunnel)
conn = pool.acquire()
conn.ping()
tunnel.cut()
try:
pool.release(conn)
print(" release(): ok")
except oracledb.Error as exc:
print(f" release(): {first_line(exc)}")
del conn
gc.collect()
time.sleep(2)
print(f" pool.busy after gc.collect(): {pool.busy}")
try_acquire(pool)
sys.stdout.flush()
- 主要语言
- Python
- 星标
- 451
- 派生
- 118
- PR 合并指标
- 30 天内没有已合并 PR
环境准备
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
oracle/python-oracledb 的其他 Issue
-
bug
难度 4/5 3-5 天 新手友好度 45/100
oracle/python-oracledb#608 · 2 条评论 ·
-
bug
难度 5/5 一周以上 新手友好度 20/100
oracle/python-oracledb#607 · 4 条评论 ·
-
bug
难度 4/5 3-5 天 新手友好度 48/100
oracle/python-oracledb#592 · 6 条评论 · 1 个 reaction ·
-
enhancement
难度 4/5 3-5 天 新手友好度 35/100
oracle/python-oracledb#564 ·
-
bug
难度 4/5 3-5 天 新手友好度 45/100
oracle/python-oracledb#502 · 5 条评论 ·
查看 oracle/python-oracledb 的全部 Issue
相似的 Issue
-
难度 2/5 1-3 小时 新手友好度 72/100
-
bug
难度 1/5 1 小时以内 新手友好度 88/100
qgis/QGIS-Plugins-Website#459 ·
-
bug severity:medium
难度 2/5 1-3 小时 新手友好度 78/100
维护者通常 2 天内回复
-
bot-found bug priority: P3
难度 2/5 1-3 小时 新手友好度 84/100
madenvel/KalinkaPlayer#179 ·
-
难度 2/5 1-3 小时 新手友好度 68/100
ls1intum/edutelligence#1098 ·
维护者通常 1 天内回复