Reduce controller polling and bound chat admission waits
This commit is contained in:
1 parent
53320fa6c3
commit
6071b1a2cd
10 files changed
+303
-134
No files matched your search
@@ -323,6 +323,8 @@ def stop_container(item: dict, timeout: int = 120) -> None:
|
||||
|
||||
|
||||
def start_container(item: dict) -> None:
|
||||
if item.get("State") == "running":
|
||||
return
|
||||
status, _ = docker_request("POST", f"/containers/{item['Id']}/start")
|
||||
if status not in (204, 304):
|
||||
raise RuntimeError(f"failed to start container: HTTP {status}")
|
||||
@@ -571,7 +573,7 @@ def set_video_worker(running: bool) -> dict:
|
||||
|
||||
|
||||
def active_profile(items: dict[str, dict] | None = None) -> str | None:
|
||||
items = items or containers()
|
||||
items = containers() if items is None else items
|
||||
active = [name for name, item in items.items() if item.get("State") == "running"]
|
||||
if len(active) > 1:
|
||||
raise RuntimeError(f"multiple llama profiles active: {', '.join(active)}")
|
||||
@@ -582,6 +584,11 @@ def activate(profile: str) -> dict:
|
||||
if profile not in ALLOWED:
|
||||
raise ValueError("profile is not allowlisted")
|
||||
with LOCK:
|
||||
# Validate before stopping any working service.
|
||||
items = containers()
|
||||
missing = [name for name in ALLOWED if name not in items]
|
||||
if missing:
|
||||
raise RuntimeError("profile containers missing: " + ", ".join(missing))
|
||||
# Defensive mutual exclusion even if a caller bypasses the router.
|
||||
for worker in image_containers():
|
||||
stop_container(worker)
|
||||
@@ -592,10 +599,6 @@ def activate(profile: str) -> dict:
|
||||
stop_trellis_if_configured()
|
||||
stop_video_if_configured()
|
||||
start_container(tts_container())
|
||||
items = containers()
|
||||
missing = [name for name in ALLOWED if name not in items]
|
||||
if missing:
|
||||
raise RuntimeError("profile containers missing: " + ", ".join(missing))
|
||||
current = active_profile(items)
|
||||
if current == profile:
|
||||
return {"active_profile": current, "changed": False}
|
||||
@@ -613,6 +616,58 @@ def activate(profile: str) -> dict:
|
||||
return {"active_profile": profile, "changed": True}
|
||||
|
||||
|
||||
def status_snapshot() -> dict:
|
||||
"""One Docker snapshot; missing optional workers cannot hide the LLM."""
|
||||
status, body = docker_request("GET", "/containers/json?all=1")
|
||||
if status != 200:
|
||||
raise RuntimeError(f"Docker list failed with HTTP {status}")
|
||||
records = json.loads(body)
|
||||
profiles = {}
|
||||
for item in records:
|
||||
name = item.get("Labels", {}).get(LABEL_KEY)
|
||||
if name in ALLOWED:
|
||||
if name in profiles:
|
||||
raise RuntimeError(f"duplicate container for profile {name}")
|
||||
profiles[name] = item
|
||||
result = {"active_profile": active_profile(profiles),
|
||||
"profiles": {name: profiles.get(name, {}).get("State", "missing")
|
||||
for name in ALLOWED}, "worker_errors": {}}
|
||||
workers = (
|
||||
("music", MUSIC_LABEL_KEY, MUSIC_WORKER),
|
||||
("yue2", MUSIC_LABEL_KEY, YUE2_WORKER),
|
||||
("separator", SEPARATOR_LABEL_KEY, SEPARATOR_WORKER),
|
||||
("voice", VOICE_LABEL_KEY, VOICE_WORKER),
|
||||
("voice_change", VOICE_CHANGE_LABEL_KEY, VOICE_CHANGE_WORKER),
|
||||
("applio", APPLIO_LABEL_KEY, APPLIO_WORKER),
|
||||
("trellis", TRELLIS_LABEL_KEY, TRELLIS_WORKER),
|
||||
("video", VIDEO_LABEL_KEY, VIDEO_WORKER),
|
||||
)
|
||||
result["video_ui"] = "unavailable"
|
||||
for name, label, kind in workers:
|
||||
matches = [item for item in records
|
||||
if kind and item.get("Labels", {}).get(label) == kind]
|
||||
if not kind:
|
||||
state = health = "disabled"
|
||||
elif len(matches) != 1:
|
||||
state = health = "missing" if not matches else "error"
|
||||
result["worker_errors"][name] = f"expected one container, found {len(matches)}"
|
||||
else:
|
||||
item = matches[0]
|
||||
state = item.get("State", "unknown")
|
||||
description = item.get("Status", "")
|
||||
health = ("healthy" if "(healthy)" in description else
|
||||
"unhealthy" if "(unhealthy)" in description else
|
||||
"starting" if state == "running" else "stopped")
|
||||
if name == "video":
|
||||
try:
|
||||
result["video_ui"] = cached_video_ui_state(item)
|
||||
except Exception as exc:
|
||||
result["worker_errors"]["video_ui"] = str(exc)
|
||||
result[f"{name}_worker"] = state
|
||||
result[f"{name}_health"] = health
|
||||
return result
|
||||
|
||||
|
||||
def load_token() -> str:
|
||||
token = os.environ.get("CONTROLLER_TOKEN", "").strip()
|
||||
if not token:
|
||||
@@ -647,90 +702,18 @@ class Handler(BaseHTTPRequestHandler):
|
||||
if self.path == "/health":
|
||||
self.reply(200, {"status": "ok"})
|
||||
return
|
||||
if self.path != "/status":
|
||||
if self.path not in {"/status", "/profiles/status"}:
|
||||
self.reply(404, {"error": "not found"})
|
||||
return
|
||||
if not self.authenticated():
|
||||
self.reply(401, {"error": "unauthorized"})
|
||||
return
|
||||
try:
|
||||
items = containers()
|
||||
music = music_container() if MUSIC_WORKER else {}
|
||||
music_status = music.get("Status", "")
|
||||
music_health = ("disabled" if not MUSIC_WORKER else
|
||||
"healthy" if "(healthy)" in music_status else
|
||||
"unhealthy" if "(unhealthy)" in music_status else
|
||||
"starting" if music.get("State") == "running" else
|
||||
"stopped")
|
||||
yue2 = yue2_container() if YUE2_WORKER else {}
|
||||
yue2_status = yue2.get("Status", "")
|
||||
yue2_health = ("disabled" if not YUE2_WORKER else
|
||||
"healthy" if "(healthy)" in yue2_status else
|
||||
"unhealthy" if "(unhealthy)" in yue2_status else
|
||||
"starting" if yue2.get("State") == "running" else
|
||||
"stopped")
|
||||
separator = separator_container() if SEPARATOR_WORKER else {}
|
||||
separator_status = separator.get("Status", "")
|
||||
separator_health = ("disabled" if not SEPARATOR_WORKER else
|
||||
"healthy" if "(healthy)" in separator_status else
|
||||
"unhealthy" if "(unhealthy)" in separator_status else
|
||||
"starting" if separator.get("State") == "running" else
|
||||
"stopped")
|
||||
voice = voice_container() if VOICE_WORKER else {}
|
||||
voice_status = voice.get("Status", "")
|
||||
voice_health = ("disabled" if not VOICE_WORKER else
|
||||
"healthy" if "(healthy)" in voice_status else
|
||||
"unhealthy" if "(unhealthy)" in voice_status else
|
||||
"starting" if voice.get("State") == "running" else
|
||||
"stopped")
|
||||
voice_change = voice_change_container() if VOICE_CHANGE_WORKER else {}
|
||||
voice_change_status = voice_change.get("Status", "")
|
||||
voice_change_health = ("disabled" if not VOICE_CHANGE_WORKER else
|
||||
"healthy" if "(healthy)" in voice_change_status else
|
||||
"unhealthy" if "(unhealthy)" in voice_change_status else
|
||||
"starting" if voice_change.get("State") == "running" else
|
||||
"stopped")
|
||||
applio = applio_container() if APPLIO_WORKER else {}
|
||||
applio_status = applio.get("Status", "")
|
||||
applio_health = ("disabled" if not APPLIO_WORKER else
|
||||
"healthy" if "(healthy)" in applio_status else
|
||||
"unhealthy" if "(unhealthy)" in applio_status else
|
||||
"starting" if applio.get("State") == "running" else
|
||||
"stopped")
|
||||
trellis = trellis_container() if TRELLIS_WORKER else {}
|
||||
trellis_status = trellis.get("Status", "")
|
||||
trellis_health = ("disabled" if not TRELLIS_WORKER else
|
||||
"healthy" if "(healthy)" in trellis_status else
|
||||
"unhealthy" if "(unhealthy)" in trellis_status else
|
||||
"starting" if trellis.get("State") == "running" else
|
||||
"stopped")
|
||||
video = video_container() if VIDEO_WORKER else {}
|
||||
video_status = video.get("Status", "")
|
||||
video_health = ("disabled" if not VIDEO_WORKER else
|
||||
"healthy" if "(healthy)" in video_status else
|
||||
"unhealthy" if "(unhealthy)" in video_status else
|
||||
"starting" if video.get("State") == "running" else
|
||||
"stopped")
|
||||
self.reply(200, {"active_profile": active_profile(items),
|
||||
"music_worker": music.get("State", "disabled"),
|
||||
"music_health": music_health,
|
||||
"yue2_worker": yue2.get("State", "disabled"),
|
||||
"yue2_health": yue2_health,
|
||||
"separator_worker": separator.get("State", "disabled"),
|
||||
"separator_health": separator_health,
|
||||
"voice_worker": voice.get("State", "disabled"),
|
||||
"voice_health": voice_health,
|
||||
"voice_change_worker": voice_change.get("State", "disabled"),
|
||||
"voice_change_health": voice_change_health,
|
||||
"applio_worker": applio.get("State", "disabled"),
|
||||
"applio_health": applio_health,
|
||||
"trellis_worker": trellis.get("State", "disabled"),
|
||||
"trellis_health": trellis_health,
|
||||
"video_worker": video.get("State", "disabled"),
|
||||
"video_health": video_health,
|
||||
"video_ui": cached_video_ui_state(video) if video else "unavailable",
|
||||
"profiles": {name: items.get(name, {}).get(
|
||||
"State", "missing") for name in ALLOWED}})
|
||||
if self.path == "/profiles/status":
|
||||
items = containers()
|
||||
self.reply(200, {"active_profile": active_profile(items)})
|
||||
else:
|
||||
self.reply(200, status_snapshot())
|
||||
except Exception as exc:
|
||||
log.exception("status failed")
|
||||
self.reply(503, {"error": str(exc)})
|
||||
|
||||
Reference in new issue
Block a user