from __future__ import annotations
import errno
import os
import socket
import subprocess
import sys
import textwrap
import threading
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from modelexpress.metrics import (
ENV_ENABLED,
LIST_SOURCES_RESULTS,
MetricsCollector,
enable_metrics,
push_metrics_if_enabled,
reset_multiproc_dir,
)
_RECORDERS = [
("record_selection", ("random", "a1b2c3d4")),
("record_attempt", ("random", "success")),
("record_metadata_failure", ("random",)),
("record_list_sources", ("random", "ok")),
("observe_candidates", ("random", "listed", 2)),
("observe_selection_seconds", ("random", 0.001)),
("observe_transfer_seconds", ("random", "success", 1.0)),
("record_nixl_error", ("timeout",)),
("record_nixl_receive", ("complete",)),
]
_PACKAGE_ROOT = Path(__file__).resolve().parents[1]
def _fresh_collector(monkeypatch, **env):
from prometheus_client import CollectorRegistry
monkeypatch.setenv(ENV_ENABLED, "1")
env.setdefault("PROMETHEUS_MULTIPROC_DIR", None)
for key, value in env.items():
if value is None:
monkeypatch.delenv(key, raising=False)
else:
monkeypatch.setenv(key, value)
return MetricsCollector(registry=CollectorRegistry())
def _exposition(collector) -> str:
from prometheus_client import generate_latest
return generate_latest(collector._exposition_registry()).decode()
@pytest.fixture
def squatted_port():
squatter = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
squatter.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
squatter.bind(("0.0.0.0", 0))
squatter.listen(1)
try:
yield squatter.getsockname()[1]
finally:
squatter.close()
def test_recorders_are_noop_when_disabled(monkeypatch):
monkeypatch.delenv(ENV_ENABLED, raising=False)
m = MetricsCollector()
for name, args in _RECORDERS:
assert getattr(m, name)(*args) is None
assert m._ready is False
assert m._init_attempted is True
assert not hasattr(m, "selections")
assert m._server_started is False
def test_recorders_never_raise_into_load_path(monkeypatch):
m = MetricsCollector()
m._ready = True
m._init_attempted = True
boom = MagicMock()
boom.labels.side_effect = RuntimeError("collector exploded")
m.selections = boom
m.attempts = boom
m.metadata_failures = boom
m.list_sources = boom
m.candidates = boom
m.selection_seconds = boom
m.transfer_seconds = boom
m.nixl_errors = boom
m.nixl_receives = boom
for name, args in _RECORDERS:
getattr(m, name)(*args)
def test_push_is_noop_when_disabled(monkeypatch):
monkeypatch.delenv(ENV_ENABLED, raising=False)
push_metrics_if_enabled()
def test_enable_is_a_noop_and_never_raises_when_disabled(monkeypatch):
monkeypatch.delenv(ENV_ENABLED, raising=False)
assert enable_metrics() is False
def test_enable_brings_up_the_exporter_with_zero_recordings(monkeypatch):
collector = _fresh_collector(monkeypatch, MX_METRICS_PORT=None, MX_METRICS_PUSHGATEWAY=None)
monkeypatch.setattr("modelexpress.metrics.metrics", collector)
assert enable_metrics() is True
assert collector._ready is True
assert collector._server_started is True
body = _exposition(collector)
assert "mx_build_info" in body
def test_build_info_is_a_gauge_set_to_one(monkeypatch):
collector = _fresh_collector(monkeypatch)
assert collector._ensure() is True
body = _exposition(collector)
assert "# TYPE mx_build_info gauge" in body
series = [
line for line in body.splitlines() if line.startswith("mx_build_info{")
]
assert len(series) == 1, body
assert series[0].endswith(" 1.0"), series[0]
assert "mx_build_info_info" not in body
def test_selection_counter_carries_no_per_process_worker_id(monkeypatch):
collector = _fresh_collector(monkeypatch, MX_METRICS_SOURCE_ID_LABEL=None)
assert collector._ensure() is True
collector.record_selection("random", "a1b2c3d4")
collector.record_selection("random", "e5f6a7b8")
body = _exposition(collector)
assert "source_worker_id" not in body
assert "a1b2c3d4" not in body
selections = [
line
for line in body.splitlines()
if line.startswith("mx_p2p_source_selections_total{")
]
assert len(selections) == 1, body
assert selections[0].endswith(" 2.0"), selections[0]
def test_source_id_label_can_be_opted_into_for_benchmarks(monkeypatch):
collector = _fresh_collector(monkeypatch, MX_METRICS_SOURCE_ID_LABEL="1")
assert collector._ensure() is True
collector.record_selection("random", "a1b2c3d4")
collector.record_selection("random", "a1b2c3d4")
collector.record_selection("random", "e5f6a7b8")
body = _exposition(collector)
selections = [
line
for line in body.splitlines()
if line.startswith("mx_p2p_source_selections_total{")
]
assert len(selections) == 2, body
assert any('source_worker_id="a1b2c3d4"' in line and line.endswith(" 2.0") for line in selections)
assert any('source_worker_id="e5f6a7b8"' in line and line.endswith(" 1.0") for line in selections)
def test_source_id_label_survives_a_caller_that_passes_nothing(monkeypatch):
collector = _fresh_collector(monkeypatch, MX_METRICS_SOURCE_ID_LABEL="1")
assert collector._ensure() is True
collector.record_selection("random")
body = _exposition(collector)
assert 'source_worker_id="unknown"' in body, body
def test_source_id_label_is_latched_not_re_read_per_call(monkeypatch):
collector = _fresh_collector(monkeypatch, MX_METRICS_SOURCE_ID_LABEL=None)
assert collector._ensure() is True
monkeypatch.setenv("MX_METRICS_SOURCE_ID_LABEL", "1")
collector.record_selection("random", "a1b2c3d4")
body = _exposition(collector)
assert "source_worker_id" not in body
selections = [
line
for line in body.splitlines()
if line.startswith("mx_p2p_source_selections_total{")
]
assert len(selections) == 1, body
assert selections[0].endswith(" 1.0"), "the selection was silently dropped"
def test_candidate_funnel_can_observe_zero(monkeypatch):
collector = _fresh_collector(monkeypatch)
assert collector._ensure() is True
collector.observe_candidates("random", "listed", 0)
body = _exposition(collector)
zero_bucket = [
line
for line in body.splitlines()
if line.startswith("mx_p2p_candidates_bucket{")
and 'stage="listed"' in line
and 'le="0.0"' in line
]
assert zero_bucket, body
assert zero_bucket[0].endswith(" 1.0"), zero_bucket[0]
def test_list_sources_outcomes_are_a_closed_enum(monkeypatch):
collector = _fresh_collector(monkeypatch)
assert collector._ensure() is True
for result in LIST_SOURCES_RESULTS:
collector.record_list_sources("random", result)
collector.record_list_sources("random", "totally-unexpected-" + "x" * 64)
body = _exposition(collector)
series = [
line
for line in body.splitlines()
if line.startswith("mx_p2p_list_sources_total{")
]
assert len(series) == len(LIST_SOURCES_RESULTS), body
assert any('result="error"' in line and line.endswith(" 2.0") for line in series)
def test_losing_the_metrics_bind_is_not_fatal(monkeypatch, tmp_path, squatted_port):
collector = _fresh_collector(
monkeypatch,
MX_METRICS_PORT=str(squatted_port),
PROMETHEUS_MULTIPROC_DIR=str(tmp_path),
MX_METRICS_BIND_RETRY_SECS="3600",
)
assert collector._ensure() is True
assert collector._ready is True, "a lost bind must not disable recording"
assert collector._bind_owner is False
assert collector._retry_thread is not None
assert collector._retry_thread.daemon is True
def test_bind_failure_without_multiproc_is_warned_not_silent(
monkeypatch, caplog, squatted_port
):
collector = _fresh_collector(
monkeypatch,
MX_METRICS_PORT=str(squatted_port),
PROMETHEUS_MULTIPROC_DIR=None,
)
with caplog.at_level("WARNING", logger="modelexpress.metrics"):
assert collector._ensure() is True
assert any(
"PROMETHEUS_MULTIPROC_DIR" in record.message for record in caplog.records
), caplog.text
def test_invalid_metrics_port_does_not_raise(monkeypatch):
collector = _fresh_collector(monkeypatch, MX_METRICS_PORT="not-a-port")
assert collector._ensure() is True
assert collector._bind_owner is False
@pytest.mark.parametrize("port", ["0", "-1", "not-a-port", " "])
def test_an_unusable_port_does_not_suppress_the_push(monkeypatch, port):
collector = _fresh_collector(
monkeypatch,
MX_METRICS_PORT=port,
MX_METRICS_PUSHGATEWAY="http://pushgateway:9091",
)
assert collector._ensure() is True
assert collector._bind_owner is False, "an unusable port must not bind"
assert collector._push_registered is True, (
"the push is the only exposition left; it must not be disabled by a port "
"that does not exist"
)
@pytest.mark.parametrize("port", ["0", "-1", "not-a-port"])
def test_push_still_runs_when_the_port_is_unusable(monkeypatch, port):
monkeypatch.setenv(ENV_ENABLED, "1")
monkeypatch.delenv("PROMETHEUS_MULTIPROC_DIR", raising=False)
monkeypatch.setenv("MX_METRICS_PUSHGATEWAY", "http://pushgateway:9091")
monkeypatch.setenv("MX_METRICS_PORT", port)
pushes = []
import prometheus_client
monkeypatch.setattr(
prometheus_client, "push_to_gateway", lambda *a, **kw: pushes.append(kw)
)
push_metrics_if_enabled()
assert len(pushes) == 1, f"MX_METRICS_PORT={port!r} wrongly suppressed the push"
def test_an_unbindable_port_is_reported_as_an_error_not_a_lost_race(
monkeypatch, tmp_path, caplog
):
collector = _fresh_collector(
monkeypatch,
MX_METRICS_PORT="443",
PROMETHEUS_MULTIPROC_DIR=str(tmp_path),
)
def denied(*args, **kwargs):
raise PermissionError(errno.EACCES, "Permission denied")
import prometheus_client
monkeypatch.setattr(prometheus_client, "start_http_server", denied)
with caplog.at_level("DEBUG", logger="modelexpress.metrics"):
assert collector._ensure() is True
assert collector._bind_owner is False
assert collector._retry_thread is None, "a retry cannot fix EACCES"
errors = [r for r in caplog.records if r.levelname == "ERROR"]
assert errors, caplog.text
assert any("not a lost race" in r.message for r in errors), caplog.text
assert not any(
"held by another rank" in r.message for r in caplog.records
), caplog.text
def test_exposition_starts_from_the_merged_registry(monkeypatch, tmp_path):
collector = _fresh_collector(
monkeypatch,
MX_METRICS_PORT="19999",
PROMETHEUS_MULTIPROC_DIR=str(tmp_path),
)
captured = {}
def fake_start(port, registry=None, **kwargs):
captured["port"] = port
captured["registry"] = registry
return None
import prometheus_client
monkeypatch.setattr(prometheus_client, "start_http_server", fake_start)
assert collector._ensure() is True
assert captured["port"] == 19999
served = captured["registry"]
assert served is not collector._registry, "served the local registry, not the union"
assert any(
type(c).__name__ == "MultiProcessCollector" for c in served._collector_to_names
), served._collector_to_names
def test_a_recorder_before_enable_never_raises(monkeypatch, tmp_path):
collector = _fresh_collector(
monkeypatch,
MX_METRICS_PORT="19998",
PROMETHEUS_MULTIPROC_DIR=str(tmp_path),
)
def in_use(*args, **kwargs):
raise OSError(errno.EADDRINUSE, "Address already in use")
def no_threads(self):
raise RuntimeError("can't start new thread")
import prometheus_client
monkeypatch.setattr(prometheus_client, "start_http_server", in_use)
monkeypatch.setattr(threading.Thread, "start", no_threads)
collector.record_selection("random", "a1b2c3d4")
assert collector._ready is True
from prometheus_client import generate_latest
body = generate_latest(collector._registry).decode()
assert "mx_p2p_source_selections_total" in body
def test_push_grouping_key_is_the_pod_not_the_host(monkeypatch):
monkeypatch.setenv(ENV_ENABLED, "1")
monkeypatch.setenv("MX_METRICS_PUSHGATEWAY", "http://pushgateway:9091")
monkeypatch.delenv("MX_METRICS_PORT", raising=False)
monkeypatch.setenv("POD_UID", "pod-uid-1234")
captured = {}
def fake_push(gateway, job, grouping_key, registry):
captured.update(
gateway=gateway, job=job, grouping_key=grouping_key, registry=registry
)
import prometheus_client
monkeypatch.setattr(prometheus_client, "push_to_gateway", fake_push)
push_metrics_if_enabled()
assert captured["grouping_key"] == {"pod": "pod-uid-1234"}
assert "instance" not in captured["grouping_key"]
def test_push_and_scrape_are_mutually_exclusive(monkeypatch):
monkeypatch.setenv(ENV_ENABLED, "1")
monkeypatch.setenv("MX_METRICS_PUSHGATEWAY", "http://pushgateway:9091")
monkeypatch.setenv("MX_METRICS_PORT", "9402")
called = []
def fake_push(*args, **kwargs):
called.append(kwargs)
import prometheus_client
monkeypatch.setattr(prometheus_client, "push_to_gateway", fake_push)
push_metrics_if_enabled()
assert called == [], "the push must stand down when a scrape endpoint exists"
def test_push_is_not_armed_when_a_scrape_port_is_configured(monkeypatch, squatted_port):
collector = _fresh_collector(
monkeypatch,
MX_METRICS_PORT=str(squatted_port),
MX_METRICS_PUSHGATEWAY="http://pushgateway:9091",
)
assert collector._ensure() is True
assert collector._push_registered is False
def test_enable_never_wipes_the_multiproc_directory(monkeypatch, tmp_path):
existing = tmp_path / "counter_1234.db"
existing.write_bytes(b"pretend this is rank 0's mmap")
collector = _fresh_collector(
monkeypatch, PROMETHEUS_MULTIPROC_DIR=str(tmp_path), MX_METRICS_PORT=None
)
monkeypatch.setattr("modelexpress.metrics.metrics", collector)
assert enable_metrics() is True
assert existing.exists(), "enable_metrics() must never unlink another rank's files"
def test_reset_multiproc_dir_clears_stale_files(tmp_path):
stale = tmp_path / "counter_999.db"
stale.write_bytes(b"from the previous container start")
keep = tmp_path / "not-a-metrics-file.txt"
keep.write_text("untouched")
reset_multiproc_dir(str(tmp_path))
assert not stale.exists()
assert keep.exists()
def test_reset_multiproc_dir_is_a_noop_without_a_directory(monkeypatch):
monkeypatch.delenv("PROMETHEUS_MULTIPROC_DIR", raising=False)
reset_multiproc_dir(None)
def test_inactive_multiprocess_mode_is_reported(tmp_path, caplog):
from modelexpress.metrics import _warn_if_multiprocess_inactive
with caplog.at_level("ERROR", logger="modelexpress.metrics"):
_warn_if_multiprocess_inactive(str(tmp_path))
assert any(
"no .db files were written" in record.message for record in caplog.records
), caplog.text
def test_active_multiprocess_mode_is_silent(tmp_path, caplog):
from modelexpress.metrics import _warn_if_multiprocess_inactive
(tmp_path / "counter_1234.db").write_bytes(b"pretend this is a rank's mmap")
with caplog.at_level("ERROR", logger="modelexpress.metrics"):
_warn_if_multiprocess_inactive(str(tmp_path))
assert not caplog.records, caplog.text
_MULTIRANK_SCRIPT = textwrap.dedent(
"""
import os
import signal
import sys
sys.path.insert(0, sys.argv[1])
ranks = int(sys.argv[2])
import modelexpress.metrics as mx
# prometheus_client is imported lazily inside enable_metrics(), so the value class
# latches per child, after the fork, with the directory already set.
children = []
for rank in range(ranks):
pid = os.fork()
if pid == 0:
try:
mx.enable_metrics()
for _ in range(rank + 1):
mx.metrics.record_attempt("random", "success")
except BaseException:
os._exit(1)
if rank == 0:
os._exit(0) # hard exit: no atexit hook
os.kill(os.getpid(), signal.SIGKILL) # OOM-kill simulation
children.append(pid)
failures = 0
for pid in children:
_, status = os.waitpid(pid, 0)
if os.WIFEXITED(status) and os.WEXITSTATUS(status) != 0:
failures += 1
print("failures", failures)
"""
)
@pytest.mark.parametrize("ranks", [2, 8])
def test_merged_endpoint_serves_every_rank_including_hard_killed(tmp_path, ranks):
multiproc_dir = tmp_path / "mx-metrics"
multiproc_dir.mkdir()
env = dict(os.environ)
env["MX_METRICS_ENABLED"] = "1"
env["PROMETHEUS_MULTIPROC_DIR"] = str(multiproc_dir)
env.pop("MX_METRICS_PORT", None)
env.pop("MX_METRICS_PUSHGATEWAY", None)
result = subprocess.run(
[sys.executable, "-c", _MULTIRANK_SCRIPT, str(_PACKAGE_ROOT), str(ranks)],
env=env,
capture_output=True,
text=True,
timeout=300,
)
assert result.returncode == 0, result.stderr
assert "failures 0" in result.stdout, result.stdout + result.stderr
db_files = sorted(p.name for p in multiproc_dir.glob("*.db"))
assert db_files, "no rank wrote an mmap file"
from prometheus_client import CollectorRegistry, generate_latest, multiprocess
registry = CollectorRegistry()
multiprocess.MultiProcessCollector(registry, path=str(multiproc_dir))
body = generate_latest(registry).decode()
attempts = [
line
for line in body.splitlines()
if line.startswith("mx_p2p_source_attempts_total{")
]
assert len(attempts) == 1, body
total = float(attempts[0].rsplit(" ", 1)[1])
expected = ranks * (ranks + 1) / 2 assert total == expected, (
f"merged total {total} != {expected}; a rank's data was lost. "
f"files={db_files}\n{body}"
)
assert "pid=" not in body, body
build_info = [
line for line in body.splitlines() if line.startswith("mx_build_info{")
]
assert len(build_info) == 1, body
assert build_info[0].endswith(" 1.0"), (
f"mx_build_info merged to {build_info[0].rsplit(' ', 1)[1]} across {ranks} "
f"ranks; a summing multiprocess_mode has been introduced"
)
def test_nixl_labels_are_closed_enums(monkeypatch):
m = _fresh_collector(monkeypatch, MX_METRICS_PORT=None, MX_METRICS_PUSHGATEWAY=None)
assert m._ensure() is True
m.record_nixl_error("timeout")
m.record_nixl_error("status_error")
m.record_nixl_error("QP 3 wedged on device mlx5_2")
m.record_nixl_receive("complete")
m.record_nixl_receive("partial")
m.record_nixl_receive("something new")
kinds = _label_values(m.nixl_errors, "kind")
assert kinds == {"timeout", "status_error"}, kinds
m.record_nixl_receive("rejected")
results = _label_values(m.nixl_receives, "result")
assert results == {"complete", "partial", "empty", "rejected"}, results
def test_a_nixl_recording_counts_exactly_one_event(monkeypatch):
collector = _fresh_collector(
monkeypatch, MX_METRICS_PORT=None, MX_METRICS_PUSHGATEWAY=None
)
assert collector._ensure() is True
collector.record_nixl_error("timeout")
collector.record_nixl_error("timeout")
collector.record_nixl_error("QP 3 wedged on device mlx5_2")
collector.record_nixl_receive("complete")
collector.record_nixl_receive("complete")
collector.record_nixl_receive("partial")
assert _label_counts(collector.nixl_errors, "kind") == {
"timeout": 2.0,
"status_error": 1.0,
}
assert _label_counts(collector.nixl_receives, "result") == {
"complete": 2.0,
"partial": 1.0,
}
body = _exposition(collector)
assert 'mx_nixl_data_plane_errors_total{kind="timeout"' in body, body
assert 'mx_nixl_receive_total{result="partial"' in body, body
@pytest.mark.parametrize(
"recorder,args,family,label",
[
("record_nixl_error", ("timeout",), "nixl_errors", "kind"),
("record_nixl_receive", ("partial",), "nixl_receives", "result"),
],
)
def test_a_nixl_event_can_be_the_first_metrics_call_of_a_process(
monkeypatch, recorder, args, family, label
):
collector = _fresh_collector(
monkeypatch, MX_METRICS_PORT=None, MX_METRICS_PUSHGATEWAY=None
)
assert collector._ready is False, "the fixture must not pre-initialize"
getattr(collector, recorder)(*args)
assert collector._ready is True, "the recorder did not initialize the collector"
assert _label_counts(getattr(collector, family), label) == {args[0]: 1.0}
def _label_values(collector, label):
values = set()
for metric in collector.collect():
for sample in metric.samples:
if label in sample.labels:
values.add(sample.labels[label])
return values
def _label_counts(collector, label):
counts = {}
for metric in collector.collect():
for sample in metric.samples:
if sample.name.endswith("_total") and label in sample.labels:
counts[sample.labels[label]] = (
counts.get(sample.labels[label], 0.0) + sample.value
)
return counts