222 lines
6.6 KiB
Markdown
222 lines
6.6 KiB
Markdown
# `lmcache ping` — Design & Implementation Plan
|
|
|
|
**Status:** Proposal | **Date:** 2026-03-23
|
|
|
|
## Context
|
|
|
|
The CLI framework (Phase 0) and `describe kvcache` (Phase 1, PR #2825) are
|
|
complete. The next Phase 1 command is `lmcache ping`, a pure liveness check for
|
|
the LMCache server process and the vLLM server process.
|
|
|
|
---
|
|
|
|
## Command UX
|
|
|
|
```bash
|
|
$ lmcache ping kvcache --url http://localhost:8080
|
|
|
|
======= Ping KV Cache =======
|
|
Status: OK
|
|
Round trip time (ms): 0.42
|
|
==============================
|
|
```
|
|
|
|
```bash
|
|
$ lmcache ping engine --url http://localhost:8000
|
|
|
|
======== Ping Engine =========
|
|
Status: OK
|
|
Round trip time (ms): 12.3
|
|
==============================
|
|
```
|
|
|
|
JSON output:
|
|
|
|
```bash
|
|
$ lmcache ping kvcache --url http://localhost:8080 --format json
|
|
```
|
|
|
|
```json
|
|
{
|
|
"title": "Ping KV Cache",
|
|
"metrics": {
|
|
"status": "OK",
|
|
"round_trip_time_ms": 0.42
|
|
}
|
|
}
|
|
```
|
|
|
|
```bash
|
|
$ lmcache ping engine --url http://localhost:8000 --format json
|
|
```
|
|
|
|
```json
|
|
{
|
|
"title": "Ping Engine",
|
|
"metrics": {
|
|
"status": "OK",
|
|
"round_trip_time_ms": 12.3
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Design Decisions
|
|
|
|
### 1. Sub-target as positional argument
|
|
|
|
```
|
|
lmcache ping kvcache --url http://localhost:8080
|
|
lmcache ping engine --url http://localhost:8000
|
|
```
|
|
|
|
Uses a positional `target` argument with `choices=["kvcache", "engine"]`.
|
|
Matches the `ping {kvcache,engine}` pattern in [commands.md](../commands.md).
|
|
|
|
### 2. Both targets use HTTP
|
|
|
|
Both `ping kvcache` and `ping engine` use a simple HTTP GET to the respective
|
|
server's health endpoint. No ZMQ client is needed.
|
|
|
|
| Target | Server process | Endpoint | Healthy | Unhealthy |
|
|
|--------|---------------|----------|---------|-----------|
|
|
| `kvcache` | LMCache MP server | `GET /healthcheck` | 200 `{"status": "healthy"}` | 503 `{"status": "unhealthy", "reason": "..."}` |
|
|
| `engine` | vLLM server | `GET /health` | 200 (empty body) | 503 (empty body) |
|
|
|
|
### 3. `--url` defaults
|
|
|
|
| Target | Default URL |
|
|
|--------|-------------|
|
|
| `kvcache` | `http://localhost:8080` |
|
|
| `engine` | `http://localhost:8000` |
|
|
|
|
The default for `kvcache` matches `describe kvcache`. The default for `engine`
|
|
matches the standard vLLM serving port.
|
|
|
|
### 4. Round-trip time measurement
|
|
|
|
The round-trip time measures only the HTTP request-response cycle, using
|
|
`time.monotonic()` around the `urllib.request.urlopen()` call. This excludes
|
|
Python startup, argument parsing, and output formatting overhead.
|
|
|
|
### 5. HTTP client: stdlib `urllib`
|
|
|
|
Same as `describe` — uses `urllib.request` with no new dependencies.
|
|
|
|
### 6. Error handling
|
|
|
|
| Condition | Status | Exit code |
|
|
|-----------|--------|-----------|
|
|
| 200 response | OK | 0 |
|
|
| 503 response | FAIL | 1 |
|
|
| Connection refused / timeout | FAIL (with error detail) | 1 |
|
|
| Other HTTP error | FAIL (with HTTP status) | 1 |
|
|
|
|
On failure, `status` is reported as `"FAIL"` and the round-trip time is still
|
|
reported (it shows how long we waited before the error). A detail message is
|
|
printed to stderr.
|
|
|
|
---
|
|
|
|
## CLI Implementation
|
|
|
|
### New file: `lmcache/cli/commands/ping.py`
|
|
|
|
```python
|
|
class PingCommand(BaseCommand):
|
|
name() → "ping"
|
|
help() → "Ping LMCache or vLLM server (liveness check)."
|
|
|
|
add_arguments(parser):
|
|
parser.add_argument("target", choices=["kvcache", "engine"],
|
|
help="What to ping.")
|
|
parser.add_argument("--url", default=None,
|
|
help="Server URL (default: http://localhost:8080 "
|
|
"for kvcache, http://localhost:8000 for engine)")
|
|
|
|
execute(args):
|
|
url = args.url or DEFAULT_URLS[args.target]
|
|
url = normalize_url(url)
|
|
endpoint = HEALTH_ENDPOINTS[args.target] # "/healthcheck" or "/health"
|
|
title = TITLES[args.target] # "Ping KV Cache" or "Ping Engine"
|
|
|
|
status, rtt_ms, error = ping(f"{url}{endpoint}")
|
|
|
|
metrics = self.create_metrics(title, args, width=30)
|
|
metrics.add("status", "Status", status)
|
|
metrics.add("round_trip_time_ms", "Round trip time (ms)", round(rtt_ms, 2))
|
|
metrics.emit()
|
|
|
|
if error:
|
|
print(error, file=sys.stderr)
|
|
sys.exit(1)
|
|
```
|
|
|
|
Module-level helper:
|
|
|
|
```python
|
|
HEALTH_ENDPOINTS = {"kvcache": "/healthcheck", "engine": "/health"}
|
|
DEFAULT_URLS = {"kvcache": "http://localhost:8080", "engine": "http://localhost:8000"}
|
|
TITLES = {"kvcache": "Ping KV Cache", "engine": "Ping Engine"}
|
|
|
|
def ping(url: str, timeout: int = 10) -> tuple[str, float, str | None]:
|
|
"""GET *url* and return (status, rtt_ms, error_msg).
|
|
|
|
Returns:
|
|
("OK", rtt_ms, None) on 200.
|
|
("FAIL", rtt_ms, detail) on error.
|
|
"""
|
|
start = time.monotonic()
|
|
try:
|
|
with urllib.request.urlopen(url, timeout=timeout) as resp:
|
|
rtt_ms = (time.monotonic() - start) * 1000
|
|
if resp.status == 200:
|
|
return ("OK", rtt_ms, None)
|
|
return ("FAIL", rtt_ms, f"HTTP {resp.status}")
|
|
except urllib.error.HTTPError as exc:
|
|
rtt_ms = (time.monotonic() - start) * 1000
|
|
return ("FAIL", rtt_ms, f"HTTP {exc.code}: {exc.reason}")
|
|
except (urllib.error.URLError, OSError) as exc:
|
|
rtt_ms = (time.monotonic() - start) * 1000
|
|
reason = getattr(exc, "reason", str(exc))
|
|
return ("FAIL", rtt_ms, f"Cannot connect to {url}: {reason}")
|
|
```
|
|
|
|
Reuses `normalize_url()` from `describe.py` (import it, or move to a shared
|
|
`lmcache/cli/utils.py` if preferred).
|
|
|
|
### Modify: `lmcache/cli/commands/__init__.py`
|
|
|
|
```python
|
|
from lmcache.cli.commands.ping import PingCommand
|
|
|
|
ALL_COMMANDS: list[BaseCommand] = [
|
|
MockCommand(),
|
|
DescribeCommand(),
|
|
PingCommand(),
|
|
]
|
|
```
|
|
|
|
---
|
|
|
|
## Verification
|
|
|
|
1. **Unit tests** (`tests/cli/test_ping.py`):
|
|
- Test `ping()` helper with a real local `HTTPServer` returning 200 and 503.
|
|
- Test connection refused (unreachable port) returns `"FAIL"`.
|
|
- Test `PingCommand.execute()` end-to-end with mocked `ping()` for both
|
|
targets, verifying JSON output fields.
|
|
- Test `--url` default resolution per target.
|
|
2. **Manual tests:**
|
|
```bash
|
|
lmcache ping kvcache --url http://localhost:8080
|
|
lmcache ping kvcache --url http://localhost:8080 --format json
|
|
lmcache ping engine --url http://localhost:8000
|
|
lmcache ping engine --url http://localhost:8000 --format json
|
|
lmcache ping kvcache --url http://localhost:9999 # connection refused → FAIL, exit 1
|
|
lmcache ping engine --url http://localhost:9999 # connection refused → FAIL, exit 1
|
|
lmcache ping kvcache # uses default http://localhost:8080
|
|
lmcache ping engine # uses default http://localhost:8000
|
|
```
|