Spaces:
Paused
Paused
File size: 9,249 Bytes
6778532 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | """Rolling performance telemetry for ranking a node inside the mesh.
``meshstack_model_endpoints_v2`` has **no** latency, success-rate or load
columns. It has ``capabilities jsonb``, ``resource_requirements jsonb`` and a
``status`` enum. Rather than propose new columns (which would fork the Mesh
backend lane's schema mid-flight), this module packs telemetry into the jsonb
fields the contract already defines, under a namespaced ``thox_telemetry`` key
so it cannot collide with capability flags that ThoxRoute matches on.
That choice has a real consequence worth stating: jsonb values are not indexed
for range queries here, so ranking on latency happens in the router, not in
Postgres. At this tier's endpoint counts that is the right trade; if the mesh
ever holds thousands of endpoints, promoting these to real columns becomes
worthwhile and this module is the single place that changes.
The ``status`` field is the one piece of telemetry the schema *does* model, so
health degradation is mapped onto it: a node whose recent success rate collapses
reports ``degraded`` instead of silently continuing to advertise ``healthy``.
"""
from __future__ import annotations
import threading
import time
from collections import deque
from dataclasses import dataclass, field
from typing import Any, Deque
#: Endpoint status values permitted by the v2 schema's CHECK constraint.
STATUS_HEALTHY = "healthy"
STATUS_DEGRADED = "degraded"
STATUS_OFFLINE = "offline"
#: Success rate below which the node self-reports ``degraded``.
DEGRADED_SUCCESS_RATE = 0.80
#: Minimum completed requests before success rate is trusted for status.
MIN_SAMPLES_FOR_STATUS = 5
@dataclass
class _Sample:
"""One completed inference request."""
latency_ms: float
ok: bool
completion_tokens: int
at: float = field(default_factory=time.time)
class TelemetryRecorder:
"""Thread-safe rolling window of request outcomes.
A bounded deque is used rather than cumulative counters so the reported
figures track *current* behaviour. On a Colab node that matters: a session
that was fast for an hour and then hits a throttled GPU should start
advertising the degraded reality within a window, not average it away.
"""
def __init__(self, window: int = 100, *, clock=time.time) -> None:
if window < 1:
raise ValueError("window must be >= 1")
self._samples: Deque[_Sample] = deque(maxlen=window)
self._lock = threading.Lock()
self._clock = clock
self._in_flight = 0
self._started_at = clock()
self._total = 0
self._total_failed = 0
# ββ Recording ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def request_started(self) -> None:
with self._lock:
self._in_flight += 1
def request_finished(self, *, latency_ms: float, ok: bool, completion_tokens: int = 0) -> None:
with self._lock:
self._in_flight = max(0, self._in_flight - 1)
self._total += 1
if not ok:
self._total_failed += 1
self._samples.append(
_Sample(
latency_ms=float(latency_ms),
ok=bool(ok),
completion_tokens=int(completion_tokens),
at=self._clock(),
)
)
# ββ Derived metrics ββββββββββββββββββββββββββββββββββββββββββββββββββ
def snapshot(self) -> dict[str, Any]:
"""Current metrics. Safe to call from the heartbeat thread."""
with self._lock:
samples = list(self._samples)
in_flight = self._in_flight
total = self._total
total_failed = self._total_failed
uptime = max(0.0, self._clock() - self._started_at)
if not samples:
return {
"samples": 0,
"in_flight": in_flight,
"total_requests": total,
"total_failed": total_failed,
"success_rate": None,
"latency_ms_p50": None,
"latency_ms_p95": None,
"tokens_per_second": None,
"uptime_seconds": round(uptime, 1),
}
latencies = sorted(s.latency_ms for s in samples)
ok_count = sum(1 for s in samples if s.ok)
elapsed = max(1e-6, samples[-1].at - samples[0].at) if len(samples) > 1 else None
tokens = sum(s.completion_tokens for s in samples)
return {
"samples": len(samples),
"in_flight": in_flight,
"total_requests": total,
"total_failed": total_failed,
"success_rate": round(ok_count / len(samples), 4),
"latency_ms_p50": round(_percentile(latencies, 0.50), 1),
"latency_ms_p95": round(_percentile(latencies, 0.95), 1),
"tokens_per_second": round(tokens / elapsed, 2) if elapsed and tokens else None,
"uptime_seconds": round(uptime, 1),
}
def status(self) -> str:
"""Map telemetry onto the schema's ``status`` enum.
Only demotes once there is enough evidence; a single early failure on a
cold node should not pull the endpoint out of the candidate set.
"""
snap = self.snapshot()
rate = snap["success_rate"]
if rate is None or snap["samples"] < MIN_SAMPLES_FOR_STATUS:
return STATUS_HEALTHY
return STATUS_HEALTHY if rate >= DEGRADED_SUCCESS_RATE else STATUS_DEGRADED
def load_factor(self) -> float:
"""Concurrent requests in flight, as a plain gauge for the router."""
with self._lock:
return float(self._in_flight)
def _percentile(sorted_values: list[float], q: float) -> float:
"""Nearest-rank percentile over a pre-sorted list."""
if not sorted_values:
raise ValueError("cannot take a percentile of an empty list")
if len(sorted_values) == 1:
return sorted_values[0]
index = min(len(sorted_values) - 1, max(0, int(round(q * (len(sorted_values) - 1)))))
return sorted_values[index]
def build_capabilities(
*,
base: dict[str, Any],
recorder: TelemetryRecorder,
node_kind: str,
ephemeral: bool,
heartbeat_seconds: int,
ttl_seconds: int,
) -> dict[str, Any]:
"""Compose the ``capabilities`` jsonb sent on register and heartbeat.
``base`` holds the classification/capability flags ThoxRoute matches on
(``chat``, ``code``, ``reasoning``, ...). ``resolve_route`` in the edge
function does a truthiness lookup ``capabilities[requested_capability]``, so
those flags must stay at the top level as plain booleans β telemetry is
namespaced beneath ``thox_telemetry`` precisely to keep that lookup clean.
``thox_lease`` carries the ephemerality contract. The backend does not reap
stale endpoints today, so the node publishes its own TTL and the router
treats an endpoint whose ``expires_at`` has passed as ineligible. This is a
client-side lease: it degrades safely if the backend later grows a reaper.
"""
now = time.time()
capabilities = dict(base)
capabilities["thox_telemetry"] = recorder.snapshot()
capabilities["thox_node"] = {
"kind": node_kind,
"ephemeral": ephemeral,
"agent": "thoxmesh-node",
}
capabilities["thox_lease"] = {
"heartbeat_seconds": heartbeat_seconds,
"ttl_seconds": ttl_seconds,
"renewed_at": _iso(now),
"expires_at": _iso(now + ttl_seconds),
}
return capabilities
def is_lease_expired(capabilities: dict[str, Any] | None, *, now: float | None = None) -> bool:
"""True when an endpoint's self-published lease has lapsed.
Used by the router to skip endpoints left behind by a Colab session that
died without deregistering β the dominant failure mode in this tier.
Endpoints with no lease block are treated as live, so endpoints registered
by other (non-ephemeral) device types are unaffected.
"""
if not capabilities:
return False
lease = capabilities.get("thox_lease")
if not isinstance(lease, dict):
return False
expires_at = lease.get("expires_at")
if not isinstance(expires_at, str):
return False
parsed = _parse_iso(expires_at)
if parsed is None:
return False
return (now if now is not None else time.time()) > parsed
def _iso(epoch_seconds: float) -> str:
"""UTC ISO-8601 with a trailing Z, matching the backend's timestamps."""
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(epoch_seconds))
def _parse_iso(value: str) -> float | None:
"""Parse the subset of ISO-8601 this module emits. ``None`` when unparseable."""
text = value.strip().replace("Z", "+0000")
for fmt in ("%Y-%m-%dT%H:%M:%S%z", "%Y-%m-%dT%H:%M:%S.%f%z"):
try:
import datetime as _dt
return _dt.datetime.strptime(text, fmt).timestamp()
except ValueError:
continue
return None
|