From 6bda95d3bd46ebb88898689ea416e09a61960b03 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Apr 2026 12:35:19 +0000 Subject: [PATCH] =?UTF-8?q?fix(db):=20patch=20SQLAlchemy=20=E2=86=94=20aio?= =?UTF-8?q?sqlite=20=E2=89=A50.20=20terminate=5Fforce=5Fclose=20incompatib?= =?UTF-8?q?ility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SQLAlchemy 2.0.36+ added a `terminate_force_close()` path on its async DB-API adapters, used by the connection pool when a connection must be invalidated immediately (typically when an in-flight query was cancelled — request aborted by client, timeout, etc.). For the aiosqlite dialect, `_terminate_force_close` unconditionally accesses `self._connection.stop`. That attribute existed on `aiosqlite.Connection` up to 0.18 (where Connection sub-classed `threading.Thread`) and was removed in 0.20+ when the worker model was refactored. On the version pin Open WebUI ships (sqlalchemy 2.0.48 + aiosqlite 0.21.0) every cancelled aiosqlite call therefore produces a multi-page NotImplementedError: terminate_force_close() not implemented by this DBAPI shim ERROR traceback, drowning real errors in noise even though the underlying connection is correctly torn down by aiosqlite's own worker on the next loop tick. Add `internal/_aiosqlite_compat.install()` which monkey-patches the shim to understand both APIs: * aiosqlite ≤ 0.18 — call `Connection.stop()` as before. * aiosqlite ≥ 0.20 — flip the worker thread's `_running` flag so it exits on the next iteration (the same end-state the original code achieved on the old API). * Anything else — log at debug and fall back to GC. The patch is idempotent, runs once at db.py import time before the async engine is created, and is a no-op on installations whose SQLAlchemy doesn't ship the affected symbol — so it won't break future upstream rewrites. https://claude.ai/code/session_01JSr4NZSskEUQvoJnavVXh8 --- .../open_webui/internal/_aiosqlite_compat.py | 110 ++++++++++++++++++ backend/open_webui/internal/db.py | 9 ++ 2 files changed, 119 insertions(+) create mode 100644 backend/open_webui/internal/_aiosqlite_compat.py diff --git a/backend/open_webui/internal/_aiosqlite_compat.py b/backend/open_webui/internal/_aiosqlite_compat.py new file mode 100644 index 0000000000..10886a6d9f --- /dev/null +++ b/backend/open_webui/internal/_aiosqlite_compat.py @@ -0,0 +1,110 @@ +""" +Compatibility shim for SQLAlchemy ↔ aiosqlite ≥ 0.20. + +Background +---------- +SQLAlchemy ≥ 2.0.36 added a `terminate_force_close()` path on its +async DB-API adapters, used by the connection pool when a connection +must be invalidated immediately — typically because the operation it +was running was cancelled (for example, a request was aborted by the +client mid-query). + +For the aiosqlite dialect this implementation lives at +`sqlalchemy.dialects.sqlite.aiosqlite.AsyncAdapt_aiosqlite_connection +._terminate_force_close` and unconditionally accesses +`self._connection.stop`. That attribute existed on +`aiosqlite.Connection` in 0.18 and earlier (where Connection +sub-classed `threading.Thread` and exposed a `stop()` method). It was +removed in aiosqlite 0.20 when the worker model was refactored. + +The result, on the version combo Open WebUI pins +(SQLAlchemy 2.0.48 + aiosqlite 0.21.0), is that *every* cancellation +mid-DB-call produces a multi-page + + NotImplementedError: terminate_force_close() not implemented by this DBAPI shim + +at ERROR level — drowning real errors in noise even though the +underlying connection is being torn down correctly by aiosqlite's own +worker on the next iteration. + +Fix +--- +Replace `_terminate_force_close` with one that understands both the +old `Connection.stop()` API and the modern internal `_running` flag. +Setting `_running = False` causes the aiosqlite worker thread to +break out of its loop on the next tick, which is the same end-state +the original code was after. + +Apply this patch once, at import time, before any async engine is +created. Idempotent and safe to import on systems where the affected +SQLAlchemy/aiosqlite versions are not in use — it bails out silently +if the symbol is missing. +""" + +from __future__ import annotations + +import logging + +log = logging.getLogger(__name__) + + +def install() -> None: + try: + from sqlalchemy.dialects.sqlite import aiosqlite as _sa_aiosqlite + except ImportError: + # SQLAlchemy installed without sqlite+aiosqlite — nothing to patch. + return + + target_cls = getattr(_sa_aiosqlite, 'AsyncAdapt_aiosqlite_connection', None) + if target_cls is None or not hasattr(target_cls, '_terminate_force_close'): + # Either an older SQLAlchemy without the terminate path, or an + # upstream rewrite that no longer needs this shim. + return + + if getattr(target_cls._terminate_force_close, '__open_webui_patched__', False): + return # Idempotent — already applied. + + def _terminate_force_close(self) -> None: + conn = getattr(self, '_connection', None) + if conn is None: + return + + # aiosqlite ≤ 0.18 — original API. + stop = getattr(conn, 'stop', None) + if callable(stop): + try: + stop() + except Exception: + log.debug( + 'aiosqlite Connection.stop() raised during force-close; ' + 'connection will be cleaned up by garbage collection.', + exc_info=True, + ) + return + + # aiosqlite ≥ 0.20 — the worker thread observes its own + # `_running` flag and exits on the next loop tick, releasing + # the underlying sqlite3 connection. + if hasattr(conn, '_running'): + try: + conn._running = False + except Exception: + log.debug( + 'Could not flip aiosqlite Connection._running during ' + 'force-close; connection will be cleaned up by garbage ' + 'collection.', + exc_info=True, + ) + return + + # Unknown aiosqlite internals — fall back to leaving the + # connection to garbage collection. Do *not* re-raise: the pool + # has already removed the connection record and there is + # nothing useful for the caller to do. + log.debug( + 'aiosqlite Connection has neither stop() nor _running; relying ' + 'on garbage collection to release the underlying sqlite3 handle.' + ) + + _terminate_force_close.__open_webui_patched__ = True # type: ignore[attr-defined] + target_cls._terminate_force_close = _terminate_force_close diff --git a/backend/open_webui/internal/db.py b/backend/open_webui/internal/db.py index 3818543fc7..9b884c33cc 100644 --- a/backend/open_webui/internal/db.py +++ b/backend/open_webui/internal/db.py @@ -205,6 +205,15 @@ get_db = contextmanager(get_session) # ASYNC ENGINE (used for ALL runtime database operations) # ============================================================ +# Patch SQLAlchemy's aiosqlite connector before any async engine is +# created. Without this, every cancelled aiosqlite query produces a +# multi-page `terminate_force_close() not implemented` ERROR traceback +# because SQLAlchemy 2.0.x's shim references `Connection.stop`, which +# aiosqlite removed in 0.20+. See `_aiosqlite_compat` for details. +from open_webui.internal._aiosqlite_compat import install as _install_aiosqlite_compat + +_install_aiosqlite_compat() + ASYNC_SQLALCHEMY_DATABASE_URL = _make_async_url(SQLALCHEMY_DATABASE_URL) if 'sqlite' in ASYNC_SQLALCHEMY_DATABASE_URL: