From ccd258566399577462d7024de6863c4f5e61bc88 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Sun, 2 Aug 2026 13:04:47 +0200 Subject: [PATCH] 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 --- core/version.py | 146 +++++++++++++++++++++++++----- tests/test_refactoring_targets.py | 65 +++++++++++++ 2 files changed, 190 insertions(+), 21 deletions(-) diff --git a/core/version.py b/core/version.py index c130db3..706d850 100644 --- a/core/version.py +++ b/core/version.py @@ -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 "") diff --git a/tests/test_refactoring_targets.py b/tests/test_refactoring_targets.py index 64f6bb9..5929fd3 100644 --- a/tests/test_refactoring_targets.py +++ b/tests/test_refactoring_targets.py @@ -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()