|
2 | 2 | from __future__ import annotations |
3 | 3 |
|
4 | 4 | import asyncio |
| 5 | +import os |
5 | 6 | import time |
6 | 7 | from typing import TYPE_CHECKING |
7 | 8 |
|
| 9 | +from lmdeploy.utils import get_logger |
| 10 | + |
8 | 11 | if TYPE_CHECKING: |
9 | 12 | from .async_engine import AsyncEngine |
10 | 13 |
|
| 14 | +logger = get_logger('lmdeploy') |
| 15 | + |
| 16 | +HEALTH_POLL_INTERVAL = 'LMDEPLOY_HEALTH_POLL_INTERVAL' |
| 17 | +HEALTH_PROBE_TIMEOUT = 'LMDEPLOY_HEALTH_PROBE_TIMEOUT' |
| 18 | +HEALTH_UNHEALTHY_AFTER = 'LMDEPLOY_HEALTH_UNHEALTHY_AFTER' |
| 19 | + |
| 20 | +DEFAULT_PROBE_TIMEOUT = 10.0 |
| 21 | +DEFAULT_POLL_INTERVAL = 12.0 |
| 22 | +DEFAULT_UNHEALTHY_AFTER = 90.0 |
| 23 | + |
| 24 | + |
| 25 | +def _env_override_float(env_var: str, value: float) -> float: |
| 26 | + """Return ``value`` unless ``env_var`` is set, then parse and return it.""" |
| 27 | + env_value = os.getenv(env_var) |
| 28 | + if env_value is None: |
| 29 | + return value |
| 30 | + try: |
| 31 | + return float(env_value) |
| 32 | + except ValueError: |
| 33 | + return value |
| 34 | + |
11 | 35 |
|
12 | 36 | class EngineHealthMonitor: |
13 | 37 | """Background engine health monitor.""" |
14 | 38 |
|
15 | 39 | def __init__(self, |
16 | 40 | async_engine: AsyncEngine | None, |
17 | | - poll_interval: float = 5.0, |
18 | | - probe_timeout: float = 2.0, |
19 | | - unhealthy_after: float = 15.0): |
| 41 | + poll_interval: float = DEFAULT_POLL_INTERVAL, |
| 42 | + probe_timeout: float = DEFAULT_PROBE_TIMEOUT, |
| 43 | + unhealthy_after: float = DEFAULT_UNHEALTHY_AFTER): |
| 44 | + """Initialize the background health monitor. |
| 45 | +
|
| 46 | + Args: |
| 47 | + async_engine: Engine instance to probe; ``None`` marks the service |
| 48 | + unhealthy until an engine is attached. |
| 49 | + poll_interval: Seconds between consecutive ``probe_once()`` calls in |
| 50 | + the background loop (default 12.0). Should be greater than |
| 51 | + ``probe_timeout`` to reduce overlapping probes. Overridden by |
| 52 | + ``LMDEPLOY_HEALTH_POLL_INTERVAL`` when that variable is set. |
| 53 | + probe_timeout: Maximum seconds to wait for a single |
| 54 | + ``health_probe()`` (backend ``get_health_status()``) before |
| 55 | + reporting that probe as unhealthy (default 10.0). Overridden by |
| 56 | + ``LMDEPLOY_HEALTH_PROBE_TIMEOUT`` when that variable is set. |
| 57 | + unhealthy_after: Seconds without a successful probe or scheduler |
| 58 | + progress before the service is considered unhealthy (default |
| 59 | + 90.0). Overridden by ``LMDEPLOY_HEALTH_UNHEALTHY_AFTER`` when |
| 60 | + that variable is set. Passed to ``health_probe()`` as |
| 61 | + ``scheduler_stall_timeout``, and also used in ``snapshot()`` |
| 62 | + to expire stale healthy status or stuck ``initializing`` state. |
| 63 | + """ |
20 | 64 | self.async_engine = async_engine |
21 | | - self.poll_interval = poll_interval |
22 | | - self.probe_timeout = probe_timeout |
23 | | - self.unhealthy_after = unhealthy_after |
| 65 | + self.poll_interval = _env_override_float(HEALTH_POLL_INTERVAL, poll_interval) |
| 66 | + self.probe_timeout = _env_override_float(HEALTH_PROBE_TIMEOUT, probe_timeout) |
| 67 | + self.unhealthy_after = _env_override_float(HEALTH_UNHEALTHY_AFTER, unhealthy_after) |
| 68 | + if self.poll_interval <= self.probe_timeout: |
| 69 | + logger.warning('Engine health poll_interval (%.1fs) should be greater than probe_timeout (%.1fs) ' |
| 70 | + 'to avoid overlapping probes.', |
| 71 | + self.poll_interval, self.probe_timeout) |
24 | 72 | self._task: asyncio.Task | None = None |
25 | 73 | self._started_time = time.monotonic() |
26 | 74 | self._last_success_time: float | None = None |
@@ -61,14 +109,17 @@ async def probe_once(self): |
61 | 109 | message=f'Engine health probe failed: {e}') |
62 | 110 |
|
63 | 111 | status = result['status'] |
| 112 | + if status == 'pending': |
| 113 | + logger.info('Engine health probe skipped: previous backend health probe is still pending.') |
| 114 | + return |
64 | 115 | if status in ('healthy', 'sleeping'): |
65 | 116 | self._last_success_time = probe_time |
66 | 117 | self._snapshot = dict(status=status, message=result['message']) |
67 | 118 |
|
68 | 119 | def snapshot(self) -> dict: |
69 | 120 | snapshot = dict(self._snapshot) |
70 | 121 | now = time.monotonic() |
71 | | - if snapshot['status'] in ('healthy', 'sleeping') and self._last_success_time is not None: |
| 122 | + if snapshot['status'] == 'healthy' and self._last_success_time is not None: |
72 | 123 | if now - self._last_success_time > self.unhealthy_after: |
73 | 124 | snapshot['status'] = 'unhealthy' |
74 | 125 | snapshot['message'] = f'No successful health probe for {now - self._last_success_time:.1f}s.' |
|
0 commit comments