From 4c45ab659a477a6817dad94b9aab37216c4b33ad Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Apr 2026 12:57:26 +0000 Subject: [PATCH] =?UTF-8?q?fix(db):=20aiosqlite=20shim=20=E2=80=94=20bytec?= =?UTF-8?q?ode=20fallback=20when=20source=20inspection=20fails?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address PR review findings 1 & 2 on the detection heuristic: The previous gate relied solely on `inspect.getsource()` matching the substring `self._connection.stop`. Two failure modes: 1. Source-unavailable builds — zipped distributions, pyc-only installs, frozen runtimes — return OSError from `inspect.getsource`, so the gate silently skipped patching and the multi-page `terminate_force_close() not implemented` tracebacks reappeared. Hard-to-diagnose because startup looks successful. 2. Brittle text matching — minor upstream formatting changes to the same buggy logic could bypass the substring check. Factor the detection into `_looks_buggy(original)` with two signals in order of preference: * Source text inspection (precise, obvious). * Bytecode `co_names` inspection — works on every CPython build exposing `__code__`. Any function that does `self._connection.stop` puts both `_connection` and `stop` into `co_names`, regardless of whitespace or refactoring. If neither inspection succeeds, patch defensively: the user explicitly reported the noisy ERROR behaviour, so a possibly-redundant shim beats silently restoring it. Per user direction the third reviewer suggestion (regression test coverage) is not added in this PR. https://claude.ai/code/session_01JSr4NZSskEUQvoJnavVXh8 --- .../open_webui/internal/_aiosqlite_compat.py | 64 +++++++++++++++---- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/backend/open_webui/internal/_aiosqlite_compat.py b/backend/open_webui/internal/_aiosqlite_compat.py index 3e50535774..583c92bd4c 100644 --- a/backend/open_webui/internal/_aiosqlite_compat.py +++ b/backend/open_webui/internal/_aiosqlite_compat.py @@ -45,9 +45,11 @@ created. To keep the blast radius small the install is: `_terminate_force_close` symbol (older or upstream-fixed versions); * a no-op if the upstream implementation has already moved off the - `self._connection.stop` reference — detected by inspecting the - original source — so a future SQLAlchemy fix isn't shadowed by - this shim; + `self._connection.stop` reference — detected via source + inspection with a bytecode fallback (so the heuristic still + works on zipped, pyc-only or otherwise stripped builds where + `inspect.getsource()` raises) — so a future SQLAlchemy fix + isn't shadowed by this shim; * idempotent so repeated imports don't stack patches. The caller in `internal.db` only invokes `install()` when the runtime @@ -63,6 +65,53 @@ import logging log = logging.getLogger(__name__) +def _looks_buggy(original) -> bool: + """Return True if `original` looks like the buggy upstream impl + that references `self._connection.stop`. + + Two signals, in order of preference: + + 1. Source text inspection — exact and obvious, but fails on + deployments where source isn't on disk (zipped distributions, + pyc-only installs, frozen runtimes). + 2. Bytecode `co_names` inspection — works on every CPython build + that exposes `__code__`. Both `_connection` and `stop` appear + in `co_names` whenever the function does an attribute access of + the form `self._connection.stop`, regardless of whitespace or + formatting changes. + + If neither inspection succeeds we err on the side of patching: + the upstream `_terminate_force_close` already raised + `NotImplementedError` for us once (which is why we're here), and + a possibly-redundant shim is preferable to silently restoring the + multi-page ERROR traceback the user explicitly reported. + """ + try: + original_source = inspect.getsource(original) + except (OSError, TypeError): + original_source = None + + if original_source is not None: + return 'self._connection.stop' in original_source + + code = getattr(original, '__code__', None) + if code is not None: + names = set(getattr(code, 'co_names', ())) + if {'_connection', 'stop'}.issubset(names): + return True + # Bytecode says it's not the buggy shape — believe it. + return False + + # Neither source nor bytecode available. Patch defensively; the + # caller already established that the buggy `NotImplementedError` + # path is the existing behaviour. + log.debug( + 'aiosqlite shim: could not inspect upstream _terminate_force_close ' + 'via source or bytecode; applying compatibility patch defensively.' + ) + return True + + def install() -> None: try: from sqlalchemy.dialects.sqlite import aiosqlite as _sa_aiosqlite @@ -80,14 +129,7 @@ def install() -> None: if getattr(original, '__open_webui_patched__', False): return # Idempotent — already applied. - # Only patch the specific buggy implementation. If upstream has - # changed how `_terminate_force_close` is implemented (no longer - # touching `self._connection.stop`) defer to whatever they ship. - try: - original_source = inspect.getsource(original) - except (OSError, TypeError): - original_source = '' - if 'self._connection.stop' not in original_source: + if not _looks_buggy(original): return def _terminate_force_close(self) -> None: