3.6 KiB
Observability-aware errors (errors.py)
lmcache/v1/mp_observability/errors.py defines LMCacheTimeoutError, a
timeout exception that reports itself to the MP observability EventBus when it
is constructed. It is the single place timeouts in lmcache/ should be raised
from; the ban-raw-timeout-error pre-commit hook enforces this by rejecting
any bare raise TimeoutError(...) / raise asyncio.TimeoutError(...) under
lmcache/.
Why
Timeouts are the most common "something is stuck" failure in MP mode (MQ
round-trips, GPU transfer waits, adapter drains, NIXL handshakes). Before this,
each site raised the built-in TimeoutError, which left no trace in the
observability stack — operators only learned of a timeout if the surrounding
code happened to log it. Routing every timeout through one class makes them
uniformly observable (counter + log + trace) without touching each call site
beyond the class swap.
Contract
LMCacheTimeoutError(message: str, *, session_id: str = "")
- Subclass of the built-in
TimeoutError. Every existingexcept TimeoutErrorhandler continues to catch it unchanged, so swapping a rawraise TimeoutError(...)forraise LMCacheTimeoutError(...)is behaviour-preserving. (On Python 3.11+asyncio.TimeoutErroris the same type, soexcept asyncio.TimeoutErrorcatches it too; on 3.10 it does not — do not rely on the async alias for catching these raises.) - Emits on construction, not on raise.
__init__publishes oneEventType.TIMEOUT_RAISEDevent (see EVENTS.md) to the global EventBus viaget_event_bus(). - Zero-cost when observability is off. The emit path is guarded by
is_observability_enabled(), which is onlyTrueinside the MP server process. In single-process / CLI mode the constructor does nothing beyondsuper().__init__(message)(one boolean check) — no event, no stack-trace capture, no OTel dependency exercised. - Never raises from
__init__. Any failure to publish is swallowed and logged at debug level: observability must never break error handling. session_idis forwarded onto the event so the timeout span can nest under the originating request's root span. Pass it where the raise site has a request/IPCCacheServerKey.request_idin scope (e.g.shm.prepare_store); leave it empty otherwise.
What gets recorded
The emitted TIMEOUT_RAISED event carries message, exception_type, and a
captured stacktrace (the construction stack minus the __init__ frame,
following the OTel exception.* semantic conventions). Three subscribers
consume it, registered by init_observability under the usual
metrics/logging/tracing toggles:
| Subscriber | Output | Default |
|---|---|---|
TimeoutMetricsSubscriber |
lmcache_mp.timeouts counter, tagged exception_type |
on (metrics_enabled) |
TimeoutLoggingSubscriber |
WARNING log with message + stack trace |
on (logging_enabled) |
TimeoutTracingSubscriber |
zero-duration timeout span with an exception event + ERROR status |
on when tracing enabled |
The tracing subscriber records the exception the same way OTel's
Span.record_exception would (an exception span event with exception.type
/ exception.message / exception.stacktrace plus ERROR status), driven from
the EventBus drain thread where the original exception object is no longer
available.
Extending
To make another timeout observable, raise LMCacheTimeoutError instead of the
built-in. To add a new observable error family, define a sibling subclass of
the relevant built-in here, add a matching EventType, and add subscriber(s)
that consume it — mirroring the timeout wiring.