From 6a5632c377c812bd2ea78433bdd139150e41c83b Mon Sep 17 00:00:00 2001 From: Dominic Fallows Date: Tue, 1 Sep 2026 15:22:28 +0100 Subject: [PATCH] fix(proxy): write the Prisma engine through the public setter, not the mangled name `_write_engine` assigned directly to `prisma_client._Prisma__engine`, the name Prisma <0.13 used internally for the engine attribute. Prisma 0.13+ is a `__slots__` class that no longer has that attribute at all, only a public `_engine` property/setter backed by `_internal_engine`. Writing to the mangled name on a modern client raises `AttributeError`, which `connect()`'s `backoff` decorator retries indefinitely, spinning CPU and respawning Prisma engine subprocesses without ever connecting. Use the public `_engine` setter instead, which exists on both the legacy and current Prisma internal layouts. Reproduced locally, prisma==0.15.0: router at 196-293% CPU with a continuous `AttributeError: 'Prisma' object has no attribute '_Prisma__engine'` retry loop; 0% after the fix, connects immediately. --- litellm/proxy/db/prisma_client.py | 9 +++--- .../proxy/db/test_prisma_client.py | 29 +++++++++++++++++++ .../db/test_prisma_planned_engine_restart.py | 4 +-- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/litellm/proxy/db/prisma_client.py b/litellm/proxy/db/prisma_client.py index 4bd007769b8..251158f5961 100644 --- a/litellm/proxy/db/prisma_client.py +++ b/litellm/proxy/db/prisma_client.py @@ -53,10 +53,7 @@ class _PrismaEngine(Protocol): class _PrismaClient(Protocol): - _Prisma__engine: _PrismaEngine - - @property - def _engine(self) -> _PrismaEngine: ... + _engine: _PrismaEngine class _PrismaDrainTracker: @@ -240,7 +237,9 @@ class PrismaWrapper: @staticmethod def _write_engine(prisma_client: _PrismaClient, engine: _PrismaEngine) -> None: - prisma_client._Prisma__engine = engine + # Public setter: prisma <0.13 stored the engine on the mangled `Prisma.__engine`, + # 0.13+ on `BasePrisma._internal_engine`. Both are __slots__ classes. + prisma_client._engine = engine # rebind-ok: third-party Prisma client instance, not a local param we own def _instrument_prisma_client(self, prisma_client: _PrismaClient) -> _PrismaDrainTracker | None: from prisma.errors import ClientNotConnectedError diff --git a/tests/test_litellm/proxy/db/test_prisma_client.py b/tests/test_litellm/proxy/db/test_prisma_client.py index f0983d6bf62..17ac6e5d731 100644 --- a/tests/test_litellm/proxy/db/test_prisma_client.py +++ b/tests/test_litellm/proxy/db/test_prisma_client.py @@ -171,6 +171,35 @@ def test_get_engine_pid_returns_zero_for_disconnected_client(disconnected_prisma assert wrapper._get_engine_pid() == 0 +def test_write_engine_uses_the_public_setter_not_the_mangled_name(): + """Real Prisma clients (>=0.13) are __slots__ classes exposing only a public + `_engine` property/setter, with no `_Prisma__engine` slot. Writing to the + mangled name raises AttributeError instead of storing the engine.""" + + class _FakeSlotsBasedPrismaClient: + __slots__ = ("_internal_engine",) + + def __init__(self): + self._internal_engine = None + + @property + def _engine(self): + if self._internal_engine is None: + raise AttributeError("not connected") + return self._internal_engine + + @_engine.setter + def _engine(self, engine): + self._internal_engine = engine + + client = _FakeSlotsBasedPrismaClient() + sentinel = object() + + PrismaWrapper._write_engine(client, sentinel) + + assert PrismaWrapper._read_engine(client) is sentinel + + @pytest.mark.asyncio async def test_recreate_prisma_client_recovers_from_disconnected_client( mock_prisma_binary, disconnected_prisma diff --git a/tests/test_litellm/proxy/db/test_prisma_planned_engine_restart.py b/tests/test_litellm/proxy/db/test_prisma_planned_engine_restart.py index f3f742b2023..69e1ddce5e6 100644 --- a/tests/test_litellm/proxy/db/test_prisma_planned_engine_restart.py +++ b/tests/test_litellm/proxy/db/test_prisma_planned_engine_restart.py @@ -60,7 +60,7 @@ def _make_wrapper(engine_pid: int = 111, iam: bool = False) -> PrismaWrapper: prisma = GeneratedPrisma(use_dotenv=False) engine = MagicMock() engine.process.pid = engine_pid - setattr(prisma, "_Prisma__engine", engine) + prisma._engine = engine return PrismaWrapper(original_prisma=prisma, iam_token_db_auth=iam) @@ -158,7 +158,7 @@ def _token_db_url(created: datetime, expires_in: int = 900) -> str: def test_wrapper_instruments_generated_prisma_engine() -> None: prisma = GeneratedPrisma(use_dotenv=False) engine = MagicMock() - setattr(prisma, "_Prisma__engine", engine) + prisma._engine = engine wrapper = PrismaWrapper(original_prisma=prisma, iam_token_db_auth=False)