mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
fix(observability): count a pool timeout the engine reports without a code
Pool contention raises a typed prisma error carrying `code == "P2024"`, which the attribute check already handled. When the database itself is unreachable the engine reports the same P2024 as a raw `EngineRequestError` instead, with no `code` attribute and the code recorded only in the JSON body it was built from, so those went uncounted. Seen on a live proxy at connection limit 1 with Postgres paused. Prisma classifies both as P2024, so which layer surfaced it should not decide whether the counter moves. The match is on prisma's own `error_code` field rather than the message text, so an error that merely mentions the code cannot trip it, and a different engine code is rejected. The two cases stay distinguishable in the metrics that matter: saturation holds busy at max with waiters queued, while an unreachable database drops open connections instead.
This commit is contained in:
parent
bd2a9dbdea
commit
5fdd4161d7
2 changed files with 61 additions and 1 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import re
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any, Final, TypeVar
|
||||
|
||||
|
|
@ -9,6 +10,11 @@ from litellm.proxy._types import (
|
|||
)
|
||||
from litellm.secret_managers.main import str_to_bool
|
||||
|
||||
# The engine reports a pool timeout as a raw HTTP error whose body carries the
|
||||
# code, with no `code` attribute to read. Matched on prisma's own JSON field so a
|
||||
# message that merely mentions the code cannot trip it.
|
||||
_P2024_IN_ENGINE_BODY: Final = re.compile(r'"error_code"\s*:\s*"P2024"')
|
||||
|
||||
# Bounds the __cause__/__context__ walk in is_database_service_unavailable_error_in_chain.
|
||||
# Real exception chains are a few links deep; the cap also makes the walk cycle-safe.
|
||||
_MAX_EXCEPTION_CHAIN_DEPTH: Final = 20
|
||||
|
|
@ -137,8 +143,19 @@ class PrismaDBExceptionHandler:
|
|||
The ``P####`` codes are prisma's own namespace and only prisma populates
|
||||
``code`` with one, so no type check is needed. Skipping it also keeps
|
||||
this callable on the DB failure path when prisma itself is stubbed.
|
||||
|
||||
Both shapes have to be read. Contention raises a typed prisma error
|
||||
carrying the code, which the attribute check already handles. When the
|
||||
database is unreachable the engine reports the same P2024 as a raw HTTP
|
||||
error instead, with no ``code`` attribute and the code only in the JSON
|
||||
body. Prisma classifies both as P2024, so which layer surfaced it should
|
||||
not decide whether it is counted. The neighbouring gauges separate the
|
||||
two cases: saturation holds ``busy`` at ``max`` with waiters queued,
|
||||
while an unreachable database drops ``open`` instead.
|
||||
"""
|
||||
return getattr(e, "code", None) == "P2024"
|
||||
if getattr(e, "code", None) == "P2024":
|
||||
return True
|
||||
return bool(_P2024_IN_ENGINE_BODY.search(str(e)))
|
||||
|
||||
@staticmethod
|
||||
def is_database_transport_error(e: Exception) -> bool:
|
||||
|
|
|
|||
|
|
@ -623,6 +623,49 @@ def test_only_p2024_counts_as_pool_exhaustion(other_error):
|
|||
assert PrismaDBExceptionHandler.is_connection_pool_timeout_error(other_error) is False
|
||||
|
||||
|
||||
def test_pool_exhaustion_is_detected_in_the_shape_the_engine_actually_raises():
|
||||
"""Contention raises a typed prisma error carrying the code, and that path is
|
||||
covered above. When the database itself is unreachable the engine reports the
|
||||
same P2024 as a raw HTTP error instead, with no `code` attribute at all and
|
||||
the code recorded only in the JSON body. Prisma classifies both as P2024, so
|
||||
the counter should not depend on which layer surfaced it. Body captured
|
||||
verbatim from a live proxy run at connection limit 1 with Postgres paused."""
|
||||
from types import SimpleNamespace
|
||||
|
||||
from prisma.engine.errors import EngineRequestError
|
||||
|
||||
body = (
|
||||
'{"is_panic":false,"message":"Timed out fetching a new connection from the connection pool. '
|
||||
'More info: http://pris.ly/d/connection-pool (Current connection pool timeout: 2, connection limit: 1)",'
|
||||
'"meta":{"connection_limit":1,"timeout":2},"error_code":"P2024"}'
|
||||
)
|
||||
error = EngineRequestError(response=SimpleNamespace(status=500), body=body)
|
||||
|
||||
assert not hasattr(error, "code"), "the shape under test is the one with no code attribute"
|
||||
assert PrismaDBExceptionHandler.is_connection_pool_timeout_error(error) is True
|
||||
|
||||
|
||||
def test_another_engine_error_code_is_not_read_as_pool_exhaustion():
|
||||
"""The body is matched on prisma's own error_code field, so a different
|
||||
engine failure carrying its own code must not be counted."""
|
||||
from types import SimpleNamespace
|
||||
|
||||
from prisma.engine.errors import EngineRequestError
|
||||
|
||||
error = EngineRequestError(
|
||||
response=SimpleNamespace(status=500),
|
||||
body='{"is_panic":false,"message":"boom","error_code":"P2010"}',
|
||||
)
|
||||
|
||||
assert PrismaDBExceptionHandler.is_connection_pool_timeout_error(error) is False
|
||||
|
||||
|
||||
def test_a_message_merely_mentioning_the_code_is_not_pool_exhaustion():
|
||||
"""Matching loose text would let any error whose message quotes P2024 inflate
|
||||
the exhaustion counter."""
|
||||
assert PrismaDBExceptionHandler.is_connection_pool_timeout_error(Exception("see P2024 docs")) is False
|
||||
|
||||
|
||||
def test_pool_exhaustion_is_not_mistaken_for_a_reachability_failure():
|
||||
"""P2024's message contains "Timed out", which the transport classifier
|
||||
keyword-matches. The pool predicate must not inherit that ambiguity."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue