diff --git a/code/scripts/preview_perf_benchmark.py b/code/scripts/preview_perf_benchmark.py index 94404ab..f1e60ed 100644 --- a/code/scripts/preview_perf_benchmark.py +++ b/code/scripts/preview_perf_benchmark.py @@ -38,14 +38,24 @@ def _fetch_json(url: str) -> Dict[str, Any]: def _fetch_json_with_retry(url: str, attempts: int = 3, delay: float = 0.6) -> Dict[str, Any]: + last_error: Exception | None = None for attempt in range(1, attempts + 1): try: return _fetch_json(url) - except Exception: # pragma: no cover - network variability + except Exception as exc: # pragma: no cover - network variability + last_error = exc if attempt < attempts: + print(json.dumps({ # noqa: T201 + "event": "preview_perf_fetch_retry", + "url": url, + "attempt": attempt, + "max_attempts": attempts, + "error": str(exc), + })) time.sleep(delay * attempt) else: raise + raise last_error # pragma: no cover - defensive; should be unreachable def select_theme_slugs(base_url: str, count: int) -> List[str]: @@ -97,12 +107,31 @@ def fetch_all_theme_slugs(base_url: str, page_limit: int = 200) -> List[str]: slugs: List[str] = [] offset = 0 seen: set[str] = set() + page_attempts = 5 + page_delay = 1.2 while True: - try: - url = f"{base_url.rstrip('/')}/themes/api/themes?limit={page_limit}&offset={offset}" - data = _fetch_json_with_retry(url) - except Exception as e: # pragma: no cover - network variability - raise SystemExit(f"Failed fetching themes page offset={offset}: {e}") + url = f"{base_url.rstrip('/')}/themes/api/themes?limit={page_limit}&offset={offset}" + data: Dict[str, Any] | None = None + last_error: Exception | None = None + for attempt in range(1, page_attempts + 1): + try: + data = _fetch_json_with_retry(url, attempts=4, delay=0.75) + break + except Exception as exc: # pragma: no cover - network variability + last_error = exc + if attempt < page_attempts: + print(json.dumps({ # noqa: T201 + "event": "preview_perf_page_retry", + "offset": offset, + "attempt": attempt, + "max_attempts": page_attempts, + "error": str(exc), + })) + time.sleep(page_delay * attempt) + else: + raise SystemExit(f"Failed fetching themes page offset={offset}: {exc}") + if data is None: # pragma: no cover - defensive + raise SystemExit(f"Failed fetching themes page offset={offset}: {last_error}") items = data.get("items") or [] for it in items: if not isinstance(it, dict): diff --git a/code/scripts/preview_perf_ci_check.py b/code/scripts/preview_perf_ci_check.py index 108f219..5550e4b 100644 --- a/code/scripts/preview_perf_ci_check.py +++ b/code/scripts/preview_perf_ci_check.py @@ -25,7 +25,7 @@ import time import urllib.error import urllib.request from pathlib import Path -def _wait_for_service(base_url: str, attempts: int = 8, delay: float = 1.5) -> bool: +def _wait_for_service(base_url: str, attempts: int = 12, delay: float = 1.5) -> bool: health_url = base_url.rstrip("/") + "/healthz" last_error: Exception | None = None for attempt in range(1, attempts + 1): @@ -40,7 +40,7 @@ def _wait_for_service(base_url: str, attempts: int = 8, delay: float = 1.5) -> b break except Exception as exc: # pragma: no cover - network variability last_error = exc - time.sleep(delay) + time.sleep(delay * attempt) print(json.dumps({ "event": "ci_perf_error", "stage": "startup", diff --git a/code/tests/test_preview_perf_fetch_retry.py b/code/tests/test_preview_perf_fetch_retry.py index 7d5315b..00311fb 100644 --- a/code/tests/test_preview_perf_fetch_retry.py +++ b/code/tests/test_preview_perf_fetch_retry.py @@ -18,3 +18,22 @@ def test_fetch_all_theme_slugs_retries(monkeypatch): assert slugs == ["alpha"] assert calls["count"] == 2 + + +def test_fetch_all_theme_slugs_page_level_retry(monkeypatch): + calls = {"count": 0} + + def fake_fetch_with_retry(url, attempts=3, delay=0.6): # type: ignore[override] + calls["count"] += 1 + if calls["count"] < 3: + raise RuntimeError("service warming up") + assert url.endswith("offset=0") + return {"items": [{"id": "alpha"}], "next_offset": None} + + monkeypatch.setattr(perf, "_fetch_json_with_retry", fake_fetch_with_retry) + monkeypatch.setattr(perf.time, "sleep", lambda *_args, **_kwargs: None) + + slugs = perf.fetch_all_theme_slugs("http://example.com", page_limit=1) + + assert slugs == ["alpha"] + assert calls["count"] == 3 diff --git a/code/web/app.py b/code/web/app.py index 3103c45..2335fe4 100644 --- a/code/web/app.py +++ b/code/web/app.py @@ -105,7 +105,7 @@ SHOW_SETUP = _as_bool(os.getenv("SHOW_SETUP"), True) SHOW_DIAGNOSTICS = _as_bool(os.getenv("SHOW_DIAGNOSTICS"), False) SHOW_COMMANDERS = _as_bool(os.getenv("SHOW_COMMANDERS"), True) SHOW_VIRTUALIZE = _as_bool(os.getenv("WEB_VIRTUALIZE"), False) -ENABLE_THEMES = _as_bool(os.getenv("ENABLE_THEMES"), False) +ENABLE_THEMES = _as_bool(os.getenv("ENABLE_THEMES"), True) ENABLE_PWA = _as_bool(os.getenv("ENABLE_PWA"), False) ENABLE_PRESETS = _as_bool(os.getenv("ENABLE_PRESETS"), False) ALLOW_MUST_HAVES = _as_bool(os.getenv("ALLOW_MUST_HAVES"), False)