mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-07-09 09:11:04 +00:00
fix(#5047): keep sync refresh followers coalesced
This commit is contained in:
committed by
nesquena-hermes
parent
8da4c7a334
commit
4f2bcada31
+9
-3
@@ -6890,9 +6890,15 @@ def get_available_models(*, prefer_cache: bool = False, force_refresh: bool = Fa
|
||||
# If another thread is already building, wait for its result instead
|
||||
# of re-entering the cold path (avoids duplicate 10s zai load_pool calls).
|
||||
if should_wait:
|
||||
wait_timeout = 60.0
|
||||
if force_refresh and _LIVE_REBUILD_BUDGET_SECONDS <= 0:
|
||||
# The legacy synchronous path is explicitly unbounded. A forced
|
||||
# refresh follower should keep coalescing behind that live
|
||||
# rebuild instead of giving up after 60s and duplicating it.
|
||||
wait_timeout = None
|
||||
_cache_build_cv.wait_for(
|
||||
lambda: not _cache_build_in_progress and _available_models_cache is not None,
|
||||
timeout=60
|
||||
timeout=wait_timeout
|
||||
)
|
||||
cached = _get_fresh_memory_models_cache(time.monotonic())
|
||||
if cached is not None:
|
||||
@@ -6928,13 +6934,13 @@ def get_available_models(*, prefer_cache: bool = False, force_refresh: bool = Fa
|
||||
and force_refresh_started_at is not None
|
||||
and _cache_build_in_progress
|
||||
):
|
||||
remaining_budget = 60.0
|
||||
remaining_budget = None
|
||||
if _LIVE_REBUILD_BUDGET_SECONDS > 0:
|
||||
remaining_budget = max(
|
||||
0.0,
|
||||
_LIVE_REBUILD_BUDGET_SECONDS - (time.monotonic() - force_refresh_started_at),
|
||||
)
|
||||
if remaining_budget > 0:
|
||||
if remaining_budget is None or remaining_budget > 0:
|
||||
_cache_build_cv.wait_for(
|
||||
lambda: not _cache_build_in_progress,
|
||||
timeout=remaining_budget,
|
||||
|
||||
@@ -177,6 +177,65 @@ def test_session_visit_overlapping_stale_calls_coalesce_to_single_live_rebuild(t
|
||||
assert rebuild_count == 1
|
||||
|
||||
|
||||
def test_force_refresh_sync_followers_wait_past_legacy_timeout(tmp_path, monkeypatch):
|
||||
import api.config as cfg
|
||||
import threading
|
||||
|
||||
_reset_models_memory_cache(monkeypatch)
|
||||
stale_catalog = _catalog("stale-model")
|
||||
rebuilt_catalog = _catalog("rebuilt-model")
|
||||
config_path = tmp_path / "config.yaml"
|
||||
config_path.write_text("{}", encoding="utf-8")
|
||||
cache_path = tmp_path / "models_cache.profile.json"
|
||||
cache_path.write_text("{}", encoding="utf-8")
|
||||
old = time.time() - 600.0
|
||||
os.utime(cache_path, (old, old))
|
||||
fingerprint = {"profile": "demo"}
|
||||
rebuild_count = 0
|
||||
timeout_waits = []
|
||||
original_wait_for = threading.Condition.wait_for
|
||||
|
||||
monkeypatch.setattr(cfg, "_LIVE_REBUILD_BUDGET_SECONDS", 0.0, raising=False)
|
||||
monkeypatch.setattr(cfg, "_get_config_path", lambda: config_path)
|
||||
monkeypatch.setattr(cfg, "_cfg_path", config_path, raising=False)
|
||||
monkeypatch.setattr(cfg, "_cfg_mtime", config_path.stat().st_mtime, raising=False)
|
||||
monkeypatch.setattr(cfg, "_get_models_cache_path", lambda: cache_path)
|
||||
monkeypatch.setattr(cfg, "_load_models_cache_from_disk", lambda: None)
|
||||
monkeypatch.setattr(cfg, "_load_stale_models_cache_from_disk", lambda: stale_catalog)
|
||||
monkeypatch.setattr(cfg, "_models_cache_source_fingerprint", lambda: fingerprint)
|
||||
monkeypatch.setattr(cfg, "_save_models_cache_to_disk", lambda _cache: None)
|
||||
|
||||
def _wait_for(self, predicate, timeout=None):
|
||||
if self is not cfg._cache_build_cv:
|
||||
return original_wait_for(self, predicate, timeout)
|
||||
timeout_waits.append(timeout)
|
||||
if timeout is None:
|
||||
published_at = time.monotonic()
|
||||
cfg._available_models_cache = rebuilt_catalog
|
||||
cfg._available_models_cache_ts = published_at
|
||||
cfg._available_models_live_rebuild_ts = published_at
|
||||
cfg._available_models_cache_source_fingerprint = fingerprint
|
||||
cfg._cache_build_in_progress = False
|
||||
return True
|
||||
if timeout == 60.0:
|
||||
return False
|
||||
return original_wait_for(self, predicate, timeout=timeout)
|
||||
|
||||
def _invoke_models_rebuild(_builder):
|
||||
nonlocal rebuild_count
|
||||
rebuild_count += 1
|
||||
return rebuilt_catalog
|
||||
|
||||
monkeypatch.setattr(cfg, "_cache_build_in_progress", True, raising=False)
|
||||
monkeypatch.setattr(threading.Condition, "wait_for", _wait_for)
|
||||
monkeypatch.setattr(cfg, "_invoke_models_rebuild", _invoke_models_rebuild)
|
||||
|
||||
assert cfg.get_available_models(force_refresh=True) == rebuilt_catalog
|
||||
assert rebuild_count == 0
|
||||
assert None in timeout_waits
|
||||
assert 60.0 not in timeout_waits
|
||||
|
||||
|
||||
def test_session_visit_overlapping_stale_calls_do_not_duplicate_over_budget_rebuild(tmp_path, monkeypatch):
|
||||
import api.config as cfg
|
||||
import threading
|
||||
|
||||
Reference in New Issue
Block a user