fix(us-020): silence BrokenPipeError in tracker _send_json; deterministic node IDs; HF model coverage
- Wrap wfile.write in _TrackerHandler._send_json with except BrokenPipeError
- Replace uuid4 node IDs with deterministic SHA-256 hash of endpoint+model+shards
so nodes keep the same ID on re-registration after tracker restart
- /v1/models now lists HF-repo models (not just preset models)
- /v1/coverage/{model} now resolves HF repos, not just preset names
- /v1/route response includes node_id alongside endpoint
- startup.py exposes tracker_node_id on node object and prints it in dashboard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,11 +22,11 @@ HTTP API contract:
|
||||
"""
|
||||
|
||||
import http.server
|
||||
import hashlib
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
import urllib.parse
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from .gossip import NodeGossip
|
||||
@@ -230,6 +230,27 @@ def _coverage_gaps(coverage: list[dict]) -> list[tuple[int, int]]:
|
||||
]
|
||||
|
||||
|
||||
def _nodes_and_bounds_for_model(
|
||||
server: "_TrackerHTTPServer",
|
||||
model: str,
|
||||
) -> tuple[list[_NodeEntry], int, int] | None:
|
||||
preset = server.model_presets.get(model)
|
||||
if preset is not None:
|
||||
required_start, required_end = _preset_layer_bounds(preset)
|
||||
return [node for node in server.registry.values() if node.model == model], required_start, required_end
|
||||
|
||||
nodes = [
|
||||
node for node in server.registry.values()
|
||||
if (node.hf_repo == model or node.model == model)
|
||||
and node.shard_start is not None
|
||||
and node.shard_end is not None
|
||||
and node.num_layers is not None
|
||||
]
|
||||
if not nodes:
|
||||
return None
|
||||
return nodes, 0, max(node.num_layers for node in nodes) - 1
|
||||
|
||||
|
||||
def _load_directive(node: _NodeEntry, model: str, start: int, end: int, quantization: str) -> dict:
|
||||
return {
|
||||
"action": "LOAD_SHARD",
|
||||
@@ -369,6 +390,27 @@ def _registration_ban_error(contracts: Any | None, wallet_address: str | None) -
|
||||
return None
|
||||
|
||||
|
||||
def _node_id_for_registration(
|
||||
endpoint: str,
|
||||
model: str,
|
||||
wallet_address: str | None,
|
||||
shard_start: int | None,
|
||||
shard_end: int | None,
|
||||
hf_repo: str | None,
|
||||
) -> str:
|
||||
wallet_prefix = wallet_address[:8] if wallet_address else "anon"
|
||||
stable_key = "|".join([
|
||||
wallet_address or "",
|
||||
endpoint.rstrip("/"),
|
||||
model,
|
||||
hf_repo or "",
|
||||
"" if shard_start is None else str(shard_start),
|
||||
"" if shard_end is None else str(shard_end),
|
||||
])
|
||||
digest = hashlib.sha256(stable_key.encode()).hexdigest()[:12]
|
||||
return f"{wallet_prefix}-{digest}"
|
||||
|
||||
|
||||
class _TrackerHTTPServer(http.server.HTTPServer):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -404,7 +446,10 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
try:
|
||||
self.wfile.write(body)
|
||||
except BrokenPipeError:
|
||||
pass
|
||||
|
||||
def _read_json_body(self) -> dict | None:
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
@@ -484,6 +529,7 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
||||
if not node.wallet_address or not server.contracts.registry.get_wallet(node.wallet_address).banned
|
||||
]
|
||||
data = []
|
||||
seen_ids: set[str] = set()
|
||||
for name, preset in server.model_presets.items():
|
||||
model_nodes = [node for node in alive if node.model == name]
|
||||
if not model_nodes:
|
||||
@@ -494,25 +540,73 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
||||
required_start,
|
||||
required_end,
|
||||
)
|
||||
aliases = [name]
|
||||
hf_repo = preset.get("hf_repo")
|
||||
if hf_repo and hf_repo not in aliases:
|
||||
aliases.append(hf_repo)
|
||||
data.append({
|
||||
"id": name,
|
||||
"object": "model",
|
||||
"created": created,
|
||||
"owned_by": "meshnet",
|
||||
"name": name,
|
||||
"hf_repo": hf_repo,
|
||||
"aliases": aliases,
|
||||
"shard_coverage_percentage": coverage,
|
||||
})
|
||||
seen_ids.add(name)
|
||||
|
||||
hf_model_ids = sorted({
|
||||
node.hf_repo or node.model
|
||||
for node in alive
|
||||
if node.model is not None
|
||||
and node.model not in server.model_presets
|
||||
and node.shard_start is not None
|
||||
and node.shard_end is not None
|
||||
and node.num_layers is not None
|
||||
})
|
||||
for model_id in hf_model_ids:
|
||||
if model_id is None or model_id in seen_ids:
|
||||
continue
|
||||
model_nodes = [
|
||||
node for node in alive
|
||||
if node.shard_start is not None
|
||||
and node.shard_end is not None
|
||||
and node.num_layers is not None
|
||||
and (node.hf_repo == model_id or (node.hf_repo is None and node.model == model_id))
|
||||
]
|
||||
if not model_nodes:
|
||||
continue
|
||||
short_names = sorted({node.model for node in model_nodes if node.model})
|
||||
aliases = [model_id, *[name for name in short_names if name != model_id]]
|
||||
required_start = 0
|
||||
required_end = max(node.num_layers for node in model_nodes) - 1
|
||||
data.append({
|
||||
"id": model_id,
|
||||
"object": "model",
|
||||
"created": created,
|
||||
"owned_by": "meshnet",
|
||||
"name": short_names[0] if short_names else model_id,
|
||||
"hf_repo": model_id if any(node.hf_repo == model_id for node in model_nodes) else None,
|
||||
"aliases": aliases,
|
||||
"shard_coverage_percentage": _coverage_percentage(
|
||||
model_nodes,
|
||||
required_start,
|
||||
required_end,
|
||||
),
|
||||
})
|
||||
seen_ids.add(model_id)
|
||||
self._send_json(200, {"object": "list", "data": data})
|
||||
|
||||
def _handle_coverage(self, model: str):
|
||||
server: _TrackerHTTPServer = self.server # type: ignore[assignment]
|
||||
preset = server.model_presets.get(model)
|
||||
if preset is None:
|
||||
self._send_json(404, {"error": f"unknown model preset: {model!r}"})
|
||||
return
|
||||
required_start, required_end = _preset_layer_bounds(preset)
|
||||
with server.lock:
|
||||
self._purge_expired_nodes()
|
||||
alive = [node for node in server.registry.values() if node.model == model]
|
||||
resolved = _nodes_and_bounds_for_model(server, model)
|
||||
if resolved is None:
|
||||
self._send_json(404, {"error": f"no nodes registered for model {model!r}"})
|
||||
return
|
||||
alive, required_start, required_end = resolved
|
||||
if server.contracts is not None:
|
||||
alive = [
|
||||
node for node in alive
|
||||
@@ -821,7 +915,14 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
||||
self._send_json(400, {"error": "num_layers must be an integer"})
|
||||
return
|
||||
|
||||
node_id = str(uuid.uuid4())
|
||||
node_id = _node_id_for_registration(
|
||||
endpoint,
|
||||
model,
|
||||
wallet_address,
|
||||
shard_start,
|
||||
shard_end,
|
||||
hf_repo,
|
||||
)
|
||||
entry = _NodeEntry(
|
||||
node_id=node_id,
|
||||
endpoint=endpoint.rstrip("/"),
|
||||
@@ -1191,6 +1292,7 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
||||
"route": [e.endpoint for e in route],
|
||||
"nodes": [
|
||||
{
|
||||
"node_id": e.node_id,
|
||||
"endpoint": e.endpoint,
|
||||
"wallet_address": e.wallet_address,
|
||||
"shard_start": e.shard_start,
|
||||
@@ -1250,6 +1352,7 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
|
||||
"route": [e.endpoint for e in route],
|
||||
"nodes": [
|
||||
{
|
||||
"node_id": e.node_id,
|
||||
"endpoint": e.endpoint,
|
||||
"wallet_address": e.wallet_address,
|
||||
"shard_start": e.shard_start,
|
||||
|
||||
Reference in New Issue
Block a user