feat(compute): Worker-Selbstanmeldung ueber RVS + Flotten-Anzeige (Stage 2)
Jeder GPU-Dienst meldet sich beim Connect mit worker_hello {instanceId,
service, node, gpus, model} und haelt die Registry per periodischem
worker_ping {instanceId, busy} (~10s) frisch. So weiss ARIA, was wo laeuft.
- xtts/{voxtral,whisper,f5tts}/bridge.py + llm-adapter/adapter.py:
INSTANCE_ID=service@NODE_NAME, _worker_register()-Coroutine (hello + ping),
busy-Quelle je Worker (aktive STT-Sessions / TTS-Render / in-flight LLM);
Task sauber gecancelt bei Reconnect.
- bridge/aria_bridge.py: self._workers-Registry + Handler worker_hello/
worker_ping (spiegelt sat_hello), _worker_list() (35s-Offline-TTL),
_pick_worker() (Round-Robin freie Instanz, fuer Stage-3-Routing),
/internal/worker-list-Endpoint.
- diagnostic/server.js: workers-Map, worker_hello/worker_ping-Tracking,
worker_update-Broadcast + worker_list-Action + on-connect-Snapshot.
- diagnostic/index.html: "Compute-Flotte"-Panel im Satelliten-Tab — pro Node
gruppiert, mit Dienst/Modell/GPU und frei/beschaeftigt/offline-Status.
Stage 2 von 3. Reine Sichtbarkeit, kein Routing-Verhalten geaendert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -766,6 +766,11 @@ class ARIABridge:
|
||||
# requestId → Future (sat_devices / sat_result), analog _pending_flux.
|
||||
self._satellites: dict[str, dict] = {}
|
||||
self._pending_sat: dict[str, asyncio.Future] = {}
|
||||
# Compute-Fleet: GPU-Worker (voxtral/whisper/f5tts/llm) melden sich per
|
||||
# worker_hello, halten sich per worker_ping frisch. instanceId →
|
||||
# {service, node, gpus, model, busy, last_seen}. Genutzt fuer die
|
||||
# Diagnostic-Flotten-Anzeige und (Stage 3) targetInstance-Routing.
|
||||
self._workers: dict[str, dict] = {}
|
||||
# FLUX-Render-Requests die aktuell auf Antwort der flux-bridge (Gamebox) warten.
|
||||
# requestId → Future mit dem flux_response-Payload (oder None bei Fehler).
|
||||
self._pending_flux: dict[str, asyncio.Future] = {}
|
||||
@@ -3528,6 +3533,40 @@ class ARIABridge:
|
||||
self._satellites[sid]["caps"], self._satellites[sid]["control"])
|
||||
return
|
||||
|
||||
elif msg_type == "worker_hello":
|
||||
iid = (payload.get("instanceId") or "").strip()
|
||||
if iid:
|
||||
prev = self._workers.get(iid, {})
|
||||
self._workers[iid] = {
|
||||
"instanceId": iid,
|
||||
"service": payload.get("service") or "",
|
||||
"node": payload.get("node") or "",
|
||||
"gpus": payload.get("gpus") or "",
|
||||
"model": payload.get("model") or "",
|
||||
"busy": bool(prev.get("busy", False)),
|
||||
"last_seen": time.time(),
|
||||
}
|
||||
logger.info("[worker] online: %s (service=%s node=%s gpus=%s model=%s)",
|
||||
iid, self._workers[iid]["service"], self._workers[iid]["node"],
|
||||
self._workers[iid]["gpus"] or "?", self._workers[iid]["model"] or "?")
|
||||
return
|
||||
|
||||
elif msg_type == "worker_ping":
|
||||
iid = (payload.get("instanceId") or "").strip()
|
||||
if iid:
|
||||
w = self._workers.get(iid)
|
||||
if w is None:
|
||||
# Ping ohne vorheriges hello (Bridge-Neustart) → Minimal-Eintrag,
|
||||
# service aus der instanceId ableiten (Form: "service@node").
|
||||
svc = iid.split("@", 1)[0]
|
||||
w = self._workers[iid] = {
|
||||
"instanceId": iid, "service": svc, "node": "", "gpus": "",
|
||||
"model": "", "busy": False, "last_seen": 0.0,
|
||||
}
|
||||
w["busy"] = bool(payload.get("busy", False))
|
||||
w["last_seen"] = time.time()
|
||||
return
|
||||
|
||||
elif msg_type in ("sat_devices", "sat_result"):
|
||||
req_id = payload.get("requestId", "")
|
||||
future = self._pending_sat.get(req_id)
|
||||
@@ -4435,6 +4474,9 @@ class ARIABridge:
|
||||
elif method == "POST" and path == "/internal/satellite-list":
|
||||
# Brain fragt: welche Satelliten/Netze sind online + Capabilities.
|
||||
await _send_response(writer, 200, {"ok": True, "satellites": self._satellite_list()})
|
||||
elif method in ("GET", "POST") and path == "/internal/worker-list":
|
||||
# Diagnostic/Brain fragt: welche Compute-Worker sind online (Flotte).
|
||||
await _send_response(writer, 200, {"ok": True, "workers": self._worker_list()})
|
||||
elif method == "POST" and path == "/internal/satellite":
|
||||
# Brain-Tool: Discovery oder Command an einen Satelliten.
|
||||
# body: {op:'discover'|'command', satellite, device?, action?, params?}
|
||||
@@ -4684,6 +4726,44 @@ class ARIABridge:
|
||||
})
|
||||
return out
|
||||
|
||||
# worker_ping kommt alle ~10s; nach 35s ohne Ping gilt ein Worker als offline.
|
||||
WORKER_OFFLINE_S = 35
|
||||
|
||||
def _worker_list(self) -> list[dict]:
|
||||
"""Bekannte Compute-Worker (Flotte). online = kuerzlich per Ping gesehen."""
|
||||
now = time.time()
|
||||
out = []
|
||||
for w in self._workers.values():
|
||||
out.append({
|
||||
"instanceId": w["instanceId"], "service": w.get("service") or "",
|
||||
"node": w.get("node") or "", "gpus": w.get("gpus") or "",
|
||||
"model": w.get("model") or "", "busy": bool(w.get("busy")),
|
||||
"online": (now - w.get("last_seen", 0)) < self.WORKER_OFFLINE_S,
|
||||
})
|
||||
return out
|
||||
|
||||
def _pick_worker(self, service: str) -> Optional[str]:
|
||||
"""Waehlt eine online, moeglichst freie Instanz des Diensts (Round-Robin
|
||||
ueber die freien). Gibt die instanceId oder None (keine online). Fuer
|
||||
Stage-3-Routing (targetInstance)."""
|
||||
now = time.time()
|
||||
online = [w for w in self._workers.values()
|
||||
if w.get("service") == service
|
||||
and (now - w.get("last_seen", 0)) < self.WORKER_OFFLINE_S]
|
||||
if not online:
|
||||
return None
|
||||
free = [w for w in online if not w.get("busy")]
|
||||
pool = free or online # alle busy → trotzdem eine nehmen (least-bad)
|
||||
# Round-Robin: rotierender Zeiger pro Dienst.
|
||||
rr = getattr(self, "_worker_rr", None)
|
||||
if rr is None:
|
||||
rr = self._worker_rr = {}
|
||||
idx = rr.get(service, 0) % len(pool)
|
||||
rr[service] = idx + 1
|
||||
chosen = pool[idx]
|
||||
chosen["busy"] = True # optimistisch, bis der naechste Ping korrigiert
|
||||
return chosen["instanceId"]
|
||||
|
||||
async def _satellite_request(self, op: str, satellite: str = "",
|
||||
device: str = "", action: str = "",
|
||||
params: Optional[dict] = None,
|
||||
|
||||
Reference in New Issue
Block a user