Files
lmcache--lmcache/tests/v1/multiprocess/test_usage_telemetry_server.py
2026-07-13 12:24:33 +08:00

120 lines
3.9 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""End-to-end test: the MP cache server emits usage telemetry at startup.
Spawns a real ``run_cache_server`` in a child process with the stats-server
URL pointed at a local HTTP sink, and asserts the startup report arrives
over the wire.
"""
# Standard
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import multiprocessing
import threading
import time
# Third Party
import pytest
import torch
# First Party
from lmcache.v1.distributed.config import (
EvictionConfig,
L1ManagerConfig,
L1MemoryManagerConfig,
StorageManagerConfig,
)
from lmcache.v1.mp_observability.config import DEFAULT_OBSERVABILITY_CONFIG
from lmcache.v1.multiprocess.config import MPServerConfig
from lmcache.v1.multiprocess.server import run_cache_server
if not torch.cuda.is_available():
pytest.skip(
"MP cache server requires an accelerator device",
allow_module_level=True,
)
SERVER_PORT = 15599
STARTUP_TIMEOUT_SECONDS = 90.0
class _SinkHandler(BaseHTTPRequestHandler):
def do_POST(self) -> None:
length = int(self.headers.get("Content-Length", 0))
body = json.loads(self.rfile.read(length))
self.server.received.append((self.path, body)) # type: ignore[attr-defined]
self.send_response(200)
self.end_headers()
def log_message(self, *args: object) -> None:
pass
class _UsageSink(ThreadingHTTPServer):
def __init__(self) -> None:
super().__init__(("127.0.0.1", 0), _SinkHandler)
self.received: list[tuple[str, dict[str, object]]] = []
def _usage_server_runner(port: int) -> None:
"""Child-process entry point running a minimal cache server."""
mp_config = MPServerConfig(host="localhost", port=port)
storage_manager_config = StorageManagerConfig(
l1_manager_config=L1ManagerConfig(
memory_config=L1MemoryManagerConfig(
size_in_bytes=1 << 30,
use_lazy=True,
),
),
eviction_config=EvictionConfig(eviction_policy="LRU"),
)
run_cache_server(
mp_config=mp_config,
storage_manager_config=storage_manager_config,
obs_config=DEFAULT_OBSERVABILITY_CONFIG,
)
def test_server_startup_emits_usage_report(monkeypatch, tmp_path):
sink = _UsageSink()
sink_thread = threading.Thread(target=sink.serve_forever, daemon=True)
sink_thread.start()
# The child inherits this environment at spawn: tracking enabled,
# stats URL pointed at the sink, HOME isolated so no real state files
# (machine_id) are touched.
monkeypatch.setenv("LMCACHE_TRACK_USAGE", "true")
monkeypatch.setenv(
"LMCACHE_USAGE_TRACK_URL", f"http://127.0.0.1:{sink.server_address[1]}"
)
monkeypatch.setenv("HOME", str(tmp_path))
context = multiprocessing.get_context("spawn")
process = context.Process(target=_usage_server_runner, args=(SERVER_PORT,))
process.start()
try:
deadline = time.monotonic() + STARTUP_TIMEOUT_SECONDS
while time.monotonic() < deadline and len(sink.received) < 2:
time.sleep(0.1)
received = list(sink.received)
assert len(received) == 2, f"expected 2 usage messages, got {received}"
assert {path for path, _ in received} == {"/context"}
payloads = {payload["message_type"]: payload for _, payload in received}
assert set(payloads) == {"EnvMessage", "MPServerMessage"}
mp_payload = payloads["MPServerMessage"]
assert mp_payload["deployment_mode"] == "mp_server"
assert mp_payload["chunk_size"] == 256
assert mp_payload["l1_size_bytes"] == 1 << 30
assert "instance_id" not in mp_payload
assert payloads["EnvMessage"]["session_id"] == mp_payload["session_id"]
finally:
process.terminate()
process.join(timeout=10)
if process.is_alive():
process.kill()
process.join()
sink.shutdown()
sink.server_close()