Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/prometheus_cleanup.py: 20%
45 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1"""
2Prometheus multiprocess directory cleanup utilities.
4Wipes all .db files on startup so workers start with a clean slate.
5"""
7from __future__ import annotations
9import glob
10import os
11import re
12from typing import Final
14from litellm._logging import verbose_proxy_logger
16_LIVE_GAUGE_PID: Final = re.compile(r"gauge_live[a-z]*_(\d+)\.db$")
19def wipe_directory(directory: str) -> None:
20 """Delete all .db files in the directory. Called once before workers fork."""
21 files: Final = glob.glob(os.path.join(directory, "*.db"))
22 deleted = 0
23 for filepath in files:
24 try:
25 os.remove(filepath)
26 deleted += 1
27 except OSError as e:
28 verbose_proxy_logger.warning("Failed to delete stale prometheus file %s: %s", filepath, e)
29 if deleted:
30 verbose_proxy_logger.info("Prometheus cleanup: wiped %s stale .db files from %s", deleted, directory)
33def mark_worker_exit(worker_pid: int) -> None:
34 """Remove prometheus .db files for a dead worker. Called by gunicorn child_exit hook."""
35 if not os.environ.get("PROMETHEUS_MULTIPROC_DIR"):
36 return
37 try:
38 from prometheus_client import multiprocess
40 multiprocess.mark_process_dead(worker_pid)
41 verbose_proxy_logger.info("Prometheus cleanup: marked worker %s as dead", worker_pid)
42 except Exception as e:
43 verbose_proxy_logger.warning("Failed to mark prometheus worker %s as dead: %s", worker_pid, e)
46def _is_running(pid: int) -> bool:
47 try:
48 os.kill(pid, 0)
49 except ProcessLookupError:
50 return False
51 except PermissionError:
52 return True
53 return True
56def mark_dead_workers(directory: str) -> tuple[int, ...]:
57 """Drop the live-gauge files of workers that no longer exist and return their pids.
59 Uvicorn's multi-worker supervisor has no exit hook, so a replacement worker calls this at startup; without it
60 a crashed worker's in-flight gauges stay in the aggregate forever.
61 """
62 owners: Final = frozenset(
63 int(match.group(1))
64 for match in map(_LIVE_GAUGE_PID.search, glob.glob(os.path.join(directory, "gauge_live*_*.db")))
65 if match is not None
66 )
67 dead: Final = tuple(sorted(pid for pid in owners if pid != os.getpid() and not _is_running(pid)))
68 if not dead:
69 return dead
70 from prometheus_client import multiprocess
72 for pid in dead:
73 multiprocess.mark_process_dead(pid, path=directory)
74 verbose_proxy_logger.info("Prometheus cleanup: marked dead workers %s in %s", dead, directory)
75 return dead