Add Comfy Registry fallback and retry logic to version checks

- Retry GitHub pyproject version fetch up to 3 times on failure
- Fall back to Comfy Registry install endpoint when GitHub is unavailable
- Avoid caching failed version checks, so stale unavailable states do not persist
- Add targeted tests for registry fallback and failed-check caching behavior
This commit is contained in:
Dariusz L
2026-08-02 13:04:47 +02:00
parent abb92b2138
commit ccd2585663
2 changed files with 190 additions and 21 deletions
+125 -21
View File
@@ -6,6 +6,8 @@ import threading
import time
from typing import Any
from .log_system import create_module_logger
PROJECT_VERSION_FILE = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"pyproject.toml",
@@ -14,9 +16,16 @@ PROJECT_GITHUB_URL = "https://github.com/Azornes/Comfyui-Model-Resolver"
PROJECT_GITHUB_PYPROJECT_URL = (
"https://raw.githubusercontent.com/Azornes/Comfyui-Model-Resolver/main/pyproject.toml"
)
PROJECT_REGISTRY_NODE_ID = "Comfyui-Model-Resolver"
PROJECT_REGISTRY_INSTALL_URL = (
f"https://api.comfy.org/nodes/{PROJECT_REGISTRY_NODE_ID}/install"
)
PROJECT_VERSION_CACHE_TTL_SECONDS = 6 * 60 * 60
PROJECT_VERSION_REQUEST_TIMEOUT_SECONDS = 5
PROJECT_VERSION_MAX_ATTEMPTS = 3
_project_version_cache = {"checked_at": 0.0, "latest_version": None}
_project_version_cache_lock = threading.Lock()
log = create_module_logger(__name__)
def _extract_project_version(text: str) -> str:
@@ -37,43 +46,138 @@ def _version_sort_key(version: str) -> tuple[int, ...]:
return tuple(int(part) for part in re.findall(r"\d+", str(version or "")))
def _fetch_github_project_version(request_source_response: Any) -> str:
"""Fetch the published version from GitHub, retrying every failed attempt."""
for attempt in range(1, PROJECT_VERSION_MAX_ATTEMPTS + 1):
response = None
try:
response = request_source_response(
PROJECT_GITHUB_PYPROJECT_URL,
timeout=PROJECT_VERSION_REQUEST_TIMEOUT_SECONDS,
max_attempts=1,
log_name="GitHub version check",
)
if response is None:
log.warning(
f"GitHub version check attempt {attempt}/{PROJECT_VERSION_MAX_ATTEMPTS} "
"failed: no response"
)
continue
status_code = getattr(response, "status_code", None)
if status_code != 200:
log.warning(
f"GitHub version check attempt {attempt}/{PROJECT_VERSION_MAX_ATTEMPTS} "
f"failed: HTTP {status_code}"
)
continue
version = _extract_project_version(response.text)
if version:
log.debug(
f"GitHub version check succeeded on attempt "
f"{attempt}/{PROJECT_VERSION_MAX_ATTEMPTS}: v{version}"
)
return version
log.warning(
f"GitHub version check attempt {attempt}/{PROJECT_VERSION_MAX_ATTEMPTS} "
"failed: response did not contain a valid version"
)
except Exception as exc:
log.warning(
f"GitHub version check attempt {attempt}/{PROJECT_VERSION_MAX_ATTEMPTS} "
f"failed: {exc}"
)
finally:
if response is not None:
try:
response.close()
except Exception:
pass
return ""
def _fetch_registry_project_version(request_source_response: Any) -> str:
"""Fetch the latest installable version from the Comfy Registry fallback."""
response = None
try:
response = request_source_response(
PROJECT_REGISTRY_INSTALL_URL,
timeout=PROJECT_VERSION_REQUEST_TIMEOUT_SECONDS,
max_attempts=1,
log_name="Comfy Registry install version check",
)
if response is None:
log.warning("Comfy Registry install version check failed: no response")
return ""
status_code = getattr(response, "status_code", None)
if status_code != 200:
log.warning(
f"Comfy Registry install version check failed: HTTP {status_code}"
)
return ""
payload = response.json()
version = payload.get("version") if isinstance(payload, dict) else ""
version = str(version or "").strip()
if version:
log.debug(f"Comfy Registry install version check succeeded: v{version}")
return version
log.warning(
"Comfy Registry install version check failed: response did not contain version"
)
except Exception as exc:
log.warning(f"Comfy Registry install version check failed: {exc}")
finally:
if response is not None:
try:
response.close()
except Exception:
pass
return ""
def _get_project_version_info() -> dict[str, Any]:
"""Return the installed version and the version currently published on GitHub."""
"""Return the installed version and the latest published project version."""
current_version = _get_local_project_version()
now = time.monotonic()
with _project_version_cache_lock:
cached_at = float(_project_version_cache.get("checked_at") or 0.0)
latest_version = _project_version_cache.get("latest_version")
cache_is_fresh = now - cached_at < PROJECT_VERSION_CACHE_TTL_SECONDS
cache_is_fresh = bool(latest_version) and now - cached_at < PROJECT_VERSION_CACHE_TTL_SECONDS
if not cache_is_fresh:
latest_version = None
try:
from .network_utils import request_source_response
response = request_source_response(
PROJECT_GITHUB_PYPROJECT_URL,
timeout=5,
max_attempts=1,
log_name="GitHub version check",
)
try:
if response is not None and response.status_code == 200:
latest_version = _extract_project_version(response.text)
finally:
if response is not None:
response.close()
latest_version = _fetch_github_project_version(request_source_response)
if not latest_version:
log.warning(
f"GitHub version check failed after {PROJECT_VERSION_MAX_ATTEMPTS} attempts; "
"trying Comfy Registry fallback"
)
latest_version = _fetch_registry_project_version(request_source_response)
except Exception:
log.exception("Project version checks failed unexpectedly")
latest_version = None
with _project_version_cache_lock:
_project_version_cache.update(
{
"checked_at": now,
"latest_version": latest_version or None,
}
)
if latest_version:
with _project_version_cache_lock:
_project_version_cache.update(
{
"checked_at": now,
"latest_version": latest_version,
}
)
else:
log.warning("Could not determine the latest project version; result was not cached")
current_key = _version_sort_key(current_version)
latest_key = _version_sort_key(latest_version or "")
+65
View File
@@ -350,6 +350,71 @@ class TestRefactoringTargets(unittest.IsolatedAsyncioTestCase):
self.assertEqual(result["status"], "update_available")
self.assertEqual(result["github_url"], version_module.PROJECT_GITHUB_URL)
def test_project_version_info_retries_github_and_uses_registry_fallback(self):
version_module = importlib.import_module("comfyui-model-resolver.core.version")
network_module = importlib.import_module("comfyui-model-resolver.core.network_utils")
github_failures = [MagicMock(status_code=503) for _ in range(3)]
registry_response = MagicMock(status_code=200)
registry_response.json.return_value = {"version": "1.1.0"}
request_source_response = MagicMock(
side_effect=[*github_failures, registry_response]
)
with (
patch.object(version_module, "_get_local_project_version", return_value="1.0.0"),
patch.object(
network_module,
"request_source_response",
request_source_response,
),
patch.object(version_module.log, "debug") as debug_log,
):
version_module._project_version_cache.update(
{"checked_at": 0.0, "latest_version": None}
)
result = version_module._get_project_version_info()
self.assertEqual(request_source_response.call_count, 4)
self.assertEqual(
[call.args[0] for call in request_source_response.call_args_list[:3]],
[version_module.PROJECT_GITHUB_PYPROJECT_URL] * 3,
)
self.assertEqual(
request_source_response.call_args_list[3].args[0],
version_module.PROJECT_REGISTRY_INSTALL_URL,
)
self.assertEqual(result["latest_version"], "1.1.0")
self.assertEqual(result["status"], "update_available")
self.assertGreater(version_module._project_version_cache["checked_at"], 0.0)
debug_log.assert_any_call("Comfy Registry install version check succeeded: v1.1.0")
def test_project_version_info_does_not_cache_failed_checks(self):
version_module = importlib.import_module("comfyui-model-resolver.core.version")
network_module = importlib.import_module("comfyui-model-resolver.core.network_utils")
request_source_response = MagicMock(return_value=None)
with (
patch.object(version_module, "_get_local_project_version", return_value="1.0.0"),
patch.object(
network_module,
"request_source_response",
request_source_response,
),
):
version_module._project_version_cache.update(
{"checked_at": 0.0, "latest_version": None}
)
first_result = version_module._get_project_version_info()
first_call_count = request_source_response.call_count
second_result = version_module._get_project_version_info()
self.assertEqual(first_call_count, 4)
self.assertEqual(request_source_response.call_count, 8)
self.assertEqual(first_result["status"], "unavailable")
self.assertEqual(second_result["status"], "unavailable")
self.assertEqual(
version_module._project_version_cache,
{"checked_at": 0.0, "latest_version": None},
)
async def test_version_route_returns_version_payload(self):
get_handler = routes_registered[("GET", "/model_resolver/version")]
request = MagicMock()