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:
+125
-21
@@ -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 "")
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user