fix(dashboard): warm cold check_fn verdicts with a background probe
On dashboard-only sessions nothing else executes check_fn warmers (they live only in the tool-schema build), so the hub's read-only cache lookup would report auth_required=False forever. On a cache miss, schedule a deduplicated daemon-thread probe off the request path; the short hub TTL surfaces the verdict on the next fetch.
This commit is contained in:
parent
9fa17c133b
commit
1f1acc0e4d
|
|
@ -16646,6 +16646,44 @@ def _invalidate_plugins_hub_cache() -> None:
|
|||
_plugins_hub_cache_expires_at = 0.0
|
||||
|
||||
|
||||
_plugins_hub_probe_inflight: set = set()
|
||||
_plugins_hub_probe_lock = threading.Lock()
|
||||
|
||||
|
||||
def _schedule_check_fn_probe(fn) -> Optional[threading.Thread]:
|
||||
"""Warm a cold ``check_fn`` verdict off the request path.
|
||||
|
||||
The hub read path only consumes cached availability (never probes
|
||||
inline). But the only other warmer lives in the tool-schema build, which
|
||||
a dashboard-only session never runs — so a cold cache would report
|
||||
``auth_required=False`` forever. Kick a daemon-thread probe on the miss;
|
||||
the short hub TTL picks up the verdict on the next fetch. Deduplicates
|
||||
concurrent probes per function. Returns the spawned thread (or ``None``
|
||||
when a probe for *fn* is already in flight).
|
||||
"""
|
||||
with _plugins_hub_probe_lock:
|
||||
if fn in _plugins_hub_probe_inflight:
|
||||
return None
|
||||
_plugins_hub_probe_inflight.add(fn)
|
||||
|
||||
def _probe():
|
||||
try:
|
||||
from tools.registry import _check_fn_cached
|
||||
|
||||
_check_fn_cached(fn)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
with _plugins_hub_probe_lock:
|
||||
_plugins_hub_probe_inflight.discard(fn)
|
||||
|
||||
thread = threading.Thread(
|
||||
target=_probe, name="plugins-hub-checkfn-probe", daemon=True
|
||||
)
|
||||
thread.start()
|
||||
return thread
|
||||
|
||||
|
||||
def _merged_plugins_hub(force_refresh: bool = False) -> Dict[str, Any]:
|
||||
"""Agent discovery + dashboard manifests + optional provider picker metadata.
|
||||
|
||||
|
|
@ -16731,6 +16769,14 @@ def _merged_plugins_hub(force_refresh: bool = False) -> Dict[str, Any]:
|
|||
if not entry or not entry.check_fn:
|
||||
continue
|
||||
cached_result = get_cached_check_fn_result(entry.check_fn)
|
||||
if cached_result is None:
|
||||
# Cold cache: nothing else warms check_fns on
|
||||
# dashboard-only sessions, so kick a background
|
||||
# probe; the short hub TTL surfaces the verdict on
|
||||
# the next fetch instead of pinning auth_required
|
||||
# to False forever.
|
||||
_schedule_check_fn_probe(entry.check_fn)
|
||||
continue
|
||||
if cached_result is False:
|
||||
auth_required = True
|
||||
auth_command = f"hermes auth {name}"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
|
@ -41,19 +42,61 @@ def test_plugins_hub_does_not_probe_cold_check_fns(monkeypatch):
|
|||
tools_registry.invalidate_check_fn_cache()
|
||||
web_server._invalidate_plugins_hub_cache()
|
||||
|
||||
calls = {"count": 0}
|
||||
calls = {"count": 0, "threads": set()}
|
||||
|
||||
def check_fn():
|
||||
calls["count"] += 1
|
||||
calls["threads"].add(threading.current_thread())
|
||||
return False
|
||||
|
||||
_patch_minimal_hub_dependencies(monkeypatch, check_fn=check_fn)
|
||||
|
||||
payload = web_server._merged_plugins_hub(force_refresh=True)
|
||||
|
||||
assert calls["count"] == 0
|
||||
# The request path itself must never execute the probe: the cold verdict
|
||||
# is unknown, so the payload reports no auth requirement. Any probing
|
||||
# happens on a background warmer thread, never inline.
|
||||
assert payload["plugins"][0]["auth_required"] is False
|
||||
assert payload["plugins"][0]["auth_command"] == ""
|
||||
assert threading.current_thread() not in calls["threads"]
|
||||
|
||||
|
||||
def test_plugins_hub_cold_cache_schedules_background_probe(monkeypatch):
|
||||
tools_registry.invalidate_check_fn_cache()
|
||||
web_server._invalidate_plugins_hub_cache()
|
||||
|
||||
probe_ran = threading.Event()
|
||||
|
||||
def check_fn():
|
||||
probe_ran.set()
|
||||
return False
|
||||
|
||||
_patch_minimal_hub_dependencies(monkeypatch, check_fn=check_fn)
|
||||
|
||||
scheduled: list = []
|
||||
real_schedule = web_server._schedule_check_fn_probe
|
||||
|
||||
def tracking_schedule(fn):
|
||||
thread = real_schedule(fn)
|
||||
scheduled.append(thread)
|
||||
return thread
|
||||
|
||||
monkeypatch.setattr(web_server, "_schedule_check_fn_probe", tracking_schedule)
|
||||
|
||||
# Cold cache → the fetch schedules a background probe and reports the
|
||||
# verdict as unknown (auth_required stays False for now).
|
||||
payload = web_server._merged_plugins_hub(force_refresh=True)
|
||||
assert payload["plugins"][0]["auth_required"] is False
|
||||
assert scheduled and scheduled[0] is not None
|
||||
|
||||
scheduled[0].join(timeout=5)
|
||||
assert probe_ran.wait(timeout=5)
|
||||
|
||||
# Once the TTL cache refreshes, the probed False verdict surfaces as an
|
||||
# auth requirement.
|
||||
refreshed = web_server._merged_plugins_hub(force_refresh=True)
|
||||
assert refreshed["plugins"][0]["auth_required"] is True
|
||||
assert refreshed["plugins"][0]["auth_command"] == "hermes auth demo"
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue